""" Structured JSON logging for all Rose Ash apps. Call configure_logging(app_name) once at app startup. Use get_logger(name) anywhere to get a logger that outputs JSON to stdout. """ from __future__ import annotations import json import logging import sys from datetime import datetime, timezone class JSONFormatter(logging.Formatter): """Format log records as single-line JSON objects.""" def __init__(self, app_name: str = ""): super().__init__() self.app_name = app_name def format(self, record: logging.LogRecord) -> str: entry = { "timestamp": datetime.now(timezone.utc).isoformat(), "level": record.levelname, "app": self.app_name, "logger": record.name, "message": record.getMessage(), } # Include extra fields if set on the record for key in ("event_type", "user_id", "request_id", "duration_ms"): val = getattr(record, key, None) if val is not None: entry[key] = val if record.exc_info and record.exc_info[0] is not None: entry["exception"] = self.formatException(record.exc_info) return json.dumps(entry, default=str) _configured = False def configure_logging(app_name: str, level: int = logging.INFO) -> None: """Set up structured JSON logging to stdout. Safe to call multiple times.""" global _configured if _configured: return _configured = True handler = logging.StreamHandler(sys.stdout) handler.setFormatter(JSONFormatter(app_name=app_name)) root = logging.getLogger() root.setLevel(level) root.addHandler(handler) # Quiet down noisy libraries for name in ("httpx", "httpcore", "asyncio", "sqlalchemy.engine"): logging.getLogger(name).setLevel(logging.WARNING) def get_logger(name: str) -> logging.Logger: """Get a named logger. Uses the structured JSON format once configure_logging is called.""" return logging.getLogger(name)