mirror of
https://github.com/arc53/DocsGPT.git
synced 2026-05-07 06:30:03 +00:00
113 lines
3.8 KiB
Python
113 lines
3.8 KiB
Python
import logging
|
|
import os
|
|
from logging.config import dictConfig
|
|
|
|
from application.core.log_context import snapshot as _ctx_snapshot
|
|
|
|
|
|
# Loggers with ``propagate=False`` don't share root's handlers, so the
|
|
# context filter has to be installed on their handlers directly.
|
|
_NON_PROPAGATING_LOGGERS: tuple[str, ...] = (
|
|
"uvicorn",
|
|
"uvicorn.access",
|
|
"uvicorn.error",
|
|
"celery.app.trace",
|
|
"celery.worker.strategy",
|
|
"gunicorn.error",
|
|
"gunicorn.access",
|
|
)
|
|
|
|
|
|
class _ContextFilter(logging.Filter):
|
|
"""Stamp the current ``log_context`` snapshot onto every ``LogRecord``.
|
|
|
|
Must be installed on **handlers**, not loggers: Python skips logger-level
|
|
filters when a child logger's record propagates up. The ``hasattr`` guard
|
|
keeps an explicit ``logger.info(..., extra={...})`` from being overwritten.
|
|
"""
|
|
|
|
def filter(self, record: logging.LogRecord) -> bool:
|
|
for key, value in _ctx_snapshot().items():
|
|
if not hasattr(record, key):
|
|
setattr(record, key, value)
|
|
return True
|
|
|
|
|
|
def _otlp_logs_enabled() -> bool:
|
|
"""Return True when the user has opted in to OTLP log export.
|
|
|
|
Gated by the standard OTEL env vars so no project-specific knob is needed:
|
|
set ``OTEL_LOGS_EXPORTER=otlp`` (and leave ``OTEL_SDK_DISABLED`` unset or
|
|
false) to flip it on. When false, ``setup_logging`` keeps its original
|
|
console-only behavior.
|
|
"""
|
|
exporter = os.getenv("OTEL_LOGS_EXPORTER", "").strip().lower()
|
|
disabled = os.getenv("OTEL_SDK_DISABLED", "false").strip().lower() == "true"
|
|
return exporter == "otlp" and not disabled
|
|
|
|
|
|
def setup_logging() -> None:
|
|
"""Configure the root logger with a stdout console handler.
|
|
|
|
When OTLP log export is enabled, ``opentelemetry-instrument`` attaches a
|
|
``LoggingHandler`` to the root logger before this function runs. The
|
|
``dictConfig`` call below replaces ``root.handlers`` with the console
|
|
handler, which would silently drop the OTEL handler. To make OTLP log
|
|
export work without forcing every contributor to opt in, snapshot the
|
|
OTEL handlers up front and re-attach them after ``dictConfig``.
|
|
"""
|
|
preserved_handlers: list[logging.Handler] = []
|
|
if _otlp_logs_enabled():
|
|
preserved_handlers = [
|
|
h
|
|
for h in logging.getLogger().handlers
|
|
if h.__class__.__module__.startswith("opentelemetry")
|
|
]
|
|
|
|
dictConfig({
|
|
"version": 1,
|
|
"disable_existing_loggers": False,
|
|
"formatters": {
|
|
"default": {
|
|
"format": "[%(asctime)s] %(levelname)s in %(module)s: %(message)s",
|
|
}
|
|
},
|
|
"handlers": {
|
|
"console": {
|
|
"class": "logging.StreamHandler",
|
|
"stream": "ext://sys.stdout",
|
|
"formatter": "default",
|
|
}
|
|
},
|
|
"root": {
|
|
"level": "INFO",
|
|
"handlers": ["console"],
|
|
},
|
|
})
|
|
|
|
if preserved_handlers:
|
|
root = logging.getLogger()
|
|
for handler in preserved_handlers:
|
|
if handler not in root.handlers:
|
|
root.addHandler(handler)
|
|
|
|
_install_context_filter()
|
|
|
|
|
|
def _install_context_filter() -> None:
|
|
"""Attach :class:`_ContextFilter` to root's handlers + every handler on
|
|
the known non-propagating loggers. Skipping handlers that already carry
|
|
one keeps repeat ``setup_logging`` calls from stacking filters.
|
|
"""
|
|
|
|
def _has_ctx_filter(handler: logging.Handler) -> bool:
|
|
return any(isinstance(f, _ContextFilter) for f in handler.filters)
|
|
|
|
for handler in logging.getLogger().handlers:
|
|
if not _has_ctx_filter(handler):
|
|
handler.addFilter(_ContextFilter())
|
|
for name in _NON_PROPAGATING_LOGGERS:
|
|
for handler in logging.getLogger(name).handlers:
|
|
if not _has_ctx_filter(handler):
|
|
handler.addFilter(_ContextFilter())
|