Source code for logeverything.integrations.wsgi

"""
Generic WSGI Middleware for LogEverything.

Works with any WSGI application (Flask, Django via WSGI, Bottle, etc.).
Provides automatic request correlation, logging, and timing.
"""

import logging
import time
from typing import Any, Callable, Iterable, Optional, Sequence, Set

from logeverything.correlation import clear_correlation, set_correlation_id, set_request_context


[docs] class LogEverythingWSGIMiddleware: """ WSGI middleware that adds correlation IDs and request logging. Args: app: The WSGI application to wrap. logger_name: Name of the logger to use. exclude_paths: Paths to skip (e.g. health checks). request_id_header: HTTP header name for the correlation / request ID. """ def __init__( self, app: Callable, logger_name: str = "logeverything.http", exclude_paths: Sequence[str] = ("/health", "/metrics"), request_id_header: str = "X-Request-ID", ): self.app = app self.logger = logging.getLogger(logger_name) self.exclude_paths: Set[str] = set(exclude_paths) self.request_id_header = request_id_header # WSGI normalises headers to HTTP_<NAME> with underscores self._wsgi_header_key = "HTTP_" + request_id_header.upper().replace("-", "_") def __call__(self, environ: dict, start_response: Callable) -> Iterable[bytes]: path = environ.get("PATH_INFO", "/") if path in self.exclude_paths: return self.app(environ, start_response) # type: ignore[no-any-return] # --- Correlation --- incoming_id = environ.get(self._wsgi_header_key, "") cid = set_correlation_id(incoming_id or None) method = environ.get("REQUEST_METHOD", "GET") client_ip = environ.get("REMOTE_ADDR", "unknown") set_request_context({"method": method, "path": path, "client_ip": client_ip}) self.logger.info(">>> %s %s (client=%s)", method, path, client_ip) start = time.perf_counter() status_code: Optional[str] = None def custom_start_response(status: str, response_headers: list, exc_info: Any = None) -> Any: nonlocal status_code status_code = status # Inject correlation header response_headers.append((self.request_id_header, cid)) return start_response(status, response_headers, exc_info) try: response = self.app(environ, custom_start_response) return response # type: ignore[no-any-return] except Exception: duration_ms = (time.perf_counter() - start) * 1000 self.logger.exception( "<<< %s %s 500 (%.1fms) [unhandled exception]", method, path, duration_ms ) raise finally: duration_ms = (time.perf_counter() - start) * 1000 if status_code is not None: self.logger.info("<<< %s %s %s (%.1fms)", method, path, status_code, duration_ms) clear_correlation()