Ctrl K
ComunidadGuías…

docs/adr/0064-dns-slo-observability.md

Rama master · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
# ADR-0064: DNS SLO Observability — per-leg latency histograms and tertiary rescue in the production topology

## Metadata

| Field | Value |
|-------|-------|
| **Status** | Accepted |
| **Date** | 2026-08-20 |
| **Authors** | Alessio Brillo |
| **Deciders** | Alessio Brillo |
| **Supersedes** | N/A |
| **Relates to** | ADR-0002, ADR-0039, ADR-0042, ADR-0044, ADR-0045, ADR-0046, ADR-0059, ADR-0063 |
| **Project** | DOMINUS |

## Context

The DNS consensus gate (ADR-0039, ADR-0045) makes every Available verdict
rest on up to three resolver opinions: a primary multi-resolver strategy, a
consensus secondary, and — since ADR-0045 — an optional tertiary. The gate
is conservative by construction (ADR-0002): a slow or dead leg downgrades
verdicts to Unknown, never to fabricated Availability. What the gate could
not do was tell the operator *why* a run degraded: which leg stalled, which
endpoint, and for how long. Provider call counters and breaker state
(ADR-0059) only surfaced binary health; latency shape — the p99 of a leg
creeping toward the 1.5s lookup timeout — was invisible until verdicts
turned Unknown.

Separately, the documented production override
(`docker-compose.dns-consensus.yml`, ADR-0042) pins the consensus secondary
to a co-hosted Unbound recursor on a dedicated subnet. That makes the
secondary operator-independent of the primary's public resolvers, but it
also makes Unbound a single point of failure for the whole gate: if the
container dies, the secondary cannot answer, and every Available verdict is
downgraded to Unknown for the life of the outage. The tertiary leg existed
(ADR-0045) but was default-off and `native`-by-default — a configuration
that shares operators with the default `doh-primary` primary and is
therefore vetoed by the disjointness check. The turnkey topology had no
viable third opinion wired in.

This ADR records two related decisions: (1) feed per-leg DNS and per-request
RDAP latency into Prometheus histograms so SLO alerting can quantify
degradation, and (2) wire the tertiary into the production override using a
new `doh-alternate` strategy (Cisco OpenDNS) so the gate survives a dead
recursor.

## Decision Drivers

1. **SLO alerting needs latency, not just verdicts** — a resolver
   approaching its timeout is a leading indicator; Unknown verdicts are the
   lagging outcome. Without per-leg duration data no p99 alert can exist.
2. **The recursor is a consensus SPOF in the turnkey topology** — a dead
   Unbound container downgrades every Available verdict; the documented
   override must not ship a single point of failure for the gate.
3. **Zero-cost discipline (ADR-0001)** — OpenDNS DoH is free and keyless;
   the fix must add no paid API and no new infrastructure.
4. **Operator disjointness is non-negotiable (ADR-0063)** — the tertiary
   must be independent of the primary and the secondary at operator level,
   or the disjointness check vetoes it at boot.
5. **Conservatism (ADR-0002)** — telemetry must never change verdicts;
   hooks must be best-effort and abort-immune.

## Considered Options

### Option A: Histograms + `doh-alternate` tertiary in the override

Add a Prometheus histogram family (`dominus_dns_leg_duration_ms`) fed by a
per-leg telemetry hook on the DNS provider: every non-aborted resolver leg
emits one sample labelled by transport, endpoint (the breaker key), verdict
and role (primary/consensus/tertiary). The RDAP failover provider emits a
second family (`dominus_rdap_request_duration_ms`) labelled by server and
outcome. Both render through the existing `/metrics` endpoint in the text
exposition format. The bucket set spans 5ms to 30s so p50/p99 of both
transports and the RDAP timeouts are quantifiable. In parallel, a new
`doh-alternate` lookup strategy — a single DoH group against
`https://dns.opendns.com/dns-query` (RFC 8484 wire format, live-verified) —
is enabled as the tertiary in `docker-compose.dns-consensus.yml`, giving
the gate a fourth operator (Cisco) disjoint from the primary
(Cloudflare/Google/Quad9) and the pinned recursor.

