intermediate ~75 min updated 2026-06-01
OpenTelemetry Tracing
Instrument a Python Flask service with OpenTelemetry auto-instrumentation, ship spans through the OpenTelemetry Collector via OTLP, and inspect distributed traces in Jaeger.
Objective
Auto-instrument a Flask app with OpenTelemetry, route spans through an OTel Collector pipeline, and view the resulting traces in Jaeger. You will learn the receiver → processor → exporter pipeline model that makes the Collector the standard telemetry router.
Prerequisites
- Docker and Docker Compose installed
- Python 3.10 or newer
- pip and the venv module available
- Basic familiarity with HTTP APIs
Architecture
The Flask app is instrumented with zero code changes using opentelemetry-instrument. It exports spans over OTLP/gRPC to the OpenTelemetry Collector, which batches them and forwards to Jaeger’s native OTLP endpoint. Jaeger stores spans in memory and serves the trace UI.
curl requests
|
v
+-------------+ OTLP gRPC +------------------+ OTLP gRPC +-----------+
| Flask app | -----------> | OTel Collector | ----------> | Jaeger |
| (port 5000) | :4317 | receiver: otlp | :4317 | all-in-one|
| auto-instr. | | processor: batch | | UI :16686 |
+-------------+ | exporter: otlp | +-----------+
Steps
1. Start Jaeger and the Collector with Docker Compose
# docker-compose.yml
services:
jaeger:
image: jaegertracing/jaeger:2.5.0
ports:
- "16686:16686" # UI
otel-collector:
image: otel/opentelemetry-collector-contrib:0.123.0
command: ["--config=/etc/otelcol/config.yaml"]
volumes:
- ./otel-config.yaml:/etc/otelcol/config.yaml
ports:
- "4317:4317" # OTLP gRPC in
depends_on:
- jaeger
2. Write the Collector configuration
# otel-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 5s
exporters:
otlp/jaeger:
endpoint: jaeger:4317
tls:
insecure: true
debug:
verbosity: basic
service:
pipelines:
traces:
receivers: [otlp]
processors: [batch]
exporters: [otlp/jaeger, debug]
docker compose up -d
docker compose ps
3. Create the Flask application
mkdir app && cd app
python3 -m venv .venv && source .venv/bin/activate
pip install flask requests \
opentelemetry-distro opentelemetry-exporter-otlp
opentelemetry-bootstrap -a install
# main.py
import time
import requests
from flask import Flask, jsonify
app = Flask(__name__)
@app.get("/checkout")
def checkout():
# downstream HTTP call -> creates a child span automatically
r = requests.get("https://httpbin.org/delay/1", timeout=10)
time.sleep(0.2) # simulate work
return jsonify(order="A-1001", upstream=r.status_code)
if __name__ == "__main__":
app.run(port=5000)
4. Run with auto-instrumentation
export OTEL_SERVICE_NAME=checkout-service
export OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317
export OTEL_EXPORTER_OTLP_PROTOCOL=grpc
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=none
export OTEL_LOGS_EXPORTER=none
opentelemetry-instrument flask --app main run --port 5000
5. Generate traffic
In a second terminal:
for i in $(seq 1 10); do curl -s http://localhost:5000/checkout; echo; done
6. Inspect traces in Jaeger
Open http://localhost:16686, choose service checkout-service, and click Find Traces. Open one trace: you should see a parent GET /checkout span with a child GET span for the httpbin call. Confirm the Collector saw the spans too:
docker compose logs otel-collector | grep -i "TracesExporter" | tail -3
Expected output
$ curl -s http://localhost:5000/checkout
{"order":"A-1001","upstream":200}
$ docker compose logs otel-collector | grep -i traces | tail -2
otel-collector-1 | info TracesExporter {"kind": "exporter", "name": "debug",
"resource spans": 1, "spans": 2}
Jaeger UI trace view:
checkout-service: GET /checkout 1.31s
└─ checkout-service: GET httpbin.org 1.09s
Troubleshooting
- No traces in Jaeger but the app works: the exporter endpoint is wrong. Verify
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4317and that port 4317 is published by the collector container (docker compose ps). StatusCode.UNAVAILABLEconnection errors from the SDK: the Collector is not up or crashed on a bad config. Checkdocker compose logs otel-collectorfor YAML parse errors — indentation inotel-config.yamlmatters.- Spans reach the Collector (debug exporter logs them) but not Jaeger: the Jaeger exporter endpoint must be the in-network hostname
jaeger:4317, not localhost, and needstls.insecure: truefor plaintext. - Only one span per trace, no child span:
opentelemetry-bootstrap -a installwas skipped, so the requests library is not instrumented. Run it inside the virtualenv and restart. opentelemetry-instrument: command not found: the virtualenv is not activated. Runsource .venv/bin/activatefirst.
Cleanup
# Stop the Flask app with Ctrl+C, then:
deactivate
cd ..
docker compose down -v
rm -rf app docker-compose.yml otel-config.yaml