From af471f8aecd10292bb997dd80f7e6e1dc5cd60a8 Mon Sep 17 00:00:00 2001 From: Burak Eyler Date: Sun, 13 Sep 2026 15:02:46 +0300 Subject: [PATCH 1/4] Add ox_health --format json for machine-readable output A healthcheck or monitoring agent wanting the figures had to parse the OK line or re-query the database. `--format json` prints one object with the same figures (backlog, oldest age, last claim age) plus `ok` and the list of problems. On a failing check the object is still printed before the same CommandError, so the exit status keeps its meaning and the numbers are there when they matter most. Text output is unchanged. Closes #36 Co-Authored-By: Claude Opus 5 --- CHANGELOG.md | 3 + docs/configuration.md | 1 + .../management/commands/ox_health.py | 48 +++++++++++++- tests/test_health.py | 65 +++++++++++++++++++ 4 files changed, 116 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index aa782e4..e9aa903 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 `ox_health --queue`, so queues with different retention needs can each be pruned with their own `--older-than`. Old schedule ticks are still pruned for every schedule. +- `ox_health --format json` prints the check figures as one JSON object + for container healthchecks and monitoring agents. On a failing check the + object is still printed, and the exit status is unchanged. ### Changed diff --git a/docs/configuration.md b/docs/configuration.md index 9dcae2e..38f0c4e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -195,6 +195,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure) and `problems`. It is printed on failure too, before the same non-zero exit. | | `--max-backlog` | off | Fail when more than this many READY tasks are eligible to run. Tasks deferred to a future `run_after` do not count. | | `--max-age` | off | Fail when the oldest waiting task has waited longer than this since becoming eligible. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long, or no claim was ever recorded. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | diff --git a/src/django_ox/management/commands/ox_health.py b/src/django_ox/management/commands/ox_health.py index e32d863..2a95f20 100644 --- a/src/django_ox/management/commands/ox_health.py +++ b/src/django_ox/management/commands/ox_health.py @@ -1,3 +1,4 @@ +import json from datetime import timedelta from typing import Any @@ -12,6 +13,10 @@ def _seconds(value: timedelta | None) -> str: return "none" if value is None else f"{value.total_seconds():.0f}s" +def _total_seconds(value: timedelta | None) -> float | None: + return None if value is None else value.total_seconds() + + class Command(BaseCommand): help = ( "Check queue health. Exits 0 when every enabled check passes, " @@ -20,6 +25,16 @@ class Command(BaseCommand): ) def add_arguments(self, parser: CommandParser) -> None: + parser.add_argument( + "--format", + choices=["text", "json"], + default="text", + help=( + "Output format. json prints one object with the same figures " + "on stdout, also when a check fails; the exit status is the " + "same either way (default: %(default)s)." + ), + ) parser.add_argument( "--queue", default=None, @@ -71,12 +86,16 @@ def handle(self, *args: Any, **options: Any) -> None: raise CommandError("--worker-timeout must be a positive number of seconds.") queue: str | None = options["queue"] + as_json = options["format"] == "json" try: backlog = stats.ready_count(queue) oldest = stats.oldest_ready_age(queue) claim_age = stats.last_claim_age(queue) except DatabaseError as exc: - raise CommandError(f"Database unreachable: {exc}") from exc + reason = f"Database unreachable: {exc}" + if as_json: + self._write_json(queue, None, None, None, [reason]) + raise CommandError(reason) from exc problems: list[str] = [] if max_backlog is not None and backlog > max_backlog: @@ -100,10 +119,37 @@ def handle(self, *args: Any, **options: Any) -> None: f"last task claim was {_seconds(claim_age)} ago, " f"over --worker-timeout {worker_timeout:g}s" ) + if as_json: + # A monitoring agent wants the figures most when a check fails, so + # the object is printed before the non-zero exit, not instead of it. + self._write_json(queue, backlog, oldest, claim_age, problems) if problems: raise CommandError("; ".join(problems)) + if as_json: + return self.stdout.write( f"OK: backlog={backlog} oldest_age={_seconds(oldest)} " f"last_claim_age={_seconds(claim_age)}" ) + + def _write_json( + self, + queue: str | None, + backlog: int | None, + oldest: timedelta | None, + claim_age: timedelta | None, + problems: list[str], + ) -> None: + self.stdout.write( + json.dumps( + { + "ok": not problems, + "queue": queue, + "backlog": backlog, + "oldest_age_seconds": _total_seconds(oldest), + "last_claim_age_seconds": _total_seconds(claim_age), + "problems": problems, + } + ) + ) diff --git a/tests/test_health.py b/tests/test_health.py index d90ae5a..4be8d51 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -1,4 +1,5 @@ import argparse +import json from datetime import timedelta from io import StringIO @@ -47,12 +48,76 @@ def health(*args): return out.getvalue() +def health_json(*args): + out = StringIO() + error = None + try: + call_command("ox_health", "--format", "json", *args, stdout=out) + except CommandError as exc: + error = exc + return json.loads(out.getvalue()), error + + @pytest.mark.django_db class TestHealth: def test_ok_with_no_flags_on_empty_database(self): out = health() assert out.startswith("OK: backlog=0 oldest_age=none last_claim_age=none") + def test_json_ok_reports_the_same_figures(self): + make_ready(seconds_ago=30) + make_claimed(seconds_ago=60) + + report, error = health_json("--max-backlog=5") + + assert error is None + assert report["ok"] is True + assert report["queue"] is None + assert report["backlog"] == 1 + assert report["oldest_age_seconds"] == pytest.approx(30, abs=5) + assert report["last_claim_age_seconds"] == pytest.approx(60, abs=5) + assert report["problems"] == [] + + def test_json_on_empty_database_uses_null_ages(self): + report, error = health_json("--queue", "emails") + + assert error is None + assert report == { + "ok": True, + "queue": "emails", + "backlog": 0, + "oldest_age_seconds": None, + "last_claim_age_seconds": None, + "problems": [], + } + + def test_json_failure_prints_the_object_and_exits_non_zero(self): + make_ready() + make_ready() + + report, error = health_json("--max-backlog=1", "--worker-timeout=60") + + assert isinstance(error, CommandError) + assert report["ok"] is False + assert report["backlog"] == 2 + assert report["problems"] == [ + "backlog is 2, over --max-backlog 1", + "no task claim recorded (--worker-timeout 60s)", + ] + assert str(error) == "; ".join(report["problems"]) + + def test_json_database_unreachable_still_prints_the_object(self, monkeypatch): + def boom(queue_name=None): + raise DatabaseError("connection refused") + + monkeypatch.setattr(ox_health.stats, "ready_count", boom) + report, error = health_json() + + assert isinstance(error, CommandError) + assert report["ok"] is False + assert report["backlog"] is None + assert report["problems"] == ["Database unreachable: connection refused"] + def test_database_unreachable_fails_with_reason(self, monkeypatch): def boom(queue_name=None): raise DatabaseError("connection refused") From ef966f4b21c3ef9e9641094f5d8b47c33cbc2780 Mon Sep 17 00:00:00 2001 From: Burak Eyler Date: Mon, 14 Sep 2026 21:11:36 +0300 Subject: [PATCH 2/4] docs: regenerate llms-full.txt for the ox_health --format json docs Co-Authored-By: Claude Sonnet 5 --- docs/llms-full.txt | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/docs/llms-full.txt b/docs/llms-full.txt index f4c842f..60aba6d 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -370,7 +370,6 @@ python manage.py ox_prune --older-than 7d | Flag | Default | Meaning | | --- | --- | --- | -| `--queue` | all queues | Delete only this queue's task rows, so queues with different retention needs can be pruned separately. Same name and meaning as `ox_health --queue`. Old schedule ticks are still pruned for every schedule. | | `--older-than` | `7d` | Minimum time since the task finished. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--include-failed` | off | Also delete FAILED and LOST rows. By default they are kept, because they hold the per-attempt tracebacks and can be retried. | | `--batch-size` | `1000` | Rows per DELETE statement, so pruning a large table never takes a long lock or builds a giant IN clause. Must be at least 1. | @@ -386,11 +385,6 @@ been removed from settings is kept by the same rule; such rows are harmless and can be deleted by hand if unwanted. See [Recurring tasks](recurring-tasks.md#missed-ticks). -`--queue` narrows the task rows, not the tick log. A run for one queue -prunes every schedule's old ticks at its own cutoff, so when queues are -pruned separately, the shortest `--older-than` decides how much tick -history stays. - ## ox_health A health check for cron alerting and container probes: exits 0 when @@ -404,6 +398,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure) and `problems`. It is printed on failure too, before the same non-zero exit. | | `--max-backlog` | off | Fail when more than this many READY tasks are eligible to run. Tasks deferred to a future `run_after` do not count. | | `--max-age` | off | Fail when the oldest waiting task has waited longer than this since becoming eligible. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long, or no claim was ever recorded. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | @@ -3536,10 +3531,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- `ox_prune --queue` restricts pruning to one queue's task rows, matching - `ox_health --queue`, so queues with different retention needs can each - be pruned with their own `--older-than`. Old schedule ticks are still - pruned for every schedule. +- `ox_health --format json` prints the check figures as one JSON object + for container healthchecks and monitoring agents. On a failing check the + object is still printed, and the exit status is unchanged. ### Changed From 49d3c1a74a56a1b9eff9679fac4b58fc8a8a82e0 Mon Sep 17 00:00:00 2001 From: Burak Eyler Date: Tue, 15 Sep 2026 03:12:01 +0300 Subject: [PATCH 3/4] Address review: rebase, print JSON on invalid thresholds, doc updates Rebases onto main (CHANGELOG entries combined under one Added heading). ox_health --format json now prints the object (ok: false, figures null) before raising CommandError on an invalid --max-backlog/--max-age/ --worker-timeout, matching the existing database-unreachable path. Adds the --format row to README.md and docs/monitoring.md, clarifies that null also covers a check that could not run, and regenerates docs/llms-full.txt. Co-Authored-By: Claude Opus 5 --- README.md | 1 + docs/configuration.md | 2 +- docs/llms-full.txt | 13 ++++++++++++- docs/monitoring.md | 1 + src/django_ox/management/commands/ox_health.py | 16 +++++++++++----- tests/test_health.py | 14 ++++++++++++++ 6 files changed, 40 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 9a7da61..602db60 100644 --- a/README.md +++ b/README.md @@ -227,6 +227,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure or the check could not run) and `problems`. It is printed on failure too, before the same non-zero exit. | | `--max-backlog` | off | Fail when more than this many READY tasks are eligible to run. | | `--max-age` | off | Fail when the oldest waiting task has waited longer than this. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | diff --git a/docs/configuration.md b/docs/configuration.md index 38f0c4e..6f3338b 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -195,7 +195,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | -| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure) and `problems`. It is printed on failure too, before the same non-zero exit. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure or the check could not run, e.g. an unreachable database or an invalid threshold) and `problems`. It is printed on failure too, before the same non-zero exit. | | `--max-backlog` | off | Fail when more than this many READY tasks are eligible to run. Tasks deferred to a future `run_after` do not count. | | `--max-age` | off | Fail when the oldest waiting task has waited longer than this since becoming eligible. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long, or no claim was ever recorded. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | diff --git a/docs/llms-full.txt b/docs/llms-full.txt index 60aba6d..f77ee78 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -370,6 +370,7 @@ python manage.py ox_prune --older-than 7d | Flag | Default | Meaning | | --- | --- | --- | +| `--queue` | all queues | Delete only this queue's task rows, so queues with different retention needs can be pruned separately. Same name and meaning as `ox_health --queue`. Old schedule ticks are still pruned for every schedule. | | `--older-than` | `7d` | Minimum time since the task finished. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--include-failed` | off | Also delete FAILED and LOST rows. By default they are kept, because they hold the per-attempt tracebacks and can be retried. | | `--batch-size` | `1000` | Rows per DELETE statement, so pruning a large table never takes a long lock or builds a giant IN clause. Must be at least 1. | @@ -385,6 +386,11 @@ been removed from settings is kept by the same rule; such rows are harmless and can be deleted by hand if unwanted. See [Recurring tasks](recurring-tasks.md#missed-ticks). +`--queue` narrows the task rows, not the tick log. A run for one queue +prunes every schedule's old ticks at its own cutoff, so when queues are +pruned separately, the shortest `--older-than` decides how much tick +history stays. + ## ox_health A health check for cron alerting and container probes: exits 0 when @@ -398,7 +404,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | -| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure) and `problems`. It is printed on failure too, before the same non-zero exit. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure or the check could not run, e.g. an unreachable database or an invalid threshold) and `problems`. It is printed on failure too, before the same non-zero exit. | | `--max-backlog` | off | Fail when more than this many READY tasks are eligible to run. Tasks deferred to a future `run_after` do not count. | | `--max-age` | off | Fail when the oldest waiting task has waited longer than this since becoming eligible. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long, or no claim was ever recorded. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | @@ -2477,6 +2483,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure or the check could not run) and `problems`. It is printed on failure too, before the same non-zero exit. | | `--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 since becoming eligible. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long, or no claim was ever recorded. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | @@ -3531,6 +3538,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added +- `ox_prune --queue` restricts pruning to one queue's task rows, matching + `ox_health --queue`, so queues with different retention needs can each + be pruned with their own `--older-than`. Old schedule ticks are still + pruned for every schedule. - `ox_health --format json` prints the check figures as one JSON object for container healthchecks and monitoring agents. On a failing check the object is still printed, and the exit status is unchanged. diff --git a/docs/monitoring.md b/docs/monitoring.md index 3b78016..49f207c 100644 --- a/docs/monitoring.md +++ b/docs/monitoring.md @@ -62,6 +62,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure or the check could not run) and `problems`. It is printed on failure too, before the same non-zero exit. | | `--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 since becoming eligible. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long, or no claim was ever recorded. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | diff --git a/src/django_ox/management/commands/ox_health.py b/src/django_ox/management/commands/ox_health.py index 2a95f20..99028ea 100644 --- a/src/django_ox/management/commands/ox_health.py +++ b/src/django_ox/management/commands/ox_health.py @@ -78,15 +78,21 @@ def handle(self, *args: Any, **options: Any) -> None: max_backlog: int | None = options["max_backlog"] max_age: float | None = options["max_age"] worker_timeout: float | None = options["worker_timeout"] + queue: str | None = options["queue"] + as_json = options["format"] == "json" + + def _invalid(reason: str) -> None: + if as_json: + self._write_json(queue, None, None, None, [reason]) + raise CommandError(reason) + if max_backlog is not None and max_backlog < 0: - raise CommandError("--max-backlog must be zero or a positive integer.") + _invalid("--max-backlog must be zero or a positive integer.") if max_age is not None and max_age <= 0: - raise CommandError("--max-age must be a positive number of seconds.") + _invalid("--max-age must be a positive number of seconds.") if worker_timeout is not None and worker_timeout <= 0: - raise CommandError("--worker-timeout must be a positive number of seconds.") + _invalid("--worker-timeout must be a positive number of seconds.") - queue: str | None = options["queue"] - as_json = options["format"] == "json" try: backlog = stats.ready_count(queue) oldest = stats.oldest_ready_age(queue) diff --git a/tests/test_health.py b/tests/test_health.py index 4be8d51..a10253a 100644 --- a/tests/test_health.py +++ b/tests/test_health.py @@ -118,6 +118,20 @@ def boom(queue_name=None): assert report["backlog"] is None assert report["problems"] == ["Database unreachable: connection refused"] + def test_json_rejects_a_bad_threshold_but_still_prints_the_object(self): + report, error = health_json("--max-backlog=-1") + + assert isinstance(error, CommandError) + assert report == { + "ok": False, + "queue": None, + "backlog": None, + "oldest_age_seconds": None, + "last_claim_age_seconds": None, + "problems": ["--max-backlog must be zero or a positive integer."], + } + assert str(error) == "--max-backlog must be zero or a positive integer." + def test_database_unreachable_fails_with_reason(self, monkeypatch): def boom(queue_name=None): raise DatabaseError("connection refused") From 3aa68f704367e166d36ab100ac7d90a7ae603189 Mon Sep 17 00:00:00 2001 From: PhiLily <252857470+PhiLily@users.noreply.github.com> Date: Tue, 15 Sep 2026 08:33:59 +0300 Subject: [PATCH 4/4] Word the ox_health --format row the same in all three tables The README, configuration and monitoring tables each described --format in slightly different words, and only one named the cases where the figures are null. All three now carry the same row. The validation helper in handle() always raises, so it is typed NoReturn rather than None. --- README.md | 2 +- docs/configuration.md | 2 +- docs/llms-full.txt | 4 ++-- docs/monitoring.md | 2 +- src/django_ox/management/commands/ox_health.py | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 602db60..a6105b4 100644 --- a/README.md +++ b/README.md @@ -227,7 +227,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | -| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure or the check could not run) and `problems`. It is printed on failure too, before the same non-zero exit. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` and `problems`. `queue` is `null` when no `--queue` is given. The figures are `null` when there is nothing to measure or the check could not run, as with an unreachable database or an invalid threshold. The object is printed on failure too, before the same non-zero exit. | | `--max-backlog` | off | Fail when more than this many READY tasks are eligible to run. | | `--max-age` | off | Fail when the oldest waiting task has waited longer than this. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | diff --git a/docs/configuration.md b/docs/configuration.md index 6f3338b..a3185fc 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -195,7 +195,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | -| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure or the check could not run, e.g. an unreachable database or an invalid threshold) and `problems`. It is printed on failure too, before the same non-zero exit. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` and `problems`. `queue` is `null` when no `--queue` is given. The figures are `null` when there is nothing to measure or the check could not run, as with an unreachable database or an invalid threshold. The object is printed on failure too, before the same non-zero exit. | | `--max-backlog` | off | Fail when more than this many READY tasks are eligible to run. Tasks deferred to a future `run_after` do not count. | | `--max-age` | off | Fail when the oldest waiting task has waited longer than this since becoming eligible. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long, or no claim was ever recorded. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | diff --git a/docs/llms-full.txt b/docs/llms-full.txt index f77ee78..ae3281b 100644 --- a/docs/llms-full.txt +++ b/docs/llms-full.txt @@ -404,7 +404,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | -| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure or the check could not run, e.g. an unreachable database or an invalid threshold) and `problems`. It is printed on failure too, before the same non-zero exit. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` and `problems`. `queue` is `null` when no `--queue` is given. The figures are `null` when there is nothing to measure or the check could not run, as with an unreachable database or an invalid threshold. The object is printed on failure too, before the same non-zero exit. | | `--max-backlog` | off | Fail when more than this many READY tasks are eligible to run. Tasks deferred to a future `run_after` do not count. | | `--max-age` | off | Fail when the oldest waiting task has waited longer than this since becoming eligible. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long, or no claim was ever recorded. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | @@ -2483,7 +2483,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | -| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure or the check could not run) and `problems`. It is printed on failure too, before the same non-zero exit. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` and `problems`. `queue` is `null` when no `--queue` is given. The figures are `null` when there is nothing to measure or the check could not run, as with an unreachable database or an invalid threshold. The object is printed on failure too, before the same non-zero exit. | | `--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 since becoming eligible. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long, or no claim was ever recorded. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | diff --git a/docs/monitoring.md b/docs/monitoring.md index 49f207c..63b9259 100644 --- a/docs/monitoring.md +++ b/docs/monitoring.md @@ -62,7 +62,7 @@ python manage.py ox_health --max-backlog 1000 --max-age 600 | Flag | Default | Meaning | | --- | --- | --- | | `--queue` | all queues | Restrict the checks to one queue. | -| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` (`null` when there is nothing to measure or the check could not run) and `problems`. It is printed on failure too, before the same non-zero exit. | +| `--format` | `text` | `json` prints one object on stdout instead of the `OK:` line: `ok`, `queue`, `backlog`, `oldest_age_seconds`, `last_claim_age_seconds` and `problems`. `queue` is `null` when no `--queue` is given. The figures are `null` when there is nothing to measure or the check could not run, as with an unreachable database or an invalid threshold. The object is printed on failure too, before the same non-zero exit. | | `--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 since becoming eligible. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | | `--worker-timeout` | off | Fail when no worker has claimed a task within this long, or no claim was ever recorded. Accepts `7d`, `24h`, `90m`, `45s`, or a plain number of seconds. | diff --git a/src/django_ox/management/commands/ox_health.py b/src/django_ox/management/commands/ox_health.py index 99028ea..af31fe9 100644 --- a/src/django_ox/management/commands/ox_health.py +++ b/src/django_ox/management/commands/ox_health.py @@ -1,6 +1,6 @@ import json from datetime import timedelta -from typing import Any +from typing import Any, NoReturn from django.core.management.base import BaseCommand, CommandError, CommandParser from django.db import DatabaseError @@ -81,7 +81,7 @@ def handle(self, *args: Any, **options: Any) -> None: queue: str | None = options["queue"] as_json = options["format"] == "json" - def _invalid(reason: str) -> None: + def _invalid(reason: str) -> NoReturn: if as_json: self._write_json(queue, None, None, None, [reason]) raise CommandError(reason)