**Advantages:**
- Directly serves both drivers: p99 alert rules can be written against the
  histograms, and the SPOF disappears with a two-line env change.
- No new dependencies, no paid APIs, no schema change; reuses the existing
  metrics collector, `/metrics` endpoint and Prometheus rules file.
- The telemetry hook is additive and best-effort: it cannot alter verdicts
  or take DNS down (ADR-0002 intact).
- OpenDNS is a genuinely fourth operator over a transport (DoH) the pinned
  recursor and native legs do not use, so the disjointness check passes in
  the override.

**Disadvantages:**
- Another operator dependency in the DNS path (a third-party resolver whose
  DoH endpoint could change or be geo-blocked).
- Label cardinality: endpoint labels multiply series; bounded because
  endpoints are a small fixed set and legs are low-volume.
- Histogram memory grows per unique label set for the process lifetime.

**Cost Implications:** ~1 day of implementation across provider, collector,
renderer, rules and tests. Zero operational cost (OpenDNS free tier,
keyless).

**Risk Assessment:** Low technical risk; the DoH endpoint was live-verified
at decision time. Vendor risk mitigated by the breaker (ADR-0059): if
OpenDNS degrades, the tertiary leg is skipped, never used to fabricate
verdicts. No migration risk — the tertiary is opt-in via the override.

---

### Option B: Histograms only, keep the recursor as sole secondary

Implement the latency observability but leave the override unchanged: no
tertiary, no `doh-alternate` strategy.

**Advantages:**
- Smallest change; no new DNS endpoint or operator to maintain.
- Alerts will at least *detect* a dying recursor (secondary leg p99 grows,
  Unavailable share rises).

**Disadvantages:**
- The recursor SPOF remains: detection without mitigation leaves every
  Available verdict downgraded during any recursor outage.
- No way to rescue verdicts; the gate degrades by design in the exact
  topology the override documents.
- Detecting a problem the product cannot route around is half a fix.

**Cost Implications:** ~0.5 day; the alerting alone.

**Risk Assessment:** Low technical risk, but the central production risk
(the SPOF) stays open.

---

### Option C: Replace the consensus secondary with a second public DoH strategy

Drop the pinned recursor from the override and let the secondary use a
public DoH/DoT strategy instead, removing the SPOF by removing the recursor.

**Advantages:**
- No private recursor to run or monitor; no dedicated container.

**Disadvantages:**
- Loses the operator-independence rationale of ADR-0042 (a co-hosted
  DNSSEC-validating recursor is the most independent opinion available).
- Any public strategy choice shares an operator with the primary
  (Cloudflare/Google/Quad9 are behind every mainstream resolver), which the
  disjointness check would veto — the gate would silently disable in the
  override, the exact failure ADR-0063 eliminated.
- Loses DNSSEC-validating local recursion for the primary's native leg.

**Cost Implications:** ~0.5 day; cheaper in code, more expensive in
architecture fidelity.

**Risk Assessment:** High architectural risk: would re-introduce the
ADR-0063 class of silent gate vetoes.

---

## Decision

**Chosen option: Option A — histograms + `doh-alternate` tertiary in the override.**

The histogram decision is unambiguous: latency data is the only way to turn
"verdicts turned Unknown" into an actionable SLO alert with a margin before
the failure. The bucket set (5ms–30s) is chosen so the same family covers
native/DoH/DoT legs (typical p50 5–50ms, timeout 1.5s) and RDAP requests
(timeout 10s), keeping two families instead of bespoke per-transport
buckets. The endpoint label uses the existing breaker key (ADR-0059) so
alert triage maps a slow leg straight to the circuit state that guards it.
Aborted legs are deliberately excluded from the samples: winner-child
aborts are scheduling artifacts, not resolver health signals, and would
skew percentiles downward with meaningless sub-millisecond points.

