aegislm / logger.py
ACA050's picture
Upload folder using huggingface_hub
7343c19 verified
Raw
History Blame Contribute Delete
6.93 kB
"""
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
# Configure logger if not already configured
if not self.logger.handlers:
self._configure_logger()
def _configure_logger(self) -> None:
"""Configure the logger with JSON formatter."""
# Set level from settings
level = getattr(logging, settings.log_level.upper(), logging.INFO)
self.logger.setLevel(level)
# Create handler
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(level)
# Use JSON formatter
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,
}
# Add metadata
if extra_metadata:
log_entry["metadata"] = extra_metadata
# Add exception info if provided
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)
# Use standard logging
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:
# Try to parse the message as JSON
log_data = json.loads(record.getMessage())
except (json.JSONDecodeError, ValueError):
# If not JSON, use as-is
log_data = {"message": record.getMessage()}
# Add standard fields
log_entry = {
"timestamp": datetime.fromtimestamp(
record.created, tz=timezone.utc
).isoformat(),
"level": record.levelname,
"logger": record.name,
**log_data,
}
# Add extra fields from record
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
# Add exception info
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)
# =============================================================================
# Logger factory functions
# =============================================================================
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
default_logger = get_logger("aegislm")