From e5d776505be9845a2e4b8b029d4d1eff40387e07 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Tue, 30 Sep 2025 09:43:15 +0200 Subject: [PATCH 1/7] opentelemetry-sdk: Implement tracer configurator Implement part of the tracing SDK spec to configure the tracers https://opentelemetry.io/docs/specs/otel/trace/sdk/#configuration At the moment this adds helper in order to enable or disable a tracer after it has been created. The spec in is development so attributes, helpers and classes are prefixed with underscore. TODO: hook into sdk configuration --- .../sdk/_configuration/__init__.py | 1 + .../src/opentelemetry/sdk/trace/__init__.py | 115 ++++++++++++++++-- opentelemetry-sdk/tests/trace/test_trace.py | 63 +++++++++- 3 files changed, 170 insertions(+), 9 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 7c0d0468f8..963e3a236b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -211,6 +211,7 @@ def _init_tracing( resource: Resource | None = None, exporter_args_map: ExporterArgsMap | None = None, ): + # FIXME: get configurator from entrypoints / env var provider = TracerProvider( id_generator=id_generator, sampler=sampler, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 0e7e1f6db3..776e150363 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -21,6 +21,7 @@ import threading import traceback import typing +from dataclasses import dataclass from os import environ from time import time_ns from types import MappingProxyType, TracebackType @@ -39,6 +40,7 @@ Union, ) from warnings import filterwarnings +from weakref import WeakSet from typing_extensions import deprecated @@ -71,7 +73,7 @@ EXCEPTION_STACKTRACE, EXCEPTION_TYPE, ) -from opentelemetry.trace import NoOpTracer, SpanContext +from opentelemetry.trace import INVALID_SPAN, NoOpTracer, SpanContext from opentelemetry.trace.status import Status, StatusCode from opentelemetry.util import types from opentelemetry.util._decorator import _agnosticcontextmanager @@ -1071,6 +1073,11 @@ class _Span(Span): """ +@dataclass +class _TracerConfig: + is_enabled: bool + + class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`.""" @@ -1085,6 +1092,8 @@ def __init__( instrumentation_info: InstrumentationInfo, span_limits: SpanLimits, instrumentation_scope: InstrumentationScope, + *, + _tracer_config: _TracerConfig | None = None, ) -> None: self.sampler = sampler self.resource = resource @@ -1094,6 +1103,19 @@ def __init__( self._span_limits = span_limits self._instrumentation_scope = instrumentation_scope + self._enabled = ( + _tracer_config.is_enabled if _tracer_config is not None else True + ) + + def _update_tracer_config(self, tracer_config: _TracerConfig): + self._enabled = tracer_config.is_enabled + + @property + def _is_enabled(self) -> bool: + """Instrumentations needs to call this API each time to check if they should + create a new span.""" + return self._enabled + @_agnosticcontextmanager # pylint: disable=protected-access def start_as_current_span( self, @@ -1136,6 +1158,9 @@ def start_span( # pylint: disable=too-many-locals record_exception: bool = True, set_status_on_exception: bool = True, ) -> trace_api.Span: + if not self._is_enabled: + return INVALID_SPAN + parent_span_context = trace_api.get_current_span( context ).get_span_context() @@ -1202,6 +1227,26 @@ def start_span( # pylint: disable=too-many-locals return span +_TracerConfiguratorT = Callable[[InstrumentationScope], _TracerConfig] + + +def _default_tracer_configurator( + tracer_scope: InstrumentationScope, +) -> _TracerConfig: + """Default configurator functions for Tracers + + In order to update Tracers configs you need to call + TracerProvider._set_tracer_configurator with a function + implementing this interface returning a Tracer Config.""" + return _TracerConfig(is_enabled=True) + + +def _disable_tracer_configurator( + tracer_scope: InstrumentationScope, +) -> _TracerConfig: + return _TracerConfig(is_enabled=False) + + class TracerProvider(trace_api.TracerProvider): """See `opentelemetry.trace.TracerProvider`.""" @@ -1215,6 +1260,8 @@ def __init__( ] = None, id_generator: Optional[IdGenerator] = None, span_limits: Optional[SpanLimits] = None, + *, + _tracer_configurator: Optional[_TracerConfiguratorT] = None, ) -> None: self._active_span_processor = ( active_span_processor or SynchronousMultiSpanProcessor() @@ -1238,6 +1285,49 @@ def __init__( if shutdown_on_exit: self._atexit_handler = atexit.register(self.shutdown) + self._tracer_configurator = ( + _tracer_configurator or _default_tracer_configurator + ) + self._cached_tracers: WeakSet[Tracer] = WeakSet() + + def _set_tracer_configurator( + self, *, tracer_configurator: _TracerConfiguratorT + ): + self._tracer_configurator = tracer_configurator + self._update_tracers(tracer_configurator) + + def _update_tracers( + self, + *, + tracer_names: Optional[Sequence[str]] = None, + tracer_configurator: _TracerConfiguratorT, + ): + if tracer_names: + tracers = [ + t + for t in self._cached_tracers + if t._instrumentation_scope.name in tracer_names + ] + else: + tracers = self._cached_tracers + for tracer in tracers: + tracer_config = tracer_configurator(tracer._instrumentation_scope) + tracer._update_tracer_config(tracer_config) + + def _enable_tracers(self, *, tracer_names: Optional[Sequence[str]] = None): + self._update_tracers( + tracer_names=tracer_names, + tracer_configurator=_default_tracer_configurator, + ) + + def _disable_tracers( + self, *, tracer_names: Optional[Sequence[str]] = None + ): + self._update_tracers( + tracer_names=tracer_names, + tracer_configurator=_disable_tracer_configurator, + ) + @property def resource(self) -> Resource: return self._resource @@ -1272,21 +1362,30 @@ def get_tracer( schema_url, ) - return Tracer( + instrumentation_scope = InstrumentationScope( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + attributes, + ) + + tracer_config = self._tracer_configurator(instrumentation_scope) + + tracer = Tracer( self.sampler, self.resource, self._active_span_processor, self.id_generator, instrumentation_info, self._span_limits, - InstrumentationScope( - instrumenting_module_name, - instrumenting_library_version, - schema_url, - attributes, - ), + instrumentation_scope, + _tracer_config=tracer_config, ) + self._cached_tracers.add(tracer) + + return tracer + def add_span_processor(self, span_processor: SpanProcessor) -> None: """Registers a new :class:`SpanProcessor` for this `TracerProvider`. diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index b83b000f4d..765e9b1949 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -43,7 +43,7 @@ OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) -from opentelemetry.sdk.trace import Resource, TracerProvider +from opentelemetry.sdk.trace import Resource, TracerProvider, _TracerConfig from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( ALWAYS_OFF, @@ -64,6 +64,7 @@ get_tracer, set_tracer_provider, ) +from opentelemetry.trace.span import INVALID_SPAN class TestTracer(unittest.TestCase): @@ -196,6 +197,43 @@ def test_get_tracer_with_sdk_disabled(self): tracer_provider.get_tracer(Mock()), trace_api.NoOpTracer ) + def test_update_tracer_config(self): + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer( + "module_name", + "library_version", + "schema_url", + {}, + ) + + self.assertEqual(tracer._is_enabled, True) + + tracer_config = _TracerConfig(is_enabled=False) + tracer._update_tracer_config(tracer_config) + self.assertEqual(tracer._is_enabled, False) + + tracer_config = _TracerConfig(is_enabled=True) + tracer._update_tracer_config(tracer_config) + self.assertEqual(tracer._is_enabled, True) + + def test_start_span_returns_invalid_span_if_not_enabled(self): + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer( + "module_name", + "library_version", + "schema_url", + {}, + ) + + self.assertEqual(tracer._is_enabled, True) + + tracer_config = _TracerConfig(is_enabled=False) + tracer._update_tracer_config(tracer_config) + self.assertEqual(tracer._is_enabled, False) + + span = tracer.start_span(name="invalid span") + self.assertIs(span, INVALID_SPAN) + class TestTracerSampling(unittest.TestCase): def tearDown(self): @@ -2183,6 +2221,29 @@ def test_tracer_provider_init_default(self, resource_patch, sample_patch): self.assertIsNotNone(tracer_provider._span_limits) self.assertIsNotNone(tracer_provider._atexit_handler) + def test_tracer_configurator(self): + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer( + "module_name", + "library_version", + "schema_url", + {}, + ) + # pylint: disable=protected-access + self.assertEqual(tracer._instrumentation_scope.name, "module_name") + + # pylint: disable=protected-access + self.assertEqual(tracer._is_enabled, True) + + tracer_provider._disable_tracers(tracer_names=["different_name"]) + self.assertEqual(tracer._is_enabled, True) + + tracer_provider._disable_tracers(tracer_names=["module_name"]) + self.assertEqual(tracer._is_enabled, False) + + tracer_provider._enable_tracers(tracer_names=["module_name"]) + self.assertEqual(tracer._is_enabled, True) + class TestRandomIdGenerator(unittest.TestCase): _TRACE_ID_MAX_VALUE = 2**128 - 1 From 6e232613df9b93a407dd62e652963c792df41127 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 17 Dec 2025 12:43:26 +0100 Subject: [PATCH 2/7] Please lint --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 7 +++++-- opentelemetry-sdk/tests/trace/test_trace.py | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 776e150363..3e249c0d68 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -1093,7 +1093,7 @@ def __init__( span_limits: SpanLimits, instrumentation_scope: InstrumentationScope, *, - _tracer_config: _TracerConfig | None = None, + _tracer_config: Optional[_TracerConfig] = None, ) -> None: self.sampler = sampler self.resource = resource @@ -1294,7 +1294,7 @@ def _set_tracer_configurator( self, *, tracer_configurator: _TracerConfiguratorT ): self._tracer_configurator = tracer_configurator - self._update_tracers(tracer_configurator) + self._update_tracers(tracer_configurator=tracer_configurator) def _update_tracers( self, @@ -1302,6 +1302,9 @@ def _update_tracers( tracer_names: Optional[Sequence[str]] = None, tracer_configurator: _TracerConfiguratorT, ): + # pylint: disable=protected-access + # FIXME: the configurator should be rule based and so the logic to filter to which tracer applies this to + # should be there and not a parameter here so that _enable_tracers / _disable_tracers should call _set_tracer_configurator if tracer_names: tracers = [ t diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 765e9b1949..2f725325ad 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -198,6 +198,7 @@ def test_get_tracer_with_sdk_disabled(self): ) def test_update_tracer_config(self): + # pylint: disable=protected-access tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer( "module_name", @@ -217,6 +218,7 @@ def test_update_tracer_config(self): self.assertEqual(tracer._is_enabled, True) def test_start_span_returns_invalid_span_if_not_enabled(self): + # pylint: disable=protected-access tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer( "module_name", From c4578ac641dbbf2e6e831ddab542a37439e9dfb8 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 17 Dec 2025 15:34:33 +0100 Subject: [PATCH 3/7] Add rule based tracer configurator --- .../src/opentelemetry/sdk/trace/__init__.py | 33 ++++++++++++++++--- 1 file changed, 29 insertions(+), 4 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 3e249c0d68..41b8cfa525 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -294,7 +294,7 @@ def force_flush(self, timeout_millis: int = 30000) -> bool: timeout, False otherwise. """ futures = [] - for sp in self._span_processors: # type: SpanProcessor + for sp in self._span_processors: future = self._executor.submit(sp.force_flush, timeout_millis) futures.append(future) @@ -1228,23 +1228,48 @@ def start_span( # pylint: disable=too-many-locals _TracerConfiguratorT = Callable[[InstrumentationScope], _TracerConfig] +_TracerConfiguratorRulesPredicateT = Callable[ + [Optional[InstrumentationScope]], bool +] +_TracerConfiguratorRulesT = Sequence[ + Tuple[_TracerConfiguratorRulesPredicateT, _TracerConfig] +] + + +class _RuleBaseTracerConfigurator: + def __init__(self, *, rules: _TracerConfiguratorRulesT): + self._rules = rules + + def __call__( + self, tracer_scope: Optional[InstrumentationScope] = None + ) -> _TracerConfig: + for predicate, tracer_config in self._rules: + if predicate(tracer_scope): + return tracer_config + + # if no rule matched return a default one + return _TracerConfig(is_enabled=True) def _default_tracer_configurator( tracer_scope: InstrumentationScope, ) -> _TracerConfig: - """Default configurator functions for Tracers + """Default Tracer Configurator implementation In order to update Tracers configs you need to call TracerProvider._set_tracer_configurator with a function implementing this interface returning a Tracer Config.""" - return _TracerConfig(is_enabled=True) + return _RuleBaseTracerConfigurator( + rules=[(lambda x: True, _TracerConfig(is_enabled=True))], + )(tracer_scope=tracer_scope) def _disable_tracer_configurator( tracer_scope: InstrumentationScope, ) -> _TracerConfig: - return _TracerConfig(is_enabled=False) + return _RuleBaseTracerConfigurator( + rules=[(lambda x: True, _TracerConfig(is_enabled=False))], + )(tracer_scope=tracer_scope) class TracerProvider(trace_api.TracerProvider): From 787e2ef45466675eb1f34bf94c2faf3fd5b1d43f Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 17 Dec 2025 16:01:30 +0100 Subject: [PATCH 4/7] Assume rule based tracer configurator in helpers --- .../src/opentelemetry/sdk/trace/__init__.py | 21 +--- opentelemetry-sdk/tests/trace/test_trace.py | 109 ++++++++++++++++-- 2 files changed, 105 insertions(+), 25 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index 41b8cfa525..771fb2f7e0 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -1324,35 +1324,20 @@ def _set_tracer_configurator( def _update_tracers( self, *, - tracer_names: Optional[Sequence[str]] = None, tracer_configurator: _TracerConfiguratorT, ): # pylint: disable=protected-access - # FIXME: the configurator should be rule based and so the logic to filter to which tracer applies this to - # should be there and not a parameter here so that _enable_tracers / _disable_tracers should call _set_tracer_configurator - if tracer_names: - tracers = [ - t - for t in self._cached_tracers - if t._instrumentation_scope.name in tracer_names - ] - else: - tracers = self._cached_tracers - for tracer in tracers: + for tracer in self._cached_tracers: tracer_config = tracer_configurator(tracer._instrumentation_scope) tracer._update_tracer_config(tracer_config) - def _enable_tracers(self, *, tracer_names: Optional[Sequence[str]] = None): + def _enable_tracers(self): self._update_tracers( - tracer_names=tracer_names, tracer_configurator=_default_tracer_configurator, ) - def _disable_tracers( - self, *, tracer_names: Optional[Sequence[str]] = None - ): + def _disable_tracers(self): self._update_tracers( - tracer_names=tracer_names, tracer_configurator=_disable_tracer_configurator, ) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 2f725325ad..4304f5d9ee 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -43,7 +43,12 @@ OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) -from opentelemetry.sdk.trace import Resource, TracerProvider, _TracerConfig +from opentelemetry.sdk.trace import ( + Resource, + TracerProvider, + _RuleBaseTracerConfigurator, + _TracerConfig, +) from opentelemetry.sdk.trace.id_generator import RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( ALWAYS_OFF, @@ -2223,7 +2228,8 @@ def test_tracer_provider_init_default(self, resource_patch, sample_patch): self.assertIsNotNone(tracer_provider._span_limits) self.assertIsNotNone(tracer_provider._atexit_handler) - def test_tracer_configurator(self): + def test_default_tracer_configurator(self): + # pylint: disable=protected-access tracer_provider = trace.TracerProvider() tracer = tracer_provider.get_tracer( "module_name", @@ -2231,20 +2237,109 @@ def test_tracer_configurator(self): "schema_url", {}, ) - # pylint: disable=protected-access + other_tracer = tracer_provider.get_tracer( + "other_module_name", + "library_version", + "schema_url", + {}, + ) self.assertEqual(tracer._instrumentation_scope.name, "module_name") + self.assertEqual( + other_tracer._instrumentation_scope.name, "other_module_name" + ) + + self.assertEqual(tracer._is_enabled, True) + self.assertEqual(other_tracer._is_enabled, True) + + tracer_provider._disable_tracers() + self.assertEqual(tracer._is_enabled, False) + self.assertEqual(other_tracer._is_enabled, False) + + tracer_provider._enable_tracers() + self.assertEqual(tracer._is_enabled, True) + self.assertEqual(other_tracer._is_enabled, True) + def test_rule_based_tracer_configurator(self): # pylint: disable=protected-access + rules = [ + ( + lambda x: True if x.name == "module_name" else False, + _TracerConfig(is_enabled=True), + ), + ( + lambda x: True if x.name == "other_module_name" else False, + _TracerConfig(is_enabled=False), + ), + ] + configurator = _RuleBaseTracerConfigurator(rules=rules) + + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer( + "module_name", + "library_version", + "schema_url", + {}, + ) + other_tracer = tracer_provider.get_tracer( + "other_module_name", + "library_version", + "schema_url", + {}, + ) + self.assertEqual(tracer._instrumentation_scope.name, "module_name") + self.assertEqual( + other_tracer._instrumentation_scope.name, "other_module_name" + ) + self.assertEqual(tracer._is_enabled, True) + self.assertEqual(other_tracer._is_enabled, True) + + tracer_provider._set_tracer_configurator( + tracer_configurator=configurator + ) - tracer_provider._disable_tracers(tracer_names=["different_name"]) self.assertEqual(tracer._is_enabled, True) + self.assertEqual(other_tracer._is_enabled, False) - tracer_provider._disable_tracers(tracer_names=["module_name"]) - self.assertEqual(tracer._is_enabled, False) + def test_rule_based_tracer_configurator_default_when_rules_dont_match( + self, + ): + # pylint: disable=protected-access + rules = [ + ( + lambda x: True if x.name == "module_name" else False, + _TracerConfig(is_enabled=False), + ), + ] + configurator = _RuleBaseTracerConfigurator(rules=rules) + + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer( + "module_name", + "library_version", + "schema_url", + {}, + ) + other_tracer = tracer_provider.get_tracer( + "other_module_name", + "library_version", + "schema_url", + {}, + ) + self.assertEqual(tracer._instrumentation_scope.name, "module_name") + self.assertEqual( + other_tracer._instrumentation_scope.name, "other_module_name" + ) - tracer_provider._enable_tracers(tracer_names=["module_name"]) self.assertEqual(tracer._is_enabled, True) + self.assertEqual(other_tracer._is_enabled, True) + + tracer_provider._set_tracer_configurator( + tracer_configurator=configurator + ) + + self.assertEqual(tracer._is_enabled, False) + self.assertEqual(other_tracer._is_enabled, True) class TestRandomIdGenerator(unittest.TestCase): From 6d212bf99fbd2e8855bc9f4766c7a2b0735457f1 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 17 Dec 2025 16:06:18 +0100 Subject: [PATCH 5/7] Fix lint --- opentelemetry-sdk/tests/trace/test_trace.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 4304f5d9ee..bf0aafea16 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -2263,11 +2263,11 @@ def test_rule_based_tracer_configurator(self): # pylint: disable=protected-access rules = [ ( - lambda x: True if x.name == "module_name" else False, + lambda x: x.name == "module_name", _TracerConfig(is_enabled=True), ), ( - lambda x: True if x.name == "other_module_name" else False, + lambda x: x.name == "other_module_name", _TracerConfig(is_enabled=False), ), ] @@ -2307,7 +2307,7 @@ def test_rule_based_tracer_configurator_default_when_rules_dont_match( # pylint: disable=protected-access rules = [ ( - lambda x: True if x.name == "module_name" else False, + lambda x: x.name == "module_name", _TracerConfig(is_enabled=False), ), ] From 9b4a4b572186bc73a1cf32aaa3adaba1b8eee850 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 24 Dec 2025 15:46:41 +0100 Subject: [PATCH 6/7] Add changelog --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e3091794e0..fc75c20de4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4798](https://github.com/open-telemetry/opentelemetry-python/pull/4798)) - Silence events API warnings for internal users ([#4847](https://github.com/open-telemetry/opentelemetry-python/pull/4847)) +- Implement experimental TracerConfigurator + ([#4861](https://github.com/open-telemetry/opentelemetry-python/pull/4861)) ## Version 1.39.0/0.60b0 (2025-12-03) From cac3f64622b8449781a7524d9e20a9a72dfd0599 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 24 Dec 2025 16:36:07 +0100 Subject: [PATCH 7/7] hook into auto-instrumentation --- .../sdk/_configuration/__init__.py | 39 +++++++++++- .../sdk/environment_variables/__init__.py | 12 ++++ opentelemetry-sdk/tests/test_configurator.py | 62 ++++++++++++++++++- 3 files changed, 110 insertions(+), 3 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 963e3a236b..f7ed28658a 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -48,6 +48,7 @@ OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + OTEL_PYTHON_TRACER_CONFIGURATOR, OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, ) @@ -58,7 +59,7 @@ PeriodicExportingMetricReader, ) from opentelemetry.sdk.resources import Attributes, Resource -from opentelemetry.sdk.trace import TracerProvider +from opentelemetry.sdk.trace import TracerProvider, _TracerConfiguratorT from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator from opentelemetry.sdk.trace.sampling import Sampler @@ -146,6 +147,10 @@ def _get_id_generator() -> str: return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR) +def _get_tracer_configurator() -> str | None: + return environ.get(OTEL_PYTHON_TRACER_CONFIGURATOR, None) + + def _get_exporter_entry_point( exporter_name: str, signal_type: Literal["traces", "metrics", "logs"] ): @@ -210,12 +215,13 @@ def _init_tracing( sampler: Sampler | None = None, resource: Resource | None = None, exporter_args_map: ExporterArgsMap | None = None, + tracer_configurator: _TracerConfiguratorT | None = None, ): - # FIXME: get configurator from entrypoints / env var provider = TracerProvider( id_generator=id_generator, sampler=sampler, resource=resource, + _tracer_configurator=tracer_configurator, ) set_tracer_provider(provider) @@ -316,6 +322,27 @@ def overwritten_config_fn(*args, **kwargs): logging.basicConfig = wrapper(logging.basicConfig) +def _import_tracer_configurator( + tracer_configurator_name: str | None, +) -> _TracerConfiguratorT | None: + if not tracer_configurator_name: + return None + + try: + _, tracer_configurator_impl = _import_config_components( + [tracer_configurator_name.strip()], + "_opentelemetry_tracer_configurator", + )[0] + except Exception as exc: # pylint: disable=broad-exception-caught + _logger.warning( + "Using default tracer configurator. Failed to load tracer configurator, %s: %s", + tracer_configurator_name, + exc, + ) + return None + return tracer_configurator_impl + + def _import_exporters( trace_exporter_names: Sequence[str], metric_exporter_names: Sequence[str], @@ -430,6 +457,7 @@ def _initialize_components( id_generator: IdGenerator | None = None, setup_logging_handler: bool | None = None, exporter_args_map: ExporterArgsMap | None = None, + tracer_configurator: _TracerConfiguratorT | None = None, ): if trace_exporter_names is None: trace_exporter_names = [] @@ -455,6 +483,12 @@ def _initialize_components( resource_attributes[ResourceAttributes.TELEMETRY_AUTO_VERSION] = ( # type: ignore[reportIndexIssue] auto_instrumentation_version ) + if tracer_configurator is None: + tracer_configurator_name = _get_tracer_configurator() + tracer_configurator = _import_tracer_configurator( + tracer_configurator_name + ) + # if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name # from the env variable else defaults to "unknown_service" resource = Resource.create(resource_attributes) @@ -465,6 +499,7 @@ def _initialize_components( sampler=sampler, resource=resource, exporter_args_map=exporter_args_map, + tracer_configurator=tracer_configurator, ) _init_metrics( metric_exporters, resource, exporter_args_map=exporter_args_map diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index 5baf5fcd55..ae3959ef15 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -866,3 +866,15 @@ def channel_credential_provider() -> grpc.ChannelCredentials: This is an experimental environment variable and the name of this variable and its behavior can change in a non-backwards compatible way. """ + +OTEL_PYTHON_TRACER_CONFIGURATOR = "OTEL_PYTHON_TRACER_CONFIGURATOR" +""" +.. envvar:: OTEL_PYTHON_TRACER_CONFIGURATOR + +The :envvar:`OTEL_PYTHON_TRACER_CONFIGURATOR` environment variable allows users to set a +custom Tracer Configurator function. +Default: opentelemetry.sdk.trace._default_tracer_configurator + +This is an experimental environment variable and the name of this variable and its behavior can +change in a non-backwards compatible way. +""" diff --git a/opentelemetry-sdk/tests/test_configurator.py b/opentelemetry-sdk/tests/test_configurator.py index 8edc9190da..a747df195b 100644 --- a/opentelemetry-sdk/tests/test_configurator.py +++ b/opentelemetry-sdk/tests/test_configurator.py @@ -11,6 +11,7 @@ # 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. + # type: ignore # pylint: skip-file from __future__ import annotations @@ -35,10 +36,12 @@ _get_exporter_names, _get_id_generator, _get_sampler, + _get_tracer_configurator, _import_config_components, _import_exporters, _import_id_generator, _import_sampler, + _import_tracer_configurator, _init_logging, _init_metrics, _init_tracing, @@ -62,6 +65,7 @@ ) from opentelemetry.sdk.metrics.view import Aggregation from opentelemetry.sdk.resources import SERVICE_NAME, Resource +from opentelemetry.sdk.trace import _RuleBaseTracerConfigurator from opentelemetry.sdk.trace.export import ConsoleSpanExporter from opentelemetry.sdk.trace.id_generator import IdGenerator, RandomIdGenerator from opentelemetry.sdk.trace.sampling import ( @@ -79,10 +83,18 @@ class Provider: - def __init__(self, resource=None, sampler=None, id_generator=None): + def __init__( + self, + resource=None, + sampler=None, + id_generator=None, + *, + _tracer_configurator=None, + ): self.sampler = sampler self.id_generator = id_generator self.processor = None + self._tracer_configurator = _tracer_configurator self.resource = resource or Resource.create({}) def add_span_processor(self, processor): @@ -597,6 +609,52 @@ def verify_default_sampler(self, tracer_provider): # pylint: disable=protected-access self.assertEqual(tracer_provider.sampler._root, ALWAYS_ON) + @patch.dict( + "os.environ", + {"OTEL_PYTHON_TRACER_CONFIGURATOR": "non_existent_entry_point"}, + ) + def test_trace_init_custom_tracer_configurator_with_env_non_existent_entry_point( + self, + ): + tracer_configurator_name = _get_tracer_configurator() + with self.assertLogs(level=WARNING): + tracer_configurator = _import_tracer_configurator( + tracer_configurator_name + ) + _init_tracing({}, tracer_configurator=tracer_configurator) + + @patch("opentelemetry.sdk._configuration.entry_points") + @patch.dict( + "os.environ", + {"OTEL_PYTHON_TRACER_CONFIGURATOR": "custom_tracer_configurator"}, + ) + def test_trace_init_custom_tracer_configurator_with_env( + self, mock_entry_points + ): + def custom_tracer_configurator(tracer_scope): + return mock.Mock(spec=_RuleBaseTracerConfigurator)( + tracer_scope=tracer_scope + ) + + mock_entry_points.configure_mock( + return_value=[ + IterEntryPoint( + "custom_tracer_configurator", + custom_tracer_configurator, + ) + ] + ) + + tracer_configurator_name = _get_tracer_configurator() + tracer_configurator = _import_tracer_configurator( + tracer_configurator_name + ) + _init_tracing({}, tracer_configurator=tracer_configurator) + provider = self.set_provider_mock.call_args[0][0] + self.assertEqual( + provider._tracer_configurator, custom_tracer_configurator + ) + class TestLoggingInit(TestCase): def setUp(self): @@ -843,6 +901,7 @@ def test_initialize_components_kwargs( "id_generator": "TEST_GENERATOR", "setup_logging_handler": True, "exporter_args_map": {1: {"compression": "gzip"}}, + "tracer_configurator": "tracer_configurator_test", } _initialize_components(**kwargs) @@ -877,6 +936,7 @@ def test_initialize_components_kwargs( sampler="TEST_SAMPLER", resource="TEST_RESOURCE", exporter_args_map={1: {"compression": "gzip"}}, + tracer_configurator="tracer_configurator_test", ) metrics_mock.assert_called_once_with( "TEST_METRICS_EXPORTERS_DICT",