Skip to content

Commit b65b742

Browse files
authored
Head SDK DSC population (#3599)
* Populates a DSC with correct values when we don't have an incoming trace. * We rely on `trace_state.add` only adding new keys to the tracestate so these values will be populated in the first sampling decision on the root and just be propagated further. Note that transaction name is missing here for now and will be dealt with separately as part of the transaction name PRs. closes #3479
1 parent 558daee commit b65b742

File tree

5 files changed

+125
-34
lines changed

5 files changed

+125
-34
lines changed

sentry_sdk/integrations/opentelemetry/consts.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
from opentelemetry.context import create_key
2+
from sentry_sdk.tracing_utils import Baggage
23

34

45
# propagation keys
@@ -11,7 +12,8 @@
1112
SENTRY_USE_CURRENT_SCOPE_KEY = create_key("sentry_use_current_scope")
1213
SENTRY_USE_ISOLATION_SCOPE_KEY = create_key("sentry_use_isolation_scope")
1314

14-
SENTRY_TRACE_STATE_DROPPED = "sentry_dropped"
15+
TRACESTATE_SAMPLED_KEY = Baggage.SENTRY_PREFIX + "sampled"
16+
TRACESTATE_SAMPLE_RATE_KEY = Baggage.SENTRY_PREFIX + "sample_rate"
1517

1618
OTEL_SENTRY_CONTEXT = "otel"
1719
SPAN_ORIGIN = "auto.otel"

sentry_sdk/integrations/opentelemetry/sampler.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from typing import cast
12
from random import random
23

34
from opentelemetry import trace
@@ -6,13 +7,17 @@
67
from opentelemetry.trace.span import TraceState
78

89
import sentry_sdk
9-
from sentry_sdk.integrations.opentelemetry.consts import SENTRY_TRACE_STATE_DROPPED
1010
from sentry_sdk.tracing_utils import has_tracing_enabled
1111
from sentry_sdk.utils import is_valid_sample_rate, logger
12+
from sentry_sdk.integrations.opentelemetry.consts import (
13+
TRACESTATE_SAMPLED_KEY,
14+
TRACESTATE_SAMPLE_RATE_KEY,
15+
)
1216

13-
from typing import TYPE_CHECKING, Optional, Sequence
17+
from typing import TYPE_CHECKING
1418

1519
if TYPE_CHECKING:
20+
from typing import Optional, Sequence, Union
1621
from opentelemetry.context import Context
1722
from opentelemetry.trace import Link, SpanKind
1823
from opentelemetry.trace.span import SpanContext
@@ -32,30 +37,42 @@ def get_parent_sampled(parent_context, trace_id):
3237
if parent_context.trace_flags.sampled:
3338
return True
3439

35-
dropped = parent_context.trace_state.get(SENTRY_TRACE_STATE_DROPPED) == "true"
36-
if dropped:
40+
dsc_sampled = parent_context.trace_state.get(TRACESTATE_SAMPLED_KEY)
41+
if dsc_sampled == "true":
42+
return True
43+
elif dsc_sampled == "false":
3744
return False
3845

39-
# TODO-anton: fall back to sampling decision in DSC (for this die DSC needs to be set in the trace_state)
40-
4146
return None
4247

4348

44-
def dropped_result(span_context):
45-
# type: (SpanContext) -> SamplingResult
46-
trace_state = span_context.trace_state.update(SENTRY_TRACE_STATE_DROPPED, "true")
49+
def dropped_result(span_context, attributes, sample_rate=None):
50+
# type: (SpanContext, Attributes, Optional[float]) -> SamplingResult
51+
# note that trace_state.add will NOT overwrite existing entries
52+
# so these will only be added the first time in a root span sampling decision
53+
trace_state = span_context.trace_state.add(TRACESTATE_SAMPLED_KEY, "false")
54+
if sample_rate:
55+
trace_state = trace_state.add(TRACESTATE_SAMPLE_RATE_KEY, str(sample_rate))
4756

4857
return SamplingResult(
4958
Decision.DROP,
59+
attributes=attributes,
5060
trace_state=trace_state,
5161
)
5262

5363

54-
def sampled_result(span_context):
55-
# type: (SpanContext) -> SamplingResult
64+
def sampled_result(span_context, attributes, sample_rate):
65+
# type: (SpanContext, Attributes, float) -> SamplingResult
66+
# note that trace_state.add will NOT overwrite existing entries
67+
# so these will only be added the first time in a root span sampling decision
68+
trace_state = span_context.trace_state.add(TRACESTATE_SAMPLED_KEY, "true").add(
69+
TRACESTATE_SAMPLE_RATE_KEY, str(sample_rate)
70+
)
71+
5672
return SamplingResult(
5773
Decision.RECORD_AND_SAMPLE,
58-
trace_state=span_context.trace_state,
74+
attributes=attributes,
75+
trace_state=trace_state,
5976
)
6077

6178

@@ -77,7 +94,7 @@ def should_sample(
7794

7895
# No tracing enabled, thus no sampling
7996
if not has_tracing_enabled(client.options):
80-
return dropped_result(parent_span_context)
97+
return dropped_result(parent_span_context, attributes)
8198

8299
sample_rate = None
83100

@@ -112,16 +129,16 @@ def should_sample(
112129
logger.warning(
113130
f"[Tracing] Discarding {name} because of invalid sample rate."
114131
)
115-
return dropped_result(parent_span_context)
132+
return dropped_result(parent_span_context, attributes)
116133

117134
# Roll the dice on sample rate
118-
sampled = random() < float(sample_rate)
135+
sample_rate = float(cast("Union[bool, float, int]", sample_rate))
136+
sampled = random() < sample_rate
119137

120-
# TODO-neel-potel set sample rate as attribute for DSC
121138
if sampled:
122-
return sampled_result(parent_span_context)
139+
return sampled_result(parent_span_context, attributes, sample_rate)
123140
else:
124-
return dropped_result(parent_span_context)
141+
return dropped_result(parent_span_context, attributes, sample_rate)
125142

126143
def get_description(self) -> str:
127144
return self.__class__.__name__

sentry_sdk/integrations/opentelemetry/scope.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
SpanContext,
99
NonRecordingSpan,
1010
TraceFlags,
11-
TraceState,
1211
use_span,
1312
)
1413

sentry_sdk/integrations/opentelemetry/utils.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from opentelemetry.semconv.trace import SpanAttributes
1616
from opentelemetry.sdk.trace import ReadableSpan
1717

18+
import sentry_sdk
1819
from sentry_sdk.utils import Dsn
1920
from sentry_sdk.consts import SPANSTATUS
2021
from sentry_sdk.tracing import get_span_status_from_http_code, DEFAULT_SPAN_ORIGIN
@@ -24,7 +25,7 @@
2425
from sentry_sdk._types import TYPE_CHECKING
2526

2627
if TYPE_CHECKING:
27-
from typing import Any, Optional, Mapping, Sequence, Union, ItemsView
28+
from typing import Any, Optional, Mapping, Sequence, Union
2829
from sentry_sdk._types import OtelExtractedSpanData
2930

3031

@@ -300,9 +301,8 @@ def get_trace_context(span, span_data=None):
300301
if span.attributes:
301302
trace_context["data"] = dict(span.attributes)
302303

303-
trace_context["dynamic_sampling_context"] = dsc_from_trace_state(
304-
span.context.trace_state
305-
)
304+
trace_state = get_trace_state(span)
305+
trace_context["dynamic_sampling_context"] = dsc_from_trace_state(trace_state)
306306

307307
# TODO-neel-potel profiler thread_id, thread_name
308308

@@ -319,6 +319,11 @@ def trace_state_from_baggage(baggage):
319319
return TraceState(items)
320320

321321

322+
def baggage_from_trace_state(trace_state):
323+
# type: (TraceState) -> Baggage
324+
return Baggage(dsc_from_trace_state(trace_state))
325+
326+
322327
def serialize_trace_state(trace_state):
323328
# type: (TraceState) -> str
324329
sentry_items = []
@@ -336,3 +341,55 @@ def dsc_from_trace_state(trace_state):
336341
key = re.sub(Baggage.SENTRY_PREFIX_REGEX, "", k)
337342
dsc[key] = v
338343
return dsc
344+
345+
346+
def has_incoming_trace(trace_state):
347+
# type: (TraceState) -> bool
348+
"""
349+
The existence a sentry-trace_id in the baggage implies we continued an upstream trace.
350+
"""
351+
return (Baggage.SENTRY_PREFIX + "trace_id") in trace_state
352+
353+
354+
def get_trace_state(span):
355+
# type: (Union[Span, ReadableSpan]) -> TraceState
356+
"""
357+
Get the existing trace_state with sentry items
358+
or populate it if we are the head SDK.
359+
"""
360+
span_context = span.get_span_context()
361+
if not span_context:
362+
return TraceState()
363+
364+
trace_state = span_context.trace_state
365+
366+
if has_incoming_trace(trace_state):
367+
return trace_state
368+
else:
369+
client = sentry_sdk.get_client()
370+
if not client.is_active():
371+
return trace_state
372+
373+
options = client.options or {}
374+
375+
trace_state = trace_state.update(
376+
Baggage.SENTRY_PREFIX + "trace_id", format_trace_id(span_context.trace_id)
377+
)
378+
379+
if options.get("environment"):
380+
trace_state = trace_state.update(
381+
Baggage.SENTRY_PREFIX + "environment", options["environment"]
382+
)
383+
384+
if options.get("release"):
385+
trace_state = trace_state.update(
386+
Baggage.SENTRY_PREFIX + "release", options["release"]
387+
)
388+
389+
if options.get("dsn"):
390+
trace_state = trace_state.update(
391+
Baggage.SENTRY_PREFIX + "public_key", Dsn(options["dsn"]).public_key
392+
)
393+
394+
# TODO-neel-potel head dsc transaction name
395+
return trace_state

sentry_sdk/tracing.py

Lines changed: 26 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
from datetime import datetime, timedelta, timezone
66

77
from opentelemetry import trace as otel_trace, context
8-
from opentelemetry.trace import format_trace_id, format_span_id, Span as OtelSpan
8+
from opentelemetry.trace import (
9+
format_trace_id,
10+
format_span_id,
11+
Span as OtelSpan,
12+
TraceState,
13+
)
914
from opentelemetry.trace.status import StatusCode
1015
from opentelemetry.sdk.trace import ReadableSpan
1116

@@ -1453,8 +1458,7 @@ def iter_headers(self):
14531458
serialize_trace_state,
14541459
)
14551460

1456-
trace_state = self._otel_span.get_span_context().trace_state
1457-
yield BAGGAGE_HEADER_NAME, serialize_trace_state(trace_state)
1461+
yield BAGGAGE_HEADER_NAME, serialize_trace_state(self.trace_state)
14581462

14591463
def to_traceparent(self):
14601464
# type: () -> str
@@ -1471,10 +1475,26 @@ def to_traceparent(self):
14711475

14721476
return traceparent
14731477

1478+
@property
1479+
def trace_state(self):
1480+
# type: () -> TraceState
1481+
from sentry_sdk.integrations.opentelemetry.utils import (
1482+
get_trace_state,
1483+
)
1484+
1485+
return get_trace_state(self._otel_span)
1486+
14741487
def to_baggage(self):
1475-
# type: () -> Optional[Baggage]
1476-
# TODO-neel-potel head SDK populate baggage mess
1477-
pass
1488+
# type: () -> Baggage
1489+
return self.get_baggage()
1490+
1491+
def get_baggage(self):
1492+
# type: () -> Baggage
1493+
from sentry_sdk.integrations.opentelemetry.utils import (
1494+
baggage_from_trace_state,
1495+
)
1496+
1497+
return baggage_from_trace_state(self.trace_state)
14781498

14791499
def set_tag(self, key, value):
14801500
# type: (str, Any) -> None
@@ -1568,10 +1588,6 @@ def set_context(self, key, value):
15681588
# type: (str, Any) -> None
15691589
pass
15701590

1571-
def get_baggage(self):
1572-
# type: () -> Baggage
1573-
pass
1574-
15751591

15761592
if TYPE_CHECKING:
15771593

0 commit comments

Comments
 (0)