Skip to content

Commit f9cdd44

Browse files
authored
Attribute namespace for tags, measurements (#3448)
1 parent 29b7fa8 commit f9cdd44

File tree

4 files changed

+86
-32
lines changed

4 files changed

+86
-32
lines changed

sentry_sdk/integrations/opentelemetry/consts.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@
1414

1515

1616
class SentrySpanAttribute:
17-
# XXX better name
1817
# XXX not all of these need separate attributes, we might just use
1918
# existing otel attrs for some
2019
DESCRIPTION = "sentry.description"
2120
OP = "sentry.op"
2221
ORIGIN = "sentry.origin"
22+
MEASUREMENT = "sentry.measurement"
23+
TAG = "sentry.tag"

sentry_sdk/integrations/opentelemetry/potel_span_processor.py

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,12 @@
99
from sentry_sdk.integrations.opentelemetry.utils import (
1010
is_sentry_span,
1111
convert_from_otel_timestamp,
12+
extract_span_attributes,
1213
extract_span_data,
1314
)
1415
from sentry_sdk.integrations.opentelemetry.consts import (
1516
OTEL_SENTRY_CONTEXT,
17+
SentrySpanAttribute,
1618
)
1719
from sentry_sdk._types import TYPE_CHECKING
1820

@@ -107,9 +109,9 @@ def _root_span_to_transaction_event(self, span):
107109
# type: (ReadableSpan) -> Optional[Event]
108110
if not span.context:
109111
return None
110-
if not span.start_time:
111-
return None
112-
if not span.end_time:
112+
113+
event = self._common_span_transaction_attributes_as_json(span)
114+
if event is None:
113115
return None
114116

115117
trace_id = format_trace_id(span.context.trace_id)
@@ -135,25 +137,25 @@ def _root_span_to_transaction_event(self, span):
135137
if span.resource.attributes:
136138
contexts[OTEL_SENTRY_CONTEXT] = {"resource": dict(span.resource.attributes)}
137139

138-
event = {
139-
"type": "transaction",
140-
"transaction": description,
141-
# TODO-neel-potel tx source based on integration
142-
"transaction_info": {"source": "custom"},
143-
"contexts": contexts,
144-
"start_timestamp": convert_from_otel_timestamp(span.start_time),
145-
"timestamp": convert_from_otel_timestamp(span.end_time),
146-
} # type: Event
140+
event.update(
141+
{
142+
"type": "transaction",
143+
"transaction": description,
144+
# TODO-neel-potel tx source based on integration
145+
"transaction_info": {"source": "custom"},
146+
"contexts": contexts,
147+
}
148+
) # type: Event
147149

148150
return event
149151

150152
def _span_to_json(self, span):
151153
# type: (ReadableSpan) -> Optional[dict[str, Any]]
152154
if not span.context:
153155
return None
154-
if not span.start_time:
155-
return None
156-
if not span.end_time:
156+
157+
span_json = self._common_span_transaction_attributes_as_json(span)
158+
if span_json is None:
157159
return None
158160

159161
trace_id = format_trace_id(span.context.trace_id)
@@ -162,20 +164,41 @@ def _span_to_json(self, span):
162164

163165
(op, description, status, _, origin) = extract_span_data(span)
164166

165-
span_json = {
166-
"trace_id": trace_id,
167-
"span_id": span_id,
168-
"op": op,
169-
"description": description,
170-
"status": status,
171-
"start_timestamp": convert_from_otel_timestamp(span.start_time),
172-
"timestamp": convert_from_otel_timestamp(span.end_time),
173-
"origin": origin or DEFAULT_SPAN_ORIGIN,
174-
} # type: dict[str, Any]
167+
span_json.update(
168+
{
169+
"trace_id": trace_id,
170+
"span_id": span_id,
171+
"op": op,
172+
"description": description,
173+
"status": status,
174+
"origin": origin or DEFAULT_SPAN_ORIGIN,
175+
}
176+
)
175177

176178
if parent_span_id:
177179
span_json["parent_span_id"] = parent_span_id
180+
178181
if span.attributes:
179182
span_json["data"] = dict(span.attributes)
180183

181184
return span_json
185+
186+
def _common_span_transaction_attributes_as_json(self, span):
187+
# type: (ReadableSpan) -> Optional[dict[str, Any]]
188+
if not span.start_time or not span.end_time:
189+
return None
190+
191+
common_json = {
192+
"start_timestamp": convert_from_otel_timestamp(span.start_time),
193+
"timestamp": convert_from_otel_timestamp(span.end_time),
194+
} # type: dict[str, Any]
195+
196+
measurements = extract_span_attributes(span, SentrySpanAttribute.MEASUREMENT)
197+
if measurements:
198+
common_json["measurements"] = measurements
199+
200+
tags = extract_span_attributes(span, SentrySpanAttribute.TAG)
201+
if tags:
202+
common_json["tags"] = tags
203+
204+
return common_json

sentry_sdk/integrations/opentelemetry/utils.py

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from sentry_sdk._types import TYPE_CHECKING
1515

1616
if TYPE_CHECKING:
17-
from typing import Optional, Mapping, Sequence, Union
17+
from typing import Any, Optional, Mapping, Sequence, Union
1818

1919

2020
GRPC_ERROR_MAP = {
@@ -238,3 +238,25 @@ def get_http_status_code(span_attributes):
238238
http_status = cast("Optional[int]", http_status)
239239

240240
return http_status
241+
242+
243+
def extract_span_attributes(span, namespace):
244+
# type: (ReadableSpan, str) -> dict[str, Any]
245+
"""
246+
Extract Sentry-specific span attributes and make them look the way Sentry expects.
247+
"""
248+
extracted_attrs = {}
249+
250+
for attr, value in (span.attributes or {}).items():
251+
if attr.startswith(namespace):
252+
key = attr[len(namespace) + 1 :]
253+
254+
if namespace == SentrySpanAttribute.MEASUREMENT:
255+
value = {
256+
"value": float(value[0]),
257+
"unit": value[1],
258+
}
259+
260+
extracted_attrs[key] = value
261+
262+
return extracted_attrs

sentry_sdk/tracing.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1466,9 +1466,15 @@ def to_baggage(self):
14661466

14671467
def set_tag(self, key, value):
14681468
# type: (str, Any) -> None
1469-
pass
1469+
from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute
1470+
1471+
self.set_attribute(f"{SentrySpanAttribute.TAG}.{key}", value)
14701472

14711473
def set_data(self, key, value):
1474+
# type: (str, Any) -> None
1475+
self.set_attribute(key, value)
1476+
1477+
def set_attribute(self, key, value):
14721478
# type: (str, Any) -> None
14731479
self._otel_span.set_attribute(key, value)
14741480

@@ -1485,10 +1491,12 @@ def set_status(self, status):
14851491

14861492
def set_measurement(self, name, value, unit=""):
14871493
# type: (str, float, MeasurementUnit) -> None
1488-
# XXX own namespace, e.g. sentry.measurement.xxx, so that we can group
1489-
# these back together in the processor?
1490-
# XXX otel throws a warning about value, unit being different types
1491-
self._otel_span.set_attribute(name, (value, unit))
1494+
from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute
1495+
1496+
# Stringify value here since OTel expects all seq items to be of one type
1497+
self.set_attribute(
1498+
f"{SentrySpanAttribute.MEASUREMENT}.{name}", (str(value), unit)
1499+
)
14921500

14931501
def set_thread(self, thread_id, thread_name):
14941502
# type: (Optional[int], Optional[str]) -> None

0 commit comments

Comments
 (0)