Production-ready FastAPI microservice template. Clone, rename, build features — structured logging, Prometheus metrics, health probes, and Kubernetes manifests are already wired up.
~2,000 req/s on a laptop, p50 12 ms — through the full middleware stack and a real Postgres round-trip:
| Concern | Solution |
|---|---|
| Logging | structlog → JSON lines, correlation_id auto-propagated via contextvars |
| Metrics | Prometheus: request count, latency histogram, in-flight gauge — at /metrics |
| Health | /health (liveness) and /ready (readiness) with different semantics — see below |
| Config | pydantic-settings — bad config kills the process at startup, not mid-request |
| DB | Async SQLAlchemy 2.0, tuned connection pool |
| Deploy | Dockerfile (non-root, multi-stage) + K8s Deployment / Service / HPA |
flowchart LR
C[Client] --> M[ObservabilityMiddleware<br/>correlation_id + metrics + timing]
M --> H["/health — liveness"]
M --> R["/ready — readiness<br/>checks Postgres"]
M --> B[your endpoints]
M -.-> P[(Prometheus<br/>/metrics)]
B --> D[(PostgreSQL<br/>async pool)]
One correlation ID, every log line — pass X-Correlation-ID (or let the service generate one) and grep the whole request story:
git clone git@github.com:loguntsovae/python-service-kit.git my-service
cd my-service && cp .env.example .env
pip install -e ".[test]"
uvicorn app.main:app --reloadpytest tests/ -v --cov=app # 3 tests, <1sLiveness ≠ readiness. /health returns 200 while the process is alive — K8s restarts the pod when it fails. /ready checks real dependencies (Postgres) — K8s stops routing traffic but does not restart. When a downstream dies, pods go unready instead of crash-looping. That distinction prevents cascading failures.
Fail at startup, not at 3 a.m. pydantic-settings validates every env var's type before the app starts. A missing DATABASE_URL or non-integer DB_POOL_SIZE exits immediately with a clear error instead of surfacing 10 minutes into production traffic.
contextvars over parameter-threading. structlog carries request_id / correlation_id into every log line emitted anywhere in the request — including deep business logic — without passing a logger through function signatures.
Non-root container. Runs as UID 1000; runAsNonRoot: true compatible. A container escape from root is an incident, from UID 1000 it's a bad day.
app/
config.py — settings from environment (validated at startup)
logging.py — structlog JSON configuration
metrics.py — Prometheus counters, histograms, gauge
middleware.py — ObservabilityMiddleware (metrics + correlation IDs)
health.py — /health and /ready
db.py — async SQLAlchemy engine + session factory
main.py — app factory and lifespan
k8s/ — Deployment, Service, HPA
tests/ — ASGI test client, probe tests
docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=pw -e POSTGRES_DB=service postgres:16-alpine
DATABASE_URL=postgresql+asyncpg://postgres:pw@localhost:5432/service uvicorn app.main:app
# then: 5000 requests, 32 concurrent against /ready (numbers above are from an M-series MacBook)