What You'll Build
In this exercise you will add production-grade Prometheus instrumentation to a FastAPI service simulating the CricketPulse scoring backend. By the end you will have a running service exposing custom metrics, a Prometheus instance scraping it, and verified PromQL queries returning meaningful data. This is the foundation every subsequent module builds on — without working application metrics, Grafana dashboards, alerting rules, and SLO calculations have nothing to visualise.
Prerequisites
- Python 3.10+ and Docker Desktop installed
- Lesson 5 (Prometheus Architecture) and Lesson 7 (Exporters and Custom Metrics) completed
- Basic FastAPI familiarity — you should understand routes and middleware
- Lessons 5 and 6 PromQL concepts — you will write queries to verify your metrics
Setup and Project Structure
Create the project directory, virtual environment, and install dependencies. The project separates metric definitions (metrics.py) from application logic (main.py) to keep instrumentation concerns isolated and testable independently.
mkdir cricketpulse-metrics && cd cricketpulse-metrics
python3 -m venv venv && source venv/bin/activate
pip install fastapi uvicorn prometheus_client httpx
# Project structure
cricketpulse-metrics/
app/
main.py
metrics.py
prometheus/
prometheus.yml
docker-compose.yml
Step 1 — Define Metrics
Create metrics.py with all four metric types. Centralising metric definitions in one module prevents duplicate registration errors (prometheus_client raises ValueError if the same metric name is registered twice) and makes it easy to audit your instrumentation surface.
# app/metrics.py — centralise all metric definitions
from prometheus_client import Counter, Gauge, Histogram, REGISTRY
from prometheus_client import CollectorRegistry
# Request tracking
HTTP_REQUESTS = Counter(
'cricketpulse_http_requests_total',
'Total HTTP requests received',
['endpoint', 'method', 'status_code']
)
# Active connections (WebSocket simulation)
ACTIVE_CONNECTIONS = Gauge(
'cricketpulse_active_connections',
'Number of active connections'
)
# Latency distribution
REQUEST_LATENCY = Histogram(
'cricketpulse_http_request_duration_seconds',
'HTTP request latency',
['endpoint'],
buckets=[0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0]
)
# Business metric: score cache performance
CACHE_HITS = Counter(
'cricketpulse_cache_hits_total',
'Cache hit count',
['cache_type']
)
CACHE_MISSES = Counter(
'cricketpulse_cache_misses_total',
'Cache miss count',
['cache_type']
)
Step 2 — Instrument the FastAPI App
Add a middleware that wraps every request with metric recording, and mount the /metrics endpoint using make_asgi_app(). The middleware approach ensures every route is automatically instrumented without per-route boilerplate.
# app/main.py — FastAPI with Prometheus instrumentation
from fastapi import FastAPI, Request, Response
from prometheus_client import make_asgi_app
import time, random, asyncio
from app.metrics import (
HTTP_REQUESTS, ACTIVE_CONNECTIONS, REQUEST_LATENCY,
CACHE_HITS, CACHE_MISSES
)
app = FastAPI(title='CricketPulse Scoring Service')
# Mount /metrics endpoint
metrics_app = make_asgi_app()
app.mount('/metrics', metrics_app)
@app.middleware('http')
async def metrics_middleware(request: Request, call_next):
start = time.time()
ACTIVE_CONNECTIONS.inc()
try:
response = await call_next(request)
status = str(response.status_code)
HTTP_REQUESTS.labels(
endpoint=request.url.path,
method=request.method,
status_code=status
).inc()
REQUEST_LATENCY.labels(endpoint=request.url.path).observe(
time.time() - start
)
return response
finally:
ACTIVE_CONNECTIONS.dec()
@app.get('/live_score')
async def live_score():
# Simulate cache hit/miss logic
if random.random() < 0.8:
CACHE_HITS.labels(cache_type='redis').inc()
return {'score': '245/4', 'overs': '38.2', 'source': 'cache'}
else:
CACHE_MISSES.labels(cache_type='redis').inc()
await asyncio.sleep(random.uniform(0.05, 0.2))
return {'score': '245/4', 'overs': '38.2', 'source': 'upstream'}
@app.get('/scorecard')
async def scorecard():
await asyncio.sleep(random.uniform(0.1, 0.5)) # DB query simulation
return {'batters': [], 'bowlers': []}
@app.get('/health')
async def health():
return {'status': 'ok'}
Step 3 — Configure Prometheus Scrape
Configure Prometheus to scrape the CricketPulse service and run both services with docker-compose. The web.enable-lifecycle flag allows reloading Prometheus configuration without a full restart, which is essential when iterating on scrape_configs during development.
# prometheus/prometheus.yml
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: 'cricketpulse'
static_configs:
- targets: ['cricketpulse:8000']
metrics_path: '/metrics'
scrape_interval: 15s
# docker-compose.yml
version: '3.8'
services:
cricketpulse:
build: .
ports:
- '8000:8000'
command: uvicorn app.main:app --host 0.0.0.0 --port 8000
prometheus:
image: prom/prometheus:latest
ports:
- '9090:9090'
volumes:
- ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml
command:
- '--config.file=/etc/prometheus/prometheus.yml'
- '--storage.tsdb.path=/prometheus'
- '--web.enable-lifecycle'
Step 4 — Generate Traffic and Verify Queries
Run the traffic generator to produce enough data for meaningful PromQL results, then verify each metric with the Prometheus HTTP API. The 5-minute rate window in PromQL requires at least 5 minutes of data — run the traffic generator for 5 minutes or run it repeatedly.
# generate_traffic.py — simulate match-day load
import httpx, time, random
BASE = 'http://localhost:8000'
ENDPOINTS = ['/live_score', '/live_score', '/live_score', # 3x weight
'/scorecard', '/health']
def burst(n=100, rps=20):
print(f'Sending {n} requests at ~{rps} rps...')
interval = 1.0 / rps
for i in range(n):
ep = random.choice(ENDPOINTS)
try:
r = httpx.get(f'{BASE}{ep}', timeout=2.0)
print(f'{ep} -> {r.status_code}')
except Exception as e:
print(f'{ep} -> ERROR: {e}')
time.sleep(interval)
if __name__ == '__main__':
burst(200, rps=10)
print('Traffic complete. Check Prometheus at http://localhost:9090')
# 1. Start the stack
docker compose up -d
# 2. Verify the /metrics endpoint is live
curl http://localhost:8000/metrics | grep cricketpulse
# 3. Generate traffic
python3 generate_traffic.py
# 4. Query via Prometheus HTTP API
# Request rate per endpoint
curl -s 'http://localhost:9090/api/v1/query?query=sum(rate(cricketpulse_http_requests_total[5m]))+by+(endpoint)' | python3 -m json.tool
# p99 latency
curl -s 'http://localhost:9090/api/v1/query?query=histogram_quantile(0.99,sum(rate(cricketpulse_http_request_duration_seconds_bucket[5m]))+by+(le,endpoint))' | python3 -m json.tool
# Cache hit ratio
curl -s 'http://localhost:9090/api/v1/query?query=rate(cricketpulse_cache_hits_total[5m])/(rate(cricketpulse_cache_hits_total[5m])+rate(cricketpulse_cache_misses_total[5m]))' | python3 -m json.tool
# 5. Open Prometheus UI: http://localhost:9090
# Navigate to Status -> Targets — cricketpulse should show State=UP
Warning: The make_asgi_app() /metrics endpoint is not authenticated by default. In production, place Prometheus behind a VPN or network policy so only authorised Prometheus instances can scrape it. Exposing /metrics publicly leaks service topology, label values, and potentially business-sensitive metrics like revenue counters to anyone who can reach the endpoint.
Extension challenge: Add a Histogram tracking the response payload size in bytes (cricketpulse_http_response_size_bytes) with appropriate buckets. Then write a PromQL query to find the p95 response size per endpoint. This exercises histogram bucket selection — think about the expected range of response sizes before choosing buckets.
- Centralise metric definitions in a single metrics.py module to prevent duplicate registration errors.
- Use FastAPI middleware to instrument all routes automatically — never rely on per-handler instrumentation.
- make_asgi_app() mounts the /metrics endpoint as an ASGI sub-application, compatible with any ASGI server.
- Run traffic for at least 5 minutes before querying rate() — the 5m window requires 5 minutes of data.
- Verify metrics via the Prometheus HTTP API before building Grafana dashboards on top of them.
- In production, restrict /metrics access to Prometheus IPs via network policy or basic authentication.