Guide 15
Observability, metrics, and Flower (FastAPI + Celery)
Audience: Teams shipping FastAPI + Celery + RabbitMQ who need production visibility. Golden rule: Flower is an ops tool. Product job status always comes from your application `jobs` table (or equivalent), never from Flower or the broker UI.
TL;DR
| Layer | What to do |
|---|---|
| Health | GET /health (liveness) + GET /ready (readiness) on the API |
| Logs | Structured JSON to stdout; propagate job_id / event_id / request id |
| Metrics | Enqueue rate, success %, retries, runtime, queue depth (backlog) |
| SLOs | Time-to-start and time-to-complete for critical job types |
| Flower | Optional Celery dashboard : private network + auth + TLS |
| Alerts | Zero consumers, DLQ depth, success drop, retry spike, SLO breach |
Client ──► FastAPI ──► jobs row + publish ──► RabbitMQ ──► workers
│ │
│ logs/metrics │ logs/metrics + Flower (ops)
▼ ▼
GET /jobs/{id} ◄── product truth ── application DBContents
- API health endpoints
- Structured logging and correlation
- Core job metrics
- SLOs
- Celery + Flower setup
- Securing admin UIs
- What to alert on
- Worker and API instrumentation pseudocode
- Dashboard sketch
- Anti-patterns
- Checklist
---
1. API health endpoints
| Endpoint | Purpose | Typical checks |
|---|---|---|
GET /health | Liveness | Process up; return 200 quickly |
GET /ready | Readiness | DB reachable; (optional) can publish or settings loaded |
Usually do not make API readiness depend on “workers are healthy.” Scale and restart workers independently. Optionally expose a worker readiness that checks broker connectivity for the worker deployment only.
# PSEUDOCODE
@app.get("/health")
async def health():
return {"status": "ok"}
@app.get("/ready")
async def ready(db: Db = Depends()):
await db.execute("SELECT 1")
return {"status": "ready"}---
2. Structured logging and correlation
Use JSON logs to stdout (platform scrapes them).
Fields to include
| Field | Where |
|---|---|
timestamp, level, message | Always |
service (api / worker / beat) | Always |
request_id | API middleware |
job_id | Enqueue + every worker log line |
task_id | Celery task id when present |
event_id | Domain/event consumers |
queue, task_name | Workers |
attempts | On retry/fail |
# PSEUDOCODE : API enqueue
log.info(
"job_enqueued",
extra={
"job_id": str(job.id),
"job_type": job.type,
"queue": "jobs.io",
"request_id": request_id,
},
)
# PSEUDOCODE : worker
log.info(
"job_started",
extra={"job_id": job_id, "task_id": self.request.id, "attempts": self.request.retries},
)Propagate ids: HTTP header X-Request-Id → log context → message headers → worker logging context.
Optional: OpenTelemetry trace context in message headers for API → worker spans.
---
3. Core job metrics
Track at least:
| Metric | Type | Notes |
|---|---|---|
jobs_enqueued_total | Counter | By type, queue |
jobs_succeeded_total | Counter | By type |
jobs_failed_total | Counter | By type, reason |
jobs_retries_total | Counter | By type |
job_runtime_seconds | Histogram | By type |
job_time_to_start_seconds | Histogram | started_at - created_at |
job_time_to_complete_seconds | Histogram | finished_at - created_at |
rabbitmq_queue_depth | Gauge | Backlog per queue |
rabbitmq_dlq_depth | Gauge | Per DLQ |
rabbitmq_consumers | Gauge | Per queue |
Success rate ≈ succeeded / (succeeded + failed) over a window (exclude still-running).
Backlog = queue depth (and/or lag if you also use Kafka). Export via RabbitMQ exporter or management API scraper.
---
4. SLOs
Define per critical job type (not one global number):
| SLO | Example |
|---|---|
| Time-to-start | p95 < 30s for send_receipt |
| Time-to-complete | p95 < 2m for send_receipt; p95 < 30m for export_csv |
Measure from the `jobs` table timestamps so product and ops share the same truth.
time_to_start = started_at - created_at
time_to_complete = finished_at - created_at---
5. Celery + Flower setup
Workers and Beat (reminder)
celery -A app.workers.celery_app.celery_app worker -Q jobs.default,jobs.io -c 4
celery -A app.workers.celery_app.celery_app beat -l info # single leaderFlower
# PSEUDOCODE : never expose publicly without auth
celery -A app.workers.celery_app.celery_app flower \
--port=5555 \
--basic_auth=ops_user:strong_passwordCompose sketch:
# PSEUDOCODE
services:
flower:
image: your-app:tag
command: celery -A app.workers.celery_app.celery_app flower --port=5555
# no public ports in prod; private network only
environment:
CELERY_BROKER_URL: ${CELERY_BROKER_URL}What Flower is good for
- See active tasks, workers online, basic task history
- Debug “is any worker connected?”
- Inspect failures during incidents
What Flower is not
- Not the customer “is my export done?” API
- Not the source of truth for SLAs (use DB timestamps + metrics)
- Not a substitute for RabbitMQ queue depth / DLQ alerts
---
6. Securing admin UIs
Mandatory for production:
| UI | Controls |
|---|---|
| Flower | Private network / VPN / mesh; auth; TLS at proxy |
| RabbitMQ Management | Same |
| Kafka UI | Same |
Checklist:
- [ ] No public
0.0.0.0without auth - [ ] SSO or strong basic auth + network policy
- [ ] TLS terminate at reverse proxy
- [ ] Separate credentials from app DB users
- [ ] Audit who can access ops UIs
---
7. What to alert on
| Signal | Severity | Notes |
|---|---|---|
| Zero consumers on a critical queue | Critical | Nothing is processing |
| DLQ depth > 0 (critical queues) | High | Poison or repeated failure |
| Success rate drop | High | Compare to baseline window |
| Retry rate spike | Medium | Upstream flapping / overload |
| Time-to-complete SLO breach | Medium | p95/p99 over SLO |
| Beat heartbeat missing | High | Schedules stopped (14) |
| API 5xx / latency | High | Standard RED metrics |
Page on Critical/High; ticket or daytime on Medium unless prolonged.
---
8. Worker and API instrumentation pseudocode
# PSEUDOCODE : enqueue
async def enqueue(...):
job = await db.insert_job(...)
await bus.publish(queue, {"job_id": str(job.id)})
metrics.incr("jobs_enqueued_total", tags={"type": job.type, "queue": queue})
log.info("job_enqueued", extra={"job_id": str(job.id), "request_id": rid})
return job
# PSEUDOCODE : worker
@celery_app.task(bind=True)
def send_receipt(self, job_id: str):
t0 = time.perf_counter()
log.info("job_started", extra={"job_id": job_id, "task_id": self.request.id})
try:
mark_running(job_id)
do_work(job_id)
mark_succeeded(job_id)
metrics.incr("jobs_succeeded_total", tags={"type": "send_receipt"})
except TransientError:
metrics.incr("jobs_retries_total", tags={"type": "send_receipt"})
raise
except Exception:
mark_failed(job_id, ...)
metrics.incr("jobs_failed_total", tags={"type": "send_receipt"})
raise
finally:
metrics.observe("job_runtime_seconds", time.perf_counter() - t0, tags={"type": "send_receipt"})Export Prometheus (or your vendor) from API and workers; scrape RabbitMQ exporter for depth/consumers.
---
9. Dashboard sketch
Row 1 : Traffic: enqueue rate, success rate, retry rate Row 2 : Latency: time-to-start p95, time-to-complete p95 (by type) Row 3 : Broker: depth per queue, DLQ depth, consumer count Row 4 : Workers: process count, task runtime histogram Row 5 : Dependencies: DB errors, SMTP/HTTP client errors
Link runbooks: scale workers, redrive DLQ, restart Beat.
---
10. Anti-patterns
| Anti-pattern | Fix |
|---|---|
| Users poll Flower for status | GET /jobs/{id} from DB |
| Flower on the public internet | Private + auth + TLS |
Only metrics, no job_id in logs | Structured correlation |
| Alert on depth alone with no consumer check | Alert zero consumers too |
| One SLO for all job types | SLOs per critical type |
| Health check that requires workers | Keep API liveness independent |
---
11. Checklist
Observability
- [ ]
GET /healthandGET /readyon API - [ ] Structured JSON logs on API, worker, Beat
- [ ]
request_id/job_id/event_idpropagated end-to-end - [ ] Counters: enqueue, success, fail, retries
- [ ] Histograms: runtime, time-to-start, time-to-complete
- [ ] Gauges: queue depth, DLQ depth, consumer count
- [ ] SLOs defined for critical job types
Flower and admin UIs
- [ ] Flower optional; command documented
- [ ] Flower on private network
- [ ] Authentication enabled
- [ ] TLS at the edge
- [ ] Same controls for RabbitMQ Management / Kafka UI
- [ ] Documented: Flower ≠ product status
Alerts
- [ ] Zero consumers → Critical
- [ ] DLQ depth > 0 (critical queues) → High
- [ ] Success rate drop → High
- [ ] Retry spike → Medium
- [ ] Time-to-complete SLO breach → Medium
---
Deep dive: 18 Monitoring Celery with Flower
Prometheus: 19 Integrate Flower with Prometheus
See also
- 05 Celery + RabbitMQ
- 09 Errors, retries, DLQ
- 14 Celery Beat
- 03 Job lifecycle (status in DB)
- 10 Testing