Source code for logeverything.integrations.flask
"""
Flask extension for LogEverything.
Provides a Flask extension that hooks into ``before_request``,
``after_request``, and ``teardown_request`` for automatic correlation
and request logging.
Usage::
from flask import Flask
from logeverything.integrations.flask import LogEverythingFlask
app = Flask(__name__)
LogEverythingFlask(app)
# Or with the application factory pattern:
le = LogEverythingFlask()
le.init_app(app)
"""
import logging
import time
from typing import Any, Optional, Sequence, Set
from logeverything.correlation import (
clear_correlation,
set_correlation_id,
set_request_context,
)
try:
from flask import Flask, g, request
_HAS_FLASK = True
except ImportError:
_HAS_FLASK = False
[docs]
class LogEverythingFlask:
"""
Flask extension for automatic request correlation and logging.
Args:
app: Optional Flask application (pass ``None`` for factory pattern).
logger_name: Name of the logger to use.
exclude_paths: Paths to skip logging for.
request_id_header: HTTP header name for request / correlation IDs.
"""
def __init__(
self,
app: Optional["Flask"] = None,
logger_name: str = "logeverything.http",
exclude_paths: Sequence[str] = ("/health", "/metrics"),
request_id_header: str = "X-Request-ID",
):
self.logger = logging.getLogger(logger_name)
self.exclude_paths: Set[str] = set(exclude_paths)
self.request_id_header = request_id_header
if app is not None:
self.init_app(app)
[docs]
def init_app(self, app: "Flask") -> None:
"""Attach hooks to a Flask application."""
if not _HAS_FLASK:
raise RuntimeError("Flask is not installed. Install it with: pip install flask")
app.before_request(self._before_request)
app.after_request(self._after_request)
app.teardown_request(self._teardown_request)
def _before_request(self) -> None:
path = request.path
if path in self.exclude_paths:
g._le_skip = True
return
g._le_skip = False
incoming_id = request.headers.get(self.request_id_header, "")
cid = set_correlation_id(incoming_id or None)
g._le_correlation_id = cid
g._le_start_time = time.perf_counter()
set_request_context(
{
"method": request.method,
"path": path,
"client_ip": request.remote_addr or "unknown",
}
)
self.logger.info(
">>> %s %s (client=%s)",
request.method,
path,
request.remote_addr,
)
def _after_request(self, response: Any) -> Any:
if getattr(g, "_le_skip", True):
return response
duration_ms = (time.perf_counter() - g._le_start_time) * 1000
self.logger.info(
"<<< %s %s %s (%.1fms)",
request.method,
request.path,
response.status_code,
duration_ms,
)
# Inject correlation header into response
response.headers[self.request_id_header] = getattr(g, "_le_correlation_id", "")
return response
def _teardown_request(self, exception: Optional[BaseException] = None) -> None:
if getattr(g, "_le_skip", True):
return
if exception is not None:
self.logger.exception("Request failed with exception")
clear_correlation()