OpenTelemetry-native async context propagation for asyncio
Project description
aiotrace – Async context propagation for OpenTelemetry
Fixes OpenTelemetry context propagation across asyncio boundaries:
create_task, Queue, Lock, Event, Semaphore, TaskGroup, and
run_in_executor (thread pool).
Problem
OpenTelemetry stores the current span in a contextvars.ContextVar.
When asyncio tasks are created or resumed, Python copies/shallow-copies
these contextvars. On Python < 3.9.17 / 3.10.7 / 3.11.1, Task.__step
does not restore the task's own context, causing:
- Spans created in child tasks appearing under the wrong parent
- Context leaking between producer/consumer queues
- Lost context when using
run_in_executor(threads)
Quick Start
import asyncio
from opentelemetry import trace
from aiotrace import install
install()
tracer = trace.get_tracer(__name__)
async def child():
with tracer.start_as_current_span("child"):
pass
async def main():
with tracer.start_as_current_span("parent"):
await asyncio.create_task(child())
asyncio.run(main())
Manual Usage (no monkey-patching)
from aiotrace import create_task_with_context, PropagatingQueue
# Use explicit wrapper instead of monkey-patch
task = create_task_with_context(some_coro())
# Explicit propagating queue
queue = PropagatingQueue()
await queue.put(item)
item = await queue.get()
Installation
pip install aiotrace
What Gets Patched
| Primitive | Replacement | Description |
|---|---|---|
asyncio.create_task |
Wrapped | Captures OTEL context at task creation, restores inside child |
asyncio.Queue |
PropagatingQueue |
Re-attaches context after get()/put() resume |
asyncio.Lock |
PropagatingLock |
Re-attaches context after acquire() |
asyncio.Event |
PropagatingEvent |
Re-attaches context after wait() |
asyncio.Semaphore |
PropagatingSemaphore |
Re-attaches context after acquire() |
asyncio.Condition |
PropagatingCondition |
Re-attaches context after wait() |
asyncio.TaskGroup (3.11+) |
PropagatingTaskGroup |
Captures context at create_task() |
API
install(patch_queue=True, patch_locks=True)
Monkey-patches asyncio primitives. Safe to call multiple times (idempotent).
uninstall()
Restores original asyncio primitives.
PropagatingQueue(maxsize=0)
Drop-in replacement for asyncio.Queue with context propagation.
PropagatingLock, PropagatingEvent, PropagatingSemaphore, PropagatingCondition
Drop-in replacements for synchronization primitives.
create_task_with_context(coro, *, name=None, otel_ctx=None)
Create a task with explicit OTEL context propagation.
run_in_executor_with_context(executor, func, *args)
Schedule a function in a thread pool with OTEL context.
How It Works
asyncio creates new tasks by shallow-copying contextvars, which can break OpenTelemetry's span hierarchy when tasks resume in the wrong context. aiotrace fixes this at four levels:
-
Wrapping
create_task— Captures the current OTEL context before a task is spawned and restores it when the task's coroutine begins execution. -
Patching queues and locks — For synchronization primitives, the context that a waiter had when it called
get()oracquire()is stored and re-attached right before the waiter is resumed, using proper token-based attach/detach to prevent leaks. -
Propagating to threads —
run_in_executor_with_contextcopies the context to the worker thread viaotel_context.attach()inside the thread's callable. -
TaskGroup support (Python 3.11+) — Overrides
create_taskto ensure child tasks inherit the group's context.
All modifications are opt-in via install() or by using explicit classes like PropagatingQueue.
Limitations
asyncio.Eventandasyncio.Semaphoreare patched but not yet verified for all edge cases (contributions welcome).- Monkey-patching
asyncio.Queueglobally may conflict with other libraries that also replace it. Use explicitPropagatingQueuewhen possible. - High-frequency context switching adds a small overhead (approx. 1–2 µs per operation). For most applications this is negligible.
Requirements
- Python 3.8–3.12
opentelemetry-api >= 1.20.0
License
MIT
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aiotrace-0.1.1.tar.gz.
File metadata
- Download URL: aiotrace-0.1.1.tar.gz
- Upload date:
- Size: 9.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e6f16a0e48a241a4096aad366095f71004720461175bbaae5a2b5aeb9cc753cd
|
|
| MD5 |
fe851cb793df8c807d08c589a33cf7e7
|
|
| BLAKE2b-256 |
065c8b1d5ad6213a0abdc0d04865266726898937a03fe638254516064cfca6b5
|
File details
Details for the file aiotrace-0.1.1-py2.py3-none-any.whl.
File metadata
- Download URL: aiotrace-0.1.1-py2.py3-none-any.whl
- Upload date:
- Size: 9.7 kB
- Tags: Python 2, Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.12.10
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e72dca2b9847d7581d036e8fc3c6075566f3c9b6d946da215c384f07f5b16d06
|
|
| MD5 |
ba98ca4130b92cd201371b7abf47e488
|
|
| BLAKE2b-256 |
fecac4fdfb8ffe5b1c81568c265775e604053fda05f117a2b98bd7517621bd05
|