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 @@ -61,6 +61,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
([#5007](https://github.com/open-telemetry/opentelemetry-python/pull/5007))
- Redo OTLPMetricExporter unit tests of `max_export_batch_size` to use real `export`
([#5036](https://github.com/open-telemetry/opentelemetry-python/pull/5036))
- `opentelemetry-sdk`: Implement experimental Logger configurator
([#4980](https://github.com/open-telemetry/opentelemetry-python/pull/4980))

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
LoggingHandler,
LogRecordProcessor,
)
from opentelemetry.sdk._logs._internal import LoggerConfiguratorT
from opentelemetry.sdk._logs.export import (
BatchLogRecordProcessor,
LogRecordExporter,
Expand All @@ -52,6 +53,7 @@
OTEL_EXPORTER_OTLP_METRICS_PROTOCOL,
OTEL_EXPORTER_OTLP_PROTOCOL,
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL,
OTEL_PYTHON_LOGGER_CONFIGURATOR,
OTEL_PYTHON_METER_CONFIGURATOR,
OTEL_PYTHON_TRACER_CONFIGURATOR,
OTEL_TRACES_SAMPLER,
Expand Down Expand Up @@ -177,6 +179,10 @@ def _get_meter_configurator() -> str | None:
return environ.get(OTEL_PYTHON_METER_CONFIGURATOR, None)


def _get_logger_configurator() -> str | None:
return environ.get(OTEL_PYTHON_LOGGER_CONFIGURATOR, None)


def _get_exporter_entry_point(
exporter_name: str, signal_type: Literal["traces", "metrics", "logs"]
):
Expand Down Expand Up @@ -297,6 +303,7 @@ def _init_metrics(
set_meter_provider(provider)


# pylint: disable-next=too-many-locals
def _init_logging(
exporters: dict[str, Type[LogRecordExporter]],
resource: Resource | None = None,
Expand All @@ -305,8 +312,11 @@ def _init_logging(
log_record_processors: Sequence[LogRecordProcessor] | None = None,
export_log_record_processor: _ConfigurationExporterLogRecordProcessorT
| None = None,
logger_configurator: LoggerConfiguratorT | None = None,
):
provider = LoggerProvider(resource=resource)
provider = LoggerProvider(
resource=resource, logger_configurator=logger_configurator
)
set_logger_provider(provider)

exporter_args_map = exporter_args_map or {}
Expand Down Expand Up @@ -377,6 +387,27 @@ def overwritten_config_fn(*args, **kwargs):
logging.basicConfig = wrapper(logging.basicConfig)


def _import_logger_configurator(
logger_configurator_name: str | None,
) -> LoggerConfiguratorT | None:
if not logger_configurator_name:
return None

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


def _import_tracer_configurator(
tracer_configurator_name: str | None,
) -> _TracerConfiguratorT | None:
Expand Down Expand Up @@ -540,6 +571,7 @@ def _initialize_components(
| None = None,
tracer_configurator: _TracerConfiguratorT | None = None,
meter_configurator: _MeterConfiguratorT | None = None,
logger_configurator: LoggerConfiguratorT | None = None,
):
# pylint: disable=too-many-locals
if trace_exporter_names is None:
Expand Down Expand Up @@ -576,6 +608,11 @@ def _initialize_components(
meter_configurator = _import_meter_configurator(
meter_configurator_name
)
if logger_configurator is None:
logger_configurator_name = _get_logger_configurator()
logger_configurator = _import_logger_configurator(
logger_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"
Expand Down Expand Up @@ -613,6 +650,7 @@ def _initialize_components(
exporter_args_map=exporter_args_map,
log_record_processors=log_record_processors,
export_log_record_processor=export_log_record_processor,
logger_configurator=logger_configurator,
)


Expand Down
117 changes: 106 additions & 11 deletions opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,20 @@
import threading
import traceback
import warnings
from _weakrefset import WeakSet
from dataclasses import dataclass, field
from os import environ
from threading import Lock
from time import time_ns
from typing import Any, Callable, Tuple, Union, cast, overload # noqa
from typing import ( # noqa
Any,
Callable,
Sequence,
Tuple,
Union,
cast,
overload,
)

from typing_extensions import deprecated

Expand All @@ -51,7 +60,10 @@
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.util import ns_to_iso_str
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
from opentelemetry.sdk.util._configurator import RuleBasedConfigurator
from opentelemetry.sdk.util.instrumentation import (
InstrumentationScope,
)
from opentelemetry.semconv._incubating.attributes import code_attributes
from opentelemetry.semconv.attributes import exception_attributes
from opentelemetry.trace import (
Expand All @@ -63,6 +75,8 @@
_DEFAULT_OTEL_ATTRIBUTE_COUNT_LIMIT = 128
_ENV_VALUE_UNSET = ""

_logger = logging.getLogger(__name__)


class BytesEncoder(json.JSONEncoder):
def default(self, o):
Expand Down Expand Up @@ -637,6 +651,15 @@ def flush(self) -> None:
thread.start()


@dataclass
class LoggerConfig:
is_enabled: bool = True

@classmethod
def default(cls) -> LoggerConfig:
return LoggerConfig()


class Logger(APILogger):
def __init__(
self,
Expand All @@ -648,6 +671,7 @@ def __init__(
instrumentation_scope: InstrumentationScope,
*,
logger_metrics: LoggerMetrics,
logger_config: LoggerConfig,
):
super().__init__(
instrumentation_scope.name,
Expand All @@ -659,6 +683,17 @@ def __init__(
self._multi_log_record_processor = multi_log_record_processor
self._instrumentation_scope = instrumentation_scope
self._logger_metrics = logger_metrics
self._logger_config = logger_config

def _is_enabled(self) -> bool:
return self._logger_config.is_enabled

def set_logger_config(self, logger_config: LoggerConfig) -> None:
self._logger_config = logger_config

@property
def instrumentation_scope(self):
return self._instrumentation_scope

@property
def resource(self):
Expand All @@ -681,6 +716,8 @@ def emit(
"""Emits the :class:`ReadWriteLogRecord` by setting instrumentation scope
and forwarding to the processor.
"""
if not self._is_enabled():
return
# If a record is provided, use it directly
if record is not None:
if not isinstance(record, ReadWriteLogRecord):
Expand Down Expand Up @@ -715,6 +752,22 @@ def emit(
self._multi_log_record_processor.on_emit(writable_record)


LoggerConfiguratorT = Callable[[InstrumentationScope], LoggerConfig]
RuleBasedLoggerConfigurator = RuleBasedConfigurator[LoggerConfig]


def _default_logger_configurator(
_logger_scope: InstrumentationScope,
) -> LoggerConfig:
return LoggerConfig.default()


def _disable_logger_configurator(
_logger_scope: InstrumentationScope,
) -> LoggerConfig:
return LoggerConfig(is_enabled=False)


class LoggerProvider(APILoggerProvider):
def __init__(
self,
Expand All @@ -725,6 +778,7 @@ def __init__(
| None = None,
*,
meter_provider: MeterProvider | None = None,
logger_configurator: LoggerConfiguratorT | None = None,
):
if resource is None:
self._resource = Resource.create({})
Expand All @@ -738,11 +792,16 @@ def __init__(
)
disabled = environ.get(OTEL_SDK_DISABLED, "")
self._disabled = disabled.lower().strip() == "true"
self._logger_configurator = (
logger_configurator or _default_logger_configurator
)
self._at_exit_handler = None
if shutdown_on_exit:
self._at_exit_handler = atexit.register(self.shutdown)
self._logger_cache = {}
self._logger_cache_lock = Lock()
self._active_loggers: WeakSet[Logger] = WeakSet()
self._active_loggers_lock = Lock()

@property
def resource(self):
Expand All @@ -755,16 +814,14 @@ def _get_logger_no_cache(
schema_url: str | None = None,
attributes: _ExtendedAttributes | None = None,
) -> Logger:
scope = InstrumentationScope(name, version, schema_url, attributes)

return Logger(
self._resource,
self._multi_log_record_processor,
InstrumentationScope(
name,
version,
schema_url,
attributes,
),
scope,
logger_metrics=self._logger_metrics,
logger_config=self._apply_logger_configurator(scope),
)

def _get_logger_cached(
Expand Down Expand Up @@ -797,9 +854,16 @@ def get_logger(
schema_url=schema_url,
attributes=attributes,
)
if attributes is None:
return self._get_logger_cached(name, version, schema_url)
return self._get_logger_no_cache(name, version, schema_url, attributes)
logger = (
self._get_logger_cached(name, version, schema_url)
if attributes is None
else self._get_logger_no_cache(
name, version, schema_url, attributes
)
)
with self._active_loggers_lock:
self._active_loggers.add(logger)
return logger

def add_log_record_processor(
self, log_record_processor: LogRecordProcessor
Expand All @@ -812,6 +876,37 @@ def add_log_record_processor(
log_record_processor
)

def set_logger_configurator(
self, *, logger_configurator: LoggerConfiguratorT
):
"""Set a new LoggerConfigurator for this LoggerProvider.

Setting a new LoggerConfigurator will result in the configurator being called
for each outstanding Logger and for any newly created loggers thereafter.
Therefore, it is important that the provided function returns quickly.
"""
self._logger_configurator = logger_configurator
with self._active_loggers_lock:
for logger in self._active_loggers:
logger.set_logger_config(
self._apply_logger_configurator(
logger.instrumentation_scope
)
)

def _apply_logger_configurator(
self, instrumentation_scope: InstrumentationScope
) -> LoggerConfig:
try:
return self._logger_configurator(instrumentation_scope)
# pylint: disable-next=broad-exception-caught
except Exception:
_logger.exception(
"logger configurator failed for scope '%s', using default config",
instrumentation_scope.name,
)
return LoggerConfig.default()

def shutdown(self) -> None:
"""Shuts down the log processors."""
self._multi_log_record_processor.shutdown()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -826,3 +826,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_LOGGER_CONFIGURATOR = "OTEL_PYTHON_LOGGER_CONFIGURATOR"
"""
.. envvar:: OTEL_PYTHON_LOGGER_CONFIGURATOR

The :envvar:`OTEL_PYTHON_LOGGER_CONFIGURATOR` environment variable allows users to set a
custom Logger Configurator function.
Default: opentelemetry.sdk._logs._internal._default_logger_configurator

This is an experimental environment variable and the name of this variable and its behavior can
change in a non-backwards compatible way.
"""
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@
SdkConfiguration,
)
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.util._configurator import RuleBasedConfigurator
from opentelemetry.sdk.util.instrumentation import (
InstrumentationScope,
_InstrumentationScopePredicateT,
)
from opentelemetry.util._once import Once
from opentelemetry.util.types import (
Expand Down Expand Up @@ -414,9 +414,7 @@ def _get_exemplar_filter(exemplar_filter: str) -> ExemplarFilter:


_MeterConfiguratorT = Callable[[InstrumentationScope], _MeterConfig]
_MeterConfiguratorRulesT = Sequence[
tuple[_InstrumentationScopePredicateT, _MeterConfig]
]
_RuleBasedMeterConfigurator = RuleBasedConfigurator[_MeterConfig]


def _default_meter_configurator(
Expand All @@ -431,27 +429,6 @@ def _disable_meter_configurator(
return _MeterConfig(is_enabled=False)


class _RuleBasedMeterConfigurator:
def __init__(
self,
*,
rules: _MeterConfiguratorRulesT,
default_config: _MeterConfig,
):
self._rules = rules
self._default_config = default_config

def __call__(self, meter_scope: InstrumentationScope) -> _MeterConfig:
for predicate, meter_config in self._rules:
if predicate(meter_scope):
return meter_config
# by default return default config
return self._default_config

def update_rules(self, rules: _MeterConfiguratorRulesT) -> None:
self._rules = rules


class MeterProvider(APIMeterProvider):
r"""See `opentelemetry.metrics.MeterProvider`.

Expand Down
Loading
Loading