Skip to content

Commit 429f004

Browse files
committed
feat(otel): add durable sampling coordination
1 parent effdd0e commit 429f004

2 files changed

Lines changed: 404 additions & 0 deletions

File tree

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
"""Durable execution sampling support."""
2+
3+
from __future__ import annotations
4+
5+
from dataclasses import dataclass
6+
from typing import Any
7+
8+
from opentelemetry import context as otel_context
9+
from opentelemetry.context import Context
10+
from opentelemetry.sdk.trace import Tracer as SdkTracer
11+
from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult
12+
from opentelemetry.trace import Span, SpanContext, SpanKind, TraceFlags
13+
14+
from aws_durable_execution_sdk_python_otel.context_extractors import (
15+
ExtractedContext,
16+
Sampling,
17+
)
18+
19+
20+
_DURABLE_SAMPLING_INTENT_KEY = otel_context.create_key(
21+
"aws_durable_execution_sampling_intent"
22+
)
23+
24+
25+
@dataclass(frozen=True)
26+
class DurableSamplingIntent:
27+
"""Sampling result to apply to each durable span in one invocation."""
28+
29+
result: SamplingResult
30+
31+
32+
class DurableSampler(Sampler):
33+
"""Sampler that honors a durable sampling intent carried on parent context."""
34+
35+
def __init__(self, delegate: Sampler) -> None:
36+
self.delegate = delegate
37+
38+
@classmethod
39+
def install_on_tracer(cls, tracer: SdkTracer) -> "DurableSampler":
40+
current_sampler = tracer.sampler
41+
if isinstance(current_sampler, cls):
42+
return current_sampler
43+
sampler = cls(current_sampler)
44+
tracer.sampler = sampler
45+
return sampler
46+
47+
def should_sample(
48+
self,
49+
parent_context: Context | None,
50+
trace_id: int,
51+
name: str,
52+
kind: SpanKind | None = None,
53+
attributes: Any = None,
54+
links: Any = None,
55+
trace_state: Any = None,
56+
) -> SamplingResult:
57+
intent = otel_context.get_value(_DURABLE_SAMPLING_INTENT_KEY, parent_context)
58+
if isinstance(intent, DurableSamplingIntent):
59+
merged_attributes = dict(attributes or {})
60+
merged_attributes.update(dict(intent.result.attributes or {}))
61+
return SamplingResult(
62+
intent.result.decision,
63+
attributes=merged_attributes,
64+
trace_state=intent.result.trace_state,
65+
)
66+
return self.delegate.should_sample(
67+
parent_context,
68+
trace_id,
69+
name,
70+
kind,
71+
attributes,
72+
links,
73+
trace_state,
74+
)
75+
76+
def get_description(self) -> str:
77+
return f"DurableSampler{{{self.delegate.get_description()}}}"
78+
79+
80+
def store_sampling_intent(
81+
parent_context: Context,
82+
intent: DurableSamplingIntent | None,
83+
) -> Context:
84+
"""Attach a durable sampling intent to a span parent context."""
85+
if intent is None:
86+
return parent_context
87+
return otel_context.set_value(_DURABLE_SAMPLING_INTENT_KEY, intent, parent_context)
88+
89+
90+
def resolve_sampling_result(
91+
*,
92+
extracted: ExtractedContext | None,
93+
ambient_span: Span,
94+
canonical_trace_id: int,
95+
sampler: Sampler,
96+
span_name: str,
97+
attributes: dict[str, Any],
98+
) -> SamplingResult:
99+
"""Resolve one sampling decision for all durable spans in an invocation."""
100+
sampling = extracted.sampling if extracted is not None else Sampling.UNDECIDED
101+
if sampling is Sampling.SAMPLED:
102+
return SamplingResult(Decision.RECORD_AND_SAMPLE)
103+
if sampling is Sampling.NOT_SAMPLED:
104+
return SamplingResult(Decision.DROP)
105+
106+
ambient_context = ambient_span.get_span_context()
107+
if _is_same_trace(ambient_context, canonical_trace_id):
108+
if bool(ambient_context.trace_flags & TraceFlags.SAMPLED):
109+
decision = Decision.RECORD_AND_SAMPLE
110+
elif ambient_span.is_recording():
111+
decision = Decision.RECORD_ONLY
112+
else:
113+
decision = Decision.DROP
114+
return SamplingResult(
115+
decision,
116+
trace_state=ambient_context.trace_state,
117+
)
118+
119+
return sampler.should_sample(
120+
parent_context=Context(),
121+
trace_id=canonical_trace_id,
122+
name=span_name,
123+
kind=SpanKind.INTERNAL,
124+
attributes=attributes,
125+
links=(),
126+
trace_state=None,
127+
)
128+
129+
130+
def is_sampled(result: SamplingResult) -> bool:
131+
return result.decision is Decision.RECORD_AND_SAMPLE
132+
133+
134+
def _is_same_trace(span_context: SpanContext, trace_id: int) -> bool:
135+
return span_context.is_valid and span_context.trace_id == trace_id

0 commit comments

Comments
 (0)