Skip to content

Commit 9ed343a

Browse files
authored
opentelemetry-sdk: cache tracer configs in Tracer (#5007)
* opentelemetry-sdk: cache tracer configs in Tracer Instead of creating the TracerConfig at each start_span call, just compute it on change. * Add CHANGELOG * Fix benchmarks * Apply Lukas feedback * Test _set_tracer_configurator handling of broken configurator * Implement equality dunder method for _TracerConfig * Reuse tracer for the same instrumentation scope * Fix typecheck * Fix typo * Pablo comments * Assert equality of tracer configs as dict
1 parent 96435cb commit 9ed343a

4 files changed

Lines changed: 140 additions & 45 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4141
([#4910](https://github.com/open-telemetry/opentelemetry-python/pull/4910))
4242
- Add configurable `max_export_batch_size` to OTLP HTTP metrics exporter
4343
([#4576](https://github.com/open-telemetry/opentelemetry-python/pull/4576))
44+
- `opentelemetry-sdk`: cache TracerConfig into the tracer, this changes an internal interface. Only one Tracer with the same instrumentation scope will be created
45+
([#5007](https://github.com/open-telemetry/opentelemetry-python/pull/5007))
4446

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

opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
sampling,
2727
)
2828

29-
tracer = TracerProvider(
29+
tracer_provider = TracerProvider(
3030
sampler=sampling.DEFAULT_ON,
3131
resource=Resource(
3232
{
@@ -35,10 +35,11 @@
3535
"service.instance.id": "123ab456-a123-12ab-12ab-12340a1abc12",
3636
}
3737
),
38-
).get_tracer("sdk_tracer_provider")
38+
)
39+
tracer = tracer_provider.get_tracer("sdk_tracer_provider")
3940

4041

41-
@pytest.fixture(params=[None, 0, 1, 10, 50])
42+
@pytest.fixture(params=[0, 1, 10, 50])
4243
def num_tracer_configurator_rules(request):
4344
return request.param
4445

@@ -81,18 +82,13 @@ def tracer_configurator(tracer_scope):
8182
default_config=_TracerConfig(is_enabled=True),
8283
)(tracer_scope=tracer_scope)
8384

84-
tracer_provider = tracer._tracer_provider
8585
tracer_provider._set_tracer_configurator(
8686
tracer_configurator=tracer_configurator
8787
)
88-
if num_tracer_configurator_rules is None:
89-
tracer._tracer_provider = None
9088
benchmark(benchmark_start_span)
9189
tracer_provider._set_tracer_configurator(
9290
tracer_configurator=_default_tracer_configurator
9391
)
94-
if num_tracer_configurator_rules is None:
95-
tracer._tracer_provider = tracer_provider
9692

9793

9894
def test_simple_start_as_current_span(benchmark):

opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py

Lines changed: 59 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
import typing
2626
import weakref
2727
from dataclasses import dataclass
28-
from functools import lru_cache
2928
from os import environ
3029
from time import time_ns
3130
from types import MappingProxyType, TracebackType
@@ -1103,6 +1102,10 @@ class _Span(Span):
11031102
class _TracerConfig:
11041103
is_enabled: bool
11051104

1105+
@classmethod
1106+
def default(cls):
1107+
return cls(is_enabled=True)
1108+
11061109

11071110
class Tracer(trace_api.Tracer):
11081111
"""See `opentelemetry.trace.Tracer`."""
@@ -1120,7 +1123,7 @@ def __init__(
11201123
instrumentation_scope: InstrumentationScope,
11211124
*,
11221125
meter_provider: Optional[metrics_api.MeterProvider] = None,
1123-
_tracer_provider: Optional["TracerProvider"] = None,
1126+
_tracer_config: Optional[_TracerConfig] = None,
11241127
) -> None:
11251128
self.sampler = sampler
11261129
self.resource = resource
@@ -1129,20 +1132,17 @@ def __init__(
11291132
self.instrumentation_info = instrumentation_info
11301133
self._span_limits = span_limits
11311134
self._instrumentation_scope = instrumentation_scope
1132-
self._tracer_provider = _tracer_provider
1135+
self._tracer_config = _tracer_config or _TracerConfig.default()
11331136

11341137
meter_provider = meter_provider or metrics_api.get_meter_provider()
11351138
self._tracer_metrics = TracerMetrics(meter_provider)
11361139

1140+
def _set_tracer_config(self, tracer_config: _TracerConfig):
1141+
self._tracer_config = tracer_config
1142+
11371143
def _is_enabled(self) -> bool:
11381144
"""If the tracer is not enabled, start_span will create a NonRecordingSpan"""
1139-
1140-
if not self._tracer_provider:
1141-
return True
1142-
tracer_config = self._tracer_provider._tracer_configurator( # pylint: disable=protected-access
1143-
self._instrumentation_scope
1144-
)
1145-
return tracer_config.is_enabled
1145+
return self._tracer_config.is_enabled
11461146

11471147
@_agnosticcontextmanager # pylint: disable=protected-access
11481148
def start_as_current_span(
@@ -1297,7 +1297,6 @@ def __call__(self, tracer_scope: InstrumentationScope) -> _TracerConfig:
12971297
return self._default_config
12981298

12991299

1300-
@lru_cache
13011300
def _default_tracer_configurator(
13021301
tracer_scope: InstrumentationScope,
13031302
) -> _TracerConfig:
@@ -1308,11 +1307,10 @@ def _default_tracer_configurator(
13081307
implementing this interface returning a Tracer Config."""
13091308
return _RuleBasedTracerConfigurator(
13101309
rules=[],
1311-
default_config=_TracerConfig(is_enabled=True),
1310+
default_config=_TracerConfig.default(),
13121311
)(tracer_scope=tracer_scope)
13131312

13141313

1315-
@lru_cache
13161314
def _disable_tracer_configurator(
13171315
tracer_scope: InstrumentationScope,
13181316
) -> _TracerConfig:
@@ -1365,28 +1363,42 @@ def __init__(
13651363
self._tracer_configurator = (
13661364
_tracer_configurator or _default_tracer_configurator
13671365
)
1366+
self._tracers_lock = threading.Lock()
1367+
self._tracers: dict[InstrumentationScope, Tracer] = {}
13681368

13691369
def _set_tracer_configurator(
13701370
self, *, tracer_configurator: _TracerConfiguratorT
13711371
):
13721372
"""This is the function used to update the TracerProvider TracerConfigurator
13731373
1374-
Setting a new TracerConfigurator for a TracerProvider will make all the Tracers created from
1375-
this TracerProvider reference the new TracerConfigurator.
1376-
1377-
The tracer checks its configuration at span creation time. Since this is an hot path
1378-
it's important that it'll execute quickly so it is suggested to memoize it with
1379-
functools.lru_cache.
1380-
If your TracerConfigurator is using some dynamic rules you can still use functools.lru_cache
1381-
decorator if you remember to clear its cache with the decorator cache_clear() function when
1382-
the rules change.
1374+
Setting a new TracerConfigurator for a TracerProvider will update the
1375+
TracerConfig of all Tracers create by this TracerProvider.
13831376
"""
13841377
self._tracer_configurator = tracer_configurator
1378+
with self._tracers_lock:
1379+
for instrumentation_scope, tracer in self._tracers.items():
1380+
tracer_config = self._apply_tracer_configurator(
1381+
instrumentation_scope
1382+
)
1383+
# pylint: disable-next=protected-access
1384+
tracer._set_tracer_config(tracer_config)
13851385

13861386
@property
13871387
def resource(self) -> Resource:
13881388
return self._resource
13891389

1390+
def _apply_tracer_configurator(
1391+
self, instrumentation_scope: InstrumentationScope
1392+
):
1393+
try:
1394+
return self._tracer_configurator(instrumentation_scope)
1395+
except Exception: # pylint: disable=broad-exception-caught
1396+
logger.exception(
1397+
"Failed to create a Tracer Config for %s, using default Tracer config",
1398+
instrumentation_scope,
1399+
)
1400+
return _TracerConfig.default()
1401+
13901402
def get_tracer(
13911403
self,
13921404
instrumenting_module_name: str,
@@ -1417,23 +1429,33 @@ def get_tracer(
14171429
schema_url,
14181430
)
14191431

1420-
tracer = Tracer(
1421-
self.sampler,
1422-
self.resource,
1423-
self._active_span_processor,
1424-
self.id_generator,
1425-
instrumentation_info,
1426-
self._span_limits,
1427-
InstrumentationScope(
1428-
instrumenting_module_name,
1429-
instrumenting_library_version,
1430-
schema_url,
1431-
attributes,
1432-
),
1433-
meter_provider=self._meter_provider,
1434-
_tracer_provider=self,
1432+
instrumentation_scope = InstrumentationScope(
1433+
instrumenting_module_name,
1434+
instrumenting_library_version,
1435+
schema_url,
1436+
attributes,
14351437
)
14361438

1439+
with self._tracers_lock:
1440+
if instrumentation_scope in self._tracers:
1441+
return self._tracers[instrumentation_scope]
1442+
1443+
tracer_config = self._apply_tracer_configurator(
1444+
instrumentation_scope
1445+
)
1446+
tracer = Tracer(
1447+
self.sampler,
1448+
self.resource,
1449+
self._active_span_processor,
1450+
self.id_generator,
1451+
instrumentation_info,
1452+
self._span_limits,
1453+
instrumentation_scope,
1454+
meter_provider=self._meter_provider,
1455+
_tracer_config=tracer_config,
1456+
)
1457+
self._tracers[instrumentation_scope] = tracer
1458+
14371459
return tracer
14381460

14391461
def add_span_processor(self, span_processor: SpanProcessor) -> None:

opentelemetry-sdk/tests/trace/test_trace.py

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
# pylint: disable=no-member
1717

1818
import copy
19+
import dataclasses
1920
import shutil
2021
import subprocess
2122
import unittest
@@ -196,6 +197,43 @@ def test_get_tracer_sdk(self):
196197
{"key1": "value1", "key2": 6},
197198
)
198199

200+
def test_get_tracer_sdk_returns_same_tracer_when_called_with_same_instrumentation_scope(
201+
self,
202+
):
203+
tracer_provider = trace.TracerProvider()
204+
tracer1 = tracer_provider.get_tracer(
205+
"module_name",
206+
"library_version",
207+
"schema_url",
208+
{"key1": "value1", "key2": 6},
209+
)
210+
211+
tracer2 = tracer_provider.get_tracer(
212+
"module_name",
213+
"library_version",
214+
"schema_url",
215+
{"key1": "value1", "key2": 6},
216+
)
217+
218+
self.assertEqual(tracer1, tracer2)
219+
self.assertTrue(tracer1 is tracer2)
220+
221+
def test_get_tracer_sdk_sets_default_tracer_config_if_configurator_raises(
222+
self,
223+
):
224+
def raising_tracer_configurator(tracer_scope):
225+
raise ValueError()
226+
227+
tracer_provider = trace.TracerProvider(
228+
_tracer_configurator=raising_tracer_configurator
229+
)
230+
tracer = tracer_provider.get_tracer(
231+
"module_name",
232+
"library_version",
233+
)
234+
# pylint: disable=protected-access
235+
self.assertEqual(tracer._tracer_config, _TracerConfig.default())
236+
199237
@mock.patch.dict("os.environ", {OTEL_SDK_DISABLED: "true"})
200238
def test_get_tracer_with_sdk_disabled(self):
201239
tracer_provider = trace.TracerProvider()
@@ -2259,6 +2297,23 @@ def test_child_parent_span_exception(self):
22592297
self.assertTupleEqual(parent_span.events, ())
22602298

22612299

2300+
class TestTracerConfig(unittest.TestCase):
2301+
def test_default(self):
2302+
self.assertEqual(
2303+
_TracerConfig.default(),
2304+
_TracerConfig(is_enabled=True),
2305+
)
2306+
2307+
def test_equality(self):
2308+
config = _TracerConfig(is_enabled=True)
2309+
same_config = _TracerConfig(is_enabled=True)
2310+
other_config = _TracerConfig(is_enabled=False)
2311+
2312+
self.assertEqual(config, same_config)
2313+
self.assertNotEqual(config, other_config)
2314+
self.assertNotEqual(config, "string")
2315+
2316+
22622317
# pylint: disable=protected-access
22632318
class TestTracerProvider(unittest.TestCase):
22642319
@patch("opentelemetry.sdk.trace.sampling._get_from_env_or_default")
@@ -2297,6 +2352,26 @@ def test_default_tracer_configurator(self):
22972352
self.assertEqual(tracer._is_enabled(), True)
22982353
self.assertEqual(other_tracer._is_enabled(), True)
22992354

2355+
def test_set_tracer_configurator_sets_default_tracer_config_if_configurator_raises(
2356+
self,
2357+
):
2358+
def raising_tracer_configurator(tracer_scope):
2359+
raise ValueError()
2360+
2361+
tracer_provider = trace.TracerProvider()
2362+
tracer = tracer_provider.get_tracer(
2363+
"module_name",
2364+
"library_version",
2365+
)
2366+
tracer_provider._set_tracer_configurator(
2367+
tracer_configurator=raising_tracer_configurator
2368+
)
2369+
# pylint: disable=protected-access
2370+
self.assertEqual(
2371+
dataclasses.asdict(tracer._tracer_config),
2372+
dataclasses.asdict(_TracerConfig.default()),
2373+
)
2374+
23002375
def test_rule_based_tracer_configurator(self):
23012376
# pylint: disable=protected-access
23022377
rules = [

0 commit comments

Comments
 (0)