docs(06): create phase 6 plan — performance & production hardening
6 plans across 4 waves covering structlog/Loki observability, Locust load testing, multi-stage Dockerfile hardening, trusted-proxy rate limiting, and RUNBOOK.md. Verification passed (0 blockers). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
b7503cdff4
commit
70c09f6cd4
@@ -0,0 +1,90 @@
|
||||
---
|
||||
phase: 6
|
||||
slug: performance-production-hardening
|
||||
status: draft
|
||||
nyquist_compliant: false
|
||||
wave_0_complete: false
|
||||
created: 2026-06-02
|
||||
---
|
||||
|
||||
# Phase 6 — Validation Strategy
|
||||
|
||||
> Per-phase validation contract for feedback sampling during execution.
|
||||
|
||||
---
|
||||
|
||||
## Test Infrastructure
|
||||
|
||||
| Property | Value |
|
||||
|----------|-------|
|
||||
| **Framework** | pytest 8.2 + pytest-asyncio (asyncio_mode=auto) |
|
||||
| **Config file** | `backend/pytest.ini` |
|
||||
| **Quick run command** | `cd backend && pytest tests/ -v -x --tb=short` |
|
||||
| **Full suite command** | `cd backend && pytest tests/ -v` |
|
||||
| **Estimated runtime** | ~45 seconds |
|
||||
|
||||
---
|
||||
|
||||
## Sampling Rate
|
||||
|
||||
- **After every task commit:** Run `cd backend && pytest tests/ -v -x --tb=short`
|
||||
- **After every plan wave:** Run `cd backend && pytest tests/ -v`
|
||||
- **Before `/gsd:verify-work`:** Full suite must be green
|
||||
- **Max feedback latency:** 60 seconds
|
||||
|
||||
---
|
||||
|
||||
## Per-Task Verification Map
|
||||
|
||||
| Task ID | Plan | Wave | Requirement | Threat Ref | Secure Behavior | Test Type | Automated Command | File Exists | Status |
|
||||
|---------|------|------|-------------|------------|-----------------|-----------|-------------------|-------------|--------|
|
||||
| 06-W0-01 | Wave 0 | 0 | D-01 | — | structlog emits JSON with correlation_id | unit | `pytest tests/test_logging.py -x` | ❌ Wave 0 | ⬜ pending |
|
||||
| 06-W0-02 | Wave 0 | 0 | D-11 | T-06-01 | get_client_ip returns direct IP when peer is untrusted | unit | `pytest tests/test_rate_limiting.py::test_get_client_ip_untrusted -x` | ❌ Wave 0 | ⬜ pending |
|
||||
| 06-W0-03 | Wave 0 | 0 | D-11 | T-06-01 | get_client_ip reads XFF when peer is trusted proxy | unit | `pytest tests/test_rate_limiting.py::test_get_client_ip_trusted_proxy -x` | ❌ Wave 0 | ⬜ pending |
|
||||
| 06-W0-04 | Wave 0 | 0 | D-12 | T-06-01 | per-account limiter key is user.id not IP | unit | `pytest tests/test_rate_limiting.py::test_account_limiter_key -x` | ❌ Wave 0 | ⬜ pending |
|
||||
| 06-W0-05 | Wave 0 | 0 | D-12 | T-06-01 | authenticated endpoint returns 429 after 100 req/min | integration | `pytest tests/test_rate_limiting.py::test_account_rate_limit -x` | ❌ Wave 0 | ⬜ pending |
|
||||
| 06-W0-06 | Wave 0 | 0 | D-04..D-06 | — | Locust locustfile.py exists and is discoverable | smoke | `python -c "import locust; import sys; sys.path.insert(0,'backend/load_tests'); import locustfile"` | ❌ Wave 0 | ⬜ pending |
|
||||
| 06-LOG-01 | structlog | 1 | D-01/D-02 | — | JSON log line contains correlation_id and method | unit | `pytest tests/test_logging.py -x` | ❌ Wave 0 | ⬜ pending |
|
||||
| 06-LOG-02 | structlog | 1 | D-01 | — | structlog contextvars cleared between requests | unit | `pytest tests/test_logging.py::test_context_cleared -x` | ❌ Wave 0 | ⬜ pending |
|
||||
| 06-RL-01 | rate limiting | 5 | D-11 | T-06-01 | IP rate limiter uses get_client_ip not get_remote_address | unit | `pytest tests/test_rate_limiting.py -x` | ❌ Wave 0 | ⬜ pending |
|
||||
| 06-RL-02 | rate limiting | 5 | D-12 | T-06-01 | per-account 429 after 100 req/min on documents endpoint | integration | `pytest tests/test_rate_limiting.py::test_account_rate_limit -x` | ❌ Wave 0 | ⬜ pending |
|
||||
| 06-SCOUT-01 | docker scout | manual | D-10 | CVE | Zero critical CVEs in built image | manual | `docker scout cves local://docuvault-backend:latest --only-severity critical --exit-code` | N/A manual | ⬜ pending |
|
||||
| 06-DOCKER-01 | Dockerfile | manual | D-07 | EoP | Container runs as uid=1000 not root | manual | `docker run --rm docuvault-backend:latest id` outputs `uid=1000` | N/A manual | ⬜ pending |
|
||||
| 06-DOCKER-02 | docker-compose | manual | D-08 | EoP | read_only container can write to /tmp | manual | `docker compose up backend` + upload a document — no PermissionError | N/A manual | ⬜ pending |
|
||||
| 06-LOCUST-01 | Locust | manual | D-06 | — | SLA: p95 < 200ms, p99 < 500ms at 50 users | load test | `locust --headless --users 50 --spawn-rate 10 --run-time 5m -f backend/load_tests/locustfile.py --host http://localhost:8000` | ❌ Wave 0 | ⬜ pending |
|
||||
|
||||
*Status: ⬜ pending · ✅ green · ❌ red · ⚠️ flaky*
|
||||
|
||||
---
|
||||
|
||||
## Wave 0 Requirements
|
||||
|
||||
- [ ] `backend/tests/test_logging.py` — structlog config tests (D-01); xfail stubs for: JSON output contains correlation_id, context cleared between requests
|
||||
- [ ] `backend/tests/test_rate_limiting.py` — get_client_ip unit tests + per-account limiter tests (D-11, D-12); xfail stubs for: untrusted peer, trusted proxy XFF, account_limiter key, 429 on account limit
|
||||
- [ ] `backend/load_tests/__init__.py` — empty marker file (prevents pytest from discovering locustfile.py as a test file)
|
||||
- [ ] `backend/load_tests/locustfile.py` — Locust HttpUser skeleton (can be a stub with TODO body; full implementation in load test wave)
|
||||
|
||||
---
|
||||
|
||||
## Manual-Only Verifications
|
||||
|
||||
| Behavior | Requirement | Why Manual | Test Instructions |
|
||||
|----------|-------------|------------|-------------------|
|
||||
| Container runs as uid=1000 | D-07 | Requires built Docker image, cannot be run in unit test | `docker build -t docuvault-backend:latest backend/ && docker run --rm docuvault-backend:latest id` — must show `uid=1000(appuser)` |
|
||||
| read_only fs allows /tmp writes | D-08 | Requires docker compose stack | `docker compose up backend` → upload a document → confirm no PermissionError in logs |
|
||||
| SLA targets met at 50 concurrent users | D-06 | Load test requires running stack | `locust --headless --users 50 --spawn-rate 10 --run-time 5m -f backend/load_tests/locustfile.py --host http://localhost:8000` — must exit 0 |
|
||||
| docker scout reports zero critical CVEs | D-10 | Requires built image + Docker Hub login | `docker login && docker scout cves local://docuvault-backend:latest --only-severity critical --exit-code` — must exit 0 |
|
||||
| Grafana dashboard shows Loki logs | D-02 | UI verification | Open http://localhost:3000, Explore → Loki datasource → query `{service="backend"}` — should show JSON log lines with correlation_id |
|
||||
|
||||
---
|
||||
|
||||
## Validation Sign-Off
|
||||
|
||||
- [ ] All tasks have `<automated>` verify or Wave 0 dependencies
|
||||
- [ ] Sampling continuity: no 3 consecutive tasks without automated verify
|
||||
- [ ] Wave 0 covers all MISSING references
|
||||
- [ ] No watch-mode flags
|
||||
- [ ] Feedback latency < 60s
|
||||
- [ ] `nyquist_compliant: true` set in frontmatter
|
||||
|
||||
**Approval:** pending
|
||||
Reference in New Issue
Block a user