diff --git a/sentry_sdk/integrations/opentelemetry/consts.py b/sentry_sdk/integrations/opentelemetry/consts.py index 3c5fc61cf6..aca364fd54 100644 --- a/sentry_sdk/integrations/opentelemetry/consts.py +++ b/sentry_sdk/integrations/opentelemetry/consts.py @@ -14,9 +14,10 @@ class SentrySpanAttribute: - # XXX better name # XXX not all of these need separate attributes, we might just use # existing otel attrs for some DESCRIPTION = "sentry.description" OP = "sentry.op" ORIGIN = "sentry.origin" + MEASUREMENT = "sentry.measurement" + TAG = "sentry.tag" diff --git a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py index d90ac7d5e4..8b2a2f4c36 100644 --- a/sentry_sdk/integrations/opentelemetry/potel_span_processor.py +++ b/sentry_sdk/integrations/opentelemetry/potel_span_processor.py @@ -9,10 +9,12 @@ from sentry_sdk.integrations.opentelemetry.utils import ( is_sentry_span, convert_from_otel_timestamp, + extract_span_attributes, extract_span_data, ) from sentry_sdk.integrations.opentelemetry.consts import ( OTEL_SENTRY_CONTEXT, + SentrySpanAttribute, ) from sentry_sdk._types import TYPE_CHECKING @@ -107,9 +109,9 @@ def _root_span_to_transaction_event(self, span): # type: (ReadableSpan) -> Optional[Event] if not span.context: return None - if not span.start_time: - return None - if not span.end_time: + + event = self._common_span_transaction_attributes_as_json(span) + if event is None: return None trace_id = format_trace_id(span.context.trace_id) @@ -135,15 +137,15 @@ def _root_span_to_transaction_event(self, span): if span.resource.attributes: contexts[OTEL_SENTRY_CONTEXT] = {"resource": dict(span.resource.attributes)} - event = { - "type": "transaction", - "transaction": description, - # TODO-neel-potel tx source based on integration - "transaction_info": {"source": "custom"}, - "contexts": contexts, - "start_timestamp": convert_from_otel_timestamp(span.start_time), - "timestamp": convert_from_otel_timestamp(span.end_time), - } # type: Event + event.update( + { + "type": "transaction", + "transaction": description, + # TODO-neel-potel tx source based on integration + "transaction_info": {"source": "custom"}, + "contexts": contexts, + } + ) # type: Event return event @@ -151,9 +153,9 @@ def _span_to_json(self, span): # type: (ReadableSpan) -> Optional[dict[str, Any]] if not span.context: return None - if not span.start_time: - return None - if not span.end_time: + + span_json = self._common_span_transaction_attributes_as_json(span) + if span_json is None: return None trace_id = format_trace_id(span.context.trace_id) @@ -162,20 +164,41 @@ def _span_to_json(self, span): (op, description, status, _, origin) = extract_span_data(span) - span_json = { - "trace_id": trace_id, - "span_id": span_id, - "op": op, - "description": description, - "status": status, - "start_timestamp": convert_from_otel_timestamp(span.start_time), - "timestamp": convert_from_otel_timestamp(span.end_time), - "origin": origin or DEFAULT_SPAN_ORIGIN, - } # type: dict[str, Any] + span_json.update( + { + "trace_id": trace_id, + "span_id": span_id, + "op": op, + "description": description, + "status": status, + "origin": origin or DEFAULT_SPAN_ORIGIN, + } + ) if parent_span_id: span_json["parent_span_id"] = parent_span_id + if span.attributes: span_json["data"] = dict(span.attributes) return span_json + + def _common_span_transaction_attributes_as_json(self, span): + # type: (ReadableSpan) -> Optional[dict[str, Any]] + if not span.start_time or not span.end_time: + return None + + common_json = { + "start_timestamp": convert_from_otel_timestamp(span.start_time), + "timestamp": convert_from_otel_timestamp(span.end_time), + } # type: dict[str, Any] + + measurements = extract_span_attributes(span, SentrySpanAttribute.MEASUREMENT) + if measurements: + common_json["measurements"] = measurements + + tags = extract_span_attributes(span, SentrySpanAttribute.TAG) + if tags: + common_json["tags"] = tags + + return common_json diff --git a/sentry_sdk/integrations/opentelemetry/utils.py b/sentry_sdk/integrations/opentelemetry/utils.py index 2444131002..afa42ea772 100644 --- a/sentry_sdk/integrations/opentelemetry/utils.py +++ b/sentry_sdk/integrations/opentelemetry/utils.py @@ -14,7 +14,7 @@ from sentry_sdk._types import TYPE_CHECKING if TYPE_CHECKING: - from typing import Optional, Mapping, Sequence, Union + from typing import Any, Optional, Mapping, Sequence, Union GRPC_ERROR_MAP = { @@ -238,3 +238,25 @@ def get_http_status_code(span_attributes): http_status = cast("Optional[int]", http_status) return http_status + + +def extract_span_attributes(span, namespace): + # type: (ReadableSpan, str) -> dict[str, Any] + """ + Extract Sentry-specific span attributes and make them look the way Sentry expects. + """ + extracted_attrs = {} + + for attr, value in (span.attributes or {}).items(): + if attr.startswith(namespace): + key = attr[len(namespace) + 1 :] + + if namespace == SentrySpanAttribute.MEASUREMENT: + value = { + "value": float(value[0]), + "unit": value[1], + } + + extracted_attrs[key] = value + + return extracted_attrs diff --git a/sentry_sdk/tracing.py b/sentry_sdk/tracing.py index 308f4774ff..c5812c9864 100644 --- a/sentry_sdk/tracing.py +++ b/sentry_sdk/tracing.py @@ -1466,9 +1466,15 @@ def to_baggage(self): def set_tag(self, key, value): # type: (str, Any) -> None - pass + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + + self.set_attribute(f"{SentrySpanAttribute.TAG}.{key}", value) def set_data(self, key, value): + # type: (str, Any) -> None + self.set_attribute(key, value) + + def set_attribute(self, key, value): # type: (str, Any) -> None self._otel_span.set_attribute(key, value) @@ -1485,10 +1491,12 @@ def set_status(self, status): def set_measurement(self, name, value, unit=""): # type: (str, float, MeasurementUnit) -> None - # XXX own namespace, e.g. sentry.measurement.xxx, so that we can group - # these back together in the processor? - # XXX otel throws a warning about value, unit being different types - self._otel_span.set_attribute(name, (value, unit)) + from sentry_sdk.integrations.opentelemetry.consts import SentrySpanAttribute + + # Stringify value here since OTel expects all seq items to be of one type + self.set_attribute( + f"{SentrySpanAttribute.MEASUREMENT}.{name}", (str(value), unit) + ) def set_thread(self, thread_id, thread_name): # type: (Optional[int], Optional[str]) -> None