You can’t operate what you can’t see. Metrics, logs, and the four golden signals for the URL shortener.
L09–L11 got the app running on Kubernetes with TLS. It deploys. It starts.
L12 makes the running app observable — you can answer “is it slow?”, “is it failing?”, “how much traffic?” without SSH-ing into a pod.
kubectl logs.”
Source: Google SRE Book. Every dashboard in this lesson populates exactly these four.
/metrics on the APIfrom prometheus_client import Counter, Histogram, make_wsgi_app
REQ = Counter("http_requests_total", "Requests", ["route", "code"])
LAT = Histogram("http_request_duration_seconds", "Latency", ["route"])
Counter + histogram + a WSGI /metrics route. The histogram buckets default to the Prometheus standard (5 ms → 10 s), which already covers a web API. route is the Flask endpoint name — not the URL — so cardinality stays bounded.
/metrics on the workerfrom prometheus_client import start_http_server, Counter, Histogram
JOBS = Counter("worker_jobs_processed_total", "Jobs processed", ["outcome"])
DUR = Histogram("worker_job_duration_seconds", "Job duration")
# In main():
start_http_server(9090) # metrics-only sidecar port
Workers don’t have an HTTP API of their own, so they expose metrics on a dedicated port. Prometheus scrapes it via pod annotation prometheus.io/port: "9090" — no Service or Ingress needed.
observability:
enabled: true # false in dev; true in prod
prometheus:
scrapeInterval: 15s
grafana:
adminPassword: changeme # override in prod
loki:
retention: 168h
A single observability.enabled: true gates the scrape annotations on both pods. In prod, swap to a managed Prometheus (AMP, Grafana Cloud) by adding remoteWrite config.
# 5xx error rate
rate(http_requests_total{code=~"5.."}[5m])
# p95 latency
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))
# request rate by route
sum by (route) (rate(http_requests_total[1m]))
Three queries covering three of the four golden signals. Saturation is container_cpu_usage_seconds_total and container_memory_working_set_bytes.
- alert: ApiHighErrorRate
expr: sum(rate(http_requests_total{code=~"5.."}[5m]))
/ sum(rate(http_requests_total[5m])) > 0.02
for: 10m
- alert: ApiHighLatency
expr: histogram_quantile(0.95,
rate(http_request_duration_seconds_bucket[5m])) > 1.0
for: 10m
Two examples; the full file ships four. Alerts fire only after for: 10m — avoid pager-fatigue on transient blips. Never omit the for: window.
Loki stores logs indexed by labels (not full-text), so you query by:
{namespace="url-shortener", pod=~"api-.*"} |= "500"
First narrow by label (cheap), then grep the matched stream (still cheap). Promtail is the DaemonSet that scrapes /var/log/pods/… and ships to Loki.
Three cardinality rules: (1) drop __path__ — it changes per pod restart; (2) never promote request-id into a label; (3) drop debug logs in production.
level=“error”Grid layout: 2 columns × 3 rows. The dashboard JSON ships in observability/grafana-dashboards/api-golden-signals.json.
sample-app/
api/app.py # + /metrics route, + REQ/LAT counters
worker/worker.py # + start_http_server(9090), + job counters
helm/values.yaml # + observability: { enabled: false } default
helm/values-prod.yaml # observability.enabled: true
observability/
prometheus-rules.yaml
grafana-dashboards/api-golden-signals.json
loki-values.yaml
RUNBOOK.md
README.md
for: window — and a runbook link.