I was wondering – what’s the best way to add some extra context to log messages when I log inside of views/a request context, like the path, the hostname (of the source of the request), maybe things like the user ID if authenticated (if that is secure, I’m not sure)?
I’d love to be able to add them to my formatted log messages automatically without having to pass them via extra or something similar to help give me some context around log messages.
Hello there, I’ve been using structlog for a while now (1,5 year).
It’s been an amazing experience so far, both on development, having pretty output on the terminal, and on production, having JSON formatted output helps a lot while querying the logs.
If you want a quick start, here’s the middleware I’ve been using to add the request context to my logs.
# python3.12>
import time
import traceback
import uuid
from typing import NotRequired
from typing import TypedDict
import structlog
from django.conf import settings
from django.core.exceptions import PermissionDenied
from django.http import Http404
from django.http import HttpRequest
from django.http import HttpResponse
from rest_framework import status
from sentry_sdk import set_context
_logger: structlog.typing.FilteringBoundLogger = structlog.get_logger(__name__)
def get_http_request_ip_address(request: HttpRequest) -> str | None:
"""Returns the very-first found Ip address from the HTTP Request from a list of headers"""
ip_headers = {"X-Forwarded-For", "REMOTE_ADDR"}
headers = [request.headers, request.META]
for header in headers:
for ip_header_key in ip_headers:
ip_addr = header.get(ip_header_key)
if not ip_addr:
continue
return ip_addr
return None
class _TrackedHttpRequestStructlogContext(TypedDict):
id: str
correlation_id: NotRequired[str]
end_to_end_id: NotRequired[str]
ip_addr: str | None
host: str | None
class TrackedHttpRequest(HttpRequest):
id: str
correlation_id: str | None = None
end_to_end_id: str | None = None
class RequestLifeCycleMiddleware:
"""Middleware that adds some IDs to the 'global-context' based on the request headers.
By default it only adds `request_id`, either from the header `x-request-id` or generate a new one.
It also logs the life-cycle of the request/response and errors raised."""
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request: TrackedHttpRequest) -> HttpResponse | None:
request.id = request.headers.get("x-request-id") or str(uuid.uuid4())
request_structlog_context: _TrackedHttpRequestStructlogContext = {
"id": request.id,
"ip_addr": get_http_request_ip_address(request),
"host": request.headers.get("host"),
}
# Only add the below ones if they are present on the request, otherwise
# they can be set later on the application
if x_correlation_id := request.headers.get("x-correlation-id"):
request.correlation_id = x_correlation_id
request_structlog_context["correlation_id"] = x_correlation_id
if x_end_to_end_id := request.headers.get("x-end-to-end-id"):
request.end_to_end_id = x_end_to_end_id
request_structlog_context["end_to_end_id"] = x_end_to_end_id
structlog.contextvars.bind_contextvars(request=request_structlog_context)
set_context("request", request_structlog_context) # type: ignore[arg-type]
logger = _logger.new(path=request.path)
if not settings.DEBUG or not request.path.startswith("/static/"):
logger.info("Request received")
start = time.perf_counter()
response: HttpResponse = self.get_response(request)
ellapsed = time.perf_counter() - start
response["x-request-id"] = request.id
if not settings.DEBUG or (response.status_code != status.HTTP_200_OK):
logger.info("Request finished", response={"status": response.status_code, "ellapsed": ellapsed})
return response
def process_exception(self, request: TrackedHttpRequest, exception: Exception):
if isinstance(exception, Http404 | PermissionDenied):
return
log_method = _logger.info
if request.path.startswith("/admin/"):
log_method = _logger.error
log_method(
f"Request failed with {exception.__class__.__name__}",
traceback=traceback.format_exc(),
error={"type": exception.__class__.__name__, "instance": str(exception)},
)