From 311d99ffb304062aebe00073fbdcbc0b14b23ded Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 23 Mar 2026 17:54:53 +0100 Subject: [PATCH 1/6] opentelemetry-sdk: cache tracer configs in Tracer Instead of creating the TracerConfig at each start_span call, just compute it on change. --- .../src/opentelemetry/sdk/trace/__init__.py | 71 +++++++++++-------- opentelemetry-sdk/tests/trace/test_trace.py | 22 ++++++ 2 files changed, 64 insertions(+), 29 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index e0b639d81c..b2496f3c20 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -25,7 +25,6 @@ import typing import weakref from dataclasses import dataclass -from functools import lru_cache from os import environ from time import time_ns from types import MappingProxyType, TracebackType @@ -1103,6 +1102,10 @@ class _Span(Span): class _TracerConfig: is_enabled: bool + @classmethod + def default(cls): + return cls(is_enabled=True) + class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`.""" @@ -1120,7 +1123,7 @@ def __init__( instrumentation_scope: InstrumentationScope, *, meter_provider: Optional[metrics_api.MeterProvider] = None, - _tracer_provider: Optional["TracerProvider"] = None, + _tracer_config: Optional[_TracerConfig] = None, ) -> None: self.sampler = sampler self.resource = resource @@ -1129,20 +1132,17 @@ def __init__( self.instrumentation_info = instrumentation_info self._span_limits = span_limits self._instrumentation_scope = instrumentation_scope - self._tracer_provider = _tracer_provider + self._tracer_config = _tracer_config or _TracerConfig.default() meter_provider = meter_provider or metrics_api.get_meter_provider() self._tracer_metrics = TracerMetrics(meter_provider) + def _set_tracer_config(self, tracer_config: _TracerConfig): + self._tracer_config = tracer_config + def _is_enabled(self) -> bool: """If the tracer is not enabled, start_span will create a NonRecordingSpan""" - - if not self._tracer_provider: - return True - tracer_config = self._tracer_provider._tracer_configurator( # pylint: disable=protected-access - self._instrumentation_scope - ) - return tracer_config.is_enabled + return self._tracer_config.is_enabled @_agnosticcontextmanager # pylint: disable=protected-access def start_as_current_span( @@ -1297,7 +1297,6 @@ def __call__(self, tracer_scope: InstrumentationScope) -> _TracerConfig: return self._default_config -@lru_cache def _default_tracer_configurator( tracer_scope: InstrumentationScope, ) -> _TracerConfig: @@ -1308,11 +1307,10 @@ def _default_tracer_configurator( implementing this interface returning a Tracer Config.""" return _RuleBasedTracerConfigurator( rules=[], - default_config=_TracerConfig(is_enabled=True), + default_config=_TracerConfig.default(), )(tracer_scope=tracer_scope) -@lru_cache def _disable_tracer_configurator( tracer_scope: InstrumentationScope, ) -> _TracerConfig: @@ -1365,23 +1363,24 @@ def __init__( self._tracer_configurator = ( _tracer_configurator or _default_tracer_configurator ) + self._tracers_lock = threading.Lock() + self._tracers: dict[InstrumentationScope, Tracer] = {} def _set_tracer_configurator( self, *, tracer_configurator: _TracerConfiguratorT ): """This is the function used to update the TracerProvider TracerConfigurator - Setting a new TracerConfigurator for a TracerProvider will make all the Tracers created from - this TracerProvider reference the new TracerConfigurator. - - The tracer checks its configuration at span creation time. Since this is an hot path - it's important that it'll execute quickly so it is suggested to memoize it with - functools.lru_cache. - If your TracerConfigurator is using some dynamic rules you can still use functools.lru_cache - decorator if you remember to clear its cache with the decorator cache_clear() function when - the rules change. + Setting a new TracerConfigurator for a TracerProvider will make updated all the Tracers created + from this TracerProvider a new TracerConfig created with this. """ self._tracer_configurator = tracer_configurator + with self._tracers_lock: + for instrumentation_scope, tracer in self._tracers.items(): + # pylint: disable-next=protected-access + tracer._set_tracer_config( + tracer_configurator(instrumentation_scope) + ) @property def resource(self) -> Resource: @@ -1417,6 +1416,22 @@ def get_tracer( schema_url, ) + instrumentation_scope = InstrumentationScope( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + attributes, + ) + + try: + tracer_config = self._tracer_configurator(instrumentation_scope) + except Exception: # pylint: disable=broad-exception-caught + logger.exception( + "Failed to create a Tracer Config for %s, using default Tracer config", + instrumentation_scope, + ) + tracer_config = _TracerConfig.default() + tracer = Tracer( self.sampler, self.resource, @@ -1424,16 +1439,14 @@ def get_tracer( self.id_generator, instrumentation_info, self._span_limits, - InstrumentationScope( - instrumenting_module_name, - instrumenting_library_version, - schema_url, - attributes, - ), + instrumentation_scope, meter_provider=self._meter_provider, - _tracer_provider=self, + _tracer_config=tracer_config, ) + with self._tracers_lock: + self._tracers[instrumentation_scope] = tracer + return tracer def add_span_processor(self, span_processor: SpanProcessor) -> None: diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 0f61752316..6e35989e10 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -196,6 +196,22 @@ def test_get_tracer_sdk(self): {"key1": "value1", "key2": 6}, ) + def test_get_tracer_sdk_sets_default_tracer_config_if_configurator_raises( + self, + ): + def raising_tracer_configurator(tracer_scope): + raise ValueError() + + tracer_provider = trace.TracerProvider( + _tracer_configurator=raising_tracer_configurator + ) + tracer = tracer_provider.get_tracer( + "module_name", + "library_version", + ) + # pylint: disable=protected-access + self.assertEqual(tracer._tracer_config, _TracerConfig.default()) + @mock.patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"}) def test_get_tracer_with_sdk_disabled(self): tracer_provider = trace.TracerProvider() @@ -2274,6 +2290,12 @@ 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_default_tracer_config(self): + self.assertEqual( + _TracerConfig.default(), + _TracerConfig(is_enabled=True), + ) + def test_default_tracer_configurator(self): # pylint: disable=protected-access tracer_provider = trace.TracerProvider() From de9fed02e0d4683b0a1585275bd0842ac9e66f4a Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 23 Mar 2026 18:11:36 +0100 Subject: [PATCH 2/6] Add CHANGELOG --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dac90bf15f..ce621b2172 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ([#4910](https://github.com/open-telemetry/opentelemetry-python/pull/4910)) - Add configurable `max_export_batch_size` to OTLP HTTP metrics exporter ([#4576](https://github.com/open-telemetry/opentelemetry-python/pull/4576)) +- `opentelemetry-sdk`: cache TracerConfig into the tracer, this changes an internal interface + ([#5007](https://github.com/open-telemetry/opentelemetry-python/pull/5007)) ## Version 1.40.0/0.61b0 (2026-03-04) From 08808aab061d2b7babb28e281345af6efea08347 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Mon, 23 Mar 2026 18:23:01 +0100 Subject: [PATCH 3/6] Fix benchmarks --- .../benchmarks/trace/test_benchmark_trace.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py b/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py index c2d5590144..7864abbd78 100644 --- a/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py +++ b/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py @@ -26,7 +26,7 @@ sampling, ) -tracer = TracerProvider( +tracer_provider = TracerProvider( sampler=sampling.DEFAULT_ON, resource=Resource( { @@ -35,10 +35,11 @@ "service.instance.id": "123ab456-a123-12ab-12ab-12340a1abc12", } ), -).get_tracer("sdk_tracer_provider") +) +tracer = tracer_provider.get_tracer("sdk_tracer_provider") -@pytest.fixture(params=[None, 0, 1, 10, 50]) +@pytest.fixture(params=[0, 1, 10, 50]) def num_tracer_configurator_rules(request): return request.param @@ -81,18 +82,13 @@ def tracer_configurator(tracer_scope): default_config=_TracerConfig(is_enabled=True), )(tracer_scope=tracer_scope) - tracer_provider = tracer._tracer_provider tracer_provider._set_tracer_configurator( tracer_configurator=tracer_configurator ) - if num_tracer_configurator_rules is None: - tracer._tracer_provider = None benchmark(benchmark_start_span) tracer_provider._set_tracer_configurator( tracer_configurator=_default_tracer_configurator ) - if num_tracer_configurator_rules is None: - tracer._tracer_provider = tracer_provider def test_simple_start_as_current_span(benchmark): From eb9d4f54151bb9024c02f3c81c72d58a3ba45d0d Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 25 Mar 2026 10:21:08 +0100 Subject: [PATCH 4/6] Apply Lukas feedback --- .../src/opentelemetry/sdk/trace/__init__.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index b2496f3c20..a2e84df4db 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -1377,15 +1377,28 @@ def _set_tracer_configurator( self._tracer_configurator = tracer_configurator with self._tracers_lock: for instrumentation_scope, tracer in self._tracers.items(): - # pylint: disable-next=protected-access - tracer._set_tracer_config( - tracer_configurator(instrumentation_scope) + tracer_config = self._apply_tracer_configurator( + instrumentation_scope ) + # pylint: disable-next=protected-access + tracer._set_tracer_config(tracer_config) @property def resource(self) -> Resource: return self._resource + def _apply_tracer_configurator( + self, instrumentation_scope: InstrumentationScope + ): + try: + return self._tracer_configurator(instrumentation_scope) + except Exception: # pylint: disable=broad-exception-caught + logger.exception( + "Failed to create a Tracer Config for %s, using default Tracer config", + instrumentation_scope, + ) + return _TracerConfig.default() + def get_tracer( self, instrumenting_module_name: str, @@ -1422,16 +1435,7 @@ def get_tracer( schema_url, attributes, ) - - try: - tracer_config = self._tracer_configurator(instrumentation_scope) - except Exception: # pylint: disable=broad-exception-caught - logger.exception( - "Failed to create a Tracer Config for %s, using default Tracer config", - instrumentation_scope, - ) - tracer_config = _TracerConfig.default() - + tracer_config = self._apply_tracer_configurator(instrumentation_scope) tracer = Tracer( self.sampler, self.resource, From 2dbca74a26de7ac9d77415e0d51bf2d1781de31e Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 25 Mar 2026 10:48:56 +0100 Subject: [PATCH 5/6] Test _set_tracer_configurator handling of broken configurator --- opentelemetry-sdk/tests/trace/test_trace.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/opentelemetry-sdk/tests/trace/test_trace.py b/opentelemetry-sdk/tests/trace/test_trace.py index 6e35989e10..fc1f39cbcd 100644 --- a/opentelemetry-sdk/tests/trace/test_trace.py +++ b/opentelemetry-sdk/tests/trace/test_trace.py @@ -2319,6 +2319,23 @@ def test_default_tracer_configurator(self): self.assertEqual(tracer._is_enabled(), True) self.assertEqual(other_tracer._is_enabled(), True) + def test_set_tracer_configurator_sets_default_tracer_config_if_configurator_raises( + self, + ): + def raising_tracer_configurator(tracer_scope): + raise ValueError() + + tracer_provider = trace.TracerProvider() + tracer = tracer_provider.get_tracer( + "module_name", + "library_version", + ) + tracer_provider._set_tracer_configurator( + tracer_configurator=raising_tracer_configurator + ) + # pylint: disable=protected-access + self.assertEqual(tracer._tracer_config, _TracerConfig.default()) + def test_rule_based_tracer_configurator(self): # pylint: disable=protected-access rules = [ From d58478c7fe735047ec0d4d34b11379ee5b279023 Mon Sep 17 00:00:00 2001 From: Riccardo Magliocchetti Date: Wed, 25 Mar 2026 11:36:17 +0100 Subject: [PATCH 6/6] Implement equality dunder method for _TracerConfig --- opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index a2e84df4db..ba6911831b 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -1106,6 +1106,9 @@ class _TracerConfig: def default(cls): return cls(is_enabled=True) + def __eq__(self, other: "_TracerConfig"): + return self.is_enabled == other.is_enabled + class Tracer(trace_api.Tracer): """See `opentelemetry.trace.Tracer`."""