Ctrl K
ComunidadGuías…

core/logging_setup.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# SPDX-License-Identifier: BUSL-1.1
# Copyright (c) 2024-2026 Ali Sadeghi Aghili
"""Ensure the application's own loggers actually reach a handler.

Both documented ways of starting the HTTP API (``uvicorn api.server:app
--host ... --port ... --no-server-header`` and ``python -m api``) leave
the ROOT logger exactly as Python's ``logging`` module starts it: level
``WARNING``, no handlers. uvicorn's own default logging config
(``uvicorn.config.LOGGING_CONFIG``) only attaches handlers to the
``uvicorn`` / ``uvicorn.error`` / ``uvicorn.access`` loggers, each with
``propagate: False`` -- it never touches the root logger, and its
``disable_existing_loggers`` is explicitly ``False``. So every other
logger in the process, including this application's own
(``logging.getLogger(__name__)`` in ``api/server.py`` and elsewhere), is
left exactly where the interpreter started it: no handler anywhere in its
chain, and an effective level of ``WARNING`` inherited from the
unconfigured root. ``logger.info(...)`` calls -- the startup provenance
banner (:func:`core.provenance.log_startup_notice`) and the
CORS-allowlist line (``api/server.py``'s ``lifespan``) among them -- are
filtered out before a single handler is ever asked to write them; they
were never silently dropped BY a handler, they never became ``LogRecord``
objects at all, since the level check happens first.

:func:`configure_stdlib_logging` fixes this without fighting an operator
who configured logging themselves (their own ``logging.basicConfig()``, a
``dictConfig``, a framework's own setup, ...): it only acts when the ROOT
logger has no handlers at all, which is the exact, unambiguous signal
that nothing -- not this codebase, not the operator, not some other
library -- has configured logging yet. When that is true, it attaches one
``StreamHandler`` (stderr, a plain ``[LEVEL] name: message`` format, no
uvicorn-style colour codes -- this runs whether or not uvicorn is even
present) and raises the root logger's level from the default ``WARNING``
to the requested level (:data:`config.Settings.log_level`, default
``INFO``), so every first-party logger that has not set its own level --
which is all of them; nothing in this codebase calls ``setLevel`` on its
own logger -- starts actually emitting at that level.

Idempotent: calling it again once a handler is already present (whether
this function added it or an operator did) is a no-op, so calling it from
both ``api/server.py``'s ``lifespan`` and ``api/__main__.py`` (defence in
depth -- whichever runs first is enough) never attaches a second handler
and never double-prints a line.

Examples
--------
>>> import logging
>>> root = logging.getLogger()
>>> saved_handlers, saved_level = list(root.handlers), root.level
>>> root.handlers = []
>>> root.setLevel(logging.WARNING)
>>> configure_stdlib_logging("INFO")
>>> root.level == logging.INFO
True
>>> len(root.handlers)
1
>>> configure_stdlib_logging("DEBUG")  # already has a handler -- no-op
>>> len(root.handlers)
1
>>> root.level == logging.INFO
True
>>> root.handlers = saved_handlers
>>> root.setLevel(saved_level)
"""

from __future__ import annotations

import logging
import sys


def configure_stdlib_logging(level_name: str) -> None:
    """Attach a stderr handler to the ROOT logger and raise its level to
    *level_name*, but only when the root logger has no handler at all --
    see this module's docstring for why that check is the right one.

    *level_name* is a ``logging`` level name (``"INFO"``, ``"DEBUG"``,
    ...), case-insensitive; an unrecognised name falls back to ``INFO``
    rather than raising -- a typo in ``LOG_LEVEL`` should degrade to a
    sane default, not take startup logging down with it.
    """
    root = logging.getLogger()
    if root.handlers:
        return
    level = getattr(logging, str(level_name).upper(), None)
    if not isinstance(level, int):
        level = logging.INFO
    handler = logging.StreamHandler(sys.stderr)
    handler.setFormatter(logging.Formatter("%(asctime)s [%(levelname)s] %(name)s: %(message)s"))
    root.addHandler(handler)
    root.setLevel(level)