P5 · Production capstone¶
Take the P4 support agent to production without changing its code: OpenTelemetry tracing, cost and latency metrics, a load test, a hardened container, a threat model and a design doc.
Covers: 07 · Agent ops, 08 · System design, LLM06 unbounded consumption · Level: Intermediate · Time: 5 to 7 hours
What's here¶
| File | Purpose |
|---|---|
src/capstone/tracing.py |
Wraps P4's model client, tools and agent with OpenTelemetry spans using GenAI attribute names. No message text in spans |
src/capstone/app.py |
P4's API plus /metrics (requests, blocks, p50/p95 latency, cost per user) |
src/capstone/loadtest.py |
Async load test mixing normal traffic with attacks |
Dockerfile, docker-compose.yml |
Locked dependencies, non-root user, health check, read-only filesystem, Jaeger for traces |
DESIGN.md |
Design doc: targets, shape, data and action paths, trade-offs |
THREAT_MODEL.md |
Twelve threats, each with a risk ID, decision and test |
flowchart LR
L[capstone-load] --> APP[FastAPI app<br/>P4 agent + tracing]
APP --> M["/metrics<br/>cost, p50/p95, blocks"]
APP -->|OTLP spans| J[Jaeger UI :16686]
APP --> LLM[Fake model, or a real one with LIVE=1]
Run it¶
cd projects/p5-production-capstone
uv sync
uv run pytest -q # 5 tests: trace shape, no PII in spans, metrics, load behavior
uv run capstone-load # in-process load test, no server or API key needed
requests: 45 in 0.06s (716 req/s)
status codes: {200: 30, 429: 15}
latency ms: p50=3.5 p95=6.8 max=50.7
server cost: $0.000918 (estimated)
Three demo users each send 15 requests against a limit of 10 per minute, so 15 get HTTP 429. That's the rate limiter working.
With Docker and a trace viewer:
docker compose up --build
# in another terminal:
uv run capstone-load --url http://localhost:8000
curl -s localhost:8000/metrics
open http://localhost:16686 # Jaeger: service "support-agent"
In Jaeger, open a support_request trace. You'll see the model calls with token counts and the tool call as child spans, and no message text anywhere.
Steps¶
- Read
DESIGN.mdbefore the code. Could you defend each trade-off in an interview? - Run the load test and read
/metrics. Changerequests_per_minuteinapp.py'sGateway()and watch the 429s change. - Find the trace for a blocked jailbreak in Jaeger. Which attribute tells you why it was blocked?
- Go live with
LIVE=1(below) and rerun the load test at low volume. How much do p95 latency and cost change compared with the fake model? - Close an open item from
THREAT_MODEL.md: add secret scanning to CI, or move rate-limit counters to Redis and run two workers. - Add an alert. Write down the rule you'd use: for example, guardrail block rate above 20% for 10 minutes, or below 0.1% for an hour.
Use a real model or a different provider¶
cp .env.example .env # add GEMINI_API_KEY (free at aistudio.google.com/apikey), set LIVE=1
set -a; source .env; set +a
uv run uvicorn capstone.app:app
uv run capstone-load --url http://localhost:8000 --users 2 --requests 5 # stay inside free-tier limits
For Docker, uncomment LIVE and GEMINI_API_KEY in docker-compose.yml. Change MODEL for OpenAI, Anthropic, or a local Ollama model.
What you learned¶
- Adding observability around existing code with OpenTelemetry, without leaking personal data.
- Measuring cost and latency percentiles, and seeing rate limits work under load.
- Packaging an AI service safely: locked dependencies, non-root user, health checks, secrets from the environment.
- Writing a design doc and a threat model where every risk has a decision and a test.
Next: 08 · AI system design case studies, then 09 · Careers