Skip to content

src/genro_asgi/middleware/logging.py

Source from this local checkout, regenerated when the reader rebuilds. Line links use #L<number>; a GitHub line range opens its first line.

1 # Copyright 2025 Softwell S.r.l.2 #3 # Licensed under the Apache License, Version 2.0 (the "License");4 # you may not use this file except in compliance with the License.5 # You may obtain a copy of the License at6 #7 #     https://www.apache.org/licenses/LICENSE-2.08 #9 # Unless required by applicable law or agreed to in writing, software10 # distributed under the License is distributed on an "AS IS" BASIS,11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.12 # See the License for the specific language governing permissions and13 # limitations under the License.14 15 """HTTP access logging middleware.16 17 Logs each request's arrival and completion (method, path, status, timing)18 through the instance logger inherited from ``BaseMiddleware`` — never a19 module-level logger.20 """21 22 from __future__ import annotations23 24 import logging25 import time26 from collections.abc import MutableMapping27 from typing import TYPE_CHECKING, Any28 29 from .base import BaseMiddleware, headers_dict30 31 if TYPE_CHECKING:32     from ..types import ASGIApp, Receive, Scope, Send33 34 __all__ = ["LoggingMiddleware"]35 36 37 class LoggingMiddleware(BaseMiddleware):38     """Log request arrival and response completion with timing."""39 40     middleware_order = 20041     middleware_default = False42 43     def __init__(44         self,45         app: ASGIApp,46         server: Any,47         level: str = "INFO",48         include_headers: bool = False,49         include_query: bool = True,50         **options: Any,51     ) -> None:52         super().__init__(app, server, **options)53         self._level = getattr(logging, level.upper(), logging.INFO)54         self._include_headers = include_headers55         self._include_query = include_query56 57     async def __call__(self, scope: Scope, receive: Receive, send: Send) -> None:58         """Log the request, run the chain, then log status and timing."""59         start = time.perf_counter()60         method = scope.get("method", "?")61         path = scope.get("path", "/")62         query = scope.get("query_string", b"").decode("latin-1")63         request_info = f"{method} {path}"64         if self._include_query and query:65             request_info += f"?{query}"66         client = scope.get("client")67         client_ip = client[0] if client else "unknown"68 69         self.logger.log(self._level, "<- %s from %s", request_info, client_ip)70         if self._include_headers:71             self.logger.debug("   Headers: %s", headers_dict(scope))72 73         status_code = 074 75         async def send_with_logging(message: MutableMapping[str, Any]) -> None:76             nonlocal status_code77             if message["type"] == "http.response.start":78                 status_code = message.get("status", 0)79             await send(message)80 81         try:82             await self.app(scope, receive, send_with_logging)83         except Exception as exc:84             duration = (time.perf_counter() - start) * 100085             self.logger.error("-> %s ERROR: %s (%.1fms)", request_info, exc, duration)86             raise87 88         duration = (time.perf_counter() - start) * 100089         self.logger.log(self._level, "-> %s %s (%.1fms)", request_info, status_code, duration)