opentelemetry-api
opentelemetry-api is the vendor-neutral Python interface for emitting traces, metrics, and logs under the CNCF OpenTelemetry standard. On its own it does nothing: every call is a no-op until an application installs opentelemetry-sdk (or another implementation) and wires up exporters. That split is deliberate. Libraries depend only on this API so they can emit telemetry without dragging in a backend, and the app decides at deploy time whether spans go to Jaeger, Grafana, Datadog, or nowhere.
The industry-standard telemetry API for Python and the only sane choice for library authors; traces and metrics are stable and every vendor accepts the output. Application teams should budget real time for the multi-package setup, and anyone who just needs error reporting will be happier with Sentry.
Use it if
- You maintain a Python library and want to emit spans or metrics without forcing an SDK or vendor on your users; the API-only dependency is the officially sanctioned pattern
- You want telemetry that survives a vendor switch: instrument once against OTel, then swap the exporter instead of rewriting instrumentation
- Your backend already speaks OTLP (Grafana, Jaeger, Honeycomb, Datadog, and every major vendor now does), so the collector path is paved
- You need traces and metrics correlated across services in different languages; W3C trace context propagation is the whole point
- You expect one pip install: a working setup for an application is api + sdk + an exporter + per-framework instrumentation packages, usually five or more pinned distributions whose versions move in lockstep
- You mainly want error tracking with stack traces and release health; sentry-sdk does that in one package with far less ceremony
- You need stable logs today: the README marks the logs signal as still in development and warns that stabilizing it will bring deprecations and breaking changes
- It is a small script or cron job; providers, processors, resources, and propagators are real conceptual overhead that a print statement or plain logging beats
Setup reality
pip install opentelemetry-api opentelemetry-sdk is only the start. You still choose an exporter package (OTLP over gRPC and over HTTP are separate distributions), configure a TracerProvider with a BatchSpanProcessor, and set resource attributes like service.name or everything lands as unknown_service. Auto-instrumentation needs opentelemetry-distro plus opentelemetry-bootstrap to install per-library packages, all version-pinned to the api/sdk pair, and upgrades cascade through the whole set. Requires Python 3.10+ as of the current line.
Patterns
Get a tracer and open a spancreate-span
from opentelemetry import trace
tracer = trace.get_tracer("myapp.orders")
with tracer.start_as_current_span("process-order") as span:
span.set_attribute("order.id", order_id)
do_work()Without an SDK TracerProvider configured, this whole block is a silent no-op: no error, no output. That is by design for libraries, and confusing the first time you test locally.
Wire the SDK to an OTLP endpoint (app startup)configure-sdk-otlp
from opentelemetry import trace
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
provider = TracerProvider(
resource=Resource.create({"service.name": "orders-api"})
)
provider.add_span_processor(BatchSpanProcessor(OTLPSpanExporter()))
trace.set_tracer_provider(provider)Needs opentelemetry-sdk and opentelemetry-exporter-otlp-proto-http installed; the gRPC exporter is a different package. set_tracer_provider only takes effect once per process, so do this before anything grabs a tracer.
Print spans to stdout while developingdebug-with-console-exporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import (
SimpleSpanProcessor,
ConsoleSpanExporter,
)
provider = TracerProvider()
provider.add_span_processor(SimpleSpanProcessor(ConsoleSpanExporter()))SimpleSpanProcessor exports synchronously on span end, which is what you want when eyeballing output; switch back to BatchSpanProcessor for anything real or you pay the export cost inline.
Record an exception and mark the span failedrecord-exception
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode
with tracer.start_as_current_span("charge-card") as span:
try:
charge(card)
except PaymentError as exc:
span.record_exception(exc)
span.set_status(Status(StatusCode.ERROR, str(exc)))
raisestart_as_current_span already records uncaught exceptions and sets error status by default; the explicit calls matter when you catch and handle, or when you used start_span directly.
Nest spans and reach the current one from anywherenested-spans
with tracer.start_as_current_span("http-request"):
with tracer.start_as_current_span("db-query") as child:
child.set_attribute("db.statement", "SELECT ...")
# deep in a helper, no span handle in sight:
from opentelemetry import trace
trace.get_current_span().set_attribute("cache.hit", True)Parenting flows through contextvars automatically, including across await points; you never pass span objects around by hand.
Add timestamped events to a spanspan-events
with tracer.start_as_current_span("sync-job") as span:
span.add_event("cache-miss", {"key": key})
refresh(key)
span.add_event("cache-refreshed", {"size": size})Events are cheap point-in-time annotations inside one span; if the step has meaningful duration you want a child span instead so it shows up in the waterfall.
Create a counter and a histogramcount-metric
from opentelemetry import metrics
meter = metrics.get_meter("myapp.orders")
orders = meter.create_counter("orders.processed", unit="1")
latency = meter.create_histogram("orders.duration", unit="ms")
orders.add(1, {"region": "eu"})
latency.record(elapsed_ms, {"region": "eu"})Same no-op rule as tracing: nothing is exported until the app sets a MeterProvider from the SDK. Keep attribute cardinality low; user ids as attributes will melt your metrics backend.
Propagate trace context across HTTP callspropagate-context-http
from opentelemetry import trace
from opentelemetry.propagate import inject, extract
# client side: stamp outgoing headers
headers = {}
inject(headers)
requests.get(url, headers=headers)
# server side: continue the caller's trace
ctx = extract(request.headers)
with tracer.start_as_current_span("handle", context=ctx):
...inject/extract speak W3C traceparent by default. The framework instrumentation packages do this for you; hand-rolling it is only needed for custom transports like queues.
Carry app data across services with baggagebaggage-cross-service
from opentelemetry import baggage, context
token = context.attach(baggage.set_baggage("tenant.id", "acme"))
try:
call_downstream() # propagators forward the baggage header
finally:
context.detach(token)
# downstream service:
tenant = baggage.get_baggage("tenant.id")Baggage rides plaintext HTTP headers to every downstream hop, so never put secrets or PII in it. It is separate from span attributes; copy it onto spans yourself if you want it indexed.
Zero-code instrumentation via the agent CLIauto-instrument-app
pip install opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
OTEL_SERVICE_NAME=orders-api \
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317 \
opentelemetry-instrument python app.pybootstrap scans installed packages and pip-installs matching instrumentation for Flask, Django, requests, SQLAlchemy, and friends. Fast to demo, but pin the resulting package set or the lockstep versioning will bite on the next deploy.
Alternatives
| Package | Registry | Pick it when |
|---|---|---|
| sentry-sdk | PyPI | Error tracking and performance monitoring for one app with a single package and near-zero config. |
| ddtrace | PyPI | You are committed to Datadog and want their agent's auto-instrumentation instead of assembling OTel pieces. |
| logfire | PyPI | You want OTel-based observability with a batteries-included developer experience from the Pydantic team. |
| elastic-apm | PyPI | Your observability stack is Elastic and you prefer their native agent over the OTLP route. |