Skip to content

Commit 2c3f6d5

Browse files
authored
SNOW-645247: Python connector telemetry is partially broken (#1231)
1 parent a384f05 commit 2c3f6d5

File tree

7 files changed

+226
-58
lines changed

7 files changed

+226
-58
lines changed

src/snowflake/connector/cursor.py

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@
6262
from .file_transfer_agent import SnowflakeFileTransferAgent
6363
from .options import installed_pandas, pandas
6464
from .sqlstate import SQLSTATE_FEATURE_NOT_SUPPORTED
65-
from .telemetry import TelemetryData, TelemetryField
65+
from .telemetry import TelemetryData, TelemetryField, generate_telemetry_data
6666
from .time_util import get_time_millis
6767

6868
if TYPE_CHECKING: # pragma: no cover
@@ -1211,15 +1211,23 @@ def _log_telemetry_job_data(
12111211
self, telemetry_field: TelemetryField, value: Any
12121212
) -> None:
12131213
"""Builds an instance of TelemetryData with the given field and logs it."""
1214-
obj = {
1215-
"type": telemetry_field.value,
1216-
"source": self._connection.application if self._connection else CLIENT_NAME,
1217-
"query_id": self._sfqid,
1218-
"value": int(value),
1219-
}
12201214
ts = get_time_millis()
12211215
try:
1222-
self._connection._log_telemetry(TelemetryData(obj, ts))
1216+
self._connection._log_telemetry(
1217+
TelemetryData(
1218+
generate_telemetry_data(
1219+
from_dict={
1220+
TelemetryField.KEY_TYPE.value: telemetry_field.value,
1221+
TelemetryField.KEY_SOURCE.value: self._connection.application
1222+
if self._connection
1223+
else CLIENT_NAME,
1224+
TelemetryField.KEY_SFQID.value: self._sfqid,
1225+
TelemetryField.KEY_VALUE.value: value,
1226+
}
1227+
),
1228+
ts,
1229+
)
1230+
)
12231231
except AttributeError:
12241232
logger.warning(
12251233
"Cursor failed to log to telemetry. Connection object may be None.",

src/snowflake/connector/errors.py

Lines changed: 8 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,8 @@
1313
from typing import TYPE_CHECKING
1414

1515
from .compat import BASE_EXCEPTION_CLASS
16-
from .description import CLIENT_NAME, SNOWFLAKE_CONNECTOR_VERSION
1716
from .secret_detector import SecretDetector
18-
from .telemetry import TelemetryData, TelemetryField
17+
from .telemetry import TelemetryData, TelemetryField, generate_telemetry_data
1918
from .telemetry_oob import TelemetryService
2019
from .time_util import get_time_millis
2120

@@ -121,10 +120,13 @@ def telemetry_msg(self) -> str | None:
121120
def generate_telemetry_exception_data(self) -> dict[str, str]:
122121
"""Generate the data to send through telemetry."""
123122

124-
telemetry_data = {
125-
TelemetryField.KEY_DRIVER_TYPE.value: CLIENT_NAME,
126-
TelemetryField.KEY_DRIVER_VERSION.value: SNOWFLAKE_CONNECTOR_VERSION,
127-
}
123+
telemetry_data = generate_telemetry_data(
124+
from_dict={
125+
TelemetryField.KEY_STACKTRACE.value: SecretDetector.mask_secrets(
126+
self.telemetry_traceback
127+
)
128+
}
129+
)
128130
telemetry_msg = self.telemetry_msg()
129131
if self.sfqid:
130132
telemetry_data[TelemetryField.KEY_SFQID.value] = self.sfqid
@@ -135,10 +137,6 @@ def generate_telemetry_exception_data(self) -> dict[str, str]:
135137
if self.errno:
136138
telemetry_data[TelemetryField.KEY_ERROR_NUMBER.value] = str(self.errno)
137139

138-
telemetry_data[
139-
TelemetryField.KEY_STACKTRACE.value
140-
] = SecretDetector.mask_secrets(self.telemetry_traceback)
141-
142140
return telemetry_data
143141

144142
def send_exception_telemetry(

src/snowflake/connector/ocsp_snowflake.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from os.path import expanduser
2323
from threading import Lock, RLock
2424
from time import gmtime, strftime
25+
from typing import Any
2526

2627
import jwt
2728

@@ -69,6 +70,8 @@
6970

7071
from . import constants
7172
from .cache import SFDictCache, SFDictFileCache
73+
from .telemetry import TelemetryField
74+
from .telemetry import generate_telemetry_data as generate_telemetry_data_base
7275

7376
try:
7477
OCSP_CACHE: SFDictFileCache[
@@ -218,20 +221,26 @@ def set_fail_open(self, fail_open):
218221
def set_insecure_mode(self, insecure_mode):
219222
self.insecure_mode = insecure_mode
220223

221-
def generate_telemetry_data(self, event_type, urgent=False):
224+
def generate_telemetry_data(
225+
self, event_type: str, urgent: bool = False
226+
) -> dict[str, Any]:
222227
_, exception, _ = sys.exc_info()
223-
telemetry_data = {}
224-
telemetry_data.update({"eventType": event_type})
225-
telemetry_data.update({"eventSubType": self.event_sub_type})
226-
telemetry_data.update({"sfcPeerHost": self.sfc_peer_host})
227-
telemetry_data.update({"certId": self.cert_id})
228-
telemetry_data.update({"ocspRequestBase64": self.ocsp_req})
229-
telemetry_data.update({"ocspResponderURL": self.ocsp_url})
230-
telemetry_data.update({"errorMessage": self.error_msg})
231-
telemetry_data.update({"insecureMode": self.insecure_mode})
232-
telemetry_data.update({"failOpen": self.fail_open})
233-
telemetry_data.update({"cacheEnabled": self.cache_enabled})
234-
telemetry_data.update({"cacheHit": self.cache_hit})
228+
telemetry_data = generate_telemetry_data_base(
229+
from_dict={
230+
TelemetryField.KEY_OOB_EVENT_TYPE.value: event_type,
231+
TelemetryField.KEY_OOB_EVENT_SUB_TYPE.value: self.event_sub_type,
232+
TelemetryField.KEY_OOB_SFC_PEER_HOST.value: self.sfc_peer_host,
233+
TelemetryField.KEY_OOB_CERT_ID.value: self.cert_id,
234+
TelemetryField.KEY_OOB_OCSP_REQUEST_BASE64.value: self.ocsp_req,
235+
TelemetryField.KEY_OOB_OCSP_RESPONDER_URL.value: self.ocsp_url,
236+
TelemetryField.KEY_OOB_ERROR_MESSAGE.value: self.error_msg,
237+
TelemetryField.KEY_OOB_INSECURE_MODE.value: self.insecure_mode,
238+
TelemetryField.KEY_OOB_FAIL_OPEN.value: self.fail_open,
239+
TelemetryField.KEY_OOB_CACHE_ENABLED.value: self.cache_enabled,
240+
TelemetryField.KEY_OOB_CACHE_HIT.value: self.cache_hit,
241+
},
242+
is_oob_telemetry=True,
243+
)
235244

236245
telemetry_client = TelemetryService.get_instance()
237246
telemetry_client.log_ocsp_exception(

src/snowflake/connector/telemetry.py

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,9 @@
88
import logging
99
from enum import Enum, unique
1010
from threading import Lock
11-
from typing import TYPE_CHECKING
11+
from typing import TYPE_CHECKING, Any
1212

13+
from .description import CLIENT_NAME, SNOWFLAKE_CONNECTOR_VERSION
1314
from .secret_detector import SecretDetector
1415
from .test_util import ENABLE_TELEMETRY_LOG, rt_plain_logger
1516

@@ -38,14 +39,43 @@ class TelemetryField(Enum):
3839
# Keys for telemetry data sent through either in-band or out-of-band telemetry
3940
KEY_TYPE = "type"
4041
KEY_SOURCE = "source"
41-
KEY_SFQID = "QueryID"
42-
KEY_SQLSTATE = "SQLState"
43-
KEY_DRIVER_TYPE = "DriverType"
44-
KEY_DRIVER_VERSION = "DriverVersion"
42+
KEY_SFQID = "query_id"
43+
KEY_SQLSTATE = "sql_state"
44+
KEY_DRIVER_TYPE = "driver_type"
45+
KEY_DRIVER_VERSION = "driver_version"
4546
KEY_REASON = "reason"
47+
KEY_VALUE = "value"
48+
KEY_EXCEPTION = "exception"
49+
# Reserved UpperCamelName keys
4650
KEY_ERROR_NUMBER = "ErrorNumber"
51+
KEY_ERROR_MESSAGE = "ErrorMessage"
4752
KEY_STACKTRACE = "Stacktrace"
48-
KEY_EXCEPTION = "Exception"
53+
# OOB camelName keys
54+
KEY_OOB_DRIVER = "driver"
55+
KEY_OOB_VERSION = "version"
56+
KEY_OOB_TELEMETRY_SERVER_DEPLOYMENT = "telemetryServerDeployment"
57+
KEY_OOB_CONNECTION_STRING = "connectionString"
58+
KEY_OOB_EXCEPTION_MESSAGE = "exceptionMessage"
59+
KEY_OOB_ERROR_MESSAGE = "errorMessage"
60+
KEY_OOB_EXCEPTION_STACK_TRACE = "exceptionStackTrace"
61+
KEY_OOB_EVENT_TYPE = "eventType"
62+
KEY_OOB_ERROR_CODE = "errorCode"
63+
KEY_OOB_SQL_STATE = "sqlState"
64+
KEY_OOB_REQUEST = "request"
65+
KEY_OOB_RESPONSE = "response"
66+
KEY_OOB_RESPONSE_STATUS_LINE = "responseStatusLine"
67+
KEY_OOB_RESPONSE_STATUS_CODE = "responseStatusCode"
68+
KEY_OOB_RETRY_TIMEOUT = "retryTimeout"
69+
KEY_OOB_RETRY_COUNT = "retryCount"
70+
KEY_OOB_EVENT_SUB_TYPE = "eventSubType"
71+
KEY_OOB_SFC_PEER_HOST = "sfcPeerHost"
72+
KEY_OOB_CERT_ID = "certId"
73+
KEY_OOB_OCSP_REQUEST_BASE64 = "ocspRequestBase64"
74+
KEY_OOB_OCSP_RESPONDER_URL = "ocspResponderURL"
75+
KEY_OOB_INSECURE_MODE = "insecureMode"
76+
KEY_OOB_FAIL_OPEN = "failOpen"
77+
KEY_OOB_CACHE_ENABLED = "cacheEnabled"
78+
KEY_OOB_CACHE_HIT = "cacheHit"
4979

5080

5181
class TelemetryData:
@@ -161,3 +191,25 @@ def is_enabled(self):
161191

162192
def buffer_size(self):
163193
return len(self._log_batch)
194+
195+
196+
def generate_telemetry_data(
197+
from_dict: dict | None = None, is_oob_telemetry: bool = False
198+
) -> dict[str, Any]:
199+
"""
200+
Generate telemetry data with driver info. The method also takes an optional dict to update from.
201+
"""
202+
from_dict = from_dict or {}
203+
return (
204+
{
205+
TelemetryField.KEY_DRIVER_TYPE.value: CLIENT_NAME,
206+
TelemetryField.KEY_DRIVER_VERSION.value: SNOWFLAKE_CONNECTOR_VERSION,
207+
**from_dict,
208+
}
209+
if not is_oob_telemetry
210+
else {
211+
TelemetryField.KEY_OOB_DRIVER.value: CLIENT_NAME,
212+
TelemetryField.KEY_OOB_VERSION.value: SNOWFLAKE_CONNECTOR_VERSION,
213+
**from_dict,
214+
}
215+
)

src/snowflake/connector/telemetry_oob.py

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from .compat import OK
1616
from .description import CLIENT_NAME, SNOWFLAKE_CONNECTOR_VERSION
1717
from .secret_detector import SecretDetector
18+
from .telemetry import TelemetryField, generate_telemetry_data
1819
from .test_util import ENABLE_TELEMETRY_LOG, rt_plain_logger
1920
from .vendored import requests
2021

@@ -122,10 +123,14 @@ def generate_tags(self):
122123

123124
telemetry = TelemetryService.get_instance()
124125
# Add telemetry service generated tags
125-
tags["driver"] = CLIENT_NAME
126-
tags["version"] = str(SNOWFLAKE_CONNECTOR_VERSION)
127-
tags["telemetryServerDeployment"] = telemetry.deployment.name
128-
tags["connectionString"] = telemetry.get_connection_string()
126+
tags[TelemetryField.KEY_OOB_DRIVER] = CLIENT_NAME
127+
tags[TelemetryField.KEY_OOB_VERSION] = str(SNOWFLAKE_CONNECTOR_VERSION)
128+
tags[
129+
TelemetryField.KEY_OOB_TELEMETRY_SERVER_DEPLOYMENT
130+
] = telemetry.deployment.name
131+
tags[
132+
TelemetryField.KEY_OOB_CONNECTION_STRING
133+
] = telemetry.get_connection_string()
129134
if telemetry.context and len(telemetry.context) > 0:
130135
for k, v in telemetry.context.items():
131136
if v is not None:
@@ -339,14 +344,18 @@ def log_ocsp_exception(
339344
if self.enabled:
340345
event_name = "OCSPException"
341346
if exception is not None:
342-
telemetry_data["exceptionMessage"] = str(exception)
347+
telemetry_data[
348+
TelemetryField.KEY_OOB_EXCEPTION_MESSAGE.value
349+
] = str(exception)
343350
if stack_trace is not None:
344-
telemetry_data["exceptionStackTrace"] = stack_trace
351+
telemetry_data[
352+
TelemetryField.KEY_OOB_EXCEPTION_STACK_TRACE.value
353+
] = stack_trace
345354

346355
if tags is None:
347356
tags = dict()
348357

349-
tags["eventType"] = event_type
358+
tags[TelemetryField.KEY_OOB_EVENT_TYPE.value] = event_type
350359

351360
log_event = TelemetryLogEvent(
352361
name=event_name, tags=tags, urgent=urgent, value=telemetry_data
@@ -377,33 +386,53 @@ def log_http_request_error(
377386
tags = dict()
378387
try:
379388
if self.enabled:
380-
telemetry_data = dict()
381389
response_status_code = -1
382390
# This mimics the output of HttpRequestBase.toString() from JBDC
383-
telemetry_data["request"] = f"{method} {url}"
384-
telemetry_data["sqlState"] = sqlstate
385-
telemetry_data["errorCode"] = errno
391+
telemetry_data = generate_telemetry_data(
392+
from_dict={
393+
TelemetryField.KEY_OOB_REQUEST.value: f"{method} {url}",
394+
TelemetryField.KEY_OOB_SQL_STATE.value: sqlstate,
395+
TelemetryField.KEY_OOB_ERROR_CODE.value: errno,
396+
},
397+
is_oob_telemetry=True,
398+
)
386399
if response:
387-
telemetry_data["response"] = response.json()
388-
telemetry_data["responseStatusLine"] = str(response.reason)
400+
telemetry_data[
401+
TelemetryField.KEY_OOB_RESPONSE.value
402+
] = response.json()
403+
telemetry_data[
404+
TelemetryField.KEY_OOB_RESPONSE_STATUS_LINE.value
405+
] = str(response.reason)
389406
if response.status_code:
390407
response_status_code = str(response.status_code)
391-
telemetry_data["responseStatusCode"] = response_status_code
408+
telemetry_data[
409+
TelemetryField.KEY_OOB_RESPONSE_STATUS_CODE.value
410+
] = response_status_code
392411
if retry_timeout:
393-
telemetry_data["retryTimeout"] = str(retry_timeout)
412+
telemetry_data[TelemetryField.KEY_OOB_RETRY_TIMEOUT.value] = str(
413+
retry_timeout
414+
)
394415
if retry_count:
395-
telemetry_data["retryCount"] = str(retry_count)
416+
telemetry_data[TelemetryField.KEY_OOB_RETRY_COUNT.value] = str(
417+
retry_count
418+
)
396419
if exception:
397-
telemetry_data["exceptionMessage"] = str(exception)
420+
telemetry_data[
421+
TelemetryField.KEY_OOB_EXCEPTION_MESSAGE.value
422+
] = str(exception)
398423
if stack_trace:
399-
telemetry_data["exceptionStackTrace"] = stack_trace
424+
telemetry_data[
425+
TelemetryField.KEY_OOB_EXCEPTION_STACK_TRACE.value
426+
] = stack_trace
400427

401428
if tags is None:
402429
tags = dict()
403430

404-
tags["responseStatusCode"] = response_status_code
405-
tags["sqlState"] = str(sqlstate)
406-
tags["errorCode"] = errno
431+
tags[
432+
TelemetryField.KEY_OOB_RESPONSE_STATUS_CODE.value
433+
] = response_status_code
434+
tags[TelemetryField.KEY_OOB_SQL_STATE.value] = str(sqlstate)
435+
tags[TelemetryField.KEY_OOB_ERROR_CODE.value] = errno
407436

408437
log_event = TelemetryLogEvent(
409438
name=event_name, tags=tags, value=telemetry_data, urgent=urgent

test/unit/test_telemetry.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from unittest.mock import Mock
99

1010
import snowflake.connector.telemetry
11+
from snowflake.connector.description import CLIENT_NAME, SNOWFLAKE_CONNECTOR_VERSION
1112

1213

1314
def test_telemetry_data_to_dict():
@@ -132,3 +133,35 @@ def test_telemetry_send_batch_disabled():
132133
client.send_batch()
133134
assert client.buffer_size() == 1
134135
assert rest_call.call_count == 0
136+
137+
138+
def test_generate_telemetry_with_driver_info():
139+
assert snowflake.connector.telemetry.generate_telemetry_data() == {
140+
snowflake.connector.telemetry.TelemetryField.KEY_DRIVER_TYPE.value: CLIENT_NAME,
141+
snowflake.connector.telemetry.TelemetryField.KEY_DRIVER_VERSION.value: SNOWFLAKE_CONNECTOR_VERSION,
142+
}
143+
144+
assert snowflake.connector.telemetry.generate_telemetry_data(from_dict={}) == {
145+
snowflake.connector.telemetry.TelemetryField.KEY_DRIVER_TYPE.value: CLIENT_NAME,
146+
snowflake.connector.telemetry.TelemetryField.KEY_DRIVER_VERSION.value: SNOWFLAKE_CONNECTOR_VERSION,
147+
}
148+
149+
assert snowflake.connector.telemetry.generate_telemetry_data(
150+
from_dict={"key": "value"}
151+
) == {
152+
snowflake.connector.telemetry.TelemetryField.KEY_DRIVER_TYPE.value: CLIENT_NAME,
153+
snowflake.connector.telemetry.TelemetryField.KEY_DRIVER_VERSION.value: SNOWFLAKE_CONNECTOR_VERSION,
154+
"key": "value",
155+
}
156+
157+
assert snowflake.connector.telemetry.generate_telemetry_data(
158+
from_dict={
159+
snowflake.connector.telemetry.TelemetryField.KEY_DRIVER_TYPE.value: "CUSTOM_CLIENT_NAME",
160+
snowflake.connector.telemetry.TelemetryField.KEY_DRIVER_VERSION.value: "1.2.3",
161+
"key": "value",
162+
}
163+
) == {
164+
snowflake.connector.telemetry.TelemetryField.KEY_DRIVER_TYPE.value: "CUSTOM_CLIENT_NAME",
165+
snowflake.connector.telemetry.TelemetryField.KEY_DRIVER_VERSION.value: "1.2.3",
166+
"key": "value",
167+
}

0 commit comments

Comments
 (0)