How-to · OpenTelemetry
How to Instrument Python Apps with OpenTelemetry
Amber Jain
May 2026
7 min read
Instrumenting Python with OpenTelemetry means using the OpenTelemetry Python SDK and the opentelemetry-instrument agent to emit traces, metrics and logs over OTLP, often with zero code changes..
Instrumenting Python with OpenTelemetry means using the OpenTelemetry Python SDK and the opentelemetry-instrument agent to emit traces, metrics and logs over OTLP, often with zero code changes.
What to observe in a Python service
For a Python web service the signals that matter are per-endpoint latency and error rate, the latency of database queries and external calls the request makes, and unhandled exceptions. Two Python-specific realities shape what you watch: many frameworks run under multiple worker processes (gunicorn or uvicorn), so telemetry has to be initialised per worker, and CPU-bound work holds the GIL, so a single hot request can starve the whole worker.
The most valuable thing distributed traces give a Python service is the breakdown inside a slow request, whether the time is spent in your code, in a database query, or waiting on a downstream API, which is exactly what turns a vague 'the endpoint is slow' into a fix.
How OpenTelemetry collects it
Python supports zero-code instrumentation through the opentelemetry-instrument agent. You install opentelemetry-distro, run opentelemetry-bootstrap to pull in the instrumentations for the libraries you actually use, Django, Flask, FastAPI, requests, psycopg, SQLAlchemy, and launch the app under the agent with an OTLP exporter. Every request becomes a server span and every instrumented call a child span, so the request breakdown comes for free.
The gotcha that silently breaks Python setups is worker forking: gunicorn and uvicorn fork worker processes, and the tracer must be initialised in each worker, not only the parent, or you get no spans from the workers. The fix is to initialise in a post-fork hook. For your own logic, the SDK adds manual spans and custom metrics; for CPython runtime health, the process-runtime instrumentation emits CPU, memory and garbage-collection metrics.
Metric and attribute names follow the OpenTelemetry semantic conventions and vary by instrumentation version; confirm the exact identifiers for your build.
Key metrics to watch
The signals that explain most Python service incidents:
| Signal | Source | What it tells you / watch for |
|---|---|---|
| http.server.request.duration | framework instrumentation | Per-endpoint latency histogram; track p95/p99 by route. |
| db.client.operation.duration | DB instrumentation | Query latency. If DB time dominates a request, the fix is in the query or the schema, not the app. |
| span count per request (DB spans) | DB instrumentation | Many similar DB spans in one trace is the classic N+1 query pattern. |
| process.runtime.cpython.cpu.utilization | runtime instrumentation | Sustained high CPU in a worker points to GIL-bound work starving other requests. |
| process.runtime.cpython.memory | runtime instrumentation | Worker memory climbing without release indicates a leak or unbounded cache. |
| error rate (5xx by route) | framework instrumentation | Endpoint failures; correlate with a downstream span or exception. |
What to put on a dashboard
A Python dashboard should separate 'my code is slow' from 'my database is slow' from 'my workers are saturated'. Row one: endpoint p95/p99 and 5xx rate by route. Row two: database time as a share of request time, and the number of DB spans per request to catch N+1 patterns. Row three: per-worker CPU and memory, so GIL-bound saturation and leaks are visible before requests start timing out.
# p95 latency by endpoint histogram_quantile(0.95, http.server.request.duration) by (http.route) # database time as a fraction of total request time sum(db.client.operation.duration) by (http.route) / sum(http.server.request.duration) by (http.route) # worker CPU saturation (GIL-bound work) process.runtime.cpython.cpu.utilization by (worker)Reading the signals: common failure modes
N+1 queries
One endpoint is slow and its trace contains dozens of near-identical database spans, one per row of a collection. Database time dominates the request. The fix is eager loading or a single batched query, and the trace makes the pattern obvious in a way logs never would.
Blocking call in an async loop
A FastAPI or asyncio service degrades under load because a synchronous call, a blocking DB driver or a CPU-bound function, is running on the event loop. Latency rises across all routes on that worker while CPU spikes. Move the call to a threadpool or use an async driver.
Worker saturation
All workers are busy and requests queue, so latency climbs with no single slow dependency. Per-worker CPU is pinned. Add workers, or move CPU-bound work out of the request path.
Example alert conditions
Starting thresholds to tune:
| Condition | Signal | Meaning |
|---|---|---|
| 5xx rate by route > 2% for 5m | errors | Endpoint failing. |
| p95 latency > SLO for 10m | latency | Endpoint regression. |
| DB time / request > 0.6 for 10m | database | Requests dominated by database work. |
| worker CPU > 90% for 5m | runtime | GIL-bound saturation; requests queueing. |
From monitoring to autonomous resolution
Opstral's Telemetry Ops ingests Python OpenTelemetry data natively, and Sentinel AI resolves the service incidents it surfaces through governed Action Tickets. Explore Telemetry Ops.
Frequently asked questions
How do I instrument a Python app with OpenTelemetry?
Install opentelemetry-distro and instrumentation packages, run opentelemetry-bootstrap, then start the app with the opentelemetry-instrument agent and an OTLP exporter pointing at your Collector.
Does OpenTelemetry support zero-code Python instrumentation?
Yes. Running a Python app under opentelemetry-instrument auto-instruments popular frameworks like Django, Flask and FastAPI and common database drivers with no code change.