Skip to content

Commit fa75422

Browse files
xrmxherin049
andauthored
Add support for composite samplers in declarative config (open-telemetry#5201)
* opentelemetry-sdk: add declarative config support for experimental rule based sampler Assisted-by: Cursor * Make match similar to java Assisted-by: Cursor * Add changelog * Revert "Make match similar to java" This reverts commit 0c14136. --------- Co-authored-by: Lukas Hering <40302054+herin049@users.noreply.github.com>
1 parent 43f079f commit fa75422

5 files changed

Lines changed: 812 additions & 5 deletions

File tree

.changelog/5201.added

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
`opentelemetry-sdk`: Add `composite/development` samplers support to declarative file configuration

opentelemetry-sdk/src/opentelemetry/sdk/_configuration/_tracer_provider.py

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@
1212
load_entry_point,
1313
)
1414
from opentelemetry.sdk._configuration._exceptions import ConfigurationError
15+
from opentelemetry.sdk._configuration.models import (
16+
ExperimentalComposableRuleBasedSampler as RuleBasedSamplerConfig,
17+
)
18+
from opentelemetry.sdk._configuration.models import (
19+
ExperimentalComposableRuleBasedSamplerRule as RuleBasedSamplerRuleConfig,
20+
)
21+
from opentelemetry.sdk._configuration.models import (
22+
ExperimentalComposableSampler as ComposableSamplerConfig,
23+
)
1524
from opentelemetry.sdk._configuration.models import (
1625
OtlpGrpcExporter as OtlpGrpcExporterConfig,
1726
)
@@ -46,6 +55,25 @@
4655
SpanLimits,
4756
TracerProvider,
4857
)
58+
from opentelemetry.sdk.trace._sampling_experimental import (
59+
ComposableSampler,
60+
composable_always_off,
61+
composable_always_on,
62+
composable_parent_threshold,
63+
composable_rule_based,
64+
composable_traceid_ratio_based,
65+
composite_sampler,
66+
)
67+
from opentelemetry.sdk.trace._sampling_experimental._rule_based import (
68+
AllPredicate,
69+
AlwaysMatchPredicate,
70+
AttributePatternsPredicate,
71+
AttributeValuesPredicate,
72+
ParentPredicate,
73+
PredicateT,
74+
RulesT,
75+
SpanKindPredicate,
76+
)
4977
from opentelemetry.sdk.trace.export import (
5078
BatchSpanProcessor,
5179
ConsoleSpanExporter,
@@ -59,6 +87,7 @@
5987
Sampler,
6088
TraceIdRatioBased,
6189
)
90+
from opentelemetry.trace import SpanKind as TraceSpanKind
6291

6392
_logger = logging.getLogger(__name__)
6493

@@ -185,6 +214,88 @@ def _create_span_processor(
185214
)
186215

187216

