Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion sentry_sdk/integrations/opentelemetry/consts.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
56 changes: 39 additions & 17 deletions sentry_sdk/integrations/opentelemetry/potel_span_processor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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:

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)
Expand All @@ -135,25 +137,26 @@ 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 = self._common_span_transaction_attributes_as_json(span)
event.update(
{
"type": "transaction",
"transaction": description,
# TODO-neel-potel tx source based on integration
"transaction_info": {"source": "custom"},
"contexts": contexts,
}
) # type: Event

return event

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)
Expand All @@ -168,14 +171,33 @@ def _span_to_json(self, span):
"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]

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

span_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:
span_json["measurements"] = measurements

tags = extract_span_attributes(span, SentrySpanAttribute.TAG)
if tags:
span_json["tags"] = tags

return span_json
24 changes: 23 additions & 1 deletion sentry_sdk/integrations/opentelemetry/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down Expand Up @@ -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
18 changes: 13 additions & 5 deletions sentry_sdk/tracing.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand All @@ -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
Expand Down
Loading