57 lines
2.0 KiB
Python
57 lines
2.0 KiB
Python
|
|
"""Audit logging middleware.
|
||
|
|
|
||
|
|
Logs every API request with method, path, status code, response time,
|
||
|
|
and the authenticated user identity (extracted from the JWT when present).
|
||
|
|
Log lines are structured so they can be ingested by ELK / Loki.
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import time
|
||
|
|
|
||
|
|
from fastapi import Request, Response
|
||
|
|
from loguru import logger
|
||
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
||
|
|
|
||
|
|
|
||
|
|
class AuditMiddleware(BaseHTTPMiddleware):
|
||
|
|
"""Log all API calls. Skips health/docs paths to reduce noise."""
|
||
|
|
|
||
|
|
# Paths that produce no audit log entry.
|
||
|
|
_SKIP_PREFIXES = ("/health", "/docs", "/redoc", "/openapi.json")
|
||
|
|
|
||
|
|
async def dispatch(self, request: Request, call_next) -> Response:
|
||
|
|
"""Intercept the request, call the handler, and log the outcome."""
|
||
|
|
path = request.url.path
|
||
|
|
if path == "/" or any(path == p or path.startswith(p + "/") for p in self._SKIP_PREFIXES):
|
||
|
|
return await call_next(request)
|
||
|
|
|
||
|
|
start = time.perf_counter()
|
||
|
|
response = await call_next(request)
|
||
|
|
elapsed_ms = int((time.perf_counter() - start) * 1000)
|
||
|
|
|
||
|
|
# Extract user identity from JWT header for structured audit records.
|
||
|
|
# The token is not re-validated here — auth dependencies do that upstream.
|
||
|
|
user_id = "anonymous"
|
||
|
|
username = "anonymous"
|
||
|
|
auth_header = request.headers.get("authorization", "")
|
||
|
|
if auth_header.startswith("Bearer "):
|
||
|
|
try:
|
||
|
|
from app.shared.bootstrap import get_jwt_handler
|
||
|
|
claims = get_jwt_handler().decode_token(auth_header[7:])
|
||
|
|
user_id = claims.user_id
|
||
|
|
username = claims.username
|
||
|
|
except Exception:
|
||
|
|
pass
|
||
|
|
|
||
|
|
logger.info(
|
||
|
|
"AUDIT method={} path={} status={} elapsed_ms={} user_id={} username={}",
|
||
|
|
request.method,
|
||
|
|
path,
|
||
|
|
response.status_code,
|
||
|
|
elapsed_ms,
|
||
|
|
user_id,
|
||
|
|
username,
|
||
|
|
)
|
||
|
|
return response
|