Skip to content

Commit acf7746

Browse files
authored
Serialize span attrs properly (#3668)
1 parent b46faea commit acf7746

File tree

7 files changed

+100
-38
lines changed

7 files changed

+100
-38
lines changed

sentry_sdk/integrations/asyncpg.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from sentry_sdk.tracing import Span
99
from sentry_sdk.tracing_utils import add_query_source, record_sql_queries
1010
from sentry_sdk.utils import (
11+
_serialize_span_attribute,
1112
ensure_integration_enabled,
1213
parse_version,
1314
capture_internal_exceptions,
@@ -147,7 +148,7 @@ def _inner(*args: Any, **kwargs: Any) -> T: # noqa: N807
147148
) as span:
148149
_set_db_data(span, args[0])
149150
res = f(*args, **kwargs)
150-
span.set_attribute("db.cursor", str(res))
151+
span.set_attribute("db.cursor", _serialize_span_attribute(res))
151152

152153
return res
153154

sentry_sdk/integrations/clickhouse_driver.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,11 @@
33
from sentry_sdk.integrations import Integration, DidNotEnable
44
from sentry_sdk.tracing import Span
55
from sentry_sdk.scope import should_send_default_pii
6-
from sentry_sdk.utils import capture_internal_exceptions, ensure_integration_enabled
6+
from sentry_sdk.utils import (
7+
_serialize_span_attribute,
8+
capture_internal_exceptions,
9+
ensure_integration_enabled,
10+
)
711

812
from typing import TYPE_CHECKING, TypeVar
913

@@ -99,7 +103,7 @@ def _inner(*args: P.args, **kwargs: P.kwargs) -> T:
99103

100104
if params and should_send_default_pii():
101105
connection._sentry_db_params = params
102-
span.set_attribute("db.params", str(params))
106+
span.set_attribute("db.params", _serialize_span_attribute(params))
103107

104108
# run the original code
105109
ret = f(*args, **kwargs)
@@ -117,7 +121,7 @@ def _inner_end(*args: P.args, **kwargs: P.kwargs) -> T:
117121

118122
if span is not None:
119123
if res is not None and should_send_default_pii():
120-
span.set_attribute("db.result", str(res))
124+
span.set_attribute("db.result", _serialize_span_attribute(res))
121125

122126
with capture_internal_exceptions():
123127
query = span.get_attribute("db.query.text")
@@ -159,7 +163,7 @@ def _inner_send_data(*args: P.args, **kwargs: P.kwargs) -> T:
159163
getattr(instance.connection, "_sentry_db_params", None) or []
160164
)
161165
db_params.extend(data)
162-
span.set_attribute("db.params", str(db_params))
166+
span.set_attribute("db.params", _serialize_span_attribute(db_params))
163167
try:
164168
del instance.connection._sentry_db_params
165169
except AttributeError:

sentry_sdk/tracing_utils.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@
33
import os
44
import re
55
import sys
6+
import uuid
67
from collections.abc import Mapping
78
from datetime import datetime, timedelta, timezone
89
from functools import wraps
910
from urllib.parse import quote, unquote
10-
import uuid
1111

1212
import sentry_sdk
1313
from sentry_sdk.consts import OP, SPANDATA
@@ -23,6 +23,7 @@
2323
_is_external_source,
2424
_is_in_project_root,
2525
_module_in_list,
26+
_serialize_span_attribute,
2627
)
2728

2829
from typing import TYPE_CHECKING
@@ -133,13 +134,13 @@ def record_sql_queries(
133134

134135
data = {}
135136
if params_list is not None:
136-
data["db.params"] = str(params_list)
137+
data["db.params"] = _serialize_span_attribute(params_list)
137138
if paramstyle is not None:
138-
data["db.paramstyle"] = str(paramstyle)
139+
data["db.paramstyle"] = _serialize_span_attribute(paramstyle)
139140
if executemany:
140141
data["db.executemany"] = True
141142
if record_cursor_repr and cursor is not None:
142-
data["db.cursor"] = str(cursor)
143+
data["db.cursor"] = _serialize_span_attribute(cursor)
143144

144145
with capture_internal_exceptions():
145146
sentry_sdk.add_breadcrumb(message=query, category="query", data=data)

sentry_sdk/utils.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@
5151
)
5252

5353
from gevent.hub import Hub as GeventHub
54+
from opentelemetry.util.types import AttributeValue
5455

