MukeshKapoor25's picture
refactor(notification-ms): enhance structured logging across all channels
47fd0cd
"""Production-grade logging configuration for Notification 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 = "notification-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):
"""Emit log records as single-line JSON objects."""
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),
}
return json.dumps(payload, default=str)
class ConsoleFormatter(logging.Formatter):
"""Readable formatter for local development."""
FMT = "%(asctime)s %(levelname)-8s %(name)s — %(message)s"
def format(self, record: logging.LogRecord) -> str:
formatter = logging.Formatter(self.FMT, datefmt="%Y-%m-%d %H:%M:%S")
result = formatter.format(record)
extras = {
k: v
for k, v in record.__dict__.items()
if k not in JSONFormatter.RESERVED and not k.startswith("_") and k != "color_message"
}
if extras:
result += f" {extras}"
return result
def setup_logging(log_level: str = "INFO") -> None:
"""Configure root logger with JSON/console and rotating file handlers."""
numeric_level = getattr(logging, log_level.upper(), logging.INFO)
log_format = os.getenv("LOG_FORMAT", "json").lower()
LOG_DIR.mkdir(parents=True, exist_ok=True)
root = logging.getLogger()
root.setLevel(numeric_level)
root.handlers.clear()
stdout_handler = logging.StreamHandler(sys.stdout)
stdout_handler.setLevel(numeric_level)
stdout_handler.setFormatter(ConsoleFormatter() if log_format == "console" else JSONFormatter())
root.addHandler(stdout_handler)
app_log_path = LOG_DIR / "app.log"
file_handler = logging.handlers.RotatingFileHandler(
filename=app_log_path,
maxBytes=LOG_MAX_BYTES,
backupCount=LOG_BACKUP_COUNT,
encoding="utf-8",
)
file_handler.setLevel(numeric_level)
file_handler.setFormatter(JSONFormatter())
root.addHandler(file_handler)
error_log_path = LOG_DIR / "error.log"
error_handler = logging.handlers.RotatingFileHandler(
filename=error_log_path,
maxBytes=LOG_MAX_BYTES,
backupCount=LOG_BACKUP_COUNT,
encoding="utf-8",
)
error_handler.setLevel(logging.ERROR)
error_handler.setFormatter(JSONFormatter())
root.addHandler(error_handler)
for noisy in ("motor", "pymongo", "httpx", "uvicorn.access", "aioredis"):
logging.getLogger(noisy).setLevel(logging.WARNING)
startup_logger = logging.getLogger(SERVICE_NAME)
startup_logger.info(
"Logging initialised",
extra={
"event": "logging_init",
"log_level": log_level.upper(),
"log_format": log_format,
"log_dir": str(LOG_DIR.resolve()),
"app_log": str(app_log_path.resolve()),
"error_log": str(error_log_path.resolve()),
},
)
def get_logger(name: str) -> logging.Logger:
"""Return a standard logger for the given module name."""
return logging.getLogger(name)