Update logging_config.py

This commit is contained in:
Saifeddine ALOUI
2025-09-05 15:32:02 +02:00
committed by GitHub
parent 2843540686
commit 0d900f4240

View File

@@ -1,99 +1,114 @@
# app/core/logging_config.py
"""
Centralised logging configuration for the Ollama Proxy Server.
Centralised logging configuration.
The configuration is used both when the app is started directly
(e.g. `uvicorn app.main:app`) and when it is launched via Gunicorn.
It follows the standard ``logging.config.dictConfig`` schema,
including a proper ``root`` logger definition, which resolves the
“Unable to configure root logger” error.
Features
--------
* Humanreadable console output by default.
* Optional JSON output via the LOG_FORMAT envvar (kept for backwardcompatibility).
* All loggers (root, uvicorn, gunicorn, etc.) share the same configuration.
* The `setup_logging()` helper can be called from any entrypoint (FastAPI,
management scripts, tests) to guarantee a consistent format.
"""
import logging
import logging.config
import os
import sys
from pythonjsonlogger import jsonlogger
from datetime import datetime
# ----------------------------------------------------------------------
# JSON formatter adds a timestamp and forces the level name to be uppercase
# Formatter definitions
# ----------------------------------------------------------------------
class CustomJsonFormatter(jsonlogger.JsonFormatter):
class HumanReadableFormatter(logging.Formatter):
"""
A thin wrapper around ``pythonjsonlogger.JsonFormatter`` that ensures
a ``timestamp`` field is always present and that the ``level`` field
is uppercased.
Example output:
2025-09-05 12:15:50,297 [ERROR] gunicorn.error Worker (pid:590871) was sent SIGINT!
"""
DEFAULT_FORMAT = "%(asctime)s [%(levelname)s] %(name)s %(message)s"
DEFAULT_DATEFMT = "%Y-%m-%d %H:%M:%S,%f"
def __init__(self):
super().__init__(self.DEFAULT_FORMAT, self.DEFAULT_DATEFMT)
class JsonFormatter(jsonlogger.JsonFormatter):
"""
JSON formatter kept for compatibility.
The same fields as the old config are emitted.
"""
def add_fields(self, log_record, record, message_dict):
super().add_fields(log_record, record, message_dict)
# ``record.created`` is a float epoch timestamp we keep it asis
# Ensure timestamp is a float (epoch) like before
if not log_record.get("timestamp"):
log_record["timestamp"] = record.created
# Normalise the level name (e.g. ``info`` → ``INFO``)
# Normalise level name to uppercase
log_record["level"] = (log_record.get("level") or record.levelname).upper()
# ----------------------------------------------------------------------
# Helper that builds the dictconfig structure.
# Build the dictConfig selects formatter based on LOG_FORMAT envvar
# ----------------------------------------------------------------------
def _build_logging_config(log_level: str = "INFO") -> dict:
def _build_logging_config(log_level: str = "INFO"):
"""
Returns a ``dict`` compatible with ``logging.config.dictConfig``.
``log_level`` can be any standard logging level name (caseinsensitive).
Returns a dict compatible with ``logging.config.dictConfig``.
``log_level`` can be any standard level name (caseinsensitive).
"""
level = log_level.upper()
# Choose formatter: humanreadable (default) or JSON
fmt_type = os.getenv("LOG_FORMAT", "human").lower()
if fmt_type == "json":
formatter_name = "json"
formatter_cfg = {
"()": "app.core.logging_config.JsonFormatter",
"format": "%(timestamp)s %(level)s %(name)s %(module)s %(funcName)s %(lineno)d %(message)s",
}
else: # humanreadable
formatter_name = "human"
formatter_cfg = {
"()": "app.core.logging_config.HumanReadableFormatter"
}
return {
"version": 1,
"disable_existing_loggers": False,
"formatters": {
"json": {
"()": "app.core.logging_config.CustomJsonFormatter",
"format": "%(timestamp)s %(level)s %(name)s %(module)s %(funcName)s %(lineno)d %(message)s",
},
"human": formatter_cfg,
"json": formatter_cfg,
},
"handlers": {
"default": {
"class": "logging.StreamHandler",
"formatter": "json",
"formatter": formatter_name,
"stream": "ext://sys.stdout",
},
},
# ------------------------------------------------------------------
# NOTE: The *root* logger must be defined with the key ``"root"``,
# not an empty string. This is what caused the original error.
# ------------------------------------------------------------------
"root": {
"level": level,
"handlers": ["default"],
},
# Additional loggers (uvicorn, gunicorn, etc.) inherit from the root
"loggers": {
"uvicorn.error": {"level": level, "propagate": False, "handlers": ["default"]},
"uvicorn.access": {"level": level, "propagate": False, "handlers": ["default"]},
"gunicorn.error": {"level": level, "propagate": False, "handlers": ["default"]},
"gunicorn.access": {"level": level, "propagate": False, "handlers": ["default"]},
# Root logger everything falls back to this
"": {"handlers": ["default"], "level": level, "propagate": True},
# Explicitly configure the popular libraries so they dont add extra handlers
"uvicorn.error": {"handlers": ["default"], "level": level, "propagate": False},
"uvicorn.access": {"handlers": ["default"], "level": level, "propagate": False},
"gunicorn.error": {"handlers": ["default"], "level": level, "propagate": False},
"gunicorn.access": {"handlers": ["default"], "level": level, "propagate": False},
},
}
# ----------------------------------------------------------------------
# Public API configure logging once (or reconfigure safely)
# Public objects used by the rest of the codebase
# ----------------------------------------------------------------------
# Default config used by Gunicorn (imported in gunicorn_conf.py)
LOGGING_CONFIG = _build_logging_config()
def setup_logging(log_level: str = "INFO") -> None:
"""
Apply the logging configuration.
This function can be called multiple times (e.g. during tests or
when the app is started both via ``uvicorn`` and via ``gunicorn``);
Apply the logging configuration. It can be called multiple times;
each call simply reapplies the dict configuration.
"""
config = _build_logging_config(log_level)
logging.config.dictConfig(config)
# ----------------------------------------------------------------------
# Export a readytouse config for Gunicorn (imported in ``gunicorn_conf.py``)
# Convenience: expose the humanreadable formatter for external use
# ----------------------------------------------------------------------
# The environment variable ``LOG_LEVEL`` (set by the Dockerfile / .env)
# determines the default level for the server process.
LOGGING_CONFIG = _build_logging_config()
__all__ = ["setup_logging", "LOGGING_CONFIG", "HumanReadableFormatter", "JsonFormatter"]