The `doh-alternate` strategy wins over Option C because it removes the SPOF
*without* removing the recursor: the recursor stays the secondary (its
ADR-0042 independence rationale is untouched), and the tertiary becomes an
independent public opinion consulted only when the secondary cannot answer
(ADR-0045 semantics). OpenDNS was chosen over other keyless DoH providers
after live verification (HTTP 200 on RFC 8484 `application/dns-message`,
2026-08-20); alternatives like `unicast.censurfridns.dk` returned 400 and
were rejected. It is the fourth operator in the default stack and the only
one whose DoH endpoint is not one of the three primary operators, so the
triple disjointness check (tertiary vs primary, tertiary vs secondary,
secondary vs primary) passes in the override — verified by a boot-equivalent
test that constructs the gate from the override env. Steady-state cost is
unchanged: the tertiary is consulted only when the secondary fails.

## Consequences

### Positive
- p99 latency alerts for DNS legs and RDAP requests become writable and
  quantifiable (rules added to `deploy/prometheus/rules.yml`: >2s DNS leg,
  >8s RDAP, breaker-open).
- The production override's gate survives a dead recursor: the tertiary
  rescues Available verdicts instead of downgrading them.
- Telemetry is additive and cannot alter verdicts (ADR-0002) — hooks are
  best-effort, abort-immune, and rate-limited to leg/request granularity.
- The boot-equivalent wiring test now covers the tertiary env, so a future
  overlap regression cannot silently disable the gate again (ADR-0063
  lesson).

### Negative
- One more external DoH dependency in the consensus path (OpenDNS); its
  failure is mitigated by the circuit breaker but adds a third-party
  availability dependency to the tertiary opinion.
- Histogram state grows with distinct label sets for the process lifetime;
  bounded by the fixed endpoint/verdict/role label domains, but the
  collector must be reset on restart (already the case).
- The `doh-alternate` strategy is a single-endpoint group by design — no
  fallback inside the group; a dead OpenDNS endpoint yields no tertiary
  opinion (the breaker skips it), not a fabricated one.

### Compliance and Security Implications
- All queries to OpenDNS are DNS lookups of candidate domains — the same
  data already sent to Cloudflare/Google/Quad9/Unbound in the default
  topology; no new data class.
- DNSSEC validation is unaffected: the tertiary is a plain DoH resolver
  opinion like the primary's legs; the recursor keeps its rigorous
  validation role for the native legs.
- No secrets, keys, or paid APIs involved.

### Migration and Monitoring Plan
- Rollout: code lands with the tertiary default-off (`DNS_TERTIARY_ENABLED=false`);
  only the production override turns it on. Existing deployments are
  unaffected until they adopt the override.
- Monitoring: `dominus_dns_leg_duration_ms` and
  `dominus_rdap_request_duration_ms` appear on `/metrics` immediately;
  alert rules fire at p99 > 2s (DNS) and > 8s (RDAP) sustained for 10m.
- Success criteria: alert rules fire before verdict degradation in a
  deliberate endpoint slow-down drill; the override survives a simulated
  recursor kill (secondary Unknown, tertiary rescues Available).
- Rollback: removing the two `DNS_TERTIARY_*` env lines from the override
  reverts to the pre-ADR behavior; the code path is env-gated and additive.

### Validation
- Unit tests: histogram bucketing/aggregation/reset in the collector,
  histogram rendering in the Prometheus endpoint, per-leg sample emission
  (verdict/endpoint/role labels, abort exclusion) in the DNS provider, and
  the boot-equivalent gate construction from the override env with the
  tertiary attached.
- CI: full quality gate (`npm run ci:backend`) plus the existing compose
  assertions extended to the tertiary env keys.
- Production: alert rule dry-run against the metrics endpoint after the
  override ships.

---

*This ADR was created following the MADR 4.0.0 standard. All DOMINUS ADRs should be consistent with the ADR series starting at `docs/adr/0001-project-architecture.md`. Template: `.claude/skills/adr/template.md`.*