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.
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.
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.
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. |
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)
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.
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.
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.
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. |
Ops Singularity's TelemetryOps ingests Python OpenTelemetry data natively, and Sentinel AI resolves the service incidents it surfaces through governed Action Tickets. Explore TelemetryOps.
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.
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.
Bring a real incident. We will show you Sentinel investigate, act and verify end to end, with every action reversible and audited.