Skip to content

Attribute namespace for tags, measurements #3448

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Aug 14, 2024
Merged
Show file tree
Hide file tree
Changes from all 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"
73 changes: 48 additions & 25 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:

event = self._common_span_transaction_attributes_as_json(span)
if event is None:
return None

trace_id = format_trace_id(span.context.trace_id)
Expand All @@ -135,25 +137,25 @@ 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

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 @@ -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
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