Skip to content

Commit 8bca97d

Browse files
anuraagaxrmx
andauthored
Add experimental composite sampler (#4714)
* Add experimental consistent sampler * Fix import * Fix lint * CHANGELOG * Fix test name * Cleanup and match spec better * Format * Cleanup * Formatting --------- Co-authored-by: Riccardo Magliocchetti <[email protected]>
1 parent 6c8e2ab commit 8bca97d

File tree

19 files changed

+1126
-2
lines changed

19 files changed

+1126
-2
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
1313
## Unreleased
1414

15+
- Add experimental composite samplers
16+
([#4714](https://github.com/open-telemetry/opentelemetry-python/pull/4714))
1517
- Filter duplicate logs out of some internal `logger`'s logs on the export logs path that might otherwise endlessly log or cause a recursion depth exceeded issue in cases where logging itself results in an exception.
1618
([#4695](https://github.com/open-telemetry/opentelemetry-python/pull/4695)).
1719
- docs: linked the examples with their github source code location and added Prometheus example
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
__all__ = [
16+
"ComposableSampler",
17+
"SamplingIntent",
18+
"composable_always_off",
19+
"composable_always_on",
20+
"composable_parent_threshold",
21+
"composable_traceid_ratio_based",
22+
"composite_sampler",
23+
]
24+
25+
26+
from ._always_off import composable_always_off
27+
from ._always_on import composable_always_on
28+
from ._composable import ComposableSampler, SamplingIntent
29+
from ._parent_threshold import composable_parent_threshold
30+
from ._sampler import composite_sampler
31+
from ._traceid_ratio import composable_traceid_ratio_based
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import Sequence
18+
19+
from opentelemetry.context import Context
20+
from opentelemetry.trace import Link, SpanKind, TraceState
21+
from opentelemetry.util.types import Attributes
22+
23+
from ._composable import ComposableSampler, SamplingIntent
24+
from ._util import INVALID_THRESHOLD
25+
26+
_intent = SamplingIntent(threshold=INVALID_THRESHOLD, threshold_reliable=False)
27+
28+
29+
class _ComposableAlwaysOffSampler(ComposableSampler):
30+
def sampling_intent(
31+
self,
32+
parent_ctx: Context | None,
33+
name: str,
34+
span_kind: SpanKind | None,
35+
attributes: Attributes,
36+
links: Sequence[Link] | None,
37+
trace_state: TraceState | None = None,
38+
) -> SamplingIntent:
39+
return _intent
40+
41+
def get_description(self) -> str:
42+
return "ComposableAlwaysOff"
43+
44+
45+
_always_off = _ComposableAlwaysOffSampler()
46+
47+
48+
def composable_always_off() -> ComposableSampler:
49+
"""Returns a composable sampler that does not sample any span.
50+
51+
- Always returns a SamplingIntent with no threshold, indicating all spans should be dropped
52+
- Sets threshold_reliable to false
53+
- Does not add any attributes
54+
"""
55+
return _always_off
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import Sequence
18+
19+
from opentelemetry.context import Context
20+
from opentelemetry.trace import Link, SpanKind, TraceState
21+
from opentelemetry.util.types import Attributes
22+
23+
from ._composable import ComposableSampler, SamplingIntent
24+
from ._util import MIN_THRESHOLD
25+
26+
_intent = SamplingIntent(threshold=MIN_THRESHOLD)
27+
28+
29+
class _ComposableAlwaysOnSampler(ComposableSampler):
30+
def sampling_intent(
31+
self,
32+
parent_ctx: Context | None,
33+
name: str,
34+
span_kind: SpanKind | None,
35+
attributes: Attributes,
36+
links: Sequence[Link] | None,
37+
trace_state: TraceState | None = None,
38+
) -> SamplingIntent:
39+
return _intent
40+
41+
def get_description(self) -> str:
42+
return "ComposableAlwaysOn"
43+
44+
45+
_always_on = _ComposableAlwaysOnSampler()
46+
47+
48+
def composable_always_on() -> ComposableSampler:
49+
"""Returns a composable sampler that samples all spans.
50+
51+
- Always returns a SamplingIntent with threshold set to sample all spans (threshold = 0)
52+
- Sets threshold_reliable to true
53+
- Does not add any attributes
54+
"""
55+
return _always_on
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from dataclasses import dataclass, field
18+
from typing import Callable, Protocol, Sequence
19+
20+
from opentelemetry.context import Context
21+
from opentelemetry.trace import Link, SpanKind, TraceState
22+
from opentelemetry.util.types import Attributes
23+
24+
25+
@dataclass(frozen=True)
26+
class SamplingIntent:
27+
"""Information to make a consistent sampling decision."""
28+
29+
threshold: int
30+
"""The sampling threshold value. A lower threshold increases the likelihood of sampling."""
31+
32+
threshold_reliable: bool = field(default=True)
33+
"""Indicates whether the threshold is reliable for Span-to-Metrics estimation."""
34+
35+
attributes: Attributes = field(default=None)
36+
"""Any attributes to be added to a sampled span."""
37+
38+
update_trace_state: Callable[[TraceState], TraceState] = field(
39+
default=lambda ts: ts
40+
)
41+
"""Any updates to be made to trace state."""
42+
43+
44+
class ComposableSampler(Protocol):
45+
"""A sampler that can be composed to make a final sampling decision."""
46+
47+
def sampling_intent(
48+
self,
49+
parent_ctx: Context | None,
50+
name: str,
51+
span_kind: SpanKind | None,
52+
attributes: Attributes,
53+
links: Sequence[Link] | None,
54+
trace_state: TraceState | None,
55+
) -> SamplingIntent:
56+
"""Returns information to make a sampling decision."""
57+
... # pylint: disable=unnecessary-ellipsis
58+
59+
def get_description(self) -> str:
60+
"""Returns a description of the sampler."""
61+
... # pylint: disable=unnecessary-ellipsis
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# Copyright The OpenTelemetry Authors
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
from typing import Sequence
18+
19+
from opentelemetry.context import Context
20+
from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span
21+
from opentelemetry.util.types import Attributes
22+
23+
from ._composable import ComposableSampler, SamplingIntent
24+
from ._trace_state import OtelTraceState
25+
from ._util import (
26+
INVALID_THRESHOLD,
27+
MIN_THRESHOLD,
28+
is_valid_threshold,
29+
)
30+
31+
32+
class _ComposableParentThreshold(ComposableSampler):
33+
def __init__(self, root_sampler: ComposableSampler):
34+
self._root_sampler = root_sampler
35+
self._description = f"ComposableParentThreshold{{root={root_sampler.get_description()}}}"
36+
37+
def sampling_intent(
38+
self,
39+
parent_ctx: Context | None,
40+
name: str,
41+
span_kind: SpanKind | None,
42+
attributes: Attributes,
43+
links: Sequence[Link] | None,
44+
trace_state: TraceState | None = None,
45+
) -> SamplingIntent:
46+
parent_span = get_current_span(parent_ctx)
47+
parent_span_ctx = parent_span.get_span_context()
48+
is_root = not parent_span_ctx.is_valid
49+
if is_root:
50+
return self._root_sampler.sampling_intent(
51+
parent_ctx, name, span_kind, attributes, links, trace_state
52+
)
53+
54+
ot_trace_state = OtelTraceState.parse(trace_state)
55+
56+
if is_valid_threshold(ot_trace_state.threshold):
57+
return SamplingIntent(
58+
threshold=ot_trace_state.threshold,
59+
threshold_reliable=True,
60+
)
61+
62+
threshold = (
63+
MIN_THRESHOLD
64+
if parent_span_ctx.trace_flags.sampled
65+
else INVALID_THRESHOLD
66+
)
67+
return SamplingIntent(threshold=threshold, threshold_reliable=False)
68+
69+
def get_description(self) -> str:
70+
return self._description
71+
72+
73+
def composable_parent_threshold(
74+
root_sampler: ComposableSampler,
75+
) -> ComposableSampler:
76+
"""Returns a consistent sampler that respects the sampling decision of
77+
the parent span or falls-back to the given sampler if it is a root span.
78+
79+
- For spans without a parent context, delegate to the root sampler
80+
- For spans with a parent context, returns a SamplingIntent that propagates the parent's sampling decision
81+
- Returns the parent's threshold if available; otherwise, if the parent's sampled flag is set,
82+
returns threshold=0; otherwise, if the parent's sampled flag is not set, no threshold is returned.
83+
- Sets threshold_reliable to match the parent’s reliability, which is true if the parent had a threshold.
84+
- Does not add any attributes
85+
86+
Args:
87+
root_sampler: The root sampler to use for spans without a parent context.
88+
"""
89+
return _ComposableParentThreshold(root_sampler)

0 commit comments

Comments
 (0)