NeerajCodz's picture
feat: full project β€” ML simulation, dashboard UI, models on HF Hub
f381be8
"""
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)