Skip to content

HTTP wide events

What is a wide event?

A wide event is one rich, structured log line per unit of work (a handled message, an HTTP request) carrying every field an operator might want to query on — rather than a stream of thin events you later have to join by correlation ID. The pattern comes from Jamie Brandon's Logging Sucks, So Use Wide Events and Stripe's canonical log lines. See the logging concept page for the full rationale.

DomainContextMiddleware emits one wide event per HTTP request on the protean.access.http logger. The event carries the request envelope (method, path, status, duration), the commands dispatched during the request, a request_id, and a correlation_id that links the HTTP layer to the domain-layer protean.access events produced by the handler mixin.

This guide covers wiring, configuration, and enrichment. For the full field schema see the logging reference; for the design rationale behind the two-layer split see the logging concepts page.


What you get out of the box

As soon as you install DomainContextMiddleware, every HTTP request produces a wide event like this (production JSON renderer):

{
  "event": "access.http_completed",
  "level": "info",
  "logger": "protean.access.http",
  "timestamp": "2026-04-23T10:15:32.401Z",
  "http_method": "POST",
  "http_path": "/orders",
  "http_status": 201,
  "http_duration_ms": 18.92,
  "route_name": "place_order",
  "route_pattern": "/orders",
  "request_id": "req-7a2b4f",
  "correlation_id": "req-abc-123",
  "commands_dispatched": ["PlaceOrder"],
  "commands_dispatched_count": 1,
  "client_ip": "203.0.113.42",
  "user_agent": "MyApp/2.3 (iOS 17.2)"
}

Level ladders match severity — INFO for 2xx/3xx, WARNING for 4xx, ERROR for 5xx or unhandled exceptions. A 5xx event carries error_type and error_message plus the inlined traceback under exception.


Enable it

DomainContextMiddleware emits HTTP wide events by default once domain.init() has auto-configured logging:

from fastapi import FastAPI
from protean.integrations.fastapi import DomainContextMiddleware

from my_app.identity import identity_domain
from my_app.catalogue import catalogue_domain

app = FastAPI()
app.add_middleware(
    DomainContextMiddleware,
    route_domain_map={
        "/customers": identity_domain,
        "/products": catalogue_domain,
    },
)

No other wiring is required. The logger name (protean.access.http) is pre-configured at INFO by Domain.configure_logging().


Tune via domain.toml

[logging.http]
enabled = true
exclude_paths = ["/healthz", "/readyz", "/metrics"]
log_request_headers = false
log_response_headers = false
Key Default Effect
enabled true Master switch. Set to false to suppress HTTP wide events entirely.
exclude_paths [] Paths (exact match) that never emit. Use for liveness probes and high-volume health endpoints.
log_request_headers false Include the full request headers dict. Redaction still applies to tokens and cookies.
log_response_headers false Include the full response headers dict. Redaction still applies.

See the reference for full schema details.

Override per-middleware

Explicit constructor arguments on DomainContextMiddleware override the domain config for that middleware instance:

app.add_middleware(
    DomainContextMiddleware,
    route_domain_map={"/api": my_domain},
    emit_http_wide_event=True,
    exclude_paths=["/api/internal/ping"],
    log_request_headers=True,
)

Passing None (the default) defers to [logging.http]. Passing an explicit True/False or list wins over the config.


Enrich with business context

Use the same bind_event_context() API as domain handlers. Bindings made inside a FastAPI endpoint flow onto both the HTTP wide event and any domain wide events emitted by commands dispatched during the request:

from fastapi import APIRouter
from protean.utils.globals import current_domain
from protean.utils.logging import bind_event_context

router = APIRouter()

@router.post("/orders")
async def place_order(request: PlaceOrderRequest, user=Depends(get_user)):
    bind_event_context(
        user_id=user.id,
        user_tier=user.tier,
        device_platform=request.device_platform,
    )
    current_domain.process(PlaceOrder(**request.model_dump()))
    return {"ok": True}

The resulting access.http_completed event now carries user_id, user_tier, and device_platform alongside the framework fields. So does the access.handler_completed event emitted by PlaceOrderHandler.

Framework fields are protected

Keys that collide with framework-reserved HTTP fields (http_method, http_status, request_id, etc.) or stdlib LogRecord attributes are dropped before emission — application code cannot accidentally (or intentionally) overwrite http_status=200 with a value of its own. See the concept page for why.


Correlate HTTP and domain events

Every HTTP response echoes X-Request-ID back to the caller — even on synthesised 500s. Copy that value into your log aggregator to pull the full thread for a single request:

{logger=~"protean.access.*"}
  | json
  | request_id="req-7a2b4f"

One HTTP event plus every domain event emitted during the same request. If your incoming client already sends X-Request-ID, the middleware reuses it (truncated to 200 characters for safety); otherwise it generates a hex UUID.

For multi-service setups, pass through X-Correlation-ID as well — the middleware extracts it first, falling back to X-Request-ID. See Correlation and Causation IDs for the full propagation story.


Combine with tail sampling

HTTP requests can have much higher volume than domain operations (scanners, health checks, bots). The protean.access.http logger is nested under protean.access, so any tail sampling config you enable on protean.access automatically applies to HTTP wide events too.

A common production shape:

[logging.sampling]
enabled = true
default_rate = 0.01          # keep 1% of happy-path requests
always_keep_errors = true    # all 5xx and unhandled exceptions
always_keep_slow = true      # any handler over the threshold
critical_streams = ["Payment*", "Auth*"]

Combined with [logging.http].exclude_paths for liveness probes, this typically brings HTTP wide event volume down by 95 %+ without losing any error or performance signal.


See also