Skip to Content

Why pipe logs through a ContextVar instead of passing a logger everywhere?

Category: Backend & API

Answer

A ContextVar is a per-task, async-safe carrier — it threads implicit context (request_id, user_id, agent_step) through every call without you threading it as a parameter. A custom JSONFormatter reads the ContextVar at log-record-emit time and writes every log line as one structured JSON object with those fields attached.

Concrete examples from the fca project context

Example 1

request_id middleware sets the var once; every logger.info() afterwards is auto-decorated.

Example 2

SSELogHandler streams the same ContextVar into the SSE channel so logs appear live in the browser.

Example 3

agent_step is updated inside LangGraph nodes — debug logs carry the node name without per-call wiring.

Data flow / flow chart

Route -> ContextVar.set(request_id) -> log formatter -> JSON {"request_id": ..., "user_id": ...}
  The var is async-safe across awaits

Takeaway

ContextVar turns “I forgot to pass request_id to that 8-level-deep helper” from a bug into a non-issue.