From 93a29f2ec4974451e3d0008c912ce5f04d1420c7 Mon Sep 17 00:00:00 2001 From: majanjua-amzn Date: Thu, 27 Nov 2025 15:56:34 -0800 Subject: [PATCH 1/5] feat: Add AlwaysRecordSampler --- CHANGELOG.md | 2 + .../trace/_sampling_experimental/__init__.py | 2 + .../_sampling_experimental/_always_record.py | 81 +++++++++++++++++++ .../__init__.py | 0 .../test_always_off.py | 0 .../test_always_on.py | 0 .../test_always_record.py | 77 ++++++++++++++++++ .../test_sampler.py | 0 .../test_traceid_ratio.py | 0 .../test_tracestate.py | 0 10 files changed, 162 insertions(+) create mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_record.py rename opentelemetry-sdk/tests/trace/{composite_sampler => sampling_experimental}/__init__.py (100%) rename opentelemetry-sdk/tests/trace/{composite_sampler => sampling_experimental}/test_always_off.py (100%) rename opentelemetry-sdk/tests/trace/{composite_sampler => sampling_experimental}/test_always_on.py (100%) create mode 100644 opentelemetry-sdk/tests/trace/sampling_experimental/test_always_record.py rename opentelemetry-sdk/tests/trace/{composite_sampler => sampling_experimental}/test_sampler.py (100%) rename opentelemetry-sdk/tests/trace/{composite_sampler => sampling_experimental}/test_traceid_ratio.py (100%) rename opentelemetry-sdk/tests/trace/{composite_sampler => sampling_experimental}/test_tracestate.py (100%) diff --git a/CHANGELOG.md b/CHANGELOG.md index a83e9bfa2f2..c775865130e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4862](https://github.com/open-telemetry/opentelemetry-python/pull/4862)) - `opentelemetry-exporter-otlp-proto-http`: fix retry logic and error handling for connection failures in trace, metric, and log exporters ([#4709](https://github.com/open-telemetry/opentelemetry-python/pull/4709)) +- feat: Add AlwaysRecordSampler + ([#4823](https://github.com/open-telemetry/opentelemetry-python/pull/4823)) ## Version 1.39.0/0.60b0 (2025-12-03) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/__init__.py index 1a8c372276d..4a4e64bfc6b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/__init__.py @@ -13,6 +13,7 @@ # limitations under the License. __all__ = [ + "AlwaysRecordSampler", "ComposableSampler", "SamplingIntent", "composable_always_off", @@ -25,6 +26,7 @@ from ._always_off import composable_always_off from ._always_on import composable_always_on +from ._always_record import AlwaysRecordSampler from ._composable import ComposableSampler, SamplingIntent from ._parent_threshold import composable_parent_threshold from ._sampler import composite_sampler diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_record.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_record.py new file mode 100644 index 00000000000..84637bc1d00 --- /dev/null +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_record.py @@ -0,0 +1,81 @@ +# 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 typing import Optional, Sequence + +from opentelemetry.context import Context +from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult +from opentelemetry.trace import Link, SpanKind +from opentelemetry.trace.span import TraceState +from opentelemetry.util.types import Attributes + + +class AlwaysRecordSampler(Sampler): + """ + This sampler will return the sampling result of the provided `_root_sampler`, unless the + sampling result contains the sampling decision `Decision.DROP`, in which case, a + new sampling result will be returned that is functionally equivalent to the original, except that + it contains the sampling decision `SamplingDecision.RECORD_ONLY`. This ensures that all + spans are recorded, with no change to sampling. + + The intended use case of this sampler is to provide a means of sending all spans to a + processor without having an impact on the sampling rate. This may be desirable if a user wishes + to count or otherwise measure all spans produced in a service, without incurring the cost of 100% + sampling. + """ + + _root_sampler: Sampler + + def __init__(self, root_sampler: Sampler): + if not root_sampler: + raise ValueError("root_sampler must not be None") + self._root_sampler = root_sampler + + def should_sample( + self, + parent_context: Optional["Context"], + trace_id: int, + name: str, + kind: Optional[SpanKind] = None, + attributes: Attributes = None, + links: Optional[Sequence["Link"]] = None, + trace_state: Optional["TraceState"] = None, + ) -> "SamplingResult": + result: SamplingResult = self._root_sampler.should_sample( + parent_context, + trace_id, + name, + kind, + attributes, + links, + trace_state, + ) + if result.decision is Decision.DROP: + result = _wrap_result_with_record_only_result(result, attributes) + return result + + def get_description(self): + return ( + "AlwaysRecordSampler{" + self._root_sampler.get_description() + "}" + ) + + +def _wrap_result_with_record_only_result( + result: SamplingResult, attributes: Attributes +) -> SamplingResult: + return SamplingResult( + Decision.RECORD_ONLY, + attributes, + result.trace_state, + ) diff --git a/opentelemetry-sdk/tests/trace/composite_sampler/__init__.py b/opentelemetry-sdk/tests/trace/sampling_experimental/__init__.py similarity index 100% rename from opentelemetry-sdk/tests/trace/composite_sampler/__init__.py rename to opentelemetry-sdk/tests/trace/sampling_experimental/__init__.py diff --git a/opentelemetry-sdk/tests/trace/composite_sampler/test_always_off.py b/opentelemetry-sdk/tests/trace/sampling_experimental/test_always_off.py similarity index 100% rename from opentelemetry-sdk/tests/trace/composite_sampler/test_always_off.py rename to opentelemetry-sdk/tests/trace/sampling_experimental/test_always_off.py diff --git a/opentelemetry-sdk/tests/trace/composite_sampler/test_always_on.py b/opentelemetry-sdk/tests/trace/sampling_experimental/test_always_on.py similarity index 100% rename from opentelemetry-sdk/tests/trace/composite_sampler/test_always_on.py rename to opentelemetry-sdk/tests/trace/sampling_experimental/test_always_on.py diff --git a/opentelemetry-sdk/tests/trace/sampling_experimental/test_always_record.py b/opentelemetry-sdk/tests/trace/sampling_experimental/test_always_record.py new file mode 100644 index 00000000000..c556eb9f4bc --- /dev/null +++ b/opentelemetry-sdk/tests/trace/sampling_experimental/test_always_record.py @@ -0,0 +1,77 @@ +from unittest import TestCase +from unittest.mock import MagicMock + +from opentelemetry.context import Context +from opentelemetry.sdk.trace._sampling_experimental import AlwaysRecordSampler +from opentelemetry.sdk.trace.sampling import ( + Decision, + Sampler, + SamplingResult, + StaticSampler, +) +from opentelemetry.trace import SpanKind +from opentelemetry.trace.span import TraceState +from opentelemetry.util.types import Attributes + + +class TestAlwaysRecordSampler(TestCase): + def setUp(self): + self.mock_sampler: Sampler = MagicMock() + self.sampler: Sampler = AlwaysRecordSampler(self.mock_sampler) + + def test_get_description(self): + static_sampler: Sampler = StaticSampler(Decision.DROP) + test_sampler: Sampler = AlwaysRecordSampler(static_sampler) + self.assertEqual( + "AlwaysRecordSampler{AlwaysOffSampler}", + test_sampler.get_description(), + ) + + def test_record_and_sample_sampling_decision(self): + self.validate_should_sample( + Decision.RECORD_AND_SAMPLE, Decision.RECORD_AND_SAMPLE + ) + + def test_record_only_sampling_decision(self): + self.validate_should_sample(Decision.RECORD_ONLY, Decision.RECORD_ONLY) + + def test_drop_sampling_decision(self): + self.validate_should_sample(Decision.DROP, Decision.RECORD_ONLY) + + def validate_should_sample( + self, root_decision: Decision, expected_decision: Decision + ): + root_result: SamplingResult = _build_root_sampling_result( + root_decision + ) + self.mock_sampler.should_sample.return_value = root_result + actual_result: SamplingResult = self.sampler.should_sample( + parent_context=Context(), + trace_id=0, + name="name", + kind=SpanKind.CLIENT, + attributes={"key": root_decision.name}, + trace_state=TraceState(), + ) + + if root_decision == expected_decision: + self.assertEqual(actual_result, root_result) + self.assertEqual(actual_result.decision, root_decision) + else: + self.assertNotEqual(actual_result, root_result) + self.assertEqual(actual_result.decision, expected_decision) + + self.assertEqual(actual_result.attributes, root_result.attributes) + self.assertEqual(actual_result.trace_state, root_result.trace_state) + + +def _build_root_sampling_result(sampling_decision: Decision): + sampling_attr: Attributes = {"key": sampling_decision.name} + sampling_trace_state: TraceState = TraceState() + sampling_trace_state.add("key", sampling_decision.name) + sampling_result: SamplingResult = SamplingResult( + decision=sampling_decision, + attributes=sampling_attr, + trace_state=sampling_trace_state, + ) + return sampling_result diff --git a/opentelemetry-sdk/tests/trace/composite_sampler/test_sampler.py b/opentelemetry-sdk/tests/trace/sampling_experimental/test_sampler.py similarity index 100% rename from opentelemetry-sdk/tests/trace/composite_sampler/test_sampler.py rename to opentelemetry-sdk/tests/trace/sampling_experimental/test_sampler.py diff --git a/opentelemetry-sdk/tests/trace/composite_sampler/test_traceid_ratio.py b/opentelemetry-sdk/tests/trace/sampling_experimental/test_traceid_ratio.py similarity index 100% rename from opentelemetry-sdk/tests/trace/composite_sampler/test_traceid_ratio.py rename to opentelemetry-sdk/tests/trace/sampling_experimental/test_traceid_ratio.py diff --git a/opentelemetry-sdk/tests/trace/composite_sampler/test_tracestate.py b/opentelemetry-sdk/tests/trace/sampling_experimental/test_tracestate.py similarity index 100% rename from opentelemetry-sdk/tests/trace/composite_sampler/test_tracestate.py rename to opentelemetry-sdk/tests/trace/sampling_experimental/test_tracestate.py From 3ccb0940a75c5b0dc31d4edd41570a35813ca103 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Mon, 22 Jun 2026 14:19:25 +0200 Subject: [PATCH 2/5] Apply feedback. --- CHANGELOG.md | 2 -- .../_sampling_experimental/_always_record.py | 21 +++++-------------- 2 files changed, 5 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c775865130e..a83e9bfa2f2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,8 +30,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4862](https://github.com/open-telemetry/opentelemetry-python/pull/4862)) - `opentelemetry-exporter-otlp-proto-http`: fix retry logic and error handling for connection failures in trace, metric, and log exporters ([#4709](https://github.com/open-telemetry/opentelemetry-python/pull/4709)) -- feat: Add AlwaysRecordSampler - ([#4823](https://github.com/open-telemetry/opentelemetry-python/pull/4823)) ## Version 1.39.0/0.60b0 (2025-12-03) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_record.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_record.py index 84637bc1d00..0cde58452e9 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_record.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_record.py @@ -26,7 +26,7 @@ class AlwaysRecordSampler(Sampler): This sampler will return the sampling result of the provided `_root_sampler`, unless the sampling result contains the sampling decision `Decision.DROP`, in which case, a new sampling result will be returned that is functionally equivalent to the original, except that - it contains the sampling decision `SamplingDecision.RECORD_ONLY`. This ensures that all + it contains the sampling decision `Decision.RECORD_ONLY`. This ensures that all spans are recorded, with no change to sampling. The intended use case of this sampler is to provide a means of sending all spans to a @@ -38,7 +38,7 @@ class AlwaysRecordSampler(Sampler): _root_sampler: Sampler def __init__(self, root_sampler: Sampler): - if not root_sampler: + if root_sampler is None: raise ValueError("root_sampler must not be None") self._root_sampler = root_sampler @@ -62,20 +62,9 @@ def should_sample( trace_state, ) if result.decision is Decision.DROP: - result = _wrap_result_with_record_only_result(result, attributes) + result = SamplingResult(Decision.RECORD_ONLY, result.attributes, result.trace_state) + return result def get_description(self): - return ( - "AlwaysRecordSampler{" + self._root_sampler.get_description() + "}" - ) - - -def _wrap_result_with_record_only_result( - result: SamplingResult, attributes: Attributes -) -> SamplingResult: - return SamplingResult( - Decision.RECORD_ONLY, - attributes, - result.trace_state, - ) + return f"AlwaysRecordSampler{{{self._root_sampler.get_description()}}}" From 7c9afe9e740fa842a504b86b32d455b246ee86aa Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Mon, 22 Jun 2026 15:17:29 +0200 Subject: [PATCH 3/5] Prepare for merging upstream. --- .../sdk/trace/{_sampling_experimental => }/_always_record.py | 0 .../opentelemetry/sdk/trace/_sampling_experimental/__init__.py | 2 -- .../trace/{sampling_experimental => }/test_always_record.py | 0 3 files changed, 2 deletions(-) rename opentelemetry-sdk/src/opentelemetry/sdk/trace/{_sampling_experimental => }/_always_record.py (100%) rename opentelemetry-sdk/tests/trace/{sampling_experimental => }/test_always_record.py (100%) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_record.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_always_record.py similarity index 100% rename from opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/_always_record.py rename to opentelemetry-sdk/src/opentelemetry/sdk/trace/_always_record.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/__init__.py index 4a4e64bfc6b..1a8c372276d 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_sampling_experimental/__init__.py @@ -13,7 +13,6 @@ # limitations under the License. __all__ = [ - "AlwaysRecordSampler", "ComposableSampler", "SamplingIntent", "composable_always_off", @@ -26,7 +25,6 @@ from ._always_off import composable_always_off from ._always_on import composable_always_on -from ._always_record import AlwaysRecordSampler from ._composable import ComposableSampler, SamplingIntent from ._parent_threshold import composable_parent_threshold from ._sampler import composite_sampler diff --git a/opentelemetry-sdk/tests/trace/sampling_experimental/test_always_record.py b/opentelemetry-sdk/tests/trace/test_always_record.py similarity index 100% rename from opentelemetry-sdk/tests/trace/sampling_experimental/test_always_record.py rename to opentelemetry-sdk/tests/trace/test_always_record.py From 6333264f5b7b8ab5b6765e67867391da18c59bf6 Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Mon, 22 Jun 2026 18:36:02 +0200 Subject: [PATCH 4/5] restore tests. --- .../opentelemetry/sdk/trace/_always_record.py | 70 ----------------- .../src/opentelemetry/sdk/trace/sampling.py | 47 +++++++++++ .../__init__.py | 0 .../test_always_off.py | 0 .../test_always_on.py | 0 .../test_rule_based.py | 0 .../test_sampler.py | 0 .../test_traceid_ratio.py | 0 .../test_tracestate.py | 0 .../tests/trace/test_always_record.py | 77 ------------------- .../tests/trace/test_sampling.py | 51 ++++++++++++ 11 files changed, 98 insertions(+), 147 deletions(-) delete mode 100644 opentelemetry-sdk/src/opentelemetry/sdk/trace/_always_record.py rename opentelemetry-sdk/tests/trace/{sampling_experimental => composite_sampler}/__init__.py (100%) rename opentelemetry-sdk/tests/trace/{sampling_experimental => composite_sampler}/test_always_off.py (100%) rename opentelemetry-sdk/tests/trace/{sampling_experimental => composite_sampler}/test_always_on.py (100%) rename opentelemetry-sdk/tests/trace/{sampling_experimental => composite_sampler}/test_rule_based.py (100%) rename opentelemetry-sdk/tests/trace/{sampling_experimental => composite_sampler}/test_sampler.py (100%) rename opentelemetry-sdk/tests/trace/{sampling_experimental => composite_sampler}/test_traceid_ratio.py (100%) rename opentelemetry-sdk/tests/trace/{sampling_experimental => composite_sampler}/test_tracestate.py (100%) delete mode 100644 opentelemetry-sdk/tests/trace/test_always_record.py diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_always_record.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/_always_record.py deleted file mode 100644 index 0cde58452e9..00000000000 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/_always_record.py +++ /dev/null @@ -1,70 +0,0 @@ -# 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 typing import Optional, Sequence - -from opentelemetry.context import Context -from opentelemetry.sdk.trace.sampling import Decision, Sampler, SamplingResult -from opentelemetry.trace import Link, SpanKind -from opentelemetry.trace.span import TraceState -from opentelemetry.util.types import Attributes - - -class AlwaysRecordSampler(Sampler): - """ - This sampler will return the sampling result of the provided `_root_sampler`, unless the - sampling result contains the sampling decision `Decision.DROP`, in which case, a - new sampling result will be returned that is functionally equivalent to the original, except that - it contains the sampling decision `Decision.RECORD_ONLY`. This ensures that all - spans are recorded, with no change to sampling. - - The intended use case of this sampler is to provide a means of sending all spans to a - processor without having an impact on the sampling rate. This may be desirable if a user wishes - to count or otherwise measure all spans produced in a service, without incurring the cost of 100% - sampling. - """ - - _root_sampler: Sampler - - def __init__(self, root_sampler: Sampler): - if root_sampler is None: - raise ValueError("root_sampler must not be None") - self._root_sampler = root_sampler - - def should_sample( - self, - parent_context: Optional["Context"], - trace_id: int, - name: str, - kind: Optional[SpanKind] = None, - attributes: Attributes = None, - links: Optional[Sequence["Link"]] = None, - trace_state: Optional["TraceState"] = None, - ) -> "SamplingResult": - result: SamplingResult = self._root_sampler.should_sample( - parent_context, - trace_id, - name, - kind, - attributes, - links, - trace_state, - ) - if result.decision is Decision.DROP: - result = SamplingResult(Decision.RECORD_ONLY, result.attributes, result.trace_state) - - return result - - def get_description(self): - return f"AlwaysRecordSampler{{{self._root_sampler.get_description()}}}" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 1652220dfc9..1df9654a0b4 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -369,6 +369,53 @@ def get_description(self): return f"ParentBased{{root:{self._root.get_description()},remoteParentSampled:{self._remote_parent_sampled.get_description()},remoteParentNotSampled:{self._remote_parent_not_sampled.get_description()},localParentSampled:{self._local_parent_sampled.get_description()},localParentNotSampled:{self._local_parent_not_sampled.get_description()}}}" +class AlwaysRecordSampler(Sampler): + """ + This sampler will return the sampling result of the provided `root`, unless the + sampling result contains the sampling decision `Decision.DROP`, in which case, a + new sampling result will be returned that is functionally equivalent to the original, except that + it contains the sampling decision `Decision.RECORD_ONLY`. This ensures that all + spans are recorded, with no change to sampling. + + The intended use case of this sampler is to provide a means of sending all spans to a + processor without having an impact on the sampling rate. This may be desirable if a user wishes + to count or otherwise measure all spans produced in a service, without incurring the cost of 100% + sampling. + """ + + def __init__(self, root: Sampler): + if root is None: + raise ValueError("root must not be None") + self._root = root + + def should_sample( + self, + parent_context: Context | None, + trace_id: int, + name: str, + kind: SpanKind | None = None, + attributes: Attributes = None, + links: Sequence[Link] | None = None, + trace_state: TraceState | None = None, + ) -> SamplingResult: + result: SamplingResult = self._root.should_sample( + parent_context, + trace_id, + name, + kind, + attributes, + links, + trace_state, + ) + if result.decision is Decision.DROP: + result = SamplingResult(Decision.RECORD_ONLY, result.attributes, result.trace_state) + + return result + + def get_description(self): + return f"AlwaysRecordSampler{{{self._root.get_description()}}}" + + DEFAULT_OFF = ParentBased(ALWAYS_OFF) """Sampler that respects its parent span's sampling decision, but otherwise never samples.""" diff --git a/opentelemetry-sdk/tests/trace/sampling_experimental/__init__.py b/opentelemetry-sdk/tests/trace/composite_sampler/__init__.py similarity index 100% rename from opentelemetry-sdk/tests/trace/sampling_experimental/__init__.py rename to opentelemetry-sdk/tests/trace/composite_sampler/__init__.py diff --git a/opentelemetry-sdk/tests/trace/sampling_experimental/test_always_off.py b/opentelemetry-sdk/tests/trace/composite_sampler/test_always_off.py similarity index 100% rename from opentelemetry-sdk/tests/trace/sampling_experimental/test_always_off.py rename to opentelemetry-sdk/tests/trace/composite_sampler/test_always_off.py diff --git a/opentelemetry-sdk/tests/trace/sampling_experimental/test_always_on.py b/opentelemetry-sdk/tests/trace/composite_sampler/test_always_on.py similarity index 100% rename from opentelemetry-sdk/tests/trace/sampling_experimental/test_always_on.py rename to opentelemetry-sdk/tests/trace/composite_sampler/test_always_on.py diff --git a/opentelemetry-sdk/tests/trace/sampling_experimental/test_rule_based.py b/opentelemetry-sdk/tests/trace/composite_sampler/test_rule_based.py similarity index 100% rename from opentelemetry-sdk/tests/trace/sampling_experimental/test_rule_based.py rename to opentelemetry-sdk/tests/trace/composite_sampler/test_rule_based.py diff --git a/opentelemetry-sdk/tests/trace/sampling_experimental/test_sampler.py b/opentelemetry-sdk/tests/trace/composite_sampler/test_sampler.py similarity index 100% rename from opentelemetry-sdk/tests/trace/sampling_experimental/test_sampler.py rename to opentelemetry-sdk/tests/trace/composite_sampler/test_sampler.py diff --git a/opentelemetry-sdk/tests/trace/sampling_experimental/test_traceid_ratio.py b/opentelemetry-sdk/tests/trace/composite_sampler/test_traceid_ratio.py similarity index 100% rename from opentelemetry-sdk/tests/trace/sampling_experimental/test_traceid_ratio.py rename to opentelemetry-sdk/tests/trace/composite_sampler/test_traceid_ratio.py diff --git a/opentelemetry-sdk/tests/trace/sampling_experimental/test_tracestate.py b/opentelemetry-sdk/tests/trace/composite_sampler/test_tracestate.py similarity index 100% rename from opentelemetry-sdk/tests/trace/sampling_experimental/test_tracestate.py rename to opentelemetry-sdk/tests/trace/composite_sampler/test_tracestate.py diff --git a/opentelemetry-sdk/tests/trace/test_always_record.py b/opentelemetry-sdk/tests/trace/test_always_record.py deleted file mode 100644 index c556eb9f4bc..00000000000 --- a/opentelemetry-sdk/tests/trace/test_always_record.py +++ /dev/null @@ -1,77 +0,0 @@ -from unittest import TestCase -from unittest.mock import MagicMock - -from opentelemetry.context import Context -from opentelemetry.sdk.trace._sampling_experimental import AlwaysRecordSampler -from opentelemetry.sdk.trace.sampling import ( - Decision, - Sampler, - SamplingResult, - StaticSampler, -) -from opentelemetry.trace import SpanKind -from opentelemetry.trace.span import TraceState -from opentelemetry.util.types import Attributes - - -class TestAlwaysRecordSampler(TestCase): - def setUp(self): - self.mock_sampler: Sampler = MagicMock() - self.sampler: Sampler = AlwaysRecordSampler(self.mock_sampler) - - def test_get_description(self): - static_sampler: Sampler = StaticSampler(Decision.DROP) - test_sampler: Sampler = AlwaysRecordSampler(static_sampler) - self.assertEqual( - "AlwaysRecordSampler{AlwaysOffSampler}", - test_sampler.get_description(), - ) - - def test_record_and_sample_sampling_decision(self): - self.validate_should_sample( - Decision.RECORD_AND_SAMPLE, Decision.RECORD_AND_SAMPLE - ) - - def test_record_only_sampling_decision(self): - self.validate_should_sample(Decision.RECORD_ONLY, Decision.RECORD_ONLY) - - def test_drop_sampling_decision(self): - self.validate_should_sample(Decision.DROP, Decision.RECORD_ONLY) - - def validate_should_sample( - self, root_decision: Decision, expected_decision: Decision - ): - root_result: SamplingResult = _build_root_sampling_result( - root_decision - ) - self.mock_sampler.should_sample.return_value = root_result - actual_result: SamplingResult = self.sampler.should_sample( - parent_context=Context(), - trace_id=0, - name="name", - kind=SpanKind.CLIENT, - attributes={"key": root_decision.name}, - trace_state=TraceState(), - ) - - if root_decision == expected_decision: - self.assertEqual(actual_result, root_result) - self.assertEqual(actual_result.decision, root_decision) - else: - self.assertNotEqual(actual_result, root_result) - self.assertEqual(actual_result.decision, expected_decision) - - self.assertEqual(actual_result.attributes, root_result.attributes) - self.assertEqual(actual_result.trace_state, root_result.trace_state) - - -def _build_root_sampling_result(sampling_decision: Decision): - sampling_attr: Attributes = {"key": sampling_decision.name} - sampling_trace_state: TraceState = TraceState() - sampling_trace_state.add("key", sampling_decision.name) - sampling_result: SamplingResult = SamplingResult( - decision=sampling_decision, - attributes=sampling_attr, - trace_state=sampling_trace_state, - ) - return sampling_result diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index 1d33a1a2c2c..1dab53f37e8 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -4,6 +4,7 @@ import contextlib import sys import unittest +import unittest.mock from opentelemetry import context as context_api from opentelemetry import trace @@ -524,3 +525,53 @@ def implicit_parent_context(span: trace.Span): context_api.detach(token) self.exec_parent_based(implicit_parent_context) + + +class TestAlwaysRecordSampler(unittest.TestCase): + def setUp(self): + self.mock_sampler: sampling.Sampler = unittest.mock.MagicMock() + self.sampler: sampling.Sampler = sampling.AlwaysRecordSampler(self.mock_sampler) + + def test_get_description(self): + static_sampler: sampling.Sampler = sampling.StaticSampler(sampling.Decision.DROP) + test_sampler: sampling.Sampler = sampling.AlwaysRecordSampler(static_sampler) + self.assertEqual( + "AlwaysRecordSampler{AlwaysOffSampler}", + test_sampler.get_description(), + ) + + def test_record_and_sample_sampling_decision(self): + self.validate_should_sample( + sampling.Decision.RECORD_AND_SAMPLE, sampling.Decision.RECORD_AND_SAMPLE + ) + + def test_record_only_sampling_decision(self): + self.validate_should_sample(sampling.Decision.RECORD_ONLY, sampling.Decision.RECORD_ONLY) + + def test_drop_sampling_decision(self): + self.validate_should_sample(sampling.Decision.DROP, sampling.Decision.RECORD_ONLY) + + def validate_should_sample( + self, root_decision: sampling.Decision, expected_decision: sampling.Decision + ): + trace_state: trace.TraceState = trace.TraceState() + trace_state.add("key", root_decision.name) + root_result: sampling.SamplingResult = sampling.SamplingResult( + attributes={"key", root_decision.name}, + decision=root_decision, + trace_state=trace_state, + ) + self.mock_sampler.should_sample.return_value = root_result + + actual_result: sampling.SamplingResult = self.sampler.should_sample( + parent_context=context_api.Context(), + trace_id=0, + name="name", + kind=trace.SpanKind.CLIENT, + attributes={"key": root_decision.name}, + trace_state=trace.TraceState(), + ) + + self.assertEqual(actual_result.decision, expected_decision) + self.assertEqual(actual_result.attributes, root_result.attributes) + self.assertEqual(actual_result.trace_state, root_result.trace_state) From ed366247d7882967768ac80d583f701646ab7cdd Mon Sep 17 00:00:00 2001 From: Carlos Alberto Cortez Date: Fri, 26 Jun 2026 02:23:55 +0200 Subject: [PATCH 5/5] Lint. --- .../src/opentelemetry/sdk/trace/sampling.py | 4 ++- .../tests/trace/test_sampling.py | 27 ++++++++++++++----- 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py index 1df9654a0b4..cee329bb0ae 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/sampling.py @@ -408,7 +408,9 @@ def should_sample( trace_state, ) if result.decision is Decision.DROP: - result = SamplingResult(Decision.RECORD_ONLY, result.attributes, result.trace_state) + result = SamplingResult( + Decision.RECORD_ONLY, result.attributes, result.trace_state + ) return result diff --git a/opentelemetry-sdk/tests/trace/test_sampling.py b/opentelemetry-sdk/tests/trace/test_sampling.py index 1dab53f37e8..6a5c830f5e8 100644 --- a/opentelemetry-sdk/tests/trace/test_sampling.py +++ b/opentelemetry-sdk/tests/trace/test_sampling.py @@ -530,11 +530,17 @@ def implicit_parent_context(span: trace.Span): class TestAlwaysRecordSampler(unittest.TestCase): def setUp(self): self.mock_sampler: sampling.Sampler = unittest.mock.MagicMock() - self.sampler: sampling.Sampler = sampling.AlwaysRecordSampler(self.mock_sampler) + self.sampler: sampling.Sampler = sampling.AlwaysRecordSampler( + self.mock_sampler + ) def test_get_description(self): - static_sampler: sampling.Sampler = sampling.StaticSampler(sampling.Decision.DROP) - test_sampler: sampling.Sampler = sampling.AlwaysRecordSampler(static_sampler) + static_sampler: sampling.Sampler = sampling.StaticSampler( + sampling.Decision.DROP + ) + test_sampler: sampling.Sampler = sampling.AlwaysRecordSampler( + static_sampler + ) self.assertEqual( "AlwaysRecordSampler{AlwaysOffSampler}", test_sampler.get_description(), @@ -542,17 +548,24 @@ def test_get_description(self): def test_record_and_sample_sampling_decision(self): self.validate_should_sample( - sampling.Decision.RECORD_AND_SAMPLE, sampling.Decision.RECORD_AND_SAMPLE + sampling.Decision.RECORD_AND_SAMPLE, + sampling.Decision.RECORD_AND_SAMPLE, ) def test_record_only_sampling_decision(self): - self.validate_should_sample(sampling.Decision.RECORD_ONLY, sampling.Decision.RECORD_ONLY) + self.validate_should_sample( + sampling.Decision.RECORD_ONLY, sampling.Decision.RECORD_ONLY + ) def test_drop_sampling_decision(self): - self.validate_should_sample(sampling.Decision.DROP, sampling.Decision.RECORD_ONLY) + self.validate_should_sample( + sampling.Decision.DROP, sampling.Decision.RECORD_ONLY + ) def validate_should_sample( - self, root_decision: sampling.Decision, expected_decision: sampling.Decision + self, + root_decision: sampling.Decision, + expected_decision: sampling.Decision, ): trace_state: trace.TraceState = trace.TraceState() trace_state.add("key", root_decision.name)