Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
([#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
78 changes: 49 additions & 29 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,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`."""
Expand All @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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:
Expand All @@ -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:
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -1417,23 +1432,28 @@ 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,
self._active_span_processor,
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If there's already a Tracer with this instrumentation scope registered in _tracers, should it be returned? Looks like we're always creating a new Tracer, and if we overwrite a Tracer that's already registered in _tracers, it gets overwritten, then it won't be iterated over in _set_tracer_configurator, and won't receive config updates, even though it could still be in use.


return tracer

def add_span_processor(self, span_processor: SpanProcessor) -> None:
Expand Down
39 changes: 39 additions & 0 deletions opentelemetry-sdk/tests/trace/test_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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()
Expand All @@ -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 = [
Expand Down
Loading