Skip to content

Commit b4b0f1e

Browse files
SNOW-2129434: Add in-band ocsp exception telemetry (#2406)
1 parent 7722acc commit b4b0f1e

File tree

7 files changed

+135
-9
lines changed

7 files changed

+135
-9
lines changed

DESCRIPTION.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ https://docs.snowflake.com/
77
Source code is also available at: https://github.com/snowflakedb/snowflake-connector-python
88

99
# Release Notes
10+
- v3.16.1(TBD)
11+
- Added in-band OCSP exception telemetry.
12+
1013
- v3.16.0(July 04,2025)
1114
- Bumped numpy dependency from <2.1.0 to <=2.2.4.
1215
- Added Windows support for Python 3.13.

src/snowflake/connector/errors.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ def __init__(
3636
done_format_msg: bool | None = None,
3737
connection: SnowflakeConnection | None = None,
3838
cursor: SnowflakeCursor | None = None,
39+
errtype: TelemetryField = TelemetryField.SQL_EXCEPTION,
40+
send_telemetry: bool = True,
3941
) -> None:
4042
super().__init__(msg)
4143
self.msg = msg
@@ -44,6 +46,8 @@ def __init__(
4446
self.sqlstate = sqlstate or "n/a"
4547
self.sfqid = sfqid
4648
self.query = query
49+
self.errtype = errtype
50+
self.send_telemetry = send_telemetry
4751

4852
if self.msg:
4953
# TODO: If there's a message then check to see if errno (and maybe sqlstate)
@@ -74,7 +78,9 @@ def __init__(
7478

7579
# We want to skip the last frame/line in the traceback since it is the current frame
7680
self.telemetry_traceback = self.generate_telemetry_stacktrace()
77-
self.exception_telemetry(msg, cursor, connection)
81+
82+
if self.send_telemetry:
83+
self.exception_telemetry(msg, cursor, connection)
7884

7985
def __repr__(self) -> str:
8086
return self.__str__()
@@ -131,6 +137,8 @@ def generate_telemetry_exception_data(
131137
telemetry_data_dict[TelemetryField.KEY_REASON.value] = telemetry_msg
132138
if self.errno:
133139
telemetry_data_dict[TelemetryField.KEY_ERROR_NUMBER.value] = str(self.errno)
140+
if self.msg:
141+
telemetry_data_dict[TelemetryField.KEY_ERROR_MESSAGE.value] = self.msg
134142

135143
return telemetry_data_dict
136144

@@ -147,9 +155,7 @@ def send_exception_telemetry(
147155
and not connection._telemetry.is_closed
148156
):
149157
# Send with in-band telemetry
150-
telemetry_data[TelemetryField.KEY_TYPE.value] = (
151-
TelemetryField.SQL_EXCEPTION.value
152-
)
158+
telemetry_data[TelemetryField.KEY_TYPE.value] = self.errtype.value
153159
telemetry_data[TelemetryField.KEY_SOURCE.value] = connection.application
154160
telemetry_data[TelemetryField.KEY_EXCEPTION.value] = self.__class__.__name__
155161
ts = get_time_millis()
@@ -415,9 +421,14 @@ def telemetry_msg(self) -> str:
415421
class RevocationCheckError(OperationalError):
416422
"""Exception for errors during certificate revocation check."""
417423

418-
# We already send OCSP exception events
419-
def exception_telemetry(self, msg, cursor, connection) -> None:
420-
pass
424+
def __init__(self, **kwargs) -> None:
425+
send_telemetry = kwargs.pop("send_telemetry", False)
426+
Error.__init__(
427+
self,
428+
errtype=TelemetryField.OCSP_EXCEPTION,
429+
send_telemetry=send_telemetry,
430+
**kwargs,
431+
)
421432

422433

423434
# internal errors

src/snowflake/connector/network.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@
8181
OtherHTTPRetryableError,
8282
ProgrammingError,
8383
RefreshTokenError,
84+
RevocationCheckError,
8485
ServiceUnavailableError,
8586
TooManyRequests,
8687
)
@@ -991,6 +992,9 @@ def _request_exec_wrapper(
991992
raise RetryRequest(err_msg)
992993
self._handle_unknown_error(method, full_url, headers, data, conn)
993994
return {}
995+
except RevocationCheckError as rce:
996+
rce.exception_telemetry(rce.msg, None, self._connection)
997+
raise rce
994998
except RetryRequest as e:
995999
cause = e.args[0]
9961000
if no_retry:

src/snowflake/connector/ocsp_snowflake.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,7 @@ def deserialize_exception(exception_dict: dict | None) -> Exception | None:
138138
f" the original error error class and message are {exc_class} and {exception_dict['msg']}"
139139
)
140140
return RevocationCheckError(
141-
f"Got error {str(deserialize_exc)} while deserializing ocsp cache, please try "
141+
msg=f"Got error {str(deserialize_exc)} while deserializing ocsp cache, please try "
142142
f"cleaning up the "
143143
f"OCSP cache under directory {OCSP_RESPONSE_VALIDATION_CACHE.file_path}",
144144
errno=ER_OCSP_RESPONSE_LOAD_FAILURE,

src/snowflake/connector/telemetry.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ class TelemetryField(Enum):
2525
TIME_DOWNLOADING_CHUNKS = "client_time_downloading_chunks"
2626
TIME_PARSING_CHUNKS = "client_time_parsing_chunks"
2727
SQL_EXCEPTION = "client_sql_exception"
28+
OCSP_EXCEPTION = "client_ocsp_exception"
2829
GET_PARTITIONS_USED = "client_get_partitions_used"
2930
EMPTY_SEQ_INTERPOLATION = "client_pyformat_empty_seq_interpolation"
3031
# fetch_pandas_* usage

test/unit/test_telemetry.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,17 @@
11
#!/usr/bin/env python
22
from __future__ import annotations
33

4+
from unittest import mock
45
from unittest.mock import Mock
56

7+
import pytest
8+
69
import snowflake.connector.telemetry
710
from snowflake.connector.description import CLIENT_NAME, SNOWFLAKE_CONNECTOR_VERSION
11+
from src.snowflake.connector.errorcode import ER_OCSP_RESPONSE_UNAVAILABLE
12+
from src.snowflake.connector.errors import RevocationCheckError
13+
from src.snowflake.connector.network import SnowflakeRestful
14+
from src.snowflake.connector.telemetry import TelemetryData, TelemetryField
815

916

1017
def test_telemetry_data_to_dict():
@@ -235,3 +242,103 @@ def test_generate_telemetry_data():
235242
}
236243
and telemetry_data.timestamp == 123
237244
)
245+
246+
247+
def test_raising_error_generates_telemetry_event_when_connection_is_present():
248+
mock_connection = get_mocked_telemetry_connection()
249+
250+
with pytest.raises(RevocationCheckError):
251+
raise RevocationCheckError(
252+
msg="Response unavailable",
253+
errno=ER_OCSP_RESPONSE_UNAVAILABLE,
254+
connection=mock_connection,
255+
send_telemetry=True,
256+
)
257+
258+
mock_connection._log_telemetry.assert_called_once()
259+
assert_telemetry_data_for_revocation_check_error(
260+
mock_connection._log_telemetry.call_args[0][0]
261+
)
262+
263+
264+
def test_raising_error_with_send_telemetry_off_does_not_generate_telemetry_event_when_connection_is_present():
265+
mock_connection = get_mocked_telemetry_connection()
266+
267+
with pytest.raises(RevocationCheckError):
268+
raise RevocationCheckError(
269+
msg="Response unavailable",
270+
errno=ER_OCSP_RESPONSE_UNAVAILABLE,
271+
connection=mock_connection,
272+
send_telemetry=False,
273+
)
274+
275+
mock_connection._log_telemetry.assert_not_called()
276+
277+
278+
def test_request_throws_revocation_check_error():
279+
retry_ctx = Mock()
280+
retry_ctx.current_retry_count = 0
281+
retry_ctx.timeout = 10
282+
retry_ctx.add_retry_params.return_value = "https://example.com"
283+
284+
mock_connection = get_mocked_telemetry_connection()
285+
286+
with mock.patch.object(SnowflakeRestful, "_request_exec") as _request_exec_mocked:
287+
_request_exec_mocked.side_effect = RevocationCheckError(
288+
msg="Response unavailable", errno=ER_OCSP_RESPONSE_UNAVAILABLE
289+
)
290+
mock_restful = SnowflakeRestful(connection=mock_connection)
291+
with pytest.raises(RevocationCheckError):
292+
mock_restful._request_exec_wrapper(
293+
None,
294+
None,
295+
None,
296+
None,
297+
None,
298+
retry_ctx,
299+
)
300+
mock_restful._connection._log_telemetry.assert_called_once()
301+
assert_telemetry_data_for_revocation_check_error(
302+
mock_connection._log_telemetry.call_args[0][0]
303+
)
304+
305+
306+
def get_mocked_telemetry_connection(telemetry_enabled: bool = True) -> Mock:
307+
mock_connection = Mock()
308+
mock_connection.application = "test_application"
309+
mock_connection.telemetry_enabled = telemetry_enabled
310+
mock_connection.is_closed = False
311+
312+
mock_connection._log_telemetry = Mock()
313+
314+
mock_telemetry = Mock()
315+
mock_telemetry.is_closed = False
316+
mock_connection._telemetry = mock_telemetry
317+
318+
return mock_connection
319+
320+
321+
def assert_telemetry_data_for_revocation_check_error(telemetry_data: TelemetryData):
322+
assert telemetry_data.message[TelemetryField.KEY_DRIVER_TYPE.value] == CLIENT_NAME
323+
assert (
324+
telemetry_data.message[TelemetryField.KEY_DRIVER_VERSION.value]
325+
== SNOWFLAKE_CONNECTOR_VERSION
326+
)
327+
assert telemetry_data.message[TelemetryField.KEY_SOURCE.value] == "test_application"
328+
assert (
329+
telemetry_data.message[TelemetryField.KEY_TYPE.value]
330+
== TelemetryField.OCSP_EXCEPTION.value
331+
)
332+
assert telemetry_data.message[TelemetryField.KEY_ERROR_NUMBER.value] == str(
333+
ER_OCSP_RESPONSE_UNAVAILABLE
334+
)
335+
assert (
336+
telemetry_data.message[TelemetryField.KEY_EXCEPTION.value]
337+
== "RevocationCheckError"
338+
)
339+
assert (
340+
"Response unavailable"
341+
in telemetry_data.message[TelemetryField.KEY_ERROR_MESSAGE.value]
342+
)
343+
assert TelemetryField.KEY_STACKTRACE.value in telemetry_data.message
344+
assert TelemetryField.KEY_REASON.value in telemetry_data.message

test/unit/test_telemetry_oob.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
TEST_RACE_CONDITION_THREAD_COUNT = 2
1111
TEST_RACE_CONDITION_DELAY_SECONDS = 1
1212
telemetry_data = {}
13-
exception = RevocationCheckError("Test OCSP Revocation error")
13+
exception = RevocationCheckError(msg="Test OCSP Revocation error")
1414
event_type = "Test OCSP Exception"
1515
stack_trace = [
1616
"Traceback (most recent call last):\n",

0 commit comments

Comments
 (0)