Distributed tracing with contextvars in Python
Propagate trace and span IDs across function calls using contextvars to mock distributed tracing in a single process.
Python code
51 linesimport contextvars
import uuid
import time
_trace_context = contextvars.ContextVar("trace_context", default=None)
class TraceContext:
def __init__(self, trace_id, parent_span_id):
self.trace_id = trace_id
self.parent_span_id = parent_span_id
self.span_id = uuid.uuid4().hex[:16]
self.started_at = time.time()
def __repr__(self):
return f"trace={self.trace_id} parent_span={self.parent_span_id} span={self.span_id}"
def start_span(name):
parent = _trace_context.get()
if parent:
context = TraceContext(parent.trace_id, parent.span_id)
else:
context = TraceContext(uuid.uuid4().hex[:16], "root")
token = _trace_context.set(context)
print(f"START {name}: {context}")
return token
def end_span(name, token):
context = _trace_context.get()
duration_ms = (time.time() - context.started_at) * 1000
print(f"END {name}: {context} took {duration_ms:.2f}ms")
_trace_context.reset(token)
def process_payment():
token = start_span("payment")
time.sleep(0.01)
end_span("payment", token)
def main():
token = start_span("order")
process_payment()
end_span("order", token)
if __name__ == "__main__":
main()
Output
START order: trace=a1b2c3d4e5f60718 parent_span=root span=9f8e7d6c5b4a3921
START payment: trace=a1b2c3d4e5f60718 parent_span=9f8e7d6c5b4a3921 span=0f1e2d3c4b5a6978
END payment: trace=a1b2c3d4e5f60718 parent_span=9f8e7d6c5b4a3921 span=0f1e2d3c4b5a6978 took 10.00ms
END order: trace=a1b2c3d4e5f60718 parent_span=root span=9f8e7d6c5b4a3921 took 10.00ms
How it works
contextvars.ContextVar provides a thread-safe way to store context that automatically propagates through async and synchronous calls in Python. When start_span is called, it retrieves the current parent context (or creates a new root), sets a new span, and returns a token that end_span uses to restore the previous context. The nested span correctly inherits the trace ID and sets the parent span ID to the outer span. This simple mock mirrors production tracing systems like OpenTelemetry without external dependencies.
Common mistakes
- Forgetting to reset the context with the token, causing context to leak across calls.
- Using a global variable instead of a ContextVar, which breaks in async or multi-threaded code.
- Not storing the trace ID in logs or service calls, making it hard to correlate events.
Variations
- Use `contextvars.copy_context()` to run a function in an isolated context snapshot.
- Replace the custom TraceContext with OpenTelemetry's `trace.get_current_span()` to integrate with real tracing backends.
Real-world use cases
- Injecting trace IDs into HTTP headers when calling downstream microservices in a request chain.
- Logging trace and span IDs in structured logs to correlate events across services during debugging.
- Propagating the same trace context through asynchronous tasks or background workers while processing a user request.
Sponsored
More from Microservices patterns
- BFF aggregation pattern: combine multiple service responses in Python easy
- Backward Compatible Schema Evolution in Python medium
- Bulkhead Thread Pool per Service Mock in Python medium
- CQRS with Separate Read and Write Repositories in Python medium
- Cache-Aside Pattern in Python: Per-Service Mock easy
- Consumer Driven Contract Pact Mock in Python medium
Keep learning
Related tutorials and quizzes for this topic.