mirror of
https://github.com/ParisNeo/ollama_proxy_server.git
synced 2025-09-06 13:42:26 +00:00
Update logging_config.py
This commit is contained in:
@@ -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
|
||||
--------
|
||||
* Human‑readable console output by default.
|
||||
* Optional JSON output via the LOG_FORMAT env‑var (kept for backward‑compatibility).
|
||||
* All loggers (root, uvicorn, gunicorn, etc.) share the same configuration.
|
||||
* The `setup_logging()` helper can be called from any entry‑point (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 upper‑case
|
||||
# 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 upper‑cased.
|
||||
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 as‑is
|
||||
# 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 upper‑case
|
||||
log_record["level"] = (log_record.get("level") or record.levelname).upper()
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Helper that builds the dict‑config structure.
|
||||
# Build the dictConfig – selects formatter based on LOG_FORMAT env‑var
|
||||
# ----------------------------------------------------------------------
|
||||
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 (case‑insensitive).
|
||||
Returns a dict compatible with ``logging.config.dictConfig``.
|
||||
``log_level`` can be any standard level name (case‑insensitive).
|
||||
"""
|
||||
level = log_level.upper()
|
||||
|
||||
# Choose formatter: human‑readable (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: # human‑readable
|
||||
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 don’t 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 re‑configure 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 re‑applies the dict configuration.
|
||||
"""
|
||||
config = _build_logging_config(log_level)
|
||||
logging.config.dictConfig(config)
|
||||
|
||||
# ----------------------------------------------------------------------
|
||||
# Export a ready‑to‑use config for Gunicorn (imported in ``gunicorn_conf.py``)
|
||||
# Convenience: expose the human‑readable 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"]
|
||||
|
Reference in New Issue
Block a user