Source code for opentelemetry.instrumentation.logging

# Copyright The OpenTelemetry Authors
# SPDX-License-Identifier: Apache-2.0

# pylint: disable=empty-docstring,no-value-for-parameter,no-member,no-name-in-module

"""
The OpenTelemetry `logging` instrumentation automatically instruments Python logging
system with an handler to convert log messages into OpenTelemetry logs.
You can disable this setting `OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION` to `false`.

The OpenTelemetry `logging` integration can inject tracing context into
log statements, though it is opt-in and must be enabled explicitly by setting the
environment variable `OTEL_PYTHON_LOG_CORRELATION` to `true`.

.. code-block:: python

    import logging

    from opentelemetry.instrumentation.logging import LoggingInstrumentor

    LoggingInstrumentor().instrument()

    logging.warning('OTel test')

When running the above example you will see the following output:

::

    2025-03-05 09:40:04,398 WARNING [root] [example.py:7] [trace_id=0 span_id=0 resource.service.name= trace_sampled=False] - OTel test

The environment variable `OTEL_PYTHON_LOG_CORRELATION` must be set to `true`
in order to enable trace context injection into logs by calling
`logging.basicConfig()` and setting a logging format that makes use of the
injected tracing variables.

Alternatively, `set_logging_format` argument can be set to `True` when
initializing the `LoggingInstrumentor` class to achieve the same effect:

.. code-block:: python

    import logging

    from opentelemetry.instrumentation.logging import LoggingInstrumentor

    LoggingInstrumentor().instrument(set_logging_format=True)

    logging.warning('OTel test')

"""

import logging  # pylint: disable=import-self
from os import environ
from typing import Collection, Optional

from opentelemetry._logs import get_logger_provider
from opentelemetry.instrumentation.instrumentor import BaseInstrumentor
from opentelemetry.instrumentation.logging.constants import (
    _MODULE_DOC,
    DEFAULT_LOGGING_FORMAT,
)
from opentelemetry.instrumentation.logging.environment_variables import (
    OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION,
    OTEL_PYTHON_LOG_CODE_ATTRIBUTES,
    OTEL_PYTHON_LOG_CORRELATION,
    OTEL_PYTHON_LOG_FORMAT,
    OTEL_PYTHON_LOG_HANDLER_LEVEL,
    OTEL_PYTHON_LOG_LEVEL,
)
from opentelemetry.instrumentation.logging.handler import (
    _setup_logging_handler,
)
from opentelemetry.instrumentation.logging.package import _instruments
from opentelemetry.trace import (
    INVALID_SPAN,
    INVALID_SPAN_CONTEXT,
    get_current_span,
    get_tracer_provider,
)

__doc__ = _MODULE_DOC  # noqa: A001

LEVELS = {
    "debug": logging.DEBUG,
    "info": logging.INFO,
    "warning": logging.WARNING,
    "error": logging.ERROR,
}

_logger = logging.getLogger(__name__)


def _get_log_level(level_name: Optional[str]) -> Optional[int]:
    if level_name is None:
        return None
    result = logging.getLevelName(level_name.upper().strip())
    if not isinstance(result, int):
        _logger.warning(
            "Invalid log level %r for %s; defaulting to NOTSET",
            level_name,
            OTEL_PYTHON_LOG_HANDLER_LEVEL,
        )
        return logging.NOTSET
    return result


