v0.156.1
 1"""Request-exception logging.
 2
 3`log_exception` is the single logging entry point for exceptions raised
 4during request handling. Called from `View.get_response` for exceptions
 5that reach `handle_exception`, and from the framework's
 6`response_for_exception` for pre-view failures (URL resolution,
 7middleware). The sentinel attribute makes it idempotent, so an exception
 8caught at multiple layers is logged once.
 9"""
10
11from __future__ import annotations
12
13import logging
14from typing import TYPE_CHECKING
15
16from .logger import get_framework_logger
17
18if TYPE_CHECKING:
19    from plain.http import Request
20
21
22request_logger = get_framework_logger("plain.request")
23
24_LOGGED_SENTINEL = "_plain_logged"
25
26
27def log_exception(request: Request, exc: Exception) -> None:
28    """Log an exception raised during request handling.
29
30    Idempotent — setting a sentinel on the exception means multiple call
31    sites won't double-log. 404s are skipped unconditionally since
32    crawler/probe noise drowns real signal.
33    """
34    # Deferred to avoid a circular import: plain.logs is loaded during
35    # plain.runtime bootstrap, which happens before plain.http is ready.
36    from plain.http.exceptions import (
37        HTTPException,
38        NotFoundError404,
39        SuspiciousOperationError400,
40    )
41
42    if getattr(exc, _LOGGED_SENTINEL, False):
43        return
44    setattr(exc, _LOGGED_SENTINEL, True)
45
46    if isinstance(exc, NotFoundError404):
47        return
48
49    base = {"path": request.path}
50
51    if isinstance(exc, SuspiciousOperationError400):
52        # Logged on plain.security.* so operators can target an alert at
53        # security events specifically. Warning (no exc_info) because the
54        # rejection is the working-as-designed response — same noise
55        # category as 404s once a scanner is probing nonexistent paths.
56        security_logger = logging.getLogger(f"plain.security.{type(exc).__name__}")
57        security_logger.warning(str(exc), extra=base)
58        return
59
60    if isinstance(exc, HTTPException):
61        request_logger.warning(
62            "HTTP error",
63            extra={**base, "error": str(exc), "status_code": exc.status_code},
64        )
65        return
66
67    request_logger.error("Server error", extra=base, exc_info=exc)