diff --git a/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py b/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py index c2d5590144c..690c05388ec 100644 --- a/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py +++ b/opentelemetry-sdk/benchmarks/trace/test_benchmark_trace.py @@ -21,10 +21,10 @@ TracerProvider, _default_tracer_configurator, _RuleBasedTracerConfigurator, - _scope_name_matches_glob, _TracerConfig, sampling, ) +from opentelemetry.sdk.util.instrumentation import _scope_name_matches_glob tracer = TracerProvider( sampler=sampling.DEFAULT_ON, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py index 602b105ca7c..d854fe53544 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_configuration/__init__.py @@ -42,6 +42,7 @@ LoggingHandler, LogRecordProcessor, ) +from opentelemetry.sdk._logs._internal import LoggerConfiguratorT from opentelemetry.sdk._logs.export import ( BatchLogRecordProcessor, LogRecordExporter, @@ -52,6 +53,7 @@ OTEL_EXPORTER_OTLP_METRICS_PROTOCOL, OTEL_EXPORTER_OTLP_PROTOCOL, OTEL_EXPORTER_OTLP_TRACES_PROTOCOL, + OTEL_PYTHON_LOGGER_CONFIGURATOR, OTEL_PYTHON_TRACER_CONFIGURATOR, OTEL_TRACES_SAMPLER, OTEL_TRACES_SAMPLER_ARG, @@ -171,6 +173,10 @@ def _get_tracer_configurator() -> str | None: return environ.get(OTEL_PYTHON_TRACER_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"] ): @@ -294,8 +300,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 {} @@ -366,6 +375,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: @@ -507,6 +537,7 @@ def _initialize_components( export_log_record_processor: _ConfigurationExporterLogRecordProcessorT | None = None, tracer_configurator: _TracerConfiguratorT | None = None, + logger_configurator: LoggerConfiguratorT | None = None, ): # pylint: disable=too-many-locals if trace_exporter_names is None: @@ -538,6 +569,11 @@ def _initialize_components( tracer_configurator = _import_tracer_configurator( tracer_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" @@ -572,6 +608,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, ) diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py index 9029f867a7e..0b8cd551625 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/_logs/_internal/__init__.py @@ -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 @@ -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.instrumentation import ( + InstrumentationScope, + _InstrumentationScopePredicateT, +) from opentelemetry.semconv._incubating.attributes import code_attributes from opentelemetry.semconv.attributes import exception_attributes from opentelemetry.trace import ( @@ -631,6 +643,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, @@ -642,6 +663,7 @@ def __init__( instrumentation_scope: InstrumentationScope, *, logger_metrics: LoggerMetrics, + logger_config: LoggerConfig, ): super().__init__( instrumentation_scope.name, @@ -653,6 +675,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): @@ -675,6 +708,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): @@ -709,6 +744,42 @@ def emit( self._multi_log_record_processor.on_emit(writable_record) +LoggerConfiguratorT = Callable[[InstrumentationScope], LoggerConfig] +LoggerConfiguratorRulesT = Sequence[ + tuple[_InstrumentationScopePredicateT, 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 RuleBasedLoggerConfigurator: + def __init__( + self, + *, + rules: LoggerConfiguratorRulesT, + default_config: LoggerConfig, + ): + self._rules = rules + self._default_config = default_config + + def __call__(self, logger_scope: InstrumentationScope) -> LoggerConfig: + for predicate, logger_config in self._rules: + if predicate(logger_scope): + return logger_config + # by default return default config + return self._default_config + + class LoggerProvider(APILoggerProvider): def __init__( self, @@ -719,6 +790,7 @@ def __init__( | None = None, *, meter_provider: MeterProvider | None = None, + logger_configurator: LoggerConfiguratorT | None = None, ): if resource is None: self._resource = Resource.create({}) @@ -732,11 +804,14 @@ def __init__( ) disabled = environ.get(OTEL_SDK_DISABLED, "") self._disabled = disabled.lower().strip() == "true" + self._logger_configurator = 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() + self._active_loggers_lock = Lock() @property def resource(self): @@ -749,16 +824,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._logger_configurator(scope), ) def _get_logger_cached( @@ -791,9 +864,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 @@ -806,6 +886,24 @@ 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: + if not isinstance(logger, Logger): + continue + logger.set_logger_config( + self._logger_configurator(logger.instrumentation_scope) + ) + def shutdown(self): """Shuts down the log processors.""" self._multi_log_record_processor.shutdown() diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py index f049415a15b..c4f6f5d33ee 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/environment_variables/__init__.py @@ -814,3 +814,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. +""" diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py index e0b639d81cf..9f8e016e614 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/trace/__init__.py @@ -16,7 +16,6 @@ import abc import atexit import concurrent.futures -import fnmatch import json import logging import os @@ -70,6 +69,7 @@ from opentelemetry.sdk.util.instrumentation import ( InstrumentationInfo, InstrumentationScope, + _InstrumentationScopePredicateT, ) from opentelemetry.semconv.attributes.exception_attributes import ( EXCEPTION_ESCAPED, @@ -1262,22 +1262,11 @@ def start_span( # pylint: disable=too-many-locals _TracerConfiguratorT = Callable[[InstrumentationScope], _TracerConfig] -_InstrumentationScopePredicateT = Callable[[InstrumentationScope], bool] _TracerConfiguratorRulesT = Sequence[ tuple[_InstrumentationScopePredicateT, _TracerConfig] ] -# TODO: share this with configurators for other signals -def _scope_name_matches_glob( - glob_pattern: str, -) -> _InstrumentationScopePredicateT: - def inner(scope: InstrumentationScope) -> bool: - return fnmatch.fnmatch(scope.name, glob_pattern) - - return inner - - class _RuleBasedTracerConfigurator: def __init__( self, diff --git a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py index cdee837f669..fd8af277f58 100644 --- a/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py +++ b/opentelemetry-sdk/src/opentelemetry/sdk/util/instrumentation.py @@ -11,8 +11,9 @@ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. +import fnmatch from json import dumps -from typing import Optional +from typing import Callable, Optional from typing_extensions import deprecated @@ -167,3 +168,15 @@ def to_json(self, indent: Optional[int] = 4) -> str: }, indent=indent, ) + + +_InstrumentationScopePredicateT = Callable[[InstrumentationScope], bool] + + +def _scope_name_matches_glob( + glob_pattern: str, +) -> _InstrumentationScopePredicateT: + def inner(scope: InstrumentationScope) -> bool: + return fnmatch.fnmatch(scope.name, glob_pattern) + + return inner