Ctrl K
ComunidadGuías…

rex/request_logging.py

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"""Request and response logging middleware for Flask applications.

Installs ``before_request`` / ``after_request`` hooks on a Flask app that log:

- **Request**: method, path, anonymized client IP, timestamp, request ID.
- **Response**: status code, duration in milliseconds, request ID.

Request body and response body are **never** logged to avoid PII leakage.

A unique ``request_id`` (UUID4) is generated per request and stored in
``flask.g`` so that downstream code can include it in application-level logs
for correlation.

Usage::

    from rex.request_logging import install_request_logging

    app = Flask(__name__)
    install_request_logging(app)

IP anonymisation
----------------
By default the last octet of IPv4 addresses (and the last 64 bits of IPv6
addresses) are replaced with ``0`` so that no individual can be identified
from the logs.  Full IP logging can be enabled by setting
``REX_LOG_FULL_IP=1`` in the environment.
"""

from __future__ import annotations

import logging
import os
import re
import time
import uuid

from flask import Flask, Response, g, request

logger = logging.getLogger("rex.request_logging")

# Regex that matches the last octet of an IPv4 address.
_IPV4_LAST_OCTET = re.compile(r"\.\d+$")
# Regex that matches the last 64 bits (4 groups) of an IPv6 address.
_IPV6_LAST_HALF = re.compile(r"(:[0-9a-fA-F]*){1,4}$")


def _anonymize_ip(ip: str) -> str:
    """Replace the identifying portion of *ip* with zeroes.

    - IPv4: replaces the last octet  (``192.168.1.42`` → ``192.168.1.0``)
    - IPv6: replaces the last 64 bits (``2001:db8::1234:5678`` → ``2001:db8::0``)
    - Unrecognised format: returned unchanged.
    """
    if not ip:
        return "unknown"
    if ":" in ip:
        # IPv6
        return _IPV6_LAST_HALF.sub(":0", ip)
    if "." in ip:
        # IPv4
        return _IPV4_LAST_OCTET.sub(".0", ip)
    return ip


def _log_full_ip() -> bool:
    """Return True when full IP logging is enabled via environment."""
    return os.environ.get("REX_LOG_FULL_IP", "").lower() in ("1", "true", "yes")


_INSTALLED_KEY = "rex_request_logging_installed"


def install_request_logging(app: Flask) -> None:
    """Install request/response logging hooks on *app*.

    The hooks are idempotent — calling this function multiple times on the
    same app instance will not duplicate log entries.

    Args:
        app: The :class:`flask.Flask` application to instrument.
    """
    if app.extensions.get(_INSTALLED_KEY):
        return
    app.extensions[_INSTALLED_KEY] = True

    @app.before_request
    def _log_request() -> None:
        """Assign a request ID and log the incoming request."""
        g.request_id = str(uuid.uuid4())
        g.request_start = time.monotonic()

        raw_ip = request.remote_addr or "unknown"
        client_ip = raw_ip if _log_full_ip() else _anonymize_ip(raw_ip)

        logger.info(
            "REQUEST  %s %s from %s",
            request.method,
            request.path,
            client_ip,
            extra={
                "request_id": g.request_id,
                "method": request.method,
                "path": request.path,
                "client_ip": client_ip,
            },
        )

    @app.after_request
    def _log_response(response: Response) -> Response:
        """Log the outgoing response with status code and duration."""
        request_id = getattr(g, "request_id", "n/a")
        start = getattr(g, "request_start", None)
        duration_ms = int((time.monotonic() - start) * 1000) if start is not None else -1

        logger.info(
            "RESPONSE %s %s → %d (%dms)",
            request.method,
            request.path,
            response.status_code,
            duration_ms,
            extra={
                "request_id": request_id,
                "method": request.method,
                "path": request.path,
                "status_code": response.status_code,
                "duration_ms": duration_ms,
            },
        )
        return response


__all__ = [
    "install_request_logging",
    "_anonymize_ip",
    "_log_full_ip",
]