Ridha Tech
Get Started
← Back to Blog
Technical Deep Dive

Building Valo: ContextVar Logging — Thread-Safe Context Without the Chaos

June 8, 20267 min read

You can't debug what you can't trace.

Today we implemented Valo's structured logger with ContextVar-based context propagation — the exact pattern Anthropic's Sentinel uses to keep production logs traceable across thousands of concurrent AI workflows.

The Problem: Lost Context in Concurrent Operations

Standard Python logging works great for single-threaded apps, but add concurrency and you lose the thread (literally). Good luck debugging that in production.

Traditional Solutions (And Why They Fail)

Approach 1: Pass request_id everywhere

Every function signature explodes. Functions that don't care about logging now need request_id. Refactoring nightmare.

Approach 2: threading.local()

Breaks with async/await. Each coroutine shares the thread, so contexts collide.

Approach 3: Global variables

Don't. Just don't.

The Solution: ContextVar

Python 3.7 introduced contextvars — thread-safe and async-safe context storage.

Key properties:

  • Thread-safe: Each thread has isolated context
  • Async-safe: Each coroutine has isolated context
  • Copy-on-write: Child contexts inherit parent values
  • Automatic cleanup: Contexts disappear when execution ends

Implementation: Three Core Components

1. ContextVar Storage

from contextvars import ContextVar
from typing import Any

_log_context: ContextVar[dict[str, Any]] = ContextVar("log_context", default={})

This is the magic. Each execution context gets its own isolated dictionary.

2. JSON Formatter

Injects context from ContextVar automatically, along with extra fields from log calls.

3. Context Management Functions

Simple API: set_log_context() and clear_log_context()

Why JSON? CloudWatch and Beyond

Plain text logs are human-readable but machine-unparseable. JSON logs are both.

{
  "timestamp": "2026-06-08T12:01:09.776Z",
  "level": "INFO",
  "request_id": "req-123",
  "component": "orchestrator",
  "match_id": "12345",
  "message": "Processing match"
}

CloudWatch Insights query: Instant request timeline. Try doing that with plain text logs.

Usage in Practice

Set context once at request entry point. All subsequent logs include context automatically. No parameter passing. No global variables. Just works.

Quality Metrics

  • 67 tests passing (15 new logger tests + 52 previous)
  • 96% overall coverage (structured logger: 100%)
  • Zero mypy errors (strict mode)
  • Zero ruff errors

Traceable. Parseable. Production-ready.