Skip to content

Add experimental composite sampler #4714

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## Unreleased

- Add experimental composite samplers
([#4714](https://github.com/open-telemetry/opentelemetry-python/pull/4714))

## Version 1.36.0/0.57b0 (2025-07-29)

- Add missing Prometheus exporter documentation
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

__all__ = [
"ComposableSampler",
"SamplingIntent",
"composable_always_off",
"composable_always_on",
"composable_parent_threshold",
"composable_traceid_ratio_based",
"composite_sampler",
]


from ._always_off import composable_always_off
from ._always_on import composable_always_on
from ._composable import ComposableSampler, SamplingIntent
from ._parent_threshold import composable_parent_threshold
from ._sampler import composite_sampler
from ._traceid_ratio import composable_traceid_ratio_based
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from typing import Sequence

from opentelemetry.context import Context
from opentelemetry.trace import Link, SpanKind, TraceState
from opentelemetry.util.types import Attributes

from ._composable import ComposableSampler, SamplingIntent
from ._util import INVALID_THRESHOLD

_intent = SamplingIntent(threshold=INVALID_THRESHOLD, threshold_reliable=False)


class _ComposableAlwaysOffSampler(ComposableSampler):
def sampling_intent(
self,
parent_ctx: Context | None,
name: str,
span_kind: SpanKind | None,
attributes: Attributes,
links: Sequence[Link] | None,
trace_state: TraceState | None = None,
) -> SamplingIntent:
return _intent

def get_description(self) -> str:
return "ComposableAlwaysOff"


_always_off = _ComposableAlwaysOffSampler()


def composable_always_off() -> ComposableSampler:
"""Returns a composable sampler that does not sample any span.

- Always returns a SamplingIntent with no threshold, indicating all spans should be dropped
- Sets threshold_reliable to false
- Does not add any attributes
"""
return _always_off
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from typing import Sequence

from opentelemetry.context import Context
from opentelemetry.trace import Link, SpanKind, TraceState
from opentelemetry.util.types import Attributes

from ._composable import ComposableSampler, SamplingIntent
from ._util import MIN_THRESHOLD

_intent = SamplingIntent(threshold=MIN_THRESHOLD)


class _ComposableAlwaysOnSampler(ComposableSampler):
def sampling_intent(
self,
parent_ctx: Context | None,
name: str,
span_kind: SpanKind | None,
attributes: Attributes,
links: Sequence[Link] | None,
trace_state: TraceState | None = None,
) -> SamplingIntent:
return _intent

def get_description(self) -> str:
return "ComposableAlwaysOn"


_always_on = _ComposableAlwaysOnSampler()


def composable_always_on() -> ComposableSampler:
"""Returns a composable sampler that samples all spans.

- Always returns a SamplingIntent with threshold set to sample all spans (threshold = 0)
- Sets threshold_reliable to true
- Does not add any attributes
"""
return _always_on
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Callable, Protocol, Sequence

from opentelemetry.context import Context
from opentelemetry.trace import Link, SpanKind, TraceState
from opentelemetry.util.types import Attributes


@dataclass(frozen=True)
class SamplingIntent:
"""Information to make a consistent sampling decision."""

threshold: int
"""The sampling threshold value. A lower threshold increases the likelihood of sampling."""

threshold_reliable: bool = field(default=True)
"""Indicates whether the threshold is reliable for Span-to-Metrics estimation."""

attributes: Attributes = field(default=None)
"""Any attributes to be added to a sampled span."""

update_trace_state: Callable[[TraceState], TraceState] = field(
default=lambda ts: ts
)
"""Any updates to be made to trace state."""


class ComposableSampler(Protocol):
"""A sampler that can be composed to make a final sampling decision."""

def sampling_intent(
self,
parent_ctx: Context | None,
name: str,
span_kind: SpanKind | None,
attributes: Attributes,
links: Sequence[Link] | None,
trace_state: TraceState | None,
) -> SamplingIntent:
"""Returns information to make a sampling decision."""

def get_description(self) -> str:
"""Returns a description of the sampler."""
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Copyright The OpenTelemetry Authors
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from __future__ import annotations

from typing import Sequence

from opentelemetry.context import Context
from opentelemetry.trace import Link, SpanKind, TraceState, get_current_span
from opentelemetry.util.types import Attributes

from ._composable import ComposableSampler, SamplingIntent
from ._trace_state import OtelTraceState
from ._util import (
INVALID_THRESHOLD,
MIN_THRESHOLD,
is_valid_threshold,
)


class _ComposableParentThreshold(ComposableSampler):
def __init__(self, root_sampler: ComposableSampler):
self._root_sampler = root_sampler
self._description = f"ComposableParentThreshold{{root={root_sampler.get_description()}}}"

def sampling_intent(
self,
parent_ctx: Context | None,
name: str,
span_kind: SpanKind | None,
attributes: Attributes,
links: Sequence[Link] | None,
trace_state: TraceState | None = None,
) -> SamplingIntent:
parent_span = get_current_span(parent_ctx)
parent_span_ctx = parent_span.get_span_context()
is_root = not parent_span_ctx.is_valid
if is_root:
return self._root_sampler.sampling_intent(
parent_ctx, name, span_kind, attributes, links, trace_state
)

ot_trace_state = OtelTraceState.parse(trace_state)

if is_valid_threshold(ot_trace_state.threshold):
return SamplingIntent(
threshold=ot_trace_state.threshold,
threshold_reliable=True,
)

threshold = (
MIN_THRESHOLD
if parent_span_ctx.trace_flags.sampled
else INVALID_THRESHOLD
)
return SamplingIntent(threshold=threshold, threshold_reliable=False)

def get_description(self) -> str:
return self._description


def composable_parent_threshold(
root_sampler: ComposableSampler,
) -> ComposableSampler:
"""Returns a consistent sampler that respects the sampling decision of
the parent span or falls-back to the given sampler if it is a root span.

- For spans without a parent context, delegate to the root sampler
- For spans with a parent context, returns a SamplingIntent that propagates the parent's sampling decision
- Returns the parent's threshold if available; otherwise, if the parent's sampled flag is set,
returns threshold=0; otherwise, if the parent's sampled flag is not set, no threshold is returned.
- Sets threshold_reliable to match the parent’s reliability, which is true if the parent had a threshold.
- Does not add any attributes

Args:
root_sampler: The root sampler to use for spans without a parent context.
"""
return _ComposableParentThreshold(root_sampler)
Loading
Loading