Distributed tracing with contextvars in Python

Propagate trace and span IDs across function calls using contextvars to mock distributed tracing in a single process.

Medium Python 3.9+ Aug 9, 2026 Microservices patterns 13 views 0 copies

Python code

51 lines
Python 3.9+
import 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

stdout
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

  1. Use `contextvars.copy_context()` to run a function in an isolated context snapshot.
  2. 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

Run this sample

Open the browser IDE to tweak the example and see results without installing anything.

Open editor

More from Microservices patterns

Related tutorials and quizzes for this topic.