FastAPI is an ASGI framework built on Starlette, and OpenTelemetry has first-class support for it via the opentelemetry-instrumentation-fastapi package. Instrumentation can be done in two ways: automatically using the opentelemetry-instrument command with zero code changes, or programmatically by calling FastAPIInstrumentor.instrument_app(app) inside your application. Both approaches produce the same spans and metrics. This guide covers both paths end-to-end, including manual span creation, log correlation, and OTLP export to the Collector.
Key Takeaways
- The latest packages as of May 19, 2026: opentelemetry-api==1.42.0, opentelemetry-sdk==1.42.0, opentelemetry-instrumentation-fastapi==0.63b0. All require Python 3.10 or above. Python 3.12 is recommended since Python 3.10 reaches EOL in October 2026
- FastAPI instrumentation is built on top of opentelemetry-instrumentation-asgi. Install both explicitly to avoid dependency resolution issues
- The OTel Python SDK defaults to HTTP/protobuf transport (opentelemetry-exporter-otlp-proto-http), not gRPC. The HTTP exporter is lighter to install since gRPC adds the grpcio dependency. Use HTTP unless your environment specifically requires gRPC
- Auto-instrumentation via opentelemetry-instrument requires opentelemetry-distro in addition to the instrumentation packages. The distro package provides the opentelemetry-instrument command
- FastAPIInstrumentor.instrument_app(app) must be called after the FastAPI app is created but before it starts serving requests. Calling it before app = FastAPI() has no effect
- Use OTEL_PYTHON_FASTAPI_EXCLUDED_URLS to exclude health check and readiness endpoints from tracing to avoid noise in your traces
What Gets Instrumented Automatically
When you instrument a FastAPI app with OpenTelemetry, the following are captured without any additional code:
| Signal | What is captured |
| Traces | One span per HTTP request: method, route template, status code, exceptions |
| Metrics | http.server.request.duration (histogram), http.server.active_requests (gauge) |
| Context propagation | Incoming W3C TraceContext headers are read and used to continue parent traces from upstream services |
The route template (/items/{item_id}) is used for span names rather than the filled path (/items/42). This is intentional and prevents high-cardinality span names that would make aggregation unusable.
Request and response bodies are not captured by default. Capturing them requires custom middleware or request/response hooks, covered in the manual instrumentation section below.
Step 1: Install the Required Packages
pip install opentelemetry-api==1.42.0 \
opentelemetry-sdk==1.42.0 \
opentelemetry-instrumentation-fastapi==0.63b0 \
opentelemetry-instrumentation-asgi==0.63b0 \
opentelemetry-exporter-otlp-proto-http==1.41.1If your environment requires gRPC transport instead of HTTP, replace the last line with:
pip install opentelemetry-exporter-otlp-proto-grpc==1.41.1
For zero-code auto-instrumentation, also install the distro package:pip install opentelemetry-distro==0.63b0
Pin all packages in requirements.txt to keep builds reproducible:
opentelemetry-api==1.42.0
opentelemetry-sdk==1.42.0
opentelemetry-instrumentation-fastapi==0.63b0
opentelemetry-instrumentation-asgi==0.63b0
opentelemetry-exporter-otlp-proto-http==1.41.1Step 2A: Programmatic Instrumentation (Recommended)
This approach instruments the app from within your application code. It gives you full control over SDK configuration and works in all deployment environments without changes to your startup command.
Create main.py:
import fastapi
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.metrics import MeterProvider
from opentelemetry.sdk.metrics.export import PeriodicExportingMetricReader
from opentelemetry.sdk.resources import Resource
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.exporter.otlp.proto.http.metric_exporter import OTLPMetricExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
# Define service resource attributes
resource = Resource.create({
"service.name": "payment-api",
"service.version": "1.0.0",
"deployment.environment": "production",
})
# Set up tracing
tracer_provider = TracerProvider(resource=resource)
tracer_provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces")
)
)
trace.set_tracer_provider(tracer_provider)
# Set up metrics
metric_reader = PeriodicExportingMetricReader(
OTLPMetricExporter(endpoint="http://otel-collector:4318/v1/metrics"),
export_interval_millis=60000,
)
meter_provider = MeterProvider(resource=resource, metric_readers=[metric_reader])
metrics.set_meter_provider(meter_provider)
# Create the FastAPI app
app = fastapi.FastAPI()
# Instrument the app — must be called after app is created
FastAPIInstrumentor.instrument_app(app)
@app.get("/items/{item_id}")
async def get_item(item_id: int):
return {"item_id": item_id}
@app.get("/health")
async def health():
return {"status": "ok"}Note: the HTTP exporter sends to signal-specific paths (/v1/traces, /v1/metrics, /v1/logs) on port 4318. The gRPC exporter sends all signals to port 4317 on a single endpoint.
The BatchSpanProcessor buffers spans and exports them in batches. Do not use SimpleSpanProcessor in production – it exports every span synchronously on the request thread, which adds latency to every response.
Step 2B: Zero-Code Auto-Instrumentation
This approach requires no changes to your application code. It is the right choice when you cannot modify the application, or when you want consistent instrumentation across many services without per-service SDK setup.
Configure via environment variables and run with opentelemetry-instrument:
OTEL_SERVICE_NAME=payment-api \
OTEL_SERVICE_VERSION=1.0.0 \
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318 \
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf \
OTEL_PYTHON_FASTAPI_EXCLUDED_URLS="health,readyz,metrics" \
opentelemetry-instrument uvicorn main:app --host 0.0.0.0 --port 8000In Docker, set the environment variables in your docker-compose.yml:
services:
payment-api:
image: payment-api:latest
environment:
OTEL_SERVICE_NAME: payment-api
OTEL_SERVICE_VERSION: "1.0.0"
OTEL_EXPORTER_OTLP_ENDPOINT: http://otel-collector:4318
OTEL_EXPORTER_OTLP_PROTOCOL: http/protobuf
OTEL_PYTHON_FASTAPI_EXCLUDED_URLS: "health,readyz,metrics"
command: >
opentelemetry-instrument
uvicorn main:app --host 0.0.0.0 --port 8000In Kubernetes, the OpenTelemetry Operator can inject auto-instrumentation via an annotation, removing the need to change your Dockerfile or command:
apiVersion: apps/v1
kind: Deployment
metadata:
name: payment-api
spec:
template:
metadata:
annotations:
instrumentation.opentelemetry.io/inject-python: "true"The Operator injects the OTel Python agent at pod startup and reads configuration from an Instrumentation CRD in the same namespace.
Step 3: Configure the OTel Collector
The Collector receives OTLP from your FastAPI app and routes telemetry to your backends. Save this as otel-collector-config.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 512
exporters:
otlp:
endpoint: your-backend:4317
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [otlp]
logs:
receivers: [otlp]
processors: [batch]
exporters: [otlp]Step 4: Add Manual Spans for Business Logic
Auto-instrumentation captures the HTTP request lifecycle. It cannot capture what happens inside your endpoint handlers. Add manual spans to trace database queries, external API calls, or any business logic that matters for performance debugging.
from opentelemetry import trace
tracer = trace.get_tracer("payment-api", "1.0.0")
@app.post("/payments")
async def create_payment(payment: PaymentRequest):
with tracer.start_as_current_span("payment.validate") as span:
span.set_attribute("payment.amount", payment.amount)
span.set_attribute("payment.currency", payment.currency)
validate_payment(payment)
with tracer.start_as_current_span("payment.charge") as span:
span.set_attribute("payment.provider", "stripe")
result = await charge_payment(payment)
span.set_attribute("payment.transaction_id", result.transaction_id)
return {"transaction_id": result.transaction_id}Spans created inside an active span automatically become child spans. The trace viewer in your backend will show the full hierarchy: the HTTP request span at the top, with payment.validate and payment.charge nested beneath it.
Recording exceptions on a span:
with tracer.start_as_current_span("payment.charge") as span:
try:
result = await charge_payment(payment)
except PaymentDeclinedError as e:
span.record_exception(e)
span.set_status(trace.StatusCode.ERROR, str(e))
raiserecord_exception captures the exception type, message, and stack trace as span events. set_status(ERROR) marks the span as failed, so it appears correctly in error rate dashboards.
Step 5: Add Custom Metrics
The FastAPI instrumentation automatically records http.server.request.duration. Add your own business metrics alongside it:
from opentelemetry import metrics
meter = metrics.get_meter("payment-api", "1.0.0")
payment_counter = meter.create_counter(
"payments.processed",
unit="1",
description="Total number of payments processed",
)
payment_value = meter.create_histogram(
"payments.value",
unit="USD",
description="Distribution of payment values",
)
@app.post("/payments")
async def create_payment(payment: PaymentRequest):
result = await process_payment(payment)
payment_counter.add(1, {
"payment.currency": payment.currency,
"payment.status": result.status,
})
payment_value.record(payment.amount, {
"payment.currency": payment.currency,
})
return resultStep 6: Correlate Logs with Traces
The OTel Python SDK can inject the current trace ID and span ID into your log records automatically, enabling log-to-trace correlation in your backend.
import logging
from opentelemetry._logs import set_logger_provider
from opentelemetry.sdk._logs import LoggerProvider, LoggingHandler
from opentelemetry.sdk._logs.export import BatchLogRecordProcessor
from opentelemetry.exporter.otlp.proto.http._log_exporter import OTLPLogExporter
# Set up log provider alongside tracer and meter provider setup
logger_provider = LoggerProvider(resource=resource)
logger_provider.add_log_record_processor(
BatchLogRecordProcessor(
OTLPLogExporter(endpoint="http://otel-collector:4318/v1/logs")
)
)
set_logger_provider(logger_provider)
# Attach OTel handler to the root logger
handler = LoggingHandler(level=logging.NOTSET, logger_provider=logger_provider)
logging.getLogger().addHandler(handler)
logging.getLogger().setLevel(logging.INFO)After this setup, every log record emitted during a traced request automatically carries the trace_id and span_id of the active span. In a backend that supports log-trace correlation, you can jump from a slow span directly to the logs from that request.
Excluding URLs from Tracing
Health check, readiness, and liveness endpoints should be excluded to prevent them from appearing in your traces and inflating request count metrics.
Via environment variable (works with both auto and programmatic instrumentation):
OTEL_PYTHON_FASTAPI_EXCLUDED_URLS="health,readyz,livez,metrics"Via code:
FastAPIInstrumentor.instrument_app(
app,
excluded_urls="health,readyz,livez,metrics"
)The value is a comma-separated list of regex patterns matched against the full request URL path. health matches /health, /healthz, and /api/health.
Common Setup Problems
| Problem | Likely cause | Fix |
| No spans appearing in the backend | SDK not initialized before the app starts handling requests, or OTLP exporter cannot reach the Collector | Confirm trace.set_tracer_provider() is called before app = FastAPI(). Check Collector logs for connection errors on ports 4317/4318 |
| opentelemetry-instrument command not found | opentelemetry-distro not installed | Run pip install opentelemetry-distro==0.63b0 |
| Spans appear but have no route attribute | opentelemetry-instrumentation-asgi not installed or wrong version | Install opentelemetry-instrumentation-asgi==0.63b0 explicitly alongside the FastAPI package |
| Health check endpoints flooding traces | Not excluded from instrumentation | Set OTEL_PYTHON_FASTAPI_EXCLUDED_URLS=health,readyz,livez |
| Manual spans not appearing as children of the request span | tracer.start_as_current_span() called outside the request context | Ensure manual spans are created inside the endpoint handler function, not at module level |
| Logs not correlated with traces | Log provider not configured, or LoggingHandler not attached to the root logger | Add the log provider setup from Step 6 and confirm the handler is attached before your first log call |
| Metrics not exported | PeriodicExportingMetricReader export interval too long, or meter provider not set globally | Call metrics.set_meter_provider(meter_provider) after creating it. Default export interval is 60 seconds, so data may not appear immediately |
From Traces to Root Cause: Where CubeAPM Fits
OpenTelemetry instrumentation tells you a request was slow. A 1.2-second span on POST /payments is visible in any trace backend. What is harder is answering the next question: is it slow because of the database query inside it, the external API call, or the business logic itself? And is it slow for all users or only for requests with a specific payment currency or amount?
CubeAPM has a dedicated FastAPI instrumentation page (docs.cubeapm.com/instrumentation) and accepts OTLP directly from your existing FastAPI setup without any additional configuration. Because it stores traces, metrics, and logs together using the shared context OpenTelemetry provides, you can start from a slow span, see the breakdown of child spans inside it, filter by any attribute you added (such as payment.currency or payment.amount), and jump to the logs from that exact request. It runs self-hosted inside your own infrastructure at $0.15/GB ingestion pricing, so no data leaves your environment.
Summary
Instrumenting a FastAPI application with OpenTelemetry takes three things: installing the packages, initializing the SDK with a tracer provider and exporter, and calling FastAPIInstrumentor.instrument_app(app). Auto-instrumentation via opentelemetry-instrument is the fastest path when you cannot modify application code. Programmatic instrumentation gives you more control and is easier to configure in production. Add manual spans for business logic and custom metrics for domain-level visibility that automatic instrumentation cannot provide.
| Step | What to do | Key detail |
| Install packages | opentelemetry-api, opentelemetry-sdk, opentelemetry-instrumentation-fastapi, opentelemetry-instrumentation-asgi, opentelemetry-exporter-otlp-proto-http | SDK 1.42.0, instrumentation 0.63b0, exporter 1.41.1. Python 3.12 recommended |
| Initialize SDK | Create TracerProvider, MeterProvider, set globally before app creation | Use BatchSpanProcessor, not SimpleSpanProcessor in production |
| Instrument app | FastAPIInstrumentor.instrument_app(app) after app = FastAPI() | Or use opentelemetry-instrument command with opentelemetry-distro installed |
| Export to Collector | HTTP exporter pointing at port 4318. gRPC exporter points at port 4317 | HTTP is the recommended default. gRPC adds a grpcio dependency |
| Add manual spans | tracer.start_as_current_span(“name”) inside endpoint handlers | Call span.record_exception() and span.set_status(ERROR) on failure |
| Add custom metrics | meter.create_counter() or meter.create_histogram() | Attach business-relevant attributes as labels |
| Correlate logs | Add LoggingHandler to root logger | Injects trace ID and span ID into every log record automatically |
| Exclude noisy URLs | OTEL_PYTHON_FASTAPI_EXCLUDED_URLS=health,readyz,livez | Comma-separated regex patterns matched against the URL path |
Disclaimer: Package versions and API signatures are verified against PyPI (opentelemetry-api 1.42.0 and opentelemetry-sdk 1.42.0 released May 19, 2026; opentelemetry-instrumentation-fastapi 0.63b0 released May 19, 2026; opentelemetry-exporter-otlp-proto-http 1.41.1 released April 24, 2026), the OpenTelemetry Python Contrib documentation (opentelemetry-python-contrib.readthedocs.io), and CubeAPM instrumentation documentation (docs.cubeapm.com/instrumentation) as of May 2026.
Also read:
What is the Difference Between OpenTelemetry and Prometheus?
What Is OpenTelemetry and How Does It Work?
What RabbitMQ Monitoring Tools Work with Prometheus and Grafana?





