| """
|
| Structured Logging Framework
|
|
|
| Provides JSON-formatted structured logging for the AegisLM backend.
|
| """
|
|
|
| import json
|
| import logging
|
| import sys
|
| import traceback
|
| from datetime import datetime, timezone
|
| from enum import Enum
|
| from typing import Any, Dict, Optional
|
| from uuid import UUID
|
|
|
| from backend.core.config import settings
|
|
|
|
|
| class LogLevel(str, Enum):
|
| """Log level enumeration."""
|
|
|
| DEBUG = "DEBUG"
|
| INFO = "INFO"
|
| WARNING = "WARNING"
|
| ERROR = "ERROR"
|
| CRITICAL = "CRITICAL"
|
|
|
|
|
| class StructuredLogger:
|
| """
|
| Structured JSON logger for AegisLM.
|
|
|
| All logs are emitted as JSON with a consistent schema.
|
| """
|
|
|
| def __init__(
|
| self,
|
| name: str,
|
| run_id: Optional[UUID] = None,
|
| component: Optional[str] = None,
|
| ):
|
| self.logger = logging.getLogger(name)
|
| self.run_id = run_id
|
| self.component = component or name
|
|
|
|
|
| if not self.logger.handlers:
|
| self._configure_logger()
|
|
|
| def _configure_logger(self) -> None:
|
| """Configure the logger with JSON formatter."""
|
|
|
| level = getattr(logging, settings.log_level.upper(), logging.INFO)
|
| self.logger.setLevel(level)
|
|
|
|
|
| handler = logging.StreamHandler(sys.stdout)
|
| handler.setLevel(level)
|
|
|
|
|
| formatter = JSONFormatter()
|
| handler.setFormatter(formatter)
|
|
|
| self.logger.addHandler(handler)
|
| self.logger.propagate = False
|
|
|
| def _build_log_entry(
|
| self,
|
| level: str,
|
| message: str,
|
| extra_metadata: Optional[Dict[str, Any]] = None,
|
| exception: Optional[Exception] = None,
|
| ) -> Dict[str, Any]:
|
| """Build structured log entry."""
|
| log_entry = {
|
| "timestamp": datetime.now(timezone.utc).isoformat(),
|
| "level": level,
|
| "run_id": str(self.run_id) if self.run_id else None,
|
| "component": self.component,
|
| "message": message,
|
| }
|
|
|
|
|
| if extra_metadata:
|
| log_entry["metadata"] = extra_metadata
|
|
|
|
|
| if exception:
|
| log_entry["exception"] = {
|
| "type": type(exception).__name__,
|
| "message": str(exception),
|
| "traceback": traceback.format_exc(),
|
| }
|
|
|
| return log_entry
|
|
|
| def log(
|
| self,
|
| level: str,
|
| message: str,
|
| metadata: Optional[Dict[str, Any]] = None,
|
| exception: Optional[Exception] = None,
|
| ) -> None:
|
| """Emit a log entry."""
|
| log_entry = self._build_log_entry(level, message, metadata, exception)
|
|
|
|
|
| log_method = getattr(self.logger, level.lower(), self.logger.info)
|
| log_method(json.dumps(log_entry))
|
|
|
| def debug(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
| """Log debug message."""
|
| self.log(LogLevel.DEBUG.value, message, metadata)
|
|
|
| def info(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
| """Log info message."""
|
| self.log(LogLevel.INFO.value, message, metadata)
|
|
|
| def warning(self, message: str, metadata: Optional[Dict[str, Any]] = None) -> None:
|
| """Log warning message."""
|
| self.log(LogLevel.WARNING.value, message, metadata)
|
|
|
| def error(
|
| self,
|
| message: str,
|
| metadata: Optional[Dict[str, Any]] = None,
|
| exception: Optional[Exception] = None,
|
| ) -> None:
|
| """Log error message."""
|
| self.log(LogLevel.ERROR.value, message, metadata, exception)
|
|
|
| def critical(
|
| self,
|
| message: str,
|
| metadata: Optional[Dict[str, Any]] = None,
|
| exception: Optional[Exception] = None,
|
| ) -> None:
|
| """Log critical message."""
|
| self.log(LogLevel.CRITICAL.value, message, metadata, exception)
|
|
|
| def set_run_id(self, run_id: UUID) -> None:
|
| """Set the current run ID for all subsequent logs."""
|
| self.run_id = run_id
|
|
|
| def bind(self, **kwargs) -> "StructuredLogger":
|
| """
|
| Create a new logger with additional bound context.
|
|
|
| Returns a new logger instance with merged metadata.
|
| """
|
| new_logger = StructuredLogger(
|
| name=self.logger.name,
|
| run_id=self.run_id,
|
| component=self.component,
|
| )
|
| return new_logger
|
|
|
|
|
| class JSONFormatter(logging.Formatter):
|
| """JSON formatter for structured logging."""
|
|
|
| def format(self, record: logging.LogRecord) -> str:
|
| """Format log record as JSON."""
|
| try:
|
|
|
| log_data = json.loads(record.getMessage())
|
| except (json.JSONDecodeError, ValueError):
|
|
|
| log_data = {"message": record.getMessage()}
|
|
|
|
|
| log_entry = {
|
| "timestamp": datetime.fromtimestamp(
|
| record.created, tz=timezone.utc
|
| ).isoformat(),
|
| "level": record.levelname,
|
| "logger": record.name,
|
| **log_data,
|
| }
|
|
|
|
|
| if hasattr(record, "run_id") and record.run_id:
|
| log_entry["run_id"] = record.run_id
|
|
|
| if hasattr(record, "component") and record.component:
|
| log_entry["component"] = record.component
|
|
|
| if hasattr(record, "metadata") and record.metadata:
|
| log_entry["metadata"] = record.metadata
|
|
|
|
|
| if record.exc_info:
|
| log_entry["exception"] = {
|
| "type": record.exc_info[0].__name__ if record.exc_info[0] else None,
|
| "message": str(record.exc_info[1]) if record.exc_info[1] else None,
|
| }
|
|
|
| return json.dumps(log_entry)
|
|
|
|
|
|
|
|
|
|
|
|
|
| def get_logger(
|
| name: str,
|
| run_id: Optional[UUID] = None,
|
| component: Optional[str] = None,
|
| ) -> StructuredLogger:
|
| """
|
| Get a structured logger instance.
|
|
|
| Args:
|
| name: Logger name (typically __name__)
|
| run_id: Optional run ID for correlation
|
| component: Optional component name
|
|
|
| Returns:
|
| StructuredLogger instance
|
| """
|
| return StructuredLogger(name, run_id, component)
|
|
|
|
|
|
|
| default_logger = get_logger("aegislm")
|
|
|