feat(logging): add structured JSON logging for production, human-readable text for development

This commit is contained in:
2026-02-19 19:07:08 +01:00
parent f4c74230ec
commit 764a2f7579
2 changed files with 70 additions and 4 deletions

View File

@@ -0,0 +1,67 @@
"""Structured JSON logging configuration.
In **production** (``AEGIS_ENV=production``), emits one JSON object per
line so that log aggregators (ELK, CloudWatch, Datadog) can ingest them
without custom parsing.
In **development** (default), uses a human-readable text format for
comfortable local work.
"""
from __future__ import annotations
import json
import logging
import os
import sys
from datetime import datetime, timezone
class _JSONFormatter(logging.Formatter):
"""Emit each log record as a single-line JSON object."""
def format(self, record: logging.LogRecord) -> str:
payload: dict = {
"timestamp": datetime.fromtimestamp(record.created, tz=timezone.utc).isoformat(),
"level": record.levelname,
"logger": record.name,
"message": record.getMessage(),
}
if record.exc_info and record.exc_info[1] is not None:
payload["exception"] = self.formatException(record.exc_info)
extra = getattr(record, "_extra", None)
if extra:
payload.update(extra)
return json.dumps(payload, default=str)
_DEV_FORMAT = "%(asctime)s %(levelname)-8s %(name)s%(message)s"
def setup_logging() -> None:
"""Configure the root logger based on the environment."""
is_production = os.environ.get("AEGIS_ENV", "").lower() == "production"
level_name = os.environ.get("LOG_LEVEL", "INFO").upper()
level = getattr(logging, level_name, logging.INFO)
root = logging.getLogger()
root.setLevel(level)
if root.handlers:
root.handlers.clear()
handler = logging.StreamHandler(sys.stdout)
handler.setLevel(level)
if is_production:
handler.setFormatter(_JSONFormatter())
else:
handler.setFormatter(logging.Formatter(_DEV_FORMAT))
root.addHandler(handler)
logging.getLogger("uvicorn.access").setLevel(logging.WARNING)
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)