mkkbms
feat: initial foundation — workforce profiles + live tracking & route replay
3818b61
"""
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)