5556
from sentry_sdk._types import Event, ExcInfo
5657

@@ -1831,3 +1832,28 @@ def get_current_thread_meta(thread=None):
18311832

18321833
# we've tried everything, time to give up
18331834
return None, None
1835+
1836+
1837+
def _serialize_span_attribute(value):
1838+
# type: (Any) -> Optional[AttributeValue]
1839+
"""Serialize an object so that it's OTel-compatible and displays nicely in Sentry."""
1840+
# check for allowed primitives
1841+
if isinstance(value, (int, str, float, bool)):
1842+
return value
1843+
1844+
# lists are allowed too, as long as they don't mix types
1845+
if isinstance(value, (list, tuple)):
1846+
for type_ in (int, str, float, bool):
1847+
if all(isinstance(item, type_) for item in value):
1848+
return list(value)
1849+
1850+
# if this is anything else, just try to coerce to string
1851+
# we prefer json.dumps since this makes things like dictionaries display
1852+
# nicely in the UI
1853+
try:
1854+
return json.dumps(value)
1855+
except TypeError:
1856+
try:
1857+
return str(value)
1858+
except Exception:
1859+
return None

tests/integrations/clickhouse_driver/test_clickhouse_driver.py

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@
1616
if clickhouse_driver.VERSION < (0, 2, 6):
1717
EXPECT_PARAMS_IN_SELECT = False
1818

19-
PARAMS_SERIALIZER = str
20-
2119

