Spaces:
Running
Running
File size: 6,043 Bytes
f381be8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 | """
src.utils.logger
================
Project-wide structured logging configuration.
Design goals
------------
* Single call to ``get_logger(name)`` everywhere β no boilerplate per module.
* Console output uses a compact, human-readable format with log levels coloured.
* File output writes one JSON record per line for machine-parseable audit trails.
* Both handlers respect the LOG_LEVEL environment variable (default ``INFO``).
* The rotating file handler caps each log file at 10 MB with up to 5 back-ups.
Usage
-----
from src.utils.logger import get_logger
log = get_logger(__name__)
log.info("Starting training", extra={"epoch": 1, "lr": 1e-3})
log.warning("NaN loss detected")
log.error("Model checkpoint missing", exc_info=True)
"""
from __future__ import annotations
import json
import logging
import os
import sys
from logging.handlers import RotatingFileHandler
from pathlib import Path
from typing import Any
# ββ Path setup ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_PROJECT_ROOT = Path(__file__).resolve().parents[3]
_LOG_DIR = _PROJECT_ROOT / "artifacts" / "logs"
_LOG_DIR.mkdir(parents=True, exist_ok=True)
_LOG_FILE = _LOG_DIR / "battery_lifecycle.log"
# ββ Log level (override via environment) βββββββββββββββββββββββββββββββββββββ
_LEVEL_NAME: str = os.environ.get("LOG_LEVEL", "INFO").upper()
_LEVEL: int = getattr(logging, _LEVEL_NAME, logging.INFO)
# ββ ANSI colour codes βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
_RESET = "\033[0m"
_COLOURS: dict[int, str] = {
logging.DEBUG: "\033[36m", # cyan
logging.INFO: "\033[32m", # green
logging.WARNING: "\033[33m", # yellow
logging.ERROR: "\033[31m", # red
logging.CRITICAL: "\033[35m", # magenta
}
class _ColourFormatter(logging.Formatter):
"""Console formatter with level-based ANSI colour and compact layout.
Format:
2024-11-15 12:34:56.789 INFO src.models.lstm Training epoch 5
"""
_FMT = "%(asctime)s %(levelname)-8s %(name)-32s %(message)s"
_DATE = "%Y-%m-%d %H:%M:%S"
def __init__(self, use_colour: bool = True) -> None:
super().__init__(fmt=self._FMT, datefmt=self._DATE)
self._use_colour = use_colour and sys.stderr.isatty()
def format(self, record: logging.LogRecord) -> str: # noqa: A003
colour = _COLOURS.get(record.levelno, "") if self._use_colour else ""
reset = _RESET if self._use_colour else ""
record.levelname = f"{colour}{record.levelname}{reset}"
record.name = record.name.replace("src.", "").replace("api.", "")[:32]
return super().format(record)
class _JsonFormatter(logging.Formatter):
"""File formatter that emits one JSON object per log record.
Each record includes:
``time``, ``level``, ``logger``, ``message``, ``module``,
``lineno``, and any extra key/value pairs attached via
``logging.LogRecord.__dict__``.
"""
_RESERVED = frozenset(logging.LogRecord(
"", 0, "", 0, "", (), None).__dict__.keys()
) | {"message", "asctime"}
def format(self, record: logging.LogRecord) -> str: # noqa: A003
record.message = record.getMessage()
payload: dict[str, Any] = {
"time": self.formatTime(record, "%Y-%m-%dT%H:%M:%S"),
"level": record.levelname,
"logger": record.name,
"message": record.message,
"module": record.module,
"lineno": record.lineno,
}
# Attach any extra fields passed via `extra={...}`
for key, val in record.__dict__.items():
if key not in self._RESERVED and not key.startswith("_"):
payload[key] = val
if record.exc_info:
payload["exc_info"] = self.formatException(record.exc_info)
return json.dumps(payload, default=str)
# ββ Root logger configuration (run once) βββββββββββββββββββββββββββββββββββββ
def _configure_root() -> None:
root = logging.getLogger()
if root.handlers: # already configured β do not add duplicate handlers
return
root.setLevel(_LEVEL)
# --- Console handler ---
console = logging.StreamHandler(sys.stderr)
console.setLevel(_LEVEL)
console.setFormatter(_ColourFormatter())
root.addHandler(console)
# --- Rotating JSON file handler ---
file_handler = RotatingFileHandler(
_LOG_FILE,
maxBytes=10 * 1024 * 1024, # 10 MB
backupCount=5,
encoding="utf-8",
)
file_handler.setLevel(logging.DEBUG) # always capture DEBUG to file
file_handler.setFormatter(_JsonFormatter())
root.addHandler(file_handler)
# Silence noisy third-party loggers
for noisy in ("matplotlib", "PIL", "h5py", "urllib3", "httpx", "numba"):
logging.getLogger(noisy).setLevel(logging.WARNING)
_configure_root()
# ββ Public API ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
def get_logger(name: str) -> logging.Logger:
"""Return a named logger that is already attached to the project handlers.
Parameters
----------
name:
Typically ``__name__`` of the calling module, e.g.
``src.models.lstm`` or ``api.model_registry``.
Returns
-------
logging.Logger
Configured logger instance.
Example
-------
>>> from src.utils.logger import get_logger
>>> log = get_logger(__name__)
>>> log.info("Loaded %d batteries", 30)
"""
return logging.getLogger(name)
|