Skip to content

Commit 71836d2

Browse files
authored
feat(FIR-14346): Separate client tracking (#184)
1 parent 2e0dd81 commit 71836d2

File tree

4 files changed

+103
-44
lines changed

4 files changed

+103
-44
lines changed

src/firebolt/async_db/connection.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -308,14 +308,15 @@ def __init__(
308308
# Override tcp keepalive settings for connection
309309
transport = AsyncHTTPTransport()
310310
transport._pool._network_backend = OverriddenHttpBackend()
311-
connector_versions = additional_parameters.get("connector_versions", [])
311+
user_drivers = additional_parameters.get("user_drivers", [])
312+
user_clients = additional_parameters.get("user_clients", [])
312313
self._client = AsyncClient(
313314
auth=auth,
314315
base_url=engine_url,
315316
api_endpoint=api_endpoint,
316317
timeout=Timeout(DEFAULT_TIMEOUT_SECONDS, read=None),
317318
transport=transport,
318-
headers={"User-Agent": get_user_agent_header(connector_versions)},
319+
headers={"User-Agent": get_user_agent_header(user_drivers, user_clients)},
319320
)
320321
self.api_endpoint = api_endpoint
321322
self.engine_url = engine_url

src/firebolt/utils/usage_tracker.py

Lines changed: 45 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,19 +16,14 @@ class ConnectorVersions(BaseModel):
1616
Verify correct parameter types
1717
"""
1818

19-
versions: List[Tuple[str, str]]
19+
clients: List[Tuple[str, str]]
20+
drivers: List[Tuple[str, str]]
2021

2122

2223
logger = logging.getLogger(__name__)
2324

2425

25-
CONNECTOR_MAP = [
26-
(
27-
"DBT",
28-
"open",
29-
Path("dbt/adapters/firebolt/connections.py"),
30-
"dbt.adapters.firebolt",
31-
),
26+
CLIENT_MAP = [
3227
(
3328
"Airflow",
3429
"get_conn",
@@ -54,10 +49,19 @@ class ConnectorVersions(BaseModel):
5449
Path("source_firebolt/source.py"),
5550
"",
5651
),
57-
("SQLAlchemy", "connect", Path("sqlalchemy/engine/default.py"), "firebolt_db"),
5852
("FireboltCLI", "create_connection", Path("firebolt_cli/utils.py"), "firebolt_cli"),
5953
]
6054

55+
DRIVER_MAP = [
56+
(
57+
"DBT",
58+
"open",
59+
Path("dbt/adapters/firebolt/connections.py"),
60+
"dbt.adapters.firebolt",
61+
),
62+
("SQLAlchemy", "connect", Path("sqlalchemy/engine/default.py"), "firebolt_db"),
63+
]
64+
6165

6266
def _os_compare(file: Path, expected: Path) -> bool:
6367
"""
@@ -94,7 +98,9 @@ def get_sdk_properties() -> Tuple[str, str, str, str]:
9498
return (py_version, sdk_version, os_version, ciso)
9599

96100

97-
def detect_connectors() -> Dict[str, str]:
101+
def detect_connectors(
102+
connector_map: List[Tuple[str, str, Path, str]]
103+
) -> Dict[str, str]:
98104
"""
99105
Detect which connectors are running the code by parsing the stack.
100106
Exceptions are ignored since this is intended for logging only.
@@ -103,7 +109,7 @@ def detect_connectors() -> Dict[str, str]:
103109
stack = inspect.stack()
104110
for f in stack:
105111
try:
106-
for name, func, path, version_path in CONNECTOR_MAP:
112+
for name, func, path, version_path in connector_map:
107113
if f.function == func and _os_compare(Path(f.filename), path):
108114
if version_path:
109115
m = import_module(version_path)
@@ -120,7 +126,7 @@ def detect_connectors() -> Dict[str, str]:
120126
return connectors
121127

122128

123-
def format_as_user_agent(connectors: Dict[str, str]) -> str:
129+
def format_as_user_agent(drivers: Dict[str, str], clients: Dict[str, str]) -> str:
124130
"""
125131
Return a representation of a stored tracking data as a user-agent header.
126132
@@ -132,28 +138,44 @@ def format_as_user_agent(connectors: Dict[str, str]) -> str:
132138
"""
133139
py, sdk, os, ciso = get_sdk_properties()
134140
sdk_format = f"PythonSDK/{sdk} (Python {py}; {os}; {ciso})"
135-
connector_format = "".join(
136-
[f" {connector}/{version}" for connector, version in connectors.items()]
141+
driver_format = "".join(
142+
[f" {connector}/{version}" for connector, version in drivers.items()]
143+
)
144+
client_format = "".join(
145+
[f"{connector}/{version} " for connector, version in clients.items()]
137146
)
138-
return sdk_format + connector_format
147+
return client_format + sdk_format + driver_format
139148

140149

141150
def get_user_agent_header(
142-
connector_versions: Optional[List[Tuple[str, str]]] = []
151+
user_drivers: Optional[List[Tuple[str, str]]] = [],
152+
user_clients: Optional[List[Tuple[str, str]]] = [],
143153
) -> str:
144154
"""
145155
Return a user agent header with connector stack and system information.
146156
147157
Args:
148-
connector_versions(Optional): User-supplied list of tuples of all connectors
149-
and their versions intended for tracking.
158+
user_drivers(Optional): User-supplied list of tuples of all drivers
159+
and their versions intended for tracking. Driver is a programmatic
160+
module that facilitates interaction between a clients and underlying
161+
database.
162+
user_clients(Optional): User-supplied list of tuples of all clients
163+
and their versions intended for tracking. Client is a user-facing
164+
module or application that allows interaction with the database
165+
via drivers or directly.
150166
151167
Returns:
152168
String representation of a user-agent tracking information
153169
"""
154-
connectors = detect_connectors()
155-
logger.debug("Detected running from packages: %s", str(connectors))
170+
drivers = detect_connectors(DRIVER_MAP)
171+
clients = detect_connectors(CLIENT_MAP)
172+
logger.debug(
173+
"Detected running with drivers: %s and clients %s ", str(drivers), str(clients)
174+
)
156175
# Override auto-detected connectors with info provided manually
157-
for name, version in ConnectorVersions(versions=connector_versions).versions:
158-
connectors[name] = version
159-
return format_as_user_agent(connectors)
176+
versions = ConnectorVersions(clients=user_clients, drivers=user_drivers)
177+
for name, version in versions.clients:
178+
clients[name] = version
179+
for name, version in versions.drivers:
180+
drivers[name] = version
181+
return format_as_user_agent(drivers, clients)

tests/unit/async_db/test_connection.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -400,11 +400,11 @@ async def test_connect_with_user_agent(
400400
access_token: str,
401401
) -> None:
402402
with patch("firebolt.async_db.connection.get_user_agent_header") as ut:
403-
ut.return_value = "MyConnector/1.0"
403+
ut.return_value = "MyConnector/1.0 DriverA/1.1"
404404
httpx_mock.add_callback(
405405
query_callback,
406406
url=query_url,
407-
match_headers={"User-Agent": "MyConnector/1.0"},
407+
match_headers={"User-Agent": "MyConnector/1.0 DriverA/1.1"},
408408
)
409409

410410
async with await connect(
@@ -413,10 +413,13 @@ async def test_connect_with_user_agent(
413413
engine_url=settings.server,
414414
account_name=settings.account_name,
415415
api_endpoint=settings.server,
416-
additional_parameters={"connector_versions": [("MyConnector", "1.0")]},
416+
additional_parameters={
417+
"user_clients": [("MyConnector", "1.0")],
418+
"user_drivers": [("DriverA", "1.1")],
419+
},
417420
) as connection:
418421
await connection.cursor().execute("select*")
419-
ut.assert_called_once_with([("MyConnector", "1.0")])
422+
ut.assert_called_once_with([("DriverA", "1.1")], [("MyConnector", "1.0")])
420423

421424

422425
@mark.asyncio
@@ -442,4 +445,4 @@ async def test_connect_no_user_agent(
442445
api_endpoint=settings.server,
443446
) as connection:
444447
await connection.cursor().execute("select*")
445-
ut.assert_called_once_with([])
448+
ut.assert_called_once_with([], [])

tests/unit/utils/test_usage_tracker.py

Lines changed: 47 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@
55
from pytest import mark, raises
66

77
from firebolt.utils.usage_tracker import (
8+
CLIENT_MAP,
9+
DRIVER_MAP,
810
detect_connectors,
911
get_sdk_properties,
1012
get_user_agent_header,
@@ -48,13 +50,14 @@ def test_get_sdk_properties():
4850
},
4951
)
5052
@mark.parametrize(
51-
"stack,expected",
53+
"stack,map,expected",
5254
[
5355
(
5456
[
5557
StackItem("create_connection", "dir1/dir2/firebolt_cli/utils.py"),
5658
StackItem("dummy", "dummy.py"),
5759
],
60+
CLIENT_MAP,
5861
{"FireboltCLI": "0.1.1"},
5962
),
6063
(
@@ -64,22 +67,27 @@ def test_get_sdk_properties():
6467
"my_documents/some_other_dir/firebolt_cli/utils.py",
6568
)
6669
],
70+
CLIENT_MAP,
6771
{"FireboltCLI": "0.1.1"},
6872
),
6973
(
7074
[StackItem("connect", "sqlalchemy/engine/default.py")],
75+
DRIVER_MAP,
7176
{"SQLAlchemy": "0.1.2"},
7277
),
7378
(
7479
[StackItem("establish_connection", "source_firebolt/source.py")],
80+
CLIENT_MAP,
7581
{"AirbyteSource": ""},
7682
),
7783
(
7884
[StackItem("establish_async_connection", "source_firebolt/source.py")],
85+
CLIENT_MAP,
7986
{"AirbyteSource": ""},
8087
),
8188
(
8289
[StackItem("establish_connection", "destination_firebolt/destination.py")],
90+
CLIENT_MAP,
8391
{"AirbyteDestination": ""},
8492
),
8593
(
@@ -88,60 +96,85 @@ def test_get_sdk_properties():
8896
"establish_async_connection", "destination_firebolt/destination.py"
8997
)
9098
],
99+
CLIENT_MAP,
91100
{"AirbyteDestination": ""},
92101
),
93102
(
94103
[StackItem("get_conn", "firebolt_provider/hooks/firebolt.py")],
104+
CLIENT_MAP,
95105
{"Airflow": "0.1.3"},
96106
),
97-
([StackItem("open", "dbt/adapters/firebolt/connections.py")], {"DBT": "0.1.4"}),
107+
(
108+
[StackItem("open", "dbt/adapters/firebolt/connections.py")],
109+
DRIVER_MAP,
110+
{"DBT": "0.1.4"},
111+
),
112+
(
113+
[StackItem("open", "dbt/adapters/firebolt/connections.py")],
114+
CLIENT_MAP,
115+
{},
116+
),
98117
],
99118
)
100-
def test_detect_connectors(stack, expected):
119+
def test_detect_connectors(stack, map, expected):
101120
with patch(
102121
"firebolt.utils.usage_tracker.inspect.stack", MagicMock(return_value=stack)
103122
):
104-
assert detect_connectors() == expected
123+
assert detect_connectors(map) == expected
105124

106125

107126
@mark.parametrize(
108-
"connectors,expected_string",
127+
"drivers,clients,expected_string",
109128
[
110-
([], "PythonSDK/2 (Python 1; Win; ciso)"),
129+
([], [], "PythonSDK/2 (Python 1; Win; ciso)"),
111130
(
112131
[("ConnectorA", "0.1.1")],
132+
[],
113133
"PythonSDK/2 (Python 1; Win; ciso) ConnectorA/0.1.1",
114134
),
115135
(
116136
(("ConnectorA", "0.1.1"), ("ConnectorB", "0.2.0")),
137+
(),
117138
"PythonSDK/2 (Python 1; Win; ciso) ConnectorA/0.1.1 ConnectorB/0.2.0",
118139
),
119140
(
120141
[("ConnectorA", "0.1.1"), ("ConnectorB", "0.2.0")],
142+
[],
121143
"PythonSDK/2 (Python 1; Win; ciso) ConnectorA/0.1.1 ConnectorB/0.2.0",
122144
),
145+
(
146+
[("ConnectorA", "0.1.1"), ("ConnectorB", "0.2.0")],
147+
[("ClientA", "1.0.1")],
148+
"ClientA/1.0.1 PythonSDK/2 (Python 1; Win; ciso) ConnectorA/0.1.1 ConnectorB/0.2.0",
149+
),
123150
],
124151
)
125152
@patch(
126153
"firebolt.utils.usage_tracker.get_sdk_properties",
127154
MagicMock(return_value=("1", "2", "Win", "ciso")),
128155
)
129-
def test_user_agent(connectors, expected_string):
130-
assert get_user_agent_header(connectors) == expected_string
156+
def test_user_agent(drivers, clients, expected_string):
157+
assert get_user_agent_header(drivers, clients) == expected_string
131158

132159

133160
@mark.parametrize(
134-
"connectors",
161+
"drivers,clients",
135162
[
136-
([1]),
137-
((("Con1", "v1.1"), ("Con2"))),
138-
(("Connector1.1")),
163+
([1], []),
164+
((("Con1", "v1.1"), ("Con2")), []),
165+
(("Connector1.1"), ()),
166+
(
167+
[],
168+
[1],
169+
),
170+
([], (("Con1", "v1.1"), ("Con2"))),
171+
((), ("Connector1.1")),
139172
],
140173
)
141174
@patch(
142175
"firebolt.utils.usage_tracker.get_sdk_properties",
143176
MagicMock(return_value=("1", "2", "Win", "ciso")),
144177
)
145-
def test_incorrect_user_agent(connectors):
178+
def test_incorrect_user_agent(drivers, clients):
146179
with raises(ValidationError):
147-
get_user_agent_header(connectors)
180+
get_user_agent_header(drivers, clients)

0 commit comments

Comments
 (0)