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 @@ -20,6 +20,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#4798](https://github.com/open-telemetry/opentelemetry-python/pull/4798))
- Silence events API warnings for internal users
([#4847](https://github.com/open-telemetry/opentelemetry-python/pull/4847))
- Implement experimental TracerConfigurator
([#4861](https://github.com/open-telemetry/opentelemetry-python/pull/4861))

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,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 @@ -58,7 +59,7 @@
PeriodicExportingMetricReader,
)
from opentelemetry.sdk.resources import Attributes, Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace import 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 @@ -146,6 +147,10 @@ def _get_id_generator() -> str:
return environ.get(OTEL_PYTHON_ID_GENERATOR, _DEFAULT_ID_GENERATOR)


def _get_tracer_configurator() -> str | None:
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I've added the configuration via env var to match what we are doing with the others TraceProvider parameters, since this is in development I can drop it. For my use case I can just use _OTelSDKConfigurator._configure.

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 @@ -210,11 +215,13 @@ def _init_tracing(
sampler: Sampler | None = None,
resource: Resource | None = None,
exporter_args_map: ExporterArgsMap | 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 @@ -315,6 +322,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 @@ -429,6 +457,7 @@ def _initialize_components(
id_generator: IdGenerator | None = None,
setup_logging_handler: bool | None = None,
exporter_args_map: ExporterArgsMap | None = None,
tracer_configurator: _TracerConfiguratorT | None = None,
):
if trace_exporter_names is None:
trace_exporter_names = []
Expand All @@ -454,6 +483,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 @@ -464,6 +499,7 @@ def _initialize_components(
sampler=sampler,
resource=resource,
exporter_args_map=exporter_args_map,
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.
"""
130 changes: 121 additions & 9 deletions opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
import threading
import traceback
import typing
from dataclasses import dataclass
from os import environ
from time import time_ns
from types import MappingProxyType, TracebackType
Expand All @@ -39,6 +40,7 @@
Union,
)
from warnings import filterwarnings
from weakref import WeakSet

from typing_extensions import deprecated

Expand Down Expand Up @@ -71,7 +73,7 @@
EXCEPTION_STACKTRACE,
EXCEPTION_TYPE,
)
from opentelemetry.trace import NoOpTracer, SpanContext
from opentelemetry.trace import INVALID_SPAN, NoOpTracer, SpanContext
from opentelemetry.trace.status import Status, StatusCode
from opentelemetry.util import types
from opentelemetry.util._decorator import _agnosticcontextmanager
Expand Down Expand Up @@ -292,7 +294,7 @@ def force_flush(self, timeout_millis: int = 30000) -> bool:
timeout, False otherwise.
"""
futures = []
for sp in self._span_processors: # type: SpanProcessor
for sp in self._span_processors:
future = self._executor.submit(sp.force_flush, timeout_millis)
futures.append(future)

Expand Down Expand Up @@ -1071,6 +1073,11 @@ class _Span(Span):
"""


@dataclass
class _TracerConfig:
is_enabled: bool


class Tracer(trace_api.Tracer):
"""See `opentelemetry.trace.Tracer`."""

Expand All @@ -1085,6 +1092,8 @@ def __init__(
instrumentation_info: InstrumentationInfo,
span_limits: SpanLimits,
instrumentation_scope: InstrumentationScope,
*,
_tracer_config: Optional[_TracerConfig] = None,
) -> None:
self.sampler = sampler
self.resource = resource
Expand All @@ -1094,6 +1103,19 @@ def __init__(
self._span_limits = span_limits
self._instrumentation_scope = instrumentation_scope

self._enabled = (
_tracer_config.is_enabled if _tracer_config is not None else True
)

def _update_tracer_config(self, tracer_config: _TracerConfig):
self._enabled = tracer_config.is_enabled

@property
def _is_enabled(self) -> bool:
"""Instrumentations needs to call this API each time to check if they should
create a new span."""
return self._enabled

@_agnosticcontextmanager # pylint: disable=protected-access
def start_as_current_span(
self,
Expand Down Expand Up @@ -1136,6 +1158,9 @@ def start_span( # pylint: disable=too-many-locals
record_exception: bool = True,
set_status_on_exception: bool = True,
) -> trace_api.Span:
if not self._is_enabled:
return INVALID_SPAN

parent_span_context = trace_api.get_current_span(
context
).get_span_context()
Expand Down Expand Up @@ -1202,6 +1227,51 @@ def start_span( # pylint: disable=too-many-locals
return span


_TracerConfiguratorT = Callable[[InstrumentationScope], _TracerConfig]
_TracerConfiguratorRulesPredicateT = Callable[
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I guess a callable is not very declarative config friendly

[Optional[InstrumentationScope]], bool
]
_TracerConfiguratorRulesT = Sequence[
Tuple[_TracerConfiguratorRulesPredicateT, _TracerConfig]
]


class _RuleBaseTracerConfigurator:
def __init__(self, *, rules: _TracerConfiguratorRulesT):
self._rules = rules

def __call__(
self, tracer_scope: Optional[InstrumentationScope] = None
) -> _TracerConfig:
for predicate, tracer_config in self._rules:
if predicate(tracer_scope):
return tracer_config

# if no rule matched return a default one
return _TracerConfig(is_enabled=True)


def _default_tracer_configurator(
tracer_scope: InstrumentationScope,
) -> _TracerConfig:
"""Default Tracer Configurator implementation
In order to update Tracers configs you need to call
TracerProvider._set_tracer_configurator with a function
implementing this interface returning a Tracer Config."""
return _RuleBaseTracerConfigurator(
rules=[(lambda x: True, _TracerConfig(is_enabled=True))],
)(tracer_scope=tracer_scope)


def _disable_tracer_configurator(
tracer_scope: InstrumentationScope,
) -> _TracerConfig:
return _RuleBaseTracerConfigurator(
rules=[(lambda x: True, _TracerConfig(is_enabled=False))],
)(tracer_scope=tracer_scope)


class TracerProvider(trace_api.TracerProvider):
"""See `opentelemetry.trace.TracerProvider`."""

Expand All @@ -1215,6 +1285,8 @@ def __init__(
] = None,
id_generator: Optional[IdGenerator] = None,
span_limits: Optional[SpanLimits] = None,
*,
_tracer_configurator: Optional[_TracerConfiguratorT] = None,
Copy link
Member

Choose a reason for hiding this comment

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

This was the java thing I was mentioning: https://github.com/open-telemetry/opentelemetry-java/tree/main/api/incubator

I agree it might be overkill for this feature though.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yeah, I'm not sure how this maps into Python. The implementation I've added here with adding a new kwarg prefixed with the underscore is more or less the same it would be the concrete implementation when using overload to provide the one with only stable parameters.

) -> None:
self._active_span_processor = (
active_span_processor or SynchronousMultiSpanProcessor()
Expand All @@ -1238,6 +1310,37 @@ def __init__(
if shutdown_on_exit:
self._atexit_handler = atexit.register(self.shutdown)

self._tracer_configurator = (
_tracer_configurator or _default_tracer_configurator
)
self._cached_tracers: WeakSet[Tracer] = WeakSet()

def _set_tracer_configurator(
self, *, tracer_configurator: _TracerConfiguratorT
):
self._tracer_configurator = tracer_configurator
self._update_tracers(tracer_configurator=tracer_configurator)

def _update_tracers(
self,
*,
tracer_configurator: _TracerConfiguratorT,
):
# pylint: disable=protected-access
for tracer in self._cached_tracers:
tracer_config = tracer_configurator(tracer._instrumentation_scope)
tracer._update_tracer_config(tracer_config)

def _enable_tracers(self):
self._update_tracers(
tracer_configurator=_default_tracer_configurator,
)

def _disable_tracers(self):
self._update_tracers(
tracer_configurator=_disable_tracer_configurator,
)

@property
def resource(self) -> Resource:
return self._resource
Expand Down Expand Up @@ -1272,21 +1375,30 @@ def get_tracer(
schema_url,
)

return Tracer(
instrumentation_scope = InstrumentationScope(
instrumenting_module_name,
instrumenting_library_version,
schema_url,
attributes,
)

tracer_config = self._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,
_tracer_config=tracer_config,
)

self._cached_tracers.add(tracer)

return tracer

def add_span_processor(self, span_processor: SpanProcessor) -> None:
"""Registers a new :class:`SpanProcessor` for this `TracerProvider`.
Expand Down
Loading
Loading