217+
def _create_experimental_composable_sampler(
218+
config: ComposableSamplerConfig,
219+
) -> ComposableSampler:
220+
"""Create an experimental composable sampler from config"""
221+
if config.always_on is not None:
222+
return composable_always_on()
223+
if config.always_off is not None:
224+
return composable_always_off()
225+
if config.parent_threshold is not None:
226+
return composable_parent_threshold(
227+
_create_experimental_composable_sampler(
228+
config.parent_threshold.root
229+
)
230+
)
231+
if config.probability is not None:
232+
ratio = config.probability.ratio
233+
return composable_traceid_ratio_based(
234+
ratio if ratio is not None else 1.0
235+
)
236+
if config.rule_based is not None:
237+
return composable_rule_based(
238+
_create_rule_based_sampler_rules(config.rule_based)
239+
)
240+
raise ConfigurationError(
241+
f"Unknown or unsupported experimental composable sampler type in config: {config!r}. "
242+
"Supported types: always_on, always_off, parent_threshold, probability, rule_based."
243+
)
244+
245+
246+
def _create_rule_based_sampler_rules(
247+
config: RuleBasedSamplerConfig,
248+
) -> RulesT:
249+
if config.rules is None:
250+
return []
251+
return [
252+
(
253+
_create_rule_based_sampler_rule_predicate(rule),
254+
_create_experimental_composable_sampler(rule.sampler),
255+
)
256+
for rule in config.rules
257+
]
258+
259+
260+
def _create_rule_based_sampler_rule_predicate(
261+
config: RuleBasedSamplerRuleConfig,
262+
) -> PredicateT:
263+
predicates: list[PredicateT] = []
264+
if config.attribute_values is not None:
265+
predicates.append(
266+
AttributeValuesPredicate(
267+
config.attribute_values.key,
268+
config.attribute_values.values,
269+
)
270+
)
271+
if config.attribute_patterns is not None:
272+
predicates.append(
273+
AttributePatternsPredicate(
274+
config.attribute_patterns.key,
275+
config.attribute_patterns.included,
276+
config.attribute_patterns.excluded,
277+
)
278+
)
279+
if config.span_kinds is not None:
280+
predicates.append(
281+
SpanKindPredicate(
282+
[
283+
TraceSpanKind[span_kind.value.upper()]
284+
for span_kind in config.span_kinds
285+
]
286+
)
287+
)
288+
if config.parent is not None:
289+
predicates.append(
290+
ParentPredicate([parent.value for parent in config.parent])
291+
)
292+
if not predicates:
293+
return AlwaysMatchPredicate()
294+
if len(predicates) == 1:
295+
return predicates[0]
296+
return AllPredicate(predicates)
297+
298+
188299
def _create_sampler(config: SamplerConfig) -> Sampler:
189300
"""Create a sampler from config.
190301
@@ -200,14 +311,21 @@ def _create_sampler(config: SamplerConfig) -> Sampler:
200311
if config.trace_id_ratio_based is not None:
201312
ratio = config.trace_id_ratio_based.ratio
202313
return TraceIdRatioBased(ratio if ratio is not None else 1.0)
314+
if config.composite_development is not None:
315+
return composite_sampler(
316+
_create_experimental_composable_sampler(
317+
config.composite_development
318+
)
319+
)
203320
if config.parent_based is not None:
204321
return _create_parent_based_sampler(config.parent_based)
205322
if config.additional_properties:
206323
name = next(iter(config.additional_properties))
207324
return load_entry_point("opentelemetry_sampler", name)()
208325
raise ConfigurationError(
209326
f"Unsupported sampler type in config: {config!r}. "
210-
"Supported types: always_on, always_off, trace_id_ratio_based, parent_based."
327+
"Supported types: always_on, always_off, composite_development, "
328+
"trace_id_ratio_based, parent_based."
211329
)
212330

213331

opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_rule_based.py

Lines changed: 177 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,18 @@
33

44
from __future__ import annotations
55

6+
import logging
67
from collections.abc import Sequence
8+
from fnmatch import fnmatchcase
79
from typing import Protocol
810

911
from opentelemetry.context import Context
10-
from opentelemetry.trace import Link, SpanKind, TraceState
12+
from opentelemetry.trace import (
13+
Link,
14+
SpanKind,
15+
TraceState,
16+
get_current_span,
17+
)
1118
from opentelemetry.util.types import AnyValue, Attributes
1219

1320
from ._composable import ComposableSampler, SamplingIntent
@@ -32,6 +39,9 @@ class AttributePredicate:
3239
"""An exact match of an attribute value"""
3340

3441
def __init__(self, key: str, value: AnyValue):
42+
logging.warning(
43+
"This is deprecated, use AttributeValuesPredicate instead"
44+
)
3545
self.key = key
3646
self.value = value
3747

@@ -52,6 +62,172 @@ def __str__(self):
5262
return f"{self.key}={self.value}"
5363

5464

65+
class AlwaysMatchPredicate:
66+
def __call__(
67+
self,
68+
parent_ctx: Context | None,
69+
name: str,
70+
span_kind: SpanKind | None,
71+
attributes: Attributes,
72+
links: Sequence[Link] | None,
73+
trace_state: TraceState | None,
74+
) -> bool:
75+
return True
76+
77+
def __str__(self) -> str:
78+
return "AlwaysMatch"
79+
80+
81+
class AllPredicate:
82+
def __init__(self, predicates: Sequence[PredicateT]):
83+
self._predicates = tuple(predicates)
84+
85+
def __call__(
86+
self,
87+
parent_ctx: Context | None,
88+
name: str,
89+
span_kind: SpanKind | None,
90+
attributes: Attributes,
91+
links: Sequence[Link] | None,
92+
trace_state: TraceState | None,
93+
) -> bool:
94+
return all(
95+
predicate(
96+
parent_ctx,
97+
name,
98+
span_kind,
99+
attributes,
100+
links,
101+
trace_state,
102+
)
103+
for predicate in self._predicates
104+
)
105+
106+
def __str__(self) -> str:
107+
return " && ".join(str(predicate) for predicate in self._predicates)
108+
109+
110+
class AttributeValuesPredicate:
111+
def __init__(self, key: str, values: Sequence[str]):
112+
self._key = key
113+
self._values = frozenset(values)
114+
115+
def __call__(
116+
self,
117+
parent_ctx: Context | None,
118+
name: str,
119+
span_kind: SpanKind | None,
120+
attributes: Attributes,
121+
links: Sequence[Link] | None,
122+
trace_state: TraceState | None,
123+
) -> bool:
124+
if not attributes or self._key not in attributes:
125+
return False
126+
return any(
127+
str(value) in self._values
128+
for value in _attribute_values(attributes[self._key])
129+
)
130+
131+
def __str__(self) -> str:
132+
values = ",".join(sorted(self._values))
133+
return f"{self._key} in [{values}]"
134+
135+
136+
class AttributePatternsPredicate:
137+
def __init__(
138+
self,
139+
key: str,
140+
included: Sequence[str] | None = None,
141+
excluded: Sequence[str] | None = None,
142+
):
143+
self._key = key
144+
self._included = tuple(included or ())
145+
self._excluded = tuple(excluded or ())
146+
147+
def __call__(
148+
self,
149+
parent_ctx: Context | None,
150+
name: str,
151+
span_kind: SpanKind | None,
152+
attributes: Attributes,
153+
links: Sequence[Link] | None,
154+
trace_state: TraceState | None,
155+
) -> bool:
156+
if not attributes or self._key not in attributes:
157+
return False
158+
return any(
159+
self._matches_value(str(value))
160+
for value in _attribute_values(attributes[self._key])
161+
)
162+
163+
def _matches_value(self, value: str) -> bool:
164+
included = not self._included or any(
165+
fnmatchcase(value, pattern) for pattern in self._included
166+
)
167+
excluded = any(
168+
fnmatchcase(value, pattern) for pattern in self._excluded
169+
)
170+
return included and not excluded
171+
172+
def __str__(self) -> str:
173+
return f"{self._key} matches"
174+
175+
176+
class SpanKindPredicate:
177+
def __init__(self, span_kinds: Sequence[SpanKind]):
178+
self._span_kinds = frozenset(span_kinds)
179+
180+
def __call__(
181+
self,
182+
parent_ctx: Context | None,
183+
name: str,
184+
span_kind: SpanKind | None,
185+
attributes: Attributes,
186+
links: Sequence[Link] | None,
187+
trace_state: TraceState | None,
188+
) -> bool:
189+
return span_kind in self._span_kinds
190+
191+
def __str__(self) -> str:
192+
kinds = ",".join(kind.name.lower() for kind in self._span_kinds)
193+
return f"span_kind in [{kinds}]"
194+
195+
196+
class ParentPredicate:
197+
def __init__(self, parents: Sequence[str]):
198+
self._parents = frozenset(parents)
199+
200+
def __call__(
201+
self,
202+
parent_ctx: Context | None,
203+
name: str,
204+
span_kind: SpanKind | None,
205+
attributes: Attributes,
206+
links: Sequence[Link] | None,
207+
trace_state: TraceState | None,
208+
) -> bool:
209+
parent_span_context = get_current_span(parent_ctx).get_span_context()
210+
if not parent_span_context.is_valid:
211+
parent = "none"
212+
elif parent_span_context.is_remote:
213+
parent = "remote"
214+
else:
215+
parent = "local"
216+
return parent in self._parents
217+
218+
def __str__(self) -> str:
219+
parents = ",".join(self._parents)
220+
return f"parent in [{parents}]"
221+
222+
223+
def _attribute_values(value):
224+
if isinstance(value, Sequence) and not isinstance(
225+
value, (str, bytes, bytearray)
226+
):
227+
return value
228+
return (value,)
229+
230+
55231
RulesT = Sequence[tuple[PredicateT, ComposableSampler]]
56232

57233
_non_sampling_intent = SamplingIntent(

0 commit comments

Comments
 (0)