"""Structured logging (JSON in production, human-readable in development).""" from __future__ import annotations import json import logging import sys from datetime import UTC, datetime from companyatlas.config import settings _RESERVED = {"name", "msg", "args", "levelname", "levelno", "pathname", "filename", "module", "exc_info", "exc_text", "stack_info", "lineno", "funcName", "created", "msecs", "relativeCreated", "thread", "threadName", "processName", "process", "message", "taskName"} class JsonFormatter(logging.Formatter): def __init__(self, service: str): super().__init__() self.service = service def format(self, record: logging.LogRecord) -> str: payload = { "ts": datetime.now(UTC).isoformat(timespec="milliseconds"), "level": record.levelname, "logger": record.name, "service": self.service, "msg": record.getMessage(), } for key, value in record.__dict__.items(): if key not in _RESERVED and not key.startswith("_"): payload[key] = value if record.exc_info: payload["exc"] = self.formatException(record.exc_info) return json.dumps(payload, default=str) class PlainFormatter(logging.Formatter): def format(self, record: logging.LogRecord) -> str: extras = {k: v for k, v in record.__dict__.items() if k not in _RESERVED and not k.startswith("_")} base = f"{datetime.now(UTC).astimezone().strftime('%H:%M:%S')} {record.levelname:<7} {record.name}: {record.getMessage()}" if extras: base += " " + " ".join(f"{k}={v}" for k, v in extras.items()) if record.exc_info: base += "\n" + self.formatException(record.exc_info) return base def setup_logging(level: int = logging.INFO, service: str = "companyatlas") -> None: root = logging.getLogger() root.handlers.clear() handler = logging.StreamHandler(sys.stderr) handler.setFormatter(JsonFormatter(service) if settings.log_json else PlainFormatter()) root.addHandler(handler) root.setLevel(level) for noisy in ("httpx", "httpcore", "apscheduler", "asyncio", "urllib3"): logging.getLogger(noisy).setLevel(logging.WARNING)