1. Structured logs and request IDs
Start with the contract: make inputs, outputs and failure behavior explicit before adding infrastructure. This keeps the feature easy to reason about and gives tests a stable boundary.
2. Liveness and readiness checks
Apply the pattern through a small vertical slice. Keep framework wiring at the edge and business decisions in focused functions or services that can be tested without starting the whole application.
@app.get("/health/ready")
async def readiness():
await database.execute(text("SELECT 1"))
return {"status": "ready"}
REQUESTS = Counter("http_requests_total", "Requests", ["method", "route", "status"])3. Prometheus metrics and tracing context
Treat failure paths as part of the design. Add bounded resource usage, meaningful errors and a verification step so the behavior remains dependable under real production conditions.
4. Hands-on Exercise
Build the feature
Add JSON logs with correlation IDs, separate live/ready endpoints, and request latency and count metrics without using raw URLs as labels.
Definition of done
- The happy path works through the real HTTP boundary.
- At least one failure path is handled and tested.
- Configuration and secrets stay outside source code.
- The README explains how to run and verify the result.
5. Knowledge Check
Why should liveness not fail merely because the database is briefly unavailable?
Show answer
A restart cannot repair an external dependency and may create a restart loop; dependency checks belong in readiness.