[docs]class LoggingInstrumentor(BaseInstrumentor): # pylint: disable=empty-docstring __doc__ = f"""An instrumentor for stdlib logging module. This instrumentor optionally injects tracing context into logging records and sets the global logging format to the following: .. code-block:: {DEFAULT_LOGGING_FORMAT} def log_hook(span: Span, record: LogRecord): if span and span.is_recording(): record.custom_user_attribute_from_log_hook = "some-value" span_ctx = span.get_span_context() record.from_sampled_span = span_ctx.trace_flags.sampled Args: tracer_provider: Tracer provider instance that can be used to fetch a tracer. set_logging_format: When set to True, it calls logging.basicConfig() and sets a logging format. logging_format: Accepts a string and sets it as the logging format when set_logging_format is set to True. log_level: Accepts one of the following values and sets the logging level to it. logging.INFO logging.DEBUG logging.WARN logging.ERROR logging.FATAL log_hook: execute custom logic when record is created See `BaseInstrumentor` """ _old_factory = None _log_hook = None _logging_handler = None
[docs] def instrumentation_dependencies(self) -> Collection[str]: return _instruments
def _instrument(self, **kwargs): provider = kwargs.get("tracer_provider", None) or get_tracer_provider() old_factory = logging.getLogRecordFactory() LoggingInstrumentor._old_factory = old_factory LoggingInstrumentor._log_hook = kwargs.get("log_hook", None) service_name = None set_logging_format = kwargs.get( "set_logging_format", environ.get(OTEL_PYTHON_LOG_CORRELATION, "false").lower() == "true", ) if set_logging_format: log_format = kwargs.get( "logging_format", environ.get(OTEL_PYTHON_LOG_FORMAT, None) ) log_format = log_format or DEFAULT_LOGGING_FORMAT log_level = kwargs.get( "log_level", LEVELS.get(environ.get(OTEL_PYTHON_LOG_LEVEL)) ) log_level = log_level or logging.INFO logging.basicConfig(format=log_format, level=log_level) def record_factory(*args, **kwargs): record = old_factory(*args, **kwargs) # this factory is a no-op if log correlation or log hook are not set if not set_logging_format and not callable( LoggingInstrumentor._log_hook ): return record # out of spec attributes are added to the log record only if log correlation is set if set_logging_format: record.otelSpanID = "0" record.otelTraceID = "0" record.otelTraceSampled = False nonlocal service_name if service_name is None: resource = getattr(provider, "resource", None) if resource: service_name = ( resource.attributes.get("service.name") or "" ) else: service_name = "" record.otelServiceName = service_name span = get_current_span() if span != INVALID_SPAN: ctx = span.get_span_context() if ctx != INVALID_SPAN_CONTEXT: if set_logging_format: record.otelSpanID = format(ctx.span_id, "016x") record.otelTraceID = format(ctx.trace_id, "032x") record.otelTraceSampled = ctx.trace_flags.sampled if callable(LoggingInstrumentor._log_hook): try: LoggingInstrumentor._log_hook( # pylint: disable=E1102 span, record ) except Exception: # pylint: disable=W0703 pass return record logging.setLogRecordFactory(record_factory) # Here we need to handle 3 scenarios: # - the sdk logging handler is enabled and we should do no nothing # - the sdk logging handler is not enabled and we should setup the handler by default # - the sdk logging handler is not enabled and the user do not want we setup the handler sdk_autoinstrumentation_env_var = ( environ.get( "OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED", "notset" ) .strip() .lower() ) if sdk_autoinstrumentation_env_var == "true": _logger.warning( "Skipping installation of LoggingHandler from " "`opentelemetry-instrumentation-logging` to avoid duplicate logs. " "The SDK's deprecated LoggingHandler is already active " "(OTEL_PYTHON_LOGGING_AUTO_INSTRUMENTATION_ENABLED=true). To migrate, unset " "this environment variable. The SDK's handler will be removed in a future release." ) elif kwargs.get( "enable_log_auto_instrumentation", environ.get(OTEL_PYTHON_LOG_AUTO_INSTRUMENTATION, "true") .strip() .lower() == "true", ): log_code_attributes = kwargs.get( "log_code_attributes", environ.get(OTEL_PYTHON_LOG_CODE_ATTRIBUTES, "false") .strip() .lower() == "true", ) handler_level = kwargs.get( "log_handler_level", _get_log_level(environ.get(OTEL_PYTHON_LOG_HANDLER_LEVEL)), ) logger_provider = get_logger_provider() handler = _setup_logging_handler( logger_provider=logger_provider, log_code_attributes=log_code_attributes, level=handler_level, ) LoggingInstrumentor._logging_handler = handler def _uninstrument(self, **kwargs): if LoggingInstrumentor._old_factory: logging.setLogRecordFactory(LoggingInstrumentor._old_factory) LoggingInstrumentor._old_factory = None if LoggingInstrumentor._logging_handler: logging.getLogger().removeHandler( LoggingInstrumentor._logging_handler ) LoggingInstrumentor._logging_handler = None