How to include request-context data in logs (automatically)

Hi everyone,

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.

Thank you!

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)},
        )