diff --git a/CHANGELOG.md b/CHANGELOG.md index dac90bf15f3..ce621b21726 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) diff --git a/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py b/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py index c2d5590144c..7864abbd781 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): diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index e0b639d81cf..ba6911831b4 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,13 @@ class _Span(Span): class _TracerConfig: is_enabled: bool + @classmethod + 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`.""" @@ -1120,7 +1126,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 +1135,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 +1300,6 @@ def __call__(self, tracer_scope: InstrumentationScope) -> _TracerConfig: return self._default_config -@lru_cache def _default_tracer_configurator( tracer_scope: InstrumentationScope, ) -> _TracerConfig: @@ -1308,11 +1310,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,28 +1366,42 @@ 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(): + 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, @@ -1417,6 +1432,13 @@ def get_tracer( schema_url, ) + instrumentation_scope = InstrumentationScope( + instrumenting_module_name, + instrumenting_library_version, + schema_url, + attributes, + ) + tracer_config = self._apply_tracer_configurator(instrumentation_scope) tracer = Tracer( self.sampler, self.resource, @@ -1424,16 +1446,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 0f617523163..fc1f39cbcd5 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() @@ -2297,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 = [