2220
def test_clickhouse_client_breadcrumbs(sentry_init, capture_events) -> None:
2321
sentry_init(
@@ -144,7 +142,7 @@ def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) ->
144142
"db.user": "default",
145143
"server.address": "localhost",
146144
"server.port": 9000,
147-
"db.result": str([]),
145+
"db.result": [],
148146
},
149147
"message": "DROP TABLE IF EXISTS test",
150148
"type": "default",
@@ -157,7 +155,7 @@ def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) ->
157155
"db.user": "default",
158156
"server.address": "localhost",
159157
"server.port": 9000,
160-
"db.result": str([]),
158+
"db.result": [],
161159
},
162160
"message": "CREATE TABLE test (x Int32) ENGINE = Memory",
163161
"type": "default",
@@ -170,7 +168,7 @@ def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) ->
170168
"db.user": "default",
171169
"server.address": "localhost",
172170
"server.port": 9000,
173-
"db.params": PARAMS_SERIALIZER([{"x": 100}]),
171+
"db.params": '[{"x": 100}]',
174172
},
175173
"message": "INSERT INTO test (x) VALUES",
176174
"type": "default",
@@ -183,7 +181,7 @@ def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) ->
183181
"db.user": "default",
184182
"server.address": "localhost",
185183
"server.port": 9000,
186-
"db.params": PARAMS_SERIALIZER([[170], [200]]),
184+
"db.params": "[[170], [200]]",
187185
},
188186
"message": "INSERT INTO test (x) VALUES",
189187
"type": "default",
@@ -196,8 +194,8 @@ def test_clickhouse_client_breadcrumbs_with_pii(sentry_init, capture_events) ->
196194
"db.user": "default",
197195
"server.address": "localhost",
198196
"server.port": 9000,
199-
"db.result": str([[370]]),
200-
"db.params": PARAMS_SERIALIZER({"minv": 150}),
197+
"db.result": "[[370]]",
198+
"db.params": '{"minv": 150}',
201199
},
202200
"message": "SELECT sum(x) FROM test WHERE x > 150",
203201
"type": "default",
@@ -396,7 +394,7 @@ def test_clickhouse_client_spans_with_pii(
396394
"server.address": "localhost",
397395
"server.port": 9000,
398396
"db.query.text": "DROP TABLE IF EXISTS test",
399-
"db.result": str([]),
397+
"db.result": [],
400398
},
401399
"trace_id": transaction_trace_id,
402400
"parent_span_id": transaction_span_id,
@@ -413,7 +411,7 @@ def test_clickhouse_client_spans_with_pii(
413411
"db.name": "",
414412
"db.user": "default",
415413
"db.query.text": "CREATE TABLE test (x Int32) ENGINE = Memory",
416-
"db.result": str([]),
414+
"db.result": [],
417415
"server.address": "localhost",
418416
"server.port": 9000,
419417
},
@@ -432,7 +430,7 @@ def test_clickhouse_client_spans_with_pii(
432430
"db.name": "",
433431
"db.user": "default",
434432
"db.query.text": "INSERT INTO test (x) VALUES",
435-
"db.params": PARAMS_SERIALIZER([{"x": 100}]),
433+
"db.params": '[{"x": 100}]',
436434
"server.address": "localhost",
437435
"server.port": 9000,
438436
},
@@ -468,9 +466,9 @@ def test_clickhouse_client_spans_with_pii(
468466
"db.system": "clickhouse",
469467
"db.name": "",
470468
"db.user": "default",
471-
"db.params": PARAMS_SERIALIZER({"minv": 150}),
469+
"db.params": '{"minv": 150}',
472470
"db.query.text": "SELECT sum(x) FROM test WHERE x > 150",
473-
"db.result": str([(370,)]),
471+
"db.result": "[[370]]",
474472
"server.address": "localhost",
475473
"server.port": 9000,
476474
},
@@ -622,7 +620,7 @@ def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> N
622620
"db.user": "default",
623621
"server.address": "localhost",
624622
"server.port": 9000,
625-
"db.result": str([[], []]),
623+
"db.result": "[[], []]",
626624
},
627625
"message": "DROP TABLE IF EXISTS test",
628626
"type": "default",
@@ -635,7 +633,7 @@ def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> N
635633
"db.user": "default",
636634
"server.address": "localhost",
637635
"server.port": 9000,
638-
"db.result": str([[], []]),
636+
"db.result": "[[], []]",
639637
},
640638
"message": "CREATE TABLE test (x Int32) ENGINE = Memory",
641639
"type": "default",
@@ -648,7 +646,7 @@ def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> N
648646
"db.user": "default",
649647
"server.address": "localhost",
650648
"server.port": 9000,
651-
"db.params": PARAMS_SERIALIZER([{"x": 100}]),
649+
"db.params": '[{"x": 100}]',
652650
},
653651
"message": "INSERT INTO test (x) VALUES",
654652
"type": "default",
@@ -661,7 +659,7 @@ def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> N
661659
"db.user": "default",
662660
"server.address": "localhost",
663661
"server.port": 9000,
664-
"db.params": PARAMS_SERIALIZER([[170], [200]]),
662+
"db.params": "[[170], [200]]",
665663
},
666664
"message": "INSERT INTO test (x) VALUES",
667665
"type": "default",
@@ -674,8 +672,8 @@ def test_clickhouse_dbapi_breadcrumbs_with_pii(sentry_init, capture_events) -> N
674672
"db.user": "default",
675673
"server.address": "localhost",
676674
"server.port": 9000,
677-
"db.params": PARAMS_SERIALIZER({"minv": 150}),
678-
"db.result": str([[["370"]], [["'sum(x)'", "'Int64'"]]]),
675+
"db.params": '{"minv": 150}',
676+
"db.result": '[[["370"]], [["\'sum(x)\'", "\'Int64\'"]]]',
679677
},
680678
"message": "SELECT sum(x) FROM test WHERE x > 150",
681679
"type": "default",
@@ -869,7 +867,7 @@ def test_clickhouse_dbapi_spans_with_pii(
869867
"db.name": "",
870868
"db.user": "default",
871869
"db.query.text": "DROP TABLE IF EXISTS test",
872-
"db.result": str(([], [])),
870+
"db.result": "[[], []]",
873871
"server.address": "localhost",
874872
"server.port": 9000,
875873
},
@@ -888,7 +886,7 @@ def test_clickhouse_dbapi_spans_with_pii(
888886
"db.name": "",
889887
"db.user": "default",
890888
"db.query.text": "CREATE TABLE test (x Int32) ENGINE = Memory",
891-
"db.result": str(([], [])),
889+
"db.result": "[[], []]",
892890
"server.address": "localhost",
893891
"server.port": 9000,
894892
},
@@ -907,7 +905,7 @@ def test_clickhouse_dbapi_spans_with_pii(
907905
"db.name": "",
908906
"db.user": "default",
909907
"db.query.text": "INSERT INTO test (x) VALUES",
910-
"db.params": PARAMS_SERIALIZER([{"x": 100}]),
908+
"db.params": '[{"x": 100}]',
911909
"server.address": "localhost",
912910
"server.port": 9000,
913911
},
@@ -926,7 +924,7 @@ def test_clickhouse_dbapi_spans_with_pii(
926924
"db.name": "",
927925
"db.user": "default",
928926
"db.query.text": "INSERT INTO test (x) VALUES",
929-
"db.params": PARAMS_SERIALIZER([[170], [200]]),
927+
"db.params": "[[170], [200]]",
930928
"server.address": "localhost",
931929
"server.port": 9000,
932930
},
@@ -945,8 +943,8 @@ def test_clickhouse_dbapi_spans_with_pii(
945943
"db.name": "",
946944
"db.user": "default",
947945
"db.query.text": "SELECT sum(x) FROM test WHERE x > 150",
948-
"db.params": PARAMS_SERIALIZER({"minv": 150}),
949-
"db.result": str(([(370,)], [("sum(x)", "Int64")])),
946+
"db.params": '{"minv": 150}',
947+
"db.result": '[[[370]], [["sum(x)", "Int64"]]]',
950948
"server.address": "localhost",
951949
"server.port": 9000,
952950
},

