From cc27d8d957e37644bffbdaeef92c63076993ad5d Mon Sep 17 00:00:00 2001 From: sneak Date: Sun, 18 Oct 2026 14:03:58 +0000 Subject: [PATCH] docs: health check wording follows the code (rework for #302) The README, the comments and TODO.md say the health check gives error only when every lookup in a round failed because no root server gave a usable reply, and the README drops the claim that the other failures are reported as notifications. Model: opus-5-5 --- README.md | 14 +++++++------- TODO.md | 7 ++++--- internal/watcher/dnsunreachable_test.go | 9 +++++---- internal/watcher/watcher.go | 22 +++++++++++----------- 4 files changed, 27 insertions(+), 25 deletions(-) diff --git a/README.md b/README.md index b449313..3b07cba 100644 --- a/README.md +++ b/README.md @@ -356,15 +356,15 @@ dnswatcher exposes a lightweight HTTP API for operational visibility: | `GET /metrics` | Prometheus metrics, see below | The health check, at `/.well-known/healthcheck` and `/health`, follows the most -recent round of DNS checks. When no lookup in that round got a reply from a root -server, because none of them answered or because the network intercepts DNS -queries (see DNS Resolution Strategy), dnswatcher could not reach the DNS at -all. The health check then gives `status` `error` and answers +recent round of DNS checks. When every lookup in that round failed because no +root server gave a usable reply, as when none of them answers or the network +intercepts DNS queries (see DNS Resolution Strategy), dnswatcher could not reach +the DNS at all. The health check then gives `status` `error` and answers `503 Service Unavailable`, so the image's health check fails and Docker and upaas see the container as unhealthy. Otherwise it gives `ok` and answers -`200 OK`, as it also does before the first round finishes. A round in which the -root servers answered gives `ok` even when a name's own nameservers do not -answer or a domain does not exist; those are reported as notifications. +`200 OK`, as it also does before the first round finishes. A round gives `ok` +even when a name's own nameservers do not answer or a domain does not exist, +since the root servers gave a usable reply to those lookups. In `/api/v1/status`, each nameserver entry and certificate entry whose `status` is `error` also has `error`, the reason, as in the state file (see State File diff --git a/TODO.md b/TODO.md index fe97974..e3d269b 100644 --- a/TODO.md +++ b/TODO.md @@ -19,9 +19,10 @@ milestone: https://git.eeqj.de/sneak/dnswatcher/milestone/7 # Completed Steps -- 2026-10-18: the health check answers 503 with `status` `error` when no lookup - in the most recent round of DNS checks got a reply from a root server, as on a - network that intercepts DNS queries (closes #302). +- 2026-10-18: the health check answers 503 with `status` `error` when every + lookup in the most recent round of DNS checks failed because no root server + gave a usable reply, as on a network that intercepts DNS queries (closes + #302). - 2026-10-16: the first reply marked "recursion available" from a root server's address, a refusal included, stops the lookup with "this network intercepts DNS queries", so on a network that sends DNS to a resolver dnswatcher and its diff --git a/internal/watcher/dnsunreachable_test.go b/internal/watcher/dnsunreachable_test.go index 76d1c08..7a2eecb 100644 --- a/internal/watcher/dnsunreachable_test.go +++ b/internal/watcher/dnsunreachable_test.go @@ -15,9 +15,10 @@ import ( // four operators mark their replies recursion available, as on a network // that intercepts DNS queries; one that gives no reply is passed over // for the next, and if none replies, no root server answered. Either -// way no lookup got a reply from a root server, so the round could not -// reach the DNS. TestFirstRunBaseline and TestDomainThatDoesNotExist -// check rounds in which the root servers answered. +// way every lookup failed because no root server gave a usable reply, +// so the round could not reach the DNS. TestFirstRunBaseline and +// TestDomainThatDoesNotExist check rounds in which the root servers +// gave a usable reply. func TestDNSUnreachableWhenNoRootServerReplies(t *testing.T) { t.Parallel() @@ -58,7 +59,7 @@ func TestDNSUnreachableWhenNoRootServerReplies(t *testing.T) { if !w.DNSUnreachable() { t.Error("DNSUnreachable is false after a round in " + - "which no lookup got a reply from a root server") + "which no root server gave a usable reply") } }) } diff --git a/internal/watcher/watcher.go b/internal/watcher/watcher.go index 8d7c67b..64be30b 100644 --- a/internal/watcher/watcher.go +++ b/internal/watcher/watcher.go @@ -204,13 +204,13 @@ func (w *Watcher) RunOnce(ctx context.Context) { } // DNSUnreachable reports whether the most recent round of DNS checks -// could not reach the DNS at all: no lookup in it got a reply from a -// root server, because none of them answered or because the network -// intercepts DNS queries. A round in which the root servers answered -// reached the DNS, even when a name's own nameservers did not answer or -// a domain does not exist. It is false until the first round finishes, -// and a round that shutdown cut short changes nothing. It may be called -// while a round runs. +// could not reach the DNS at all: every lookup in it failed because no +// root server gave a usable reply, as when none of them answered or the +// network intercepts DNS queries. A round reached the DNS even when a +// name's own nameservers did not answer or a domain does not exist, +// since the root servers gave a usable reply to those lookups. It is +// false until the first round finishes, and a round that shutdown cut +// short changes nothing. It may be called while a round runs. func (w *Watcher) DNSUnreachable() bool { return w.dnsUnreachable.Load() } @@ -304,10 +304,10 @@ func (w *Watcher) runDNSChecks(ctx context.Context) { } // noRootServerReplied reports whether errs, the errors of the lookups in -// a round of DNS checks, nil for a lookup that succeeded, show that no -// lookup got a reply from a root server: each failed because none of -// them answered, or because the network intercepts DNS queries. A round -// with no lookups shows nothing. +// a round of DNS checks, nil for a lookup that succeeded, show that +// every lookup failed because no root server gave a usable reply: each +// is ErrNoRootServerAnswered or ErrIntercepted. A round with no lookups +// shows nothing. func noRootServerReplied(errs []error) bool { for _, err := range errs { if !errors.Is(err, resolver.ErrNoRootServerAnswered) &&