# 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