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)