Module 12 · Production Engineering · Drills

Drills: Observability

Telemetry you've only read about evaporates under pressure at 2am. Type every drill yourself, run it, and read the output before you reveal the solution. Effortful recall is the point.

How to use this page Each drill is a small task. Attempt it first, run it against a scratch FastAPI app, then click "Show solution" to compare. If yours works differently but correctly — great, that's fluency. Tick each box as you go; your progress is saved in this browser.

A · Warm-up reps Basic

Drill 1 structlog

Configure structlog to render JSON, then emit one log event called ask_started with fields doc_id and q_len. Confirm the output is a single JSON object.

Show solution
import structlog

structlog.configure(
    processors=[
        structlog.processors.add_log_level,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.JSONRenderer(),
    ],
)
log = structlog.get_logger()
log.info("ask_started", doc_id="d_12", q_len=42)
# {"doc_id": "d_12", "q_len": 42, "event": "ask_started", "level": "info", ...}

Drill 2 levels

Filter logs so DEBUG is suppressed but INFO and above show. Emit one of each level and confirm only three appear.

Show solution
import logging, structlog

structlog.configure(
    wrapper_class=structlog.make_filtering_bound_logger(logging.INFO),
)
log = structlog.get_logger()
log.debug("noisy_detail")     # suppressed (below INFO)
log.info("request_done")     # shown
log.warning("retried_llm")     # shown
log.error("call_failed")      # shown

In prod you run at INFO; flip to DEBUG only while investigating one service.

Drill 3 prometheus

Create a Prometheus Counter named ask_requests_total with a status label, increment it once for a success and once for a failure, and print the exposition text.

Show solution
from prometheus_client import Counter, generate_latest

ASKS = Counter("ask_requests_total", "Total /ask calls", ["status"])
ASKS.labels("ok").inc()
ASKS.labels("error").inc()

print(generate_latest().decode())
# ask_requests_total{status="ok"} 1.0
# ask_requests_total{status="error"} 1.0

B · Stretch Intermediate

Drill 4 request id

Write FastAPI middleware that mints a 12-char request_id (or reuses an incoming x-request-id header), binds it to structlog contextvars, and returns it in the response header.

Show solution
import uuid
from structlog.contextvars import bind_contextvars, clear_contextvars
from starlette.middleware.base import BaseHTTPMiddleware

class RequestIdMiddleware(BaseHTTPMiddleware):
    async def dispatch(self, request, call_next):
        clear_contextvars()
        rid = request.headers.get("x-request-id") or uuid.uuid4().hex[:12]
        bind_contextvars(request_id=rid, path=request.url.path)
        response = await call_next(request)
        response.headers["x-request-id"] = rid
        return response

app.add_middleware(RequestIdMiddleware)

Every log emitted inside the request now carries request_id for free — the foundation of "follow one request".

Drill 5 sentry

Capture an exception in Sentry with extra context: a tag feature=ask, a context block with doc_id, and the user id. Re-raise afterward.

Show solution
import sentry_sdk

try:
    answer = run_ask(question, doc_id)
except Exception as e:
    with sentry_sdk.new_scope() as scope:
        scope.set_tag("feature", "ask")
        scope.set_context("ask", {"doc_id": doc_id})
        scope.set_user({"id": user_id})
        sentry_sdk.capture_exception(e)
    raise

Context is what turns "something broke" into "this broke, for this user, on this doc" — reproducible.

Drill 6 otel span

Create a manual OpenTelemetry span named llm.chat around a (faked) LLM call and set attributes for the model and token counts.

Show solution
from opentelemetry import trace

tracer = trace.get_tracer("docchat")

with tracer.start_as_current_span("llm.chat") as span:
    span.set_attribute("llm.model", "claude")
    resp = call_llm(prompt)                 # the work being measured
    span.set_attribute("llm.prompt_tokens", resp.usage.input_tokens)
    span.set_attribute("llm.completion_tokens", resp.usage.output_tokens)

The span shows up as a child of the auto-instrumented request span — now you can see exactly how long the LLM took inside the whole request.

C · Build challenge Build

Mini-project Add end-to-end tracing and a cost metric to DocChat's /ask: a root span with child spans for retrieval and the LLM call, a Prometheus counter for USD spend, and a structured log line carrying tokens and cost. This is the exact instrumentation you'd be asked to add on day one of an SRE-minded backend role.

Build · traced & costed /ask

Wire it all together in the route. Assume tracing, structlog, and the metrics objects are configured at startup.

Show solution
import time
from opentelemetry import trace
from prometheus_client import Counter
from app.logging import log

tracer = trace.get_tracer("docchat")
LLM_COST = Counter("llm_cost_usd_total", "LLM spend", ["model"])

@router.post("/ask")
async def ask(body: AskIn, user=Depends(current_user)):
    with tracer.start_as_current_span("ask") as root:
        root.set_attribute("doc_id", body.doc_id)

        with tracer.start_as_current_span("retrieval") as rspan:
            chunks = await retrieve(body.doc_id, body.question, k=5)
            rspan.set_attribute("chunks", len(chunks))

        with tracer.start_as_current_span("llm.chat") as lspan:
            t0 = time.perf_counter()
            resp = await client.messages.create(
                model=MODEL, max_tokens=1024,
                messages=build_prompt(chunks, body.question),
            )
            seconds = time.perf_counter() - t0
            in_tok, out_tok = resp.usage.input_tokens, resp.usage.output_tokens
            cost = price(MODEL, in_tok, out_tok)
            lspan.set_attribute("llm.cost_usd", cost)

        LLM_COST.labels(MODEL).inc(cost)             # first-class cost metric
        log.info("ask_completed", model=MODEL,
                 prompt_tokens=in_tok, completion_tokens=out_tok,
                 cost_usd=round(cost, 5), llm_seconds=round(seconds, 2))
        return {"answer": resp.content[0].text}

Note the shape: a root span with two children, a counter incremented by real dollars, and one log line that makes a single request's cost and latency queryable. Tracing tells you where the time went; the cost metric and log tell you what it cost.

D · Rapid recall Flashcards

Click a card to flip it. Say the answer out loud before you flip — that's the rep that builds storage strength.

The three pillars of observability?
Logs (what happened), metrics (aggregate behaviour), traces (where time went).
click to flip
What does RED stand for?
Rate, Errors, Duration — the metrics that matter for request-serving systems.
click to flip
Counter vs gauge vs histogram?
Counter only goes up; gauge goes up & down; histogram buckets values for percentiles.
click to flip
Why a request_id / correlation id?
So every log, trace and error in one request shares an id — you follow it end to end.
click to flip
Liveness vs readiness?
/healthz = is the process alive (restart me); /readyz = can it serve traffic now (stop routing to me).
click to flip
Alert on causes or symptoms?
Symptoms tied to SLOs (latency, error rate) — never raw CPU. If there's no 3am action, it's a dashboard, not an alert.
click to flip

E · Self-check before moving on

Tick each only if you can do it without looking:

Next All ticked? DocChat is now observable — you can answer "why was this request slow?" from telemetry alone. Next we put it somewhere real and design it to scale: Module 13 — Cloud & System Design.