OpenTelemetry Python
Index for OTel Python -- traces, metrics, log-trace correlation, distributed propagation. References hold the gotchas; canonical reference lives at https://opentelemetry-python.readthedocs.io and https://opentelemetry.io/docs/.
When to use
- New Python service that needs distributed tracing
- Adding OTel to FastAPI / Celery / async Python
- Custom transports (AMQP, ZMQ, Kafka) needing propagator wiring
- OTLP exporter / Collector / AWS ADOT configuration
- Auditing existing instrumentation for gaps or anti-patterns
- Log-trace correlation
- Sampling strategy choice for production
Quick-start production recipe
For most Python services, start with this and iterate:
- Init:
opentelemetry-bootstrap -a install + opentelemetry-instrument wrapper
- Resource: set
service.name, service.version, deployment.environment
- Sampler:
ParentBased(TraceIdRatioBased(0.1)) -- 10% head sampling
- Exporter: OTLP gRPC to a local Collector at
localhost:4317
- Processor:
BatchSpanProcessor with default tuning (raise OTEL_BSP_MAX_QUEUE_SIZE=8192 if bursty)
- Shutdown: register
provider.shutdown() in lifespan / atexit / SIGTERM
Then escalate based on what you actually need:
- Custom business spans → manual
tracer.start_as_current_span()
- Non-HTTP transport → custom propagator (skeleton in
exporters-and-backends.md)
- AWS deployment → ADOT distro + X-Ray ID generator (
aws-deployment.md)
- High throughput → tune BSP queue size + export timeout
- Error-only retention → tail sampling at the Collector
Auto vs manual instrumentation (the matrix)
| Layer |
Approach |
Examples |
| HTTP frameworks |
Auto |
FastAPI, Django, Flask |
| Database clients |
Auto |
SQLAlchemy, psycopg2, asyncpg |
| HTTP clients |
Auto |
httpx, requests, aiohttp |
| Message queues |
Auto |
Celery, Kafka |
| Cache |
Auto |
redis, memcached |
| Business logic |
Manual |
Order processing, payment flows |
| Custom transport |
Manual |
AMQP payload, ZMQ events |
Combined pattern: opentelemetry-instrument wraps the app for auto; manual spans inside routes/handlers for business logic. See instrumentation-patterns.md for per-framework details.
Critical framework gotchas (worth memorizing)
- Celery: init OTel after fork via
@worker_process_init.connect. BatchSpanProcessor threads don't survive fork() -- export silently fails otherwise. Recipe in instrumentation-patterns.md.
- SQLAlchemy async: pass
engine.sync_engine to the instrumentor, NOT the async engine.
- FastAPI: register
provider.shutdown() in lifespan cleanup, otherwise last span batch is lost on every restart.
- HTTP status / Span Status: only 5xx →
StatusCode.ERROR. 4xx are client errors, leave UNSET. Business rejections (declined payment) use add_event, not ERROR.
Custom transport propagation (skeleton)
For AMQP, ZMQ, custom sockets -- inject on producer, extract on consumer, use W3C TraceContext format.
# Producer
from opentelemetry.propagate import inject
headers = {}; inject(headers)
message.payload["_trace_context"] = headers
# Consumer
from opentelemetry.propagate import extract
from opentelemetry import context, trace
ctx = extract(carrier=message.payload.get("_trace_context", {}))
token = context.attach(ctx)
try:
with trace.get_tracer(__name__).start_as_current_span("process"):
handle(message)
finally:
context.detach(token)
Full discussion + custom SpanProcessor patterns: exporters-and-backends.md.
Sampling -- the decision tree
ParentBased(TraceIdRatioBased(rate)) -- the right default. Respects upstream decision; only applies the delegate to root spans. Without ParentBased, downstream services re-roll → broken traces.
- Tail sampling at the Collector -- when you need to keep 100% of errors/slow traces while sampling routine traffic. Requires trace-ID affinity (loadbalancing exporter in front).
- Hybrid at scale: head-sample 10-20% in SDK + tail-sample at Collector for error/slow retention.
Env shortcut: OTEL_TRACES_SAMPLER=parentbased_traceidratio, OTEL_TRACES_SAMPLER_ARG=0.1.
Reference index
async-context-propagation.md -- contextvars mechanics, asyncio task propagation, thread boundary trap, TracedThreadPoolExecutor, fork+BSP loss, Python 3.12+ improvements (the crown jewel of this skill -- read first when debugging missing/broken context)
instrumentation-patterns.md -- auto-instrument setup, FastAPI/Celery/SQLAlchemy patterns, traced_async decorator, TracedClass mixin, sensitive-arg redaction, error handling
exporters-and-backends.md -- OTLP gRPC vs HTTP, BSP tuning, propagation formats, custom SpanProcessors, multi-backend Collector YAML
aws-deployment.md -- ADOT distro, X-Ray ID generator + propagator, ECS sidecar with memory_limiter ordering, IAM list, Lambda layer, collector-less when/when-not, X-Ray SDK migration
production-checklist.md -- do/don't operational rules, resource detection boilerplate, signal maturity, version pinning policy
Three pillars correlation (one-liner)
# Inject trace_id / span_id / service_name into every stdlib log record
from opentelemetry.instrumentation.logging import LoggingInstrumentor
LoggingInstrumentor().instrument(set_logging_format=True)
# OR env: OTEL_PYTHON_LOG_CORRELATION=true
For metrics: MeterProvider + Counter/Histogram/UpDownCounter/ObservableGauge. Shares Resource with TracerProvider so service identity is consistent.
For OTLP log export: the Logs SDK (opentelemetry._logs, leading underscore = experimental) -- in production today, use the LoggingInstrumentor bridge and ship via your existing log pipeline.
Official docs
1---2name: opentelemetry3description: Knowledge base for OTel in Python: async gotchas, non-HTTP transports, production patterns. TRIGGER WHEN: working with OpenTelemetry, distributed tracing, span instrumentation, context propagation, OTLP exporters, sampling strategies, or observability pipelines.4---56# OpenTelemetry Python78Index for OTel Python -- traces, metrics, log-trace correlation, distributed propagation. References hold the gotchas; canonical reference lives at https://opentelemetry-python.readthedocs.io and https://opentelemetry.io/docs/.910## When to use1112- New Python service that needs distributed tracing13- Adding OTel to FastAPI / Celery / async Python14- Custom transports (AMQP, ZMQ, Kafka) needing propagator wiring15- OTLP exporter / Collector / AWS ADOT configuration16- Auditing existing instrumentation for gaps or anti-patterns17- Log-trace correlation18- Sampling strategy choice for production1920## Quick-start production recipe2122For most Python services, start with this and iterate:23241. **Init**: `opentelemetry-bootstrap -a install` + `opentelemetry-instrument` wrapper252. **Resource**: set `service.name`, `service.version`, `deployment.environment`263. **Sampler**: `ParentBased(TraceIdRatioBased(0.1))` -- 10% head sampling274. **Exporter**: OTLP gRPC to a local Collector at `localhost:4317`285. **Processor**: `BatchSpanProcessor` with default tuning (raise `OTEL_BSP_MAX_QUEUE_SIZE=8192` if bursty)296. **Shutdown**: register `provider.shutdown()` in lifespan / `atexit` / `SIGTERM`3031Then escalate based on what you actually need:32- Custom business spans → manual `tracer.start_as_current_span()`33- Non-HTTP transport → custom propagator (skeleton in `exporters-and-backends.md`)34- AWS deployment → ADOT distro + X-Ray ID generator (`aws-deployment.md`)35- High throughput → tune BSP queue size + export timeout36- Error-only retention → tail sampling at the Collector3738## Auto vs manual instrumentation (the matrix)3940| Layer | Approach | Examples |41|-------|----------|----------|42| HTTP frameworks | **Auto** | FastAPI, Django, Flask |43| Database clients | **Auto** | SQLAlchemy, psycopg2, asyncpg |44| HTTP clients | **Auto** | httpx, requests, aiohttp |45| Message queues | **Auto** | Celery, Kafka |46| Cache | **Auto** | redis, memcached |47| Business logic | **Manual** | Order processing, payment flows |48| Custom transport | **Manual** | AMQP payload, ZMQ events |4950Combined pattern: `opentelemetry-instrument` wraps the app for auto; manual spans inside routes/handlers for business logic. See `instrumentation-patterns.md` for per-framework details.5152## Critical framework gotchas (worth memorizing)5354- **Celery**: init OTel **after fork** via `@worker_process_init.connect`. `BatchSpanProcessor` threads don't survive `fork()` -- export silently fails otherwise. Recipe in `instrumentation-patterns.md`.55- **SQLAlchemy async**: pass `engine.sync_engine` to the instrumentor, NOT the async engine.56- **FastAPI**: register `provider.shutdown()` in `lifespan` cleanup, otherwise last span batch is lost on every restart.57- **HTTP status / Span Status**: only 5xx → `StatusCode.ERROR`. 4xx are client errors, leave UNSET. Business rejections (declined payment) use `add_event`, not ERROR.5859## Custom transport propagation (skeleton)6061For AMQP, ZMQ, custom sockets -- inject on producer, extract on consumer, use W3C TraceContext format.6263```python64# Producer65from opentelemetry.propagate import inject66headers = {}; inject(headers)67message.payload["_trace_context"] = headers6869# Consumer70from opentelemetry.propagate import extract71from opentelemetry import context, trace72ctx = extract(carrier=message.payload.get("_trace_context", {}))73token = context.attach(ctx)74try:75 with trace.get_tracer(__name__).start_as_current_span("process"):76 handle(message)77finally:78 context.detach(token)79```8081Full discussion + custom SpanProcessor patterns: `exporters-and-backends.md`.8283## Sampling -- the decision tree8485- **`ParentBased(TraceIdRatioBased(rate))`** -- the right default. Respects upstream decision; only applies the delegate to root spans. Without ParentBased, downstream services re-roll → broken traces.86- **Tail sampling at the Collector** -- when you need to keep 100% of errors/slow traces while sampling routine traffic. Requires trace-ID affinity (loadbalancing exporter in front).87- **Hybrid** at scale: head-sample 10-20% in SDK + tail-sample at Collector for error/slow retention.8889Env shortcut: `OTEL_TRACES_SAMPLER=parentbased_traceidratio`, `OTEL_TRACES_SAMPLER_ARG=0.1`.9091## Reference index9293- `async-context-propagation.md` -- contextvars mechanics, asyncio task propagation, **thread boundary trap, TracedThreadPoolExecutor, fork+BSP loss, Python 3.12+ improvements** (the crown jewel of this skill -- read first when debugging missing/broken context)94- `instrumentation-patterns.md` -- auto-instrument setup, FastAPI/Celery/SQLAlchemy patterns, `traced_async` decorator, `TracedClass` mixin, sensitive-arg redaction, error handling95- `exporters-and-backends.md` -- OTLP gRPC vs HTTP, BSP tuning, propagation formats, custom SpanProcessors, multi-backend Collector YAML96- `aws-deployment.md` -- ADOT distro, X-Ray ID generator + propagator, ECS sidecar with memory_limiter ordering, IAM list, Lambda layer, collector-less when/when-not, X-Ray SDK migration97- `production-checklist.md` -- do/don't operational rules, resource detection boilerplate, signal maturity, version pinning policy9899## Three pillars correlation (one-liner)100101```python102# Inject trace_id / span_id / service_name into every stdlib log record103from opentelemetry.instrumentation.logging import LoggingInstrumentor104LoggingInstrumentor().instrument(set_logging_format=True)105# OR env: OTEL_PYTHON_LOG_CORRELATION=true106```107108For metrics: `MeterProvider` + `Counter`/`Histogram`/`UpDownCounter`/`ObservableGauge`. Shares `Resource` with `TracerProvider` so service identity is consistent.109110For OTLP log export: the Logs SDK (`opentelemetry._logs`, leading underscore = experimental) -- in production today, use the `LoggingInstrumentor` bridge and ship via your existing log pipeline.111112## Official docs113114- Python SDK: https://opentelemetry-python.readthedocs.io/115- Specification: https://opentelemetry.io/docs/specs/otel/116- All instrumentations index: https://github.com/open-telemetry/opentelemetry-python-contrib/tree/main/instrumentation117- Collector contrib: https://github.com/open-telemetry/opentelemetry-collector-contrib118- Release notes (always check before upgrade): https://github.com/open-telemetry/opentelemetry-python/releases