Skip to content
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
43ecdd4
opentelemetry-sdk: Implement tracer configurator
xrmx Sep 30, 2025
6b2d695
Please lint
xrmx Dec 17, 2025
2622c0c
Add rule based tracer configurator
xrmx Dec 17, 2025
82f2d05
Assume rule based tracer configurator in helpers
xrmx Dec 17, 2025
f2d8841
Fix lint
xrmx Dec 17, 2025
a17f947
Add changelog
xrmx Dec 24, 2025
5998b32
hook into auto-instrumentation
xrmx Dec 24, 2025
456a2ff
Don't return invalid spans from NoOpTracer
xrmx Jan 14, 2026
c80b2d8
Adapt after nooptracer behaviour change
xrmx Jan 14, 2026
b86b322
Please lint
xrmx Jan 14, 2026
774abd2
Rework _RuleBasedTracerConfigurator interface to match the one from d…
xrmx Jan 16, 2026
3a51dee
More correct nooptracer and more test coverage
xrmx Jan 16, 2026
6bf38a7
Ignore typechecking in validation test
xrmx Jan 20, 2026
9726fec
Handle invalid span context more gracefully
xrmx Jan 22, 2026
4714f5e
Return invalid span in case span context is invalid
xrmx Jan 22, 2026
282ed3a
Use tuple for typing
xrmx Feb 6, 2026
c64d630
Add retries in case of failure in copying the cached tracers
xrmx Feb 6, 2026
1844f8e
Calculate the TracerConfig in start_span
xrmx Feb 9, 2026
dae2163
Make Tracer.is_enabled a method since it does some computation
xrmx Feb 11, 2026
5b16f02
Add benchmarks
xrmx Feb 11, 2026
5ba15a0
Test also the case with no overhead
xrmx Feb 11, 2026
4d13f29
Use functools.lru_cache to recover the overhead of tracerConfigurator
xrmx Feb 11, 2026
b8a4bdc
Merge branch 'main' into experimental-tracer-configurator
xrmx Feb 11, 2026
36b9f6b
Fix pylint
xrmx Feb 11, 2026
70f8b63
Fix precommit
xrmx Feb 11, 2026
8b90f2f
Merge branch 'main' into experimental-tracer-configurator
xrmx Feb 12, 2026
64ccb86
Make scope matcher generic per Lukas suggestion
xrmx Feb 12, 2026
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 @@ -38,6 +38,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4862](https://github.com/open-telemetry/opentelemetry-python/pull/4862))
- `opentelemetry-exporter-otlp-proto-http`: fix retry logic and error handling for connection failures in trace, metric, and log exporters
([#4709](https://github.com/open-telemetry/opentelemetry-python/pull/4709))
- Implement experimental TracerConfigurator
([#4861](https://github.com/open-telemetry/opentelemetry-python/pull/4861))

## Version 1.39.0/0.60b0 (2025-12-03)

Expand Down
34 changes: 32 additions & 2 deletions opentelemetry-api/src/opentelemetry/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -471,7 +471,21 @@ def start_span(
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> "Span":
return INVALID_SPAN
current_span = get_current_span(context)
if isinstance(current_span, NonRecordingSpan):
return current_span
parent_span_context = current_span.get_span_context()
if parent_span_context is not None and not isinstance(
parent_span_context, SpanContext
):
logger.warning(
"Invalid span context for %s: %s",
current_span,
parent_span_context,
)
return INVALID_SPAN

return NonRecordingSpan(context=parent_span_context)

@_agnosticcontextmanager
def start_as_current_span(
Expand All @@ -486,7 +500,23 @@ def start_as_current_span(
set_status_on_exception: bool = True,
end_on_exit: bool = True,
) -> Iterator["Span"]:
yield INVALID_SPAN
span = self.start_span(
name=name,
context=context,
kind=kind,
attributes=attributes,
links=links,
start_time=start_time,
record_exception=record_exception,
set_status_on_exception=set_status_on_exception,
)
with use_span(
span,
end_on_exit=end_on_exit,
record_exception=record_exception,
set_status_on_exception=set_status_on_exception,
) as span:
yield span


@deprecated("You should use NoOpTracer. Deprecated since version 1.9.0.")
Expand Down
100 changes: 92 additions & 8 deletions opentelemetry-api/tests/test_implementation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,63 @@
from opentelemetry import trace


class RecordingSpan(trace.Span):
def __init__(self, context: trace.SpanContext) -> None:
self._context = context

def get_span_context(self) -> trace.SpanContext:
return self._context

def is_recording(self) -> bool:
return True

def end(self, end_time=None) -> None:
pass

def set_attributes(self, attributes) -> None:
pass

def set_attribute(self, key, value) -> None:
pass

def add_event(
self,
name: str,
attributes=None,
timestamp=None,
) -> None:
pass

def add_link(
self,
context,
attributes=None,
) -> None:
pass

def update_name(self, name) -> None:
pass

def set_status(
self,
status,
description=None,
) -> None:
pass

def record_exception(
self,
exception,
attributes=None,
timestamp=None,
escaped=False,
) -> None:
pass

def __repr__(self) -> str:
return f"RecordingSpan({self._context!r})"


class TestAPIOnlyImplementation(unittest.TestCase):
"""
This test is in place to ensure the API is returning values that
Expand All @@ -36,18 +93,45 @@ def test_default_tracer(self):
tracer_provider = trace.NoOpTracerProvider()
tracer = tracer_provider.get_tracer(__name__)
with tracer.start_span("test") as span:
self.assertEqual(
span.get_span_context(), trace.INVALID_SPAN_CONTEXT
)
self.assertEqual(span, trace.INVALID_SPAN)
self.assertFalse(span.get_span_context().is_valid)
self.assertIs(span.is_recording(), False)
with tracer.start_span("test2") as span2:
self.assertEqual(
span2.get_span_context(), trace.INVALID_SPAN_CONTEXT
)
self.assertEqual(span2, trace.INVALID_SPAN)
self.assertFalse(span2.get_span_context().is_valid)
self.assertIs(span2.is_recording(), False)

def test_default_tracer_context_propagation_recording_span(self):
tracer_provider = trace.NoOpTracerProvider()
tracer = tracer_provider.get_tracer(__name__)
span_context = trace.SpanContext(
2604504634922341076776623263868986797,
5213367945872657620,
False,
trace.TraceFlags(0x01),
)
ctx = trace.set_span_in_context(RecordingSpan(context=span_context))
with tracer.start_span("test", context=ctx) as span:
self.assertTrue(span.get_span_context().is_valid)
self.assertEqual(span.get_span_context(), span_context)
self.assertIs(span.is_recording(), False)

def test_default_tracer_context_propagation_non_recording_span(self):
tracer_provider = trace.NoOpTracerProvider()
tracer = tracer_provider.get_tracer(__name__)
ctx = trace.set_span_in_context(trace.INVALID_SPAN)
with tracer.start_span("test", context=ctx) as span:
self.assertFalse(span.get_span_context().is_valid)
self.assertIs(span, trace.INVALID_SPAN)

def test_default_tracer_context_propagation_with_invalid_context(self):
tracer_provider = trace.NoOpTracerProvider()
tracer = tracer_provider.get_tracer(__name__)
ctx = trace.set_span_in_context(
RecordingSpan(context="invalid_context") # type: ignore[reportArgumentType]
)
with tracer.start_span("test", context=ctx) as span:
self.assertFalse(span.get_span_context().is_valid)
self.assertIs(span, trace.INVALID_SPAN)

def test_span(self):
with self.assertRaises(TypeError):
# pylint: disable=abstract-class-instantiated
Expand Down
4 changes: 2 additions & 2 deletions opentelemetry-api/tests/trace/test_proxy.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,8 @@ def my_function() -> Span:
return trace.get_current_span()

# call function before configuring tracing provider, should
# return INVALID_SPAN from the NoOpTracer
self.assertEqual(my_function(), trace.INVALID_SPAN)
# return NonRecordingSpan from the NoOpTracer
self.assertFalse(my_function().is_recording())

# configure tracing provider
trace.set_tracer_provider(TestProvider())
Expand Down
2 changes: 1 addition & 1 deletion opentelemetry-api/tests/trace/test_tracer.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,5 +79,5 @@ async def function_async(data: str) -> int:
def test_get_current_span(self):
with self.tracer.start_as_current_span("test") as span:
get_current_span().set_attribute("test", "test")
self.assertEqual(span, INVALID_SPAN)
self.assertFalse(span.is_recording())
self.assertFalse(hasattr("span", "attributes"))
62 changes: 59 additions & 3 deletions opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,19 @@
# See the License for the specific language governing permissions and
# limitations under the License.

from functools import lru_cache

import pytest

from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider, sampling
from opentelemetry.sdk.trace import (
TracerProvider,
_default_tracer_configurator,
_RuleBasedTracerConfigurator,
_tracer_name_matches_glob,
_TracerConfig,
sampling,
)

tracer = TracerProvider(
sampler=sampling.DEFAULT_ON,
Expand All @@ -27,16 +38,61 @@
).get_tracer("sdk_tracer_provider")


@pytest.fixture(params=[None, 0, 1, 10, 50])
def num_tracer_configurator_rules(request):
return request.param


def test_simple_start_span(benchmark):
def benchmark_start_as_current_span():
def benchmark_start_span():
span = tracer.start_span(
"benchmarkedSpan",
attributes={"long.attribute": -10000000001000000000},
)
span.add_event("benchmarkEvent")
span.end()

benchmark(benchmark_start_as_current_span)
benchmark(benchmark_start_span)


# pylint: disable=protected-access,redefined-outer-name
def test_simple_start_span_with_tracer_configurator_rules(
benchmark, num_tracer_configurator_rules
):
def benchmark_start_span():
span = tracer.start_span(
"benchmarkedSpan",
attributes={"long.attribute": -10000000001000000000},
)
span.add_event("benchmarkEvent")
span.end()

@lru_cache
def tracer_configurator(tracer_scope):
# this is testing 100 rules that is an extreme case
return _RuleBasedTracerConfigurator(
rules=[
(
_tracer_name_matches_glob(glob_pattern=str(i)),
_TracerConfig(is_enabled=True),
)
for i in range(num_tracer_configurator_rules)
],
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
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
OTEL_EXPORTER_OTLP_PROTOCOL,
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
OTEL_PYTHON_TRACER_CONFIGURATOR,
OTEL_TRACES_SAMPLER,
OTEL_TRACES_SAMPLER_ARG,
)
Expand All @@ -62,7 +63,11 @@
PeriodicExportingMetricReader,
)
from opentelemetry.sdk.resources import Attributes, Resource
from opentelemetry.sdk.trace import SpanProcessor, TracerProvider
from opentelemetry.sdk.trace import (
SpanProcessor,
TracerProvider,
_TracerConfiguratorT,
)
from opentelemetry.sdk.trace.export import BatchSpanProcessor, SpanExporter
from opentelemetry.sdk.trace.id_generator import IdGenerator
from opentelemetry.sdk.trace.sampling import Sampler
Expand Down Expand Up @@ -162,6 +167,10 @@ def _get_id_generator() -> str:
return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR)


def _get_tracer_configurator() -> str | None:
Comment thread
xrmx marked this conversation as resolved.
return environ.get(OTEL_PYTHON_TRACER_CONFIGURATOR, None)


def _get_exporter_entry_point(
exporter_name: str, signal_type: Literal["traces", "metrics", "logs"]
):
Expand Down Expand Up @@ -228,11 +237,13 @@ def _init_tracing(
exporter_args_map: ExporterArgsMap | None = None,
span_processors: Sequence[SpanProcessor] | None = None,
export_span_processor: _ConfigurationExporterSpanProcessorT | None = None,
tracer_configurator: _TracerConfiguratorT | None = None,
):
provider = TracerProvider(
id_generator=id_generator,
sampler=sampler,
resource=resource,
_tracer_configurator=tracer_configurator,
)
set_tracer_provider(provider)

Expand Down Expand Up @@ -348,6 +359,27 @@ def overwritten_config_fn(*args, **kwargs):
logging.basicConfig = wrapper(logging.basicConfig)


def _import_tracer_configurator(
tracer_configurator_name: str | None,
) -> _TracerConfiguratorT | None:
if not tracer_configurator_name:
return None

try:
_, tracer_configurator_impl = _import_config_components(
[tracer_configurator_name.strip()],
"_opentelemetry_tracer_configurator",
)[0]
except Exception as exc: # pylint: disable=broad-exception-caught
_logger.warning(
"Using default tracer configurator. Failed to load tracer configurator, %s: %s",
tracer_configurator_name,
exc,
)
return None
return tracer_configurator_impl


def _import_exporters(
trace_exporter_names: Sequence[str],
metric_exporter_names: Sequence[str],
Expand Down Expand Up @@ -467,6 +499,7 @@ def _initialize_components(
log_record_processors: Sequence[LogRecordProcessor] | None = None,
export_log_record_processor: _ConfigurationExporterLogRecordProcessorT
| None = None,
tracer_configurator: _TracerConfiguratorT | None = None,
):
# pylint: disable=too-many-locals
if trace_exporter_names is None:
Expand All @@ -493,6 +526,12 @@ def _initialize_components(
resource_attributes[ResourceAttributes.TELEMETRY_AUTO_VERSION] = ( # type: ignore[reportIndexIssue]
auto_instrumentation_version
)
if tracer_configurator is None:
tracer_configurator_name = _get_tracer_configurator()
tracer_configurator = _import_tracer_configurator(
tracer_configurator_name
)

# if env var OTEL_RESOURCE_ATTRIBUTES is given, it will read the service_name
# from the env variable else defaults to "unknown_service"
resource = Resource.create(resource_attributes)
Expand All @@ -505,6 +544,7 @@ def _initialize_components(
exporter_args_map=exporter_args_map,
span_processors=span_processors,
export_span_processor=export_span_processor,
tracer_configurator=tracer_configurator,
)
_init_metrics(
metric_exporters, resource, exporter_args_map=exporter_args_map
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -866,3 +866,15 @@ def channel_credential_provider() -> grpc.ChannelCredentials:
This is an experimental environment variable and the name of this variable and its behavior can
change in a non-backwards compatible way.
"""

OTEL_PYTHON_TRACER_CONFIGURATOR = "OTEL_PYTHON_TRACER_CONFIGURATOR"
"""
.. envvar:: OTEL_PYTHON_TRACER_CONFIGURATOR

The :envvar:`OTEL_PYTHON_TRACER_CONFIGURATOR` environment variable allows users to set a
custom Tracer Configurator function.
Default: opentelemetry.sdk.trace._default_tracer_configurator

This is an experimental environment variable and the name of this variable and its behavior can
change in a non-backwards compatible way.
"""
Loading
Loading