tests/integrations/django/test_basic.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -376,7 +376,7 @@ def test_sql_queries(sentry_init, capture_events, with_integration):
376376
crumb = event["breadcrumbs"]["values"][-1]
377377

378378
assert crumb["message"] == "SELECT count(*) FROM people_person WHERE foo = %s"
379-
assert crumb["data"]["db.params"] == "[123]"
379+
assert crumb["data"]["db.params"] == [123]
380380

381381

382382
@pytest.mark.forked
@@ -411,7 +411,7 @@ def test_sql_dict_query_params(sentry_init, capture_events):
411411
assert crumb["message"] == (
412412
"SELECT count(*) FROM people_person WHERE foo = %(my_foo)s"
413413
)
414-
assert crumb["data"]["db.params"] == str({"my_foo": 10})
414+
assert crumb["data"]["db.params"] == '{"my_foo": 10}'
415415

416416

417417
@pytest.mark.forked
@@ -473,7 +473,7 @@ def test_sql_psycopg2_string_composition(sentry_init, capture_events, query):
473473
(event,) = events
474474
crumb = event["breadcrumbs"]["values"][-1]
475475
assert crumb["message"] == ('SELECT %(my_param)s FROM "foobar"')
476-
assert crumb["data"]["db.params"] == str({"my_param": 10})
476+
assert crumb["data"]["db.params"] == '{"my_param": 10}'
477477

478478

479479
@pytest.mark.forked
@@ -526,7 +526,7 @@ def test_sql_psycopg2_placeholders(sentry_init, capture_events):
526526
{
527527
"category": "query",
528528
"data": {
529-
"db.params": str({"first_var": "fizz", "second_var": "not a date"}),
529+
"db.params": '{"first_var": "fizz", "second_var": "not a date"}',
530530
"db.paramstyle": "format",
531531
},
532532
"message": 'insert into my_test_table ("foo", "bar") values (%(first_var)s, '

tests/test_utils.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
_get_installed_modules,
3131
_generate_installed_modules,
3232
ensure_integration_enabled,
33+
_serialize_span_attribute,
3334
)
3435

3536

@@ -901,3 +902,34 @@ def test_format_timestamp_naive():
901902
# Ensure that some timestamp is returned, without error. We currently treat these as local time, but this is an
902903
# implementation detail which we should not assert here.
903904
assert re.fullmatch(timestamp_regex, format_timestamp(datetime_object))
905+
906+
907+
class NoStr:
908+
def __str__(self):
909+
1 / 0
910+
911+
912+
@pytest.mark.parametrize(
913+
("value", "result"),
914+
(
915+
("meow", "meow"),
916+
(1, 1),
917+
(47.0, 47.0),
918+
(True, True),
919+
(["meow", "bark"], ["meow", "bark"]),
920+
([True, False], [True, False]),
921+
([1, 2, 3], [1, 2, 3]),
922+
([46.5, 47.0, 47.5], [46.5, 47.0, 47.5]),
923+
(["meow", 47], '["meow", 47]'), # mixed types not allowed in a list
924+
(None, "null"),
925+
(
926+
{"cat": "meow", "dog": ["bark", "woof"]},
927+
'{"cat": "meow", "dog": ["bark", "woof"]}',
928+
),
929+
(datetime(2024, 1, 1), "2024-01-01 00:00:00"),
930+
(("meow", "purr"), ["meow", "purr"]),
931+
(NoStr(), None),
932+
),
933+
)
934+
def test_serialize_span_attribute(value, result):
935+
assert _serialize_span_attribute(value) == result

0 commit comments

Comments
 (0)