Skip to content

Commit 3b54bbf

Browse files
authored
Potel Sampling (#3501)
Add a new SentrySampler that is used for sampling OpenTelemetry spans the Sentry way (using Sentrys traces_sample_rate and traces_sampler config options) Fixes #3318
1 parent 0e0b5b0 commit 3b54bbf

File tree

5 files changed

+468
-1
lines changed

5 files changed

+468
-1
lines changed

sentry_sdk/integrations/opentelemetry/consts.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
SENTRY_USE_CURRENT_SCOPE_KEY = create_key("sentry_use_current_scope")
1212
SENTRY_USE_ISOLATION_SCOPE_KEY = create_key("sentry_use_isolation_scope")
1313

14+
SENTRY_TRACE_STATE_DROPPED = "sentry_dropped"
15+
1416
OTEL_SENTRY_CONTEXT = "otel"
1517
SPAN_ORIGIN = "auto.otel"
1618

sentry_sdk/integrations/opentelemetry/integration.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
from sentry_sdk.integrations.opentelemetry.contextvars_context import (
1313
SentryContextVarsRuntimeContext,
1414
)
15+
from sentry_sdk.integrations.opentelemetry.sampler import SentrySampler
1516
from sentry_sdk.utils import logger
1617

1718
try:
@@ -55,7 +56,7 @@ def _setup_sentry_tracing():
5556

5657
opentelemetry.context._RUNTIME_CONTEXT = SentryContextVarsRuntimeContext()
5758

58-
provider = TracerProvider()
59+
provider = TracerProvider(sampler=SentrySampler())
5960
provider.add_span_processor(PotelSentrySpanProcessor())
6061
trace.set_tracer_provider(provider)
6162

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
from random import random
2+
3+
from opentelemetry import trace
4+
5+
from opentelemetry.sdk.trace.sampling import Sampler, SamplingResult, Decision
6+
from opentelemetry.trace.span import TraceState
7+
8+
import sentry_sdk
9+
from sentry_sdk.integrations.opentelemetry.consts import SENTRY_TRACE_STATE_DROPPED
10+
from sentry_sdk.tracing_utils import has_tracing_enabled
11+
from sentry_sdk.utils import is_valid_sample_rate, logger
12+
13+
from typing import TYPE_CHECKING, Optional, Sequence
14+
15+
if TYPE_CHECKING:
16+
from opentelemetry.context import Context
17+
from opentelemetry.trace import Link, SpanKind
18+
from opentelemetry.trace.span import SpanContext
19+
from opentelemetry.util.types import Attributes
20+
21+
22+
def get_parent_sampled(parent_context, trace_id):
23+
# type: (Optional[SpanContext], int) -> Optional[bool]
24+
if parent_context is None:
25+
return None
26+
27+
is_span_context_valid = parent_context is not None and parent_context.is_valid
28+
29+
# Only inherit sample rate if `traceId` is the same
30+
if is_span_context_valid and parent_context.trace_id == trace_id:
31+
# this is getSamplingDecision in JS
32+
if parent_context.trace_flags.sampled:
33+
return True
34+
35+
dropped = parent_context.trace_state.get(SENTRY_TRACE_STATE_DROPPED) == "true"
36+
if dropped:
37+
return False
38+
39+
# TODO-anton: fall back to sampling decision in DSC (for this die DSC needs to be set in the trace_state)
40+
41+
return None
42+
43+
44+
def dropped(parent_context=None):
45+
# type: (Optional[SpanContext]) -> SamplingResult
46+
trace_state = parent_context.trace_state if parent_context is not None else None
47+
updated_trace_context = trace_state or TraceState()
48+
updated_trace_context = updated_trace_context.update(
49+
SENTRY_TRACE_STATE_DROPPED, "true"
50+
)
51+
return SamplingResult(
52+
Decision.DROP,
53+
trace_state=updated_trace_context,
54+
)
55+
56+
57+
class SentrySampler(Sampler):
58+
def should_sample(
59+
self,
60+
parent_context, # type: Optional[Context]
61+
trace_id, # type: int
62+
name, # type: str
63+
kind=None, # type: Optional[SpanKind]
64+
attributes=None, # type: Attributes
65+
links=None, # type: Optional[Sequence[Link]]
66+
trace_state=None, # type: Optional[TraceState]
67+
):
68+
# type: (...) -> SamplingResult
69+
client = sentry_sdk.get_client()
70+
71+
parent_span = trace.get_current_span(parent_context)
72+
parent_context = parent_span.get_span_context() if parent_span else None
73+
74+
# No tracing enabled, thus no sampling
75+
if not has_tracing_enabled(client.options):
76+
return dropped(parent_context)
77+
78+
sample_rate = None
79+
80+
# Check if sampled=True was passed to start_transaction
81+
# TODO-anton: Do we want to keep the start_transaction(sampled=True) thing?
82+
83+
# Check if there is a traces_sampler
84+
# Traces_sampler is responsible to check parent sampled to have full transactions.
85+
has_traces_sampler = callable(client.options.get("traces_sampler"))
86+
if has_traces_sampler:
87+
# TODO-anton: Make proper sampling_context
88+
sampling_context = {
89+
"transaction_context": {
90+
"name": name,
91+
},
92+
"parent_sampled": get_parent_sampled(parent_context, trace_id),
93+
}
94+
95+
sample_rate = client.options["traces_sampler"](sampling_context)
96+
97+
else:
98+
# Check if there is a parent with a sampling decision
99+
parent_sampled = get_parent_sampled(parent_context, trace_id)
100+
if parent_sampled is not None:
101+
sample_rate = parent_sampled
102+
else:
103+
# Check if there is a traces_sample_rate
104+
sample_rate = client.options.get("traces_sample_rate")
105+
106+
# If the sample rate is invalid, drop the span
107+
if not is_valid_sample_rate(sample_rate, source=self.__class__.__name__):
108+
logger.warning(
109+
f"[Tracing] Discarding {name} because of invalid sample rate."
110+
)
111+
return dropped(parent_context)
112+
113+
# Roll the dice on sample rate
114+
sampled = random() < float(sample_rate)
115+
116+
if sampled:
117+
return SamplingResult(Decision.RECORD_AND_SAMPLE)
118+
else:
119+
return dropped(parent_context)
120+
121+
def get_description(self) -> str:
122+
return self.__class__.__name__

sentry_sdk/integrations/opentelemetry/scope.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@ def _incoming_otel_span_context(self):
9797
span_id=int(self._propagation_context.parent_span_id, 16), # type: ignore
9898
is_remote=True,
9999
trace_flags=trace_flags,
100+
# TODO-anton: add trace_state (mapping[str,str]) with the parentSpanId, dsc and sampled from self._propagation_context
101+
# trace_state={
102+
# }
100103
)
101104

102105
return span_context

0 commit comments

Comments
 (0)