"""Structured JSON logging utilities for AURA.""" from __future__ import annotations import json import logging import sys from dataclasses import dataclass from datetime import datetime, timezone from typing import Any @dataclass(slots=True) class LogContext: """Additional structured fields for a log entry.""" component: str event: str | None = None class JsonFormatter(logging.Formatter): """Format log records as JSON lines.""" 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(), } component = getattr(record, "component", None) if component is not None: payload["component"] = component event = getattr(record, "event", None) if event is not None: payload["event"] = event extra = { key: value for key, value in record.__dict__.items() if key not in { "name", "msg", "args", "levelname", "levelno", "pathname", "filename", "module", "exc_info", "exc_text", "stack_info", "lineno", "funcName", "created", "msecs", "relativeCreated", "thread", "threadName", "processName", "process", "message", "asctime", "component", "event", } } if extra: payload["extra"] = extra if record.exc_info: payload["exception"] = self.formatException(record.exc_info) return json.dumps(payload, default=str, ensure_ascii=True) def configure_logging(level: str = "INFO", stream: Any | None = None) -> None: """Configure the root logger for JSON output.""" handler = logging.StreamHandler(stream or sys.stdout) handler.setFormatter(JsonFormatter()) root = logging.getLogger() root.handlers.clear() root.addHandler(handler) root.setLevel(level.upper()) root.propagate = False def get_logger(name: str, component: str | None = None) -> logging.LoggerAdapter: """Return a logger adapter that injects a component tag.""" base_logger = logging.getLogger(name) return logging.LoggerAdapter(base_logger, {"component": component or name})