📌 Instrumenting a Python App for Distributed Tracing
We’ll create a simple Flask API that:
✅ Generates traces
✅ Generates metrics
✅ Sends them to OpenTelemetry Collector
✅ Stores them in Tempo
1️⃣ Setup Project Directory
mkdir grafana-tempo && cd grafana-temo
mkdir otel-python-app && cd otel-python-app
2️⃣ Setup a Flask Application
Create a file app.py:
from flask import Flask, request
import time
import logging
from prometheus_flask_exporter import PrometheusMetrics
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
from opentelemetry.instrumentation.flask import FlaskInstrumentor
# Setup Logging
logging.basicConfig(level=logging.INFO, format='%(asctime)s %(levelname)s %(message)s')
# Flask App
app = Flask(__name__)
# ✅ Explicitly start metrics server (fix for missing /metrics)
metrics = PrometheusMetrics(app, group_by='endpoint', buckets=[0.1, 0.2, 0.5, 1, 2, 5, 10])
metrics.start_http_server(5001) # ✅ Ensures /metrics is available
FlaskInstrumentor().instrument_app(app)
# Initialize tracing with resource attributes
resource = Resource.create({
"service.name": "flask-otel-app",
"service.version": "1.0.0",
"service.environment": "dev"
})
trace.set_tracer_provider(TracerProvider(resource=resource))
tracer = trace.get_tracer(__name__)
# Export spans to OTLP (Tempo)
try:
otlp_exporter = OTLPSpanExporter(endpoint="http://otel-collector:4318/v1/traces")
span_processor = BatchSpanProcessor(otlp_exporter)
trace.get_tracer_provider().add_span_processor(span_processor)
except Exception as e:
logging.error(f"Error initializing OTLP exporter: {e}")
@app.route('/')
def home():
with tracer.start_as_current_span("home-handler"):
time.sleep(0.2) # Simulate processing
logging.info("Processing / request")
return "Hello, OpenTelemetry with Grafana Tempo!"
@app.route('/slow')
def slow():
with tracer.start_as_current_span("slow-handler"):
time.sleep(1) # Simulate a slow response
logging.info("Processing /slow request")
return "This took a while!"
@app.route("/process")
def process():
with tracer.start_as_current_span("processing_request"):
time.sleep(1) # Simulate a delay
logging.info("Processing request completed")
return "Processing complete!"
@app.route("/external-api")
def external_api():
with tracer.start_as_current_span("calling_external_api"):
time.sleep(2) # Simulating an external API delay
logging.info("External API call completed")
return "External API response!"
@app.route("/db-query")
def db_query():
with tracer.start_as_current_span("querying_database"):
time.sleep(1.5) # Simulating a database query delay
logging.info("Database query completed")
return "Database query response!"
if __name__ == '__main__':
app.run(host='0.0.0.0', port=5000, debug=True)
3️⃣ Install Dependencies
Create a file requirements.txt:
flask
opentelemetry-sdk
opentelemetry-exporter-otlp
opentelemetry-instrumentation-flask
prometheus_flask_exporter
4️⃣ Setup Dockerfile
Create a file Dockerfile:
### Stage 1: Builder Stage
FROM python:3.12 AS builder
WORKDIR /app
### Copy requirements separately for caching
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
### Stage 2: Final Image
FROM python:3.12-slim AS runtime
WORKDIR /app
### Copy installed dependencies from the builder stage
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
COPY --from=builder /usr/local/bin /usr/local/bin
### Copy application code
COPY app.py .
### Expose the Flask port
EXPOSE 5000
### Run the app
CMD ["python3", "app.py"]
5️⃣ Setup Grafana Tempo
cd ../
pwd
## output:- grafana-tempo
6️⃣ Create Docker Compose File
Create a file docker-compose.yml:
version: '3.7'
services:
tempo:
image: grafana/tempo:latest
command: [ "-config.file=/etc/tempo.yml" ]
volumes:
- ./tempo-config.yml:/etc/tempo.yml
- tempo-data:/var/tempo
ports:
- "3200:3200" # Tempo Query Port
- "14268:14268" # Jaeger ingest
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
networks:
- observability
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
environment:
- GF_SECURITY_ADMIN_PASSWORD=admin
volumes:
- grafana-data:/var/lib/grafana
- ./grafana-datasources.yml:/etc/grafana/provisioning/datasources/datasources.yaml
depends_on:
- tempo
- prometheus
networks:
- observability
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
command: [ "--config=/etc/otel-collector-config.yml" ]
volumes:
- ./otel-collector-config.yml:/etc/otel-collector-config.yml
ports:
- "4317" # OTLP gRPC
- "4318" # OTLP HTTP
- "55680:55680" # OpenTelemetry HTTP Debugging
depends_on:
- tempo
networks:
- observability
prometheus:
image: prom/prometheus:latest
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
command:
- "--config.file=/etc/prometheus/prometheus.yml"
ports:
- "9090:9090"
networks:
- observability
depends_on:
- otel-collector
flask-otel-app:
build: ./otel-python-app/
ports:
- 5000:5000
container_name: flask-otel-app
networks:
- observability
#image: flask-otel-app
depends_on:
- otel-collector
- prometheus
volumes:
tempo-data:
grafana-data:
networks:
observability:
driver: bridge
7️⃣ Create OpenTelemetry Collector Config
Create a file otel-collector-config.yml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
exporters:
otlp:
endpoint: "tempo:4317"
tls:
insecure: true
prometheus:
endpoint: "0.0.0.0:9090"
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlp]
logs:
receivers: [otlp]
exporters: [otlp]
metrics:
receivers: [otlp]
exporters: [prometheus]
8️⃣ Create Tempo Config
Create a file tempo-config.yml:
stream_over_http_enabled: true
server:
http_listen_port: 3200
grpc_listen_port: 9095
log_level: info
query_frontend:
search:
duration_slo: 5s
throughput_bytes_slo: 1.073741824e+09
metadata_slo:
duration_slo: 5s
throughput_bytes_slo: 1.073741824e+09
trace_by_id:
duration_slo: 5s
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
ingester:
trace_idle_period: 10s
max_block_bytes: 1_000_000
max_block_duration: 5m # cut the headblock when this much time passes. this is being set for demo purposes and should probably be left alone normally
compactor:
compaction:
block_retention: 24h # overall Tempo trace retention. set for demo purposes
metrics_generator:
registry:
external_labels:
source: tempo
cluster: docker-compose
storage:
trace:
backend: local # backend configuration to use
local:
path: /var/tempo/blocks
overrides:
defaults:
metrics_generator:
processors: [service-graphs, span-metrics, local-blocks] # enables metrics generator
generate_native_histograms: both
9️⃣ Create Prometheus Config
Create a file prometheus.yml:
global:
scrape_interval: 10s
scrape_configs:
- job_name: "otel-collector"
static_configs:
- targets: ["otel-collector:9090"]
- job_name: "flask-app"
static_configs:
- targets: ["flask-otel-app:5001"]
🔟 Create Grafana Datasource Config
Create a file grafana-datasources.yml:
apiVersion: 1
datasources:
- name: Tempo
type: tempo
access: proxy
uid: tempo
url: http://tempo:3200
isDefault: true
editable: true # change to false if want to disable the edit data source from grafana console
jsonData:
httpMethod: GET
tracesToLogsV2:
datasourceUid: "loki"
spanStartTimeShift: '-1h'
spanEndTimeShift: '1h'
filterByTraceID: false
filterBySpanID: false
tracesToMetrics:
datasourceUid: "prometheus"
spanStartTimeShift: '-1h'
spanEndTimeShift: '1h'
streamingEnabled:
search: true
traceQuery:
timeShiftEnabled: true
spanStartTimeShift: '-1h'
spanEndTimeShift: '1h'
serviceMap:
datasourceUid: "prometheus"
nodeGraph:
enabled: true
spanBar:
datasourceUid: "prometheus"
lokiSearch:
datasourceUid: 'loki'
- name: Prometheus
type: prometheus
access: proxy
uid: prometheus
url: http://prometheus:9090
isDefault: false
editable: true
Start docker compose
docker images
docker compose up -d
docker compose logs -f otel-collector
docker compose logs -f flask-otel-app
docker compose logs -f tempo
docker compose logs -f grafana
docker compose logs -f prometheus
✨ This creates the following resources:
✔ flask-otel-app Built
✔ Network grafana-tempo_observability Created
✔ Volume "grafana-tempo_tempo-data" Created
✔ Volume "grafana-tempo_grafana-data" Created
✔ Container grafana-tempo-tempo-1 Started
✔ Container flask-otel-app Started
✔ Container grafana-tempo-otel-collector-1 Started
✔ Container grafana-tempo-prometheus-1 Started
✔ Container grafana-tempo-grafana-1 Started
Generate Traces
curl http://localhost:5000/
curl http://localhost:5000/slow
Each request generates traces and sends them to Grafana Tempo via the OTLP Exporter.
View Traces in Grafana
1️⃣ Open Grafana at 👉 http://localhost:3000
2️⃣ Go to Explore
3️⃣ Select Tempo as the data source
4️⃣ Click Search to view traces
5️⃣ Click on a trace to see the request flow
