Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. Only one Tracer with the same instrumentation scope will be created
([#5007](https://github.com/open-telemetry/opentelemetry-python/pull/5007))

## Version 1.40.0/0.61b0 (2026-03-04)

Expand Down
12 changes: 4 additions & 8 deletions opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
sampling,
)

tracer = TracerProvider(
tracer_provider = TracerProvider(
sampler=sampling.DEFAULT_ON,
resource=Resource(
{
Expand All @@ -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

Expand Down Expand Up @@ -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):
Expand Down
96 changes: 59 additions & 37 deletions opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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`."""
Expand All @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -1365,28 +1363,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 update the
TracerConfig of all Tracers create by this TracerProvider.
"""
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,
Expand Down Expand Up @@ -1417,23 +1429,33 @@ def get_tracer(
schema_url,
)

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,
),
meter_provider=self._meter_provider,
_tracer_provider=self,
instrumentation_scope = InstrumentationScope(
instrumenting_module_name,
instrumenting_library_version,
schema_url,
attributes,
)

with self._tracers_lock:
if instrumentation_scope in self._tracers:
Comment thread
xrmx marked this conversation as resolved.
return self._tracers[instrumentation_scope]

tracer_config = self._apply_tracer_configurator(
instrumentation_scope
)
tracer = Tracer(
self.sampler,
self.resource,
self._active_span_processor,
self.id_generator,
instrumentation_info,
self._span_limits,
instrumentation_scope,
meter_provider=self._meter_provider,
_tracer_config=tracer_config,
)
self._tracers[instrumentation_scope] = tracer
Comment thread
pmcollins marked this conversation as resolved.

return tracer

def add_span_processor(self, span_processor: SpanProcessor) -> None:
Expand Down
75 changes: 75 additions & 0 deletions opentelemetry-sdk/tests/trace/test_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
# pylint: disable=no-member

import copy
import dataclasses
import shutil
import subprocess
import unittest
Expand Down Expand Up @@ -196,6 +197,43 @@ def test_get_tracer_sdk(self):
{"key1": "value1", "key2": 6},
)

def test_get_tracer_sdk_returns_same_tracer_when_called_with_same_instrumentation_scope(
self,
):
tracer_provider = trace.TracerProvider()
tracer1 = tracer_provider.get_tracer(
"module_name",
"library_version",
"schema_url",
{"key1": "value1", "key2": 6},
)

tracer2 = tracer_provider.get_tracer(
"module_name",
"library_version",
"schema_url",
{"key1": "value1", "key2": 6},
)

self.assertEqual(tracer1, tracer2)
self.assertTrue(tracer1 is tracer2)

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()
Expand Down Expand Up @@ -2259,6 +2297,23 @@ def test_child_parent_span_exception(self):
self.assertTupleEqual(parent_span.events, ())


class TestTracerConfig(unittest.TestCase):
def test_default(self):
self.assertEqual(
_TracerConfig.default(),
_TracerConfig(is_enabled=True),
)

def test_equality(self):
config = _TracerConfig(is_enabled=True)
same_config = _TracerConfig(is_enabled=True)
other_config = _TracerConfig(is_enabled=False)

self.assertEqual(config, same_config)
self.assertNotEqual(config, other_config)
self.assertNotEqual(config, "string")


# pylint: disable=protected-access
class TestTracerProvider(unittest.TestCase):
@patch("opentelemetry.sdk.trace.sampling._get_from_env_or_default")
Expand Down Expand Up @@ -2297,6 +2352,26 @@ 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(
dataclasses.asdict(tracer._tracer_config),
dataclasses.asdict(_TracerConfig.default()),
)

def test_rule_based_tracer_configurator(self):
# pylint: disable=protected-access
rules = [
Expand Down
Loading