Monitoring
The queue and the result store are one database table. That means metrics are just queries, with no agent or exporter process to run. There are three ways in:
django_ox.stats, plain functions returning queue metrics.manage.py ox_health, the same numbers as an exit code, for cron alerting and container probes.- Structured log events on the
django_oxlogger, with stable extra keys for JSON log handlers.
Queue statistics
django_ox.stats is a small module of read-only functions. Each one is a single
ORM query over the task table. No extra state, no signals. Safe to call from a
request, a shell or a metrics collector, on every tested database.
from datetime import timedelta
from django_ox import stats
stats.queue_stats()
# [QueueStats(queue_name="default", ready=3, running=1, failed=0, successful=214),
# QueueStats(queue_name="emails", ready=0, running=0, failed=2, successful=560)]
stats.ready_count() # tasks eligible to run right now
stats.oldest_ready_age() # timedelta, or None when nothing waits
stats.throughput(timedelta(minutes=5)) # terminal outcomes per minute
stats.failure_rate(timedelta(minutes=5)) # 0.0 to 1.0, or None
stats.last_claim_age() # time since a worker last claimed
| Function | Returns | Semantics |
|---|---|---|
queue_stats() |
list[QueueStats] |
Raw row counts per queue and status (ready, running, failed, successful, lost), one entry per queue with any rows. The ready column counts every READY row, including tasks deferred to a future run_after. lost counts tasks whose worker stopped reporting with no attempts left; see the reaper. |
ready_count() |
int |
READY tasks eligible to run now, mirroring the worker's dequeue predicate: deferred tasks do not count until run_after passes. This is the backlog number. |
oldest_ready_age() |
timedelta \| None |
Age of the oldest task waiting to run, measured from when it became eligible (run_after when set, enqueued_at otherwise), so a task deferred by a week does not read as a week of backlog. |
throughput(window) |
float |
Tasks reaching a terminal state (SUCCESSFUL or FAILED) per minute over the trailing window (default 5 minutes). |
failure_rate(window) |
float \| None |
Fraction of terminal outcomes in the window that FAILED, or None when nothing finished. Retries still pending are not outcomes and do not count. |
last_claim_age() |
timedelta \| None |
Time since any worker last claimed a task, or None if none ever was. This is claim activity, not a heartbeat: idle workers over an empty queue record nothing. |
Every function except queue_stats() accepts a queue_name keyword to
scope the metric to one queue.
Alert on two numbers: backlog depth (ready_count) and backlog age
(oldest_ready_age). Neither works alone. Depth looks fine while one poisoned
task starves the queue. Age looks fine during a flood of fresh work.
Health checks: ox_health
ox_health turns thresholds on those metrics into an exit code. Zero when every
enabled check passes. Non-zero with a one-line reason on stderr when one fails.
With no flags, it checks only that the database answers.
python manage.py ox_health --max-backlog 1000 --max-age 600
| Flag | Default | Meaning |
|---|---|---|
--queue |
all queues | Restrict the checks to one queue. |
--max-backlog |
off | Fail when more than this many READY tasks are eligible to run. Deferred tasks do not count. |
--max-age |
off | Fail when the oldest waiting task has waited longer than this many seconds since becoming eligible. |
--worker-timeout |
off | Fail when no worker has claimed a task within this many seconds, or no claim was ever recorded. |
On success it prints the measured values, which is useful in cron mail and probe logs:
OK: backlog=3 oldest_age=12s last_claim_age=2s
Which check goes where:
--max-backlogand--max-agemeasure the whole queue. Put them in fleet-level alerting, from cron or a monitoring agent. Do not put them in a per-worker probe: a shared backlog would fail every worker's probe and restart healthy workers without shifting the backlog.--worker-timeoutis the closest thing to a liveness check here. Claiming is the only trace a worker leaves, so it works on queues with steady traffic and will false-alarm on ones that are legitimately idle.- For bursty queues, prefer
--max-age. It only fires when work exists and is not being picked up.
As a Kubernetes liveness probe on the worker container, for a queue with steady traffic:
livenessProbe:
exec:
command:
["python", "manage.py", "ox_health", "--worker-timeout", "300"]
periodSeconds: 60
timeoutSeconds: 10
failureThreshold: 3
From cron, for alerting on the queue itself:
*/5 * * * * cd /srv/myproject && .venv/bin/python manage.py ox_health \
--max-backlog 1000 --max-age 600 || /usr/local/bin/page-someone
Log events
The worker logs through the standard library logger named django_ox. No
logging dependency, no imposed format. Configure handlers and formatters in
LOGGING as usual.
Lifecycle events carry an extra dictionary with stable keys, so a JSON
formatter that serialises record attributes gets consistent fields to index.
The message text is not part of the contract. The keys are.
| Event | Level | When |
|---|---|---|
worker_started |
INFO | The run loop starts. |
task_claimed |
DEBUG | A task was claimed from the queue. |
task_started |
DEBUG | Execution of an attempt begins. |
task_succeeded |
INFO | The task reached SUCCESSFUL. |
task_retrying |
WARNING | An attempt failed with retries remaining. |
task_failed |
ERROR | The task reached FAILED, out of attempts. |
task_reclaimed |
WARNING | The reaper took a task back from a worker that stopped refreshing its lock. |
task_lease_lost |
WARNING | A worker finished an attempt whose lease had already been reclaimed, so its write was dropped and no result was signalled. |
lease_renew_failed |
WARNING | A lease renewal statement failed. The worker keeps going and tries again on the next interval. |
schedule_dispatched |
INFO | A recurring tick enqueued its task. |
worker_error |
ERROR | The execution wrapper itself raised (an internal worker error, not a task failure). |
worker_draining |
INFO | Shutdown began with tasks still in flight. |
worker_stopped |
INFO | The run loop exited. |
| Key | Present on | Meaning |
|---|---|---|
event |
all events | The event name from the table above. |
worker_id |
all events | Unique id of the worker emitting the record. |
task_id |
task events | The task's UUID, as a string. |
task_path |
task events | Dotted path of the task function. |
queue |
task events | Queue name. |
attempt |
task events | Attempts consumed so far, including the current one. |
duration_ms |
task_succeeded, task_retrying, task_failed, task_lease_lost |
Wall-clock duration of the attempt, in milliseconds. |
exception |
task_retrying, task_failed |
Exception class name of the failure. |
status |
task_reclaimed |
Status after reclaim: READY (requeued) or LOST (out of attempts). |
dropped_status |
task_lease_lost |
Status the dropped write would have set: SUCCESSFUL, FAILED or READY. |
schedule |
schedule_dispatched |
Schedule name from SCHEDULES. |
queues, concurrency |
worker_started |
The worker's configuration. |
pending |
worker_draining |
In-flight tasks at shutdown. |
task_claimed and task_started are DEBUG because they fire once per
attempt; run ox_worker -v 2 (or set the logger to DEBUG) when you want
them. Everything a dashboard usually wants survives at INFO.
task_lease_lost should be rare. It means a worker went unresponsive long
enough for the reaper to take its task away, and the worker's own result was
dropped when it finally finished, because the row no longer belonged to it.
Treat a steady trickle as a signal that LOCK_TIMEOUT is short relative to
how long your workers stall, rather than as noise; the
Production page covers the tuning.
throughput() and failure_rate() count SUCCESSFUL and FAILED rows only. A
LOST task is not an outcome, so it is in neither number; read the lost
column from queue_stats() for it.
Monitoring recipes
- Alerting. Alert on
ready_countandoldest_ready_age(viaox_healththresholds or the functions directly), and onfailure_raterising above your normal baseline. Throughput is better as a dashboard line than an alert: its healthy value depends entirely on offered load. - Prometheus. No exporter ships with django-ox, and none ships in
Pro either. The stats functions drop into any Django metrics setup
though. Either register a custom collector with django-prometheus, whose
collect()callsqueue_stats(),ready_count()andoldest_ready_age()and yields gauges, or render the same numbers from a plain Django view in the Prometheus text format and point a scrape job at it. Label byqueue_name. - journald. Under systemd, WARNING and above maps onto journal
priorities, so
journalctl -u ox-worker -p warningshows exactly retries, reclaims and failures. Pair it withox_healthin a timer for active checks. - Poisoned-task triage. When
failure_ratespikes, the rows have the forensics: filter FAILED rows and readerrors(per-attempt tracebacks),attemptsandworker_idsto see what died where.