""" Structured JSON logging for Workforce Microservice. """ import json import logging import logging.handlers import os import sys import traceback from datetime import datetime, timezone from pathlib import Path from typing import Any, Dict SERVICE_NAME = "workforce-ms" LOG_DIR = Path(os.getenv("LOG_DIR", "logs")) LOG_MAX_BYTES = int(os.getenv("LOG_MAX_BYTES", 50 * 1024 * 1024)) LOG_BACKUP_COUNT = int(os.getenv("LOG_BACKUP_COUNT", "10")) class JSONFormatter(logging.Formatter): RESERVED = frozenset({ "args", "created", "exc_info", "exc_text", "filename", "funcName", "levelname", "levelno", "lineno", "message", "module", "msecs", "msg", "name", "pathname", "process", "processName", "relativeCreated", "stack_info", "thread", "threadName", }) def format(self, record: logging.LogRecord) -> str: payload: Dict[str, Any] = { "timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(), "level": record.levelname, "logger": record.name, "message": record.getMessage(), "service": SERVICE_NAME, "pid": record.process, } if record.levelno >= logging.WARNING: payload["caller"] = f"{record.pathname}:{record.lineno}" for key, val in record.__dict__.items(): if key not in self.RESERVED and not key.startswith("_"): payload[key] = val if record.exc_info and record.exc_info[0] is not None: exc_type, exc_value, exc_tb = record.exc_info payload["exception"] = { "type": exc_type.__name__, "message": str(exc_value), "stacktrace": traceback.format_exception(exc_type, exc_value, exc_tb), } try: return json.dumps(payload, default=str) except Exception: return json.dumps({"message": str(record.getMessage()), "service": SERVICE_NAME}, default=str) class ConsoleFormatter(logging.Formatter): CYAN = "\x1b[36m" YELLOW = "\x1b[33m" RED = "\x1b[31m" GREY = "\x1b[38;5;240m" BOLD_RED = "\x1b[1;31m" RESET = "\x1b[0m" LEVEL_COLOURS = { logging.DEBUG: "\x1b[38;5;240m", logging.INFO: "\x1b[36m", logging.WARNING: "\x1b[33m", logging.ERROR: "\x1b[31m", logging.CRITICAL: "\x1b[1;31m", } FMT = "%(asctime)s %(levelname)-8s %(name)s — %(message)s" def format(self, record: logging.LogRecord) -> str: colour = self.LEVEL_COLOURS.get(record.levelno, self.RESET) formatter = logging.Formatter( f"{colour}{self.FMT}{self.RESET}", datefmt="%Y-%m-%d %H:%M:%S" ) return formatter.format(record) def setup_logging(level: str = "INFO") -> None: numeric_level = getattr(logging, level.upper(), logging.INFO) root = logging.getLogger() root.setLevel(numeric_level) root.handlers.clear() # Console handler ch = logging.StreamHandler(sys.stdout) ch.setLevel(numeric_level) ch.setFormatter(ConsoleFormatter()) root.addHandler(ch) # File handlers (JSON) try: LOG_DIR.mkdir(parents=True, exist_ok=True) fh = logging.handlers.RotatingFileHandler( LOG_DIR / "app.log", maxBytes=LOG_MAX_BYTES, backupCount=LOG_BACKUP_COUNT ) fh.setLevel(numeric_level) fh.setFormatter(JSONFormatter()) root.addHandler(fh) eh = logging.handlers.RotatingFileHandler( LOG_DIR / "app_errors.log", maxBytes=LOG_MAX_BYTES, backupCount=LOG_BACKUP_COUNT ) eh.setLevel(logging.ERROR) eh.setFormatter(JSONFormatter()) root.addHandler(eh) except Exception: pass # Non-critical if log dir can't be created def get_logger(name: str) -> logging.Logger: return logging.getLogger(name)