What You'll Build
In this exercise you will instrument a Python FastAPI service with OpenTelemetry, deploy an OTel Collector and Grafana Tempo using Docker Compose, configure Loki derived fields for log-to-trace navigation, and verify end-to-end correlation — from a Grafana metrics panel through logs to a full trace waterfall in Tempo.
Prerequisites
- Lessons 17, 18, and 19 completed.
- Docker and Docker Compose installed.
- The FastAPI app from Module 2 (Metrics Practice) available, or a new minimal FastAPI app.
- Grafana with Prometheus and Loki already configured (from Modules 3 and 4).
- Python packages: opentelemetry-sdk, opentelemetry-exporter-otlp-proto-grpc, opentelemetry-instrumentation-fastapi, opentelemetry-instrumentation-requests, opentelemetry-instrumentation-logging.
Step 1 — Deploy OTel Collector and Tempo
Add the OTel Collector and Grafana Tempo to your existing docker-compose.yaml. Tempo stores traces in local filesystem storage for this exercise. The Collector receives OTLP from your FastAPI app and forwards to Tempo.
# Add to docker-compose.yaml
otel-collector:
image: otel/opentelemetry-collector-contrib:0.100.0
command: ["--config=/etc/otelcol/config.yaml"]
volumes:
- ./otel-collector-config.yaml:/etc/otelcol/config.yaml
ports:
- "4317:4317" # OTLP gRPC (your app sends here)
depends_on:
- tempo
tempo:
image: grafana/tempo:latest
command: ["-config.file=/etc/tempo.yaml"]
volumes:
- ./tempo.yaml:/etc/tempo.yaml
- tempo-data:/var/tempo
ports:
- "3200:3200" # Tempo HTTP API (Grafana queries here)
- "4318:4318" # OTLP HTTP (alternative to gRPC)
volumes:
tempo-data:# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
timeout: 5s
memory_limiter:
limit_mib: 128
check_interval: 1s
exporters:
otlp/tempo:
endpoint: "tempo:4317"
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/tempo]
# tempo.yaml
server:
http_listen_port: 3200
distributor:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
storage:
trace:
backend: local
local:
path: /var/tempo/traces
compactor:
compaction:
block_retention: 24hStep 2 — Instrument the FastAPI App
Install the OTel Python packages and add SDK initialisation to your FastAPI app. Use auto-instrumentation for HTTP and logging, and add a manual span for a key business operation to demonstrate custom span enrichment.
# Install packages
pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-grpc opentelemetry-instrumentation-fastapi opentelemetry-instrumentation-requests opentelemetry-instrumentation-logging json-log-formatter
# main.py — SDK initialisation at the top, before app creation
import logging
import json_log_formatter
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor
# 1. Configure JSON logging so Loki can parse trace_id
formatter = json_log_formatter.JSONFormatter()
handler = logging.StreamHandler()
handler.setFormatter(formatter)
logging.basicConfig(level=logging.INFO, handlers=[handler])
logger = logging.getLogger(__name__)
# 2. Configure tracer provider
resource = Resource.create({"service.name": "cricketpulse-api"})
provider = TracerProvider(resource=resource)
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(endpoint="http://otel-collector:4317", insecure=True)
)
)
trace.set_tracer_provider(provider)
tracer = trace.get_tracer(__name__)
# 3. Auto-instrument logging (injects trace_id into log records)
LoggingInstrumentor().instrument(set_logging_format=True)
# 4. Create app and auto-instrument FastAPI
from fastapi import FastAPI
app = FastAPI()
FastAPIInstrumentor.instrument_app(app)
@app.get("/match/{match_id}/score")
def get_score(match_id: str):
logger.info("score request", extra={"match_id": match_id})
with tracer.start_as_current_span("fetch_score_from_db") as span:
span.set_attribute("match.id", match_id)
span.set_attribute("db.system", "postgresql")
# simulate DB call
import time; time.sleep(0.05)
score = {"match_id": match_id, "runs": 247, "wickets": 4}
span.set_attribute("score.runs", score["runs"])
return scoreStep 3 — Add Tempo as a Grafana Data Source
In Grafana, add Tempo as a data source pointing to http://tempo:3200. Then configure the Loki data source derived fields so that trace IDs in log lines become clickable links to Tempo.
# grafana/provisioning/datasources/tempo.yaml
apiVersion: 1
datasources:
- name: Tempo
type: tempo
uid: tempo-uid
url: http://tempo:3200
access: proxy
jsonData:
httpMethod: GET
serviceMap:
datasourceUid: prometheus-uid # enables service map view
# Update Loki datasource to add derived fields
- name: Loki
type: loki
uid: loki-uid
url: http://loki:3100
access: proxy
jsonData:
derivedFields:
- matcherRegex: '"trace_id":"([a-f0-9]+)"'
name: TraceID
url: "$${__value.raw}"
datasourceUid: tempo-uid
urlDisplayLabel: "View Trace in Tempo"Step 4 — Verify End-to-End Correlation
Generate traffic to your FastAPI app, then walk the full correlation chain: from Prometheus metrics through Loki logs to a Tempo trace. Each step should flow naturally using Grafana's built-in navigation — no manual copy-pasting of IDs.
# Generate traffic
for i in $(seq 1 50); do
curl -s "http://localhost:8000/match/IPL-2025-RCB-vs-CSK/score" > /dev/null
sleep 0.2
done
# Verify spans reaching Tempo
curl -s "http://localhost:3200/api/search?service.name=cricketpulse-api&limit=5" | jq .
# Verify trace_id in Loki logs (check container output)
docker compose logs cricketpulse-api 2>&1 | grep trace_id | head -5
# In Grafana Explore:
# 1. Query Loki: {service="cricketpulse-api"} | json | trace_id != ""
# 2. Click a log line — "View Trace in Tempo" button should appear
# 3. Click it — Tempo waterfall should open showing root span + child spansVerify Your Work
Run the checklist below to confirm every layer of the tracing stack is functioning. All five items should pass before you mark the exercise complete.
# Verification checklist
# 1. Spans arriving in Tempo
curl -s "http://localhost:3200/api/search?service.name=cricketpulse-api" | jq '.traces | length'
# Expect: > 0
# 2. Trace contains expected spans
TRACE_ID=$(curl -s "http://localhost:3200/api/search?service.name=cricketpulse-api&limit=1" | jq -r '.traces[0].traceID')
curl -s "http://localhost:3200/api/traces/$TRACE_ID" | jq '[.batches[].scopeSpans[].spans[].name]'
# Expect: ["GET /match/{match_id}/score", "fetch_score_from_db"]
# 3. Logs contain trace_id
docker compose logs cricketpulse-api 2>&1 | python3 -c "import sys,json; [print(json.loads(l).get('trace_id','MISSING')) for l in sys.stdin if '{' in l]" | grep -v MISSING | head -3
# Expect: 32-character hex trace IDs
# 4. Tempo data source in Grafana responds
curl -s "http://admin:admin@localhost:3000/api/datasources/name/Tempo" | jq .url
# Expect: "http://tempo:3200"
# 5. Derived fields configured on Loki
curl -s "http://admin:admin@localhost:3000/api/datasources/name/Loki" | jq '.jsonData.derivedFields[0].name'
# Expect: "TraceID"If spans appear in Collector logs but not in Tempo, check that the Tempo receiver is configured for gRPC on port 4317 and that the Collector exporter endpoint matches. Use 'docker compose logs tempo' and 'docker compose logs otel-collector' side-by-side to trace the handoff.
Grafana Tempo's 'Service Graph' view (enabled when linked to a Prometheus datasource) auto-generates a topology map of service dependencies from trace data. After generating 50+ traces, navigate to Explore > Tempo > Service Graph to see CricketPulse's service map.
- OTel SDK with BatchSpanProcessor and OTLPSpanExporter sends traces to the Collector over gRPC port 4317.
- LoggingInstrumentor automatically injects trace_id into JSON log records for Loki correlation.
- Loki derived fields with a regex on trace_id create one-click 'View Trace in Tempo' links.
- The full correlation chain: Prometheus metric panel → Loki log line → Tempo trace waterfall.
- Use 'docker compose logs' for both Collector and Tempo simultaneously to debug span delivery issues.