diff --git a/backend/app/logging_config.py b/backend/app/logging_config.py new file mode 100644 index 0000000..84dbb96 --- /dev/null +++ b/backend/app/logging_config.py @@ -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) diff --git a/backend/app/main.py b/backend/app/main.py index 65ad950..24ce66c 100644 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -47,10 +47,9 @@ from app.jobs.mitre_sync_job import start_scheduler, scheduler _IS_PRODUCTION = os.environ.get("AEGIS_ENV", "").lower() == "production" # ── Logging ─────────────────────────────────────────────────────────────── -logging.basicConfig( - level=logging.INFO, - format="%(asctime)s %(levelname)-8s %(name)s — %(message)s", -) +from app.logging_config import setup_logging + +setup_logging() @asynccontextmanager async def lifespan(app: FastAPI):