Canonical Events¶
Canonical request events are single, structured events emitted after a request finishes. They are designed for production web services where one wide event is easier to search, sample, and alert on than separate "request started" and "request completed" lines.
Install¶
Use the web extra that matches your framework:
FastAPI Setup¶
from fastapi import FastAPI
from logust import logger
from logust.contrib import add_event_fields
from logust.contrib.starlette import setup_fastapi
app = FastAPI()
logger.add("logs/app.json", serialize=True)
setup_fastapi(
app,
canonical=True,
sample_rate=0.05,
slow_ms=1000,
skip_routes=["/health"],
)
@app.post("/checkout")
async def checkout(user_id: str):
add_event_fields(
{"user.id": user_id},
feature_checkout_v2=True,
payment_provider="stripe",
)
return {"ok": True}
With canonical=True, Logust emits one http.request event at the end of the request. Endpoint code can call add_event_fields() to enrich that same final event.
Event Shape¶
A successful request produces fields like these:
{
"message": "http.request",
"extra": {
"event": "http.request",
"request_id": "a1b2c3d4",
"method": "POST",
"path": "/checkout",
"route": "/checkout",
"client_ip": "127.0.0.1",
"user_agent": "curl/8.0.0",
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"span_id": "00f067aa0ba902b7",
"status_code": 200,
"duration_ms": 34.2,
"outcome": "success",
"user.id": "u_123",
"feature_checkout_v2": true
}
}
Important fields:
request_id: incomingx-request-idwhen present; otherwise an 8-characteruuid4prefix.route: framework route template, such as/users/{user_id}.outcome:success,client_error, orerror.duration_ms: request duration in milliseconds.queryandrequest_body: included when configured, with sensitive fields masked.error.typeanderror.message: included for exception paths.
JSON sinks preserve numeric, boolean, list, dict, and null extra values as native JSON types.
Tail Sampling¶
Tail sampling decides whether to keep the completed event after status code and duration are known.
The default policy keeps:
- 5xx responses and exceptions when
always_keep_errors=True - requests at or above
slow_ms - a
sample_ratefraction of normal successful requests
sample_rate must be between 0.0 and 1.0. slow_ms must be greater than or equal to 0.
Custom Samplers¶
Use TailSampler when you need predictable rules:
from logust.contrib import TailSampler
setup_fastapi(
app,
canonical=True,
sampler=TailSampler(
rate=0.01,
slow_ms=750,
keep_if=lambda event: event.get("tenant") == "enterprise",
),
)
You can also pass a predicate directly:
When sampler is provided, it replaces sample_rate, slow_ms, and always_keep_errors.
Adding Fields¶
Use add_event_fields() from anywhere inside the request:
from logust.contrib import add_event_fields
@app.post("/payments")
async def create_payment(user_id: str, amount: float):
add_event_fields(
{
"user.id": user_id,
"payment.amount": amount,
"payment.currency": "USD",
},
feature_payments_v2=True,
)
return {"ok": True}
add_event_fields() returns True when a canonical event is active. Outside a canonical request event it returns False, so shared helper code can call it without special casing.
Request IDs¶
Use get_request_id() to access the active request ID:
from logust.contrib.starlette import get_request_id
@app.get("/me")
async def me():
return {"request_id": get_request_id()}
The request ID is reset when the request finishes.
Full Example¶
See the runnable FastAPI example:
It writes JSON logs to logs/app.json and demonstrates route templates, request-scoped fields, request ID propagation, tail sampling, and error events.