Observability

Three signals, three homes, and none of them goes through the store:

SignalAnswersWhere
Query logwhich client asked what, and why it got that answerquerylog.db on each node, plus an in-memory tail
Metricshow much, how fast, how healthyPrometheus, from :9153/metrics
Healthcan this node take traffic now?:8081/healthz/*

Plus the audit trail of who changed what. Code: internal/stages/log, internal/querylog (collector, privacy, tail) and internal/querylog/sqlitelog (writer, rollups, retention, reader), internal/metrics, internal/health, internal/audit.

The query log pipeline

flowchart LR
    OBS["log observer<br/>(after every query)"] -->|"non-blocking send"| CH[["bounded channel<br/>10 000 events"]]
    CH -->|"full: drop and count"| DROP["kudurru_querylog_dropped_total<br/>{stage=queue}"]
    CH --> COL["collector (one goroutine)<br/>exclusions, privacy mode,<br/>stamp the ID"]
    COL --> TAIL["live tail<br/>ring of 65 536 + SSE subscribers"]
    COL --> BATCH["batch: 1000 events or 1 s"]
    BATCH --> DB[("querylog.db<br/>dictionaries + query table<br/>+ rollups, one transaction")]
    TAIL --> API["dashboard: live tail,<br/>block page reasons"]
    DB --> API2["dashboard: query log,<br/>stats, top lists"]

The answer never waits for the log: when the channel is full, the event is dropped and counted. The health name and the dashboard's built-in dig are never logged.

Privacy is applied once, at the collector, before anything is stored or shown: full, anonymize_client (a /24 or /48 prefix, or a daily pseudonym), hide_domains, hide_domains_clients, or nothing (only the per-minute totals). Changing the mode does not rewrite queries already logged.

What one event holds

Time, latency, node, transport, client address, client group, policy, name, type, class, rcode, status, upstream (<group>/<server>), zone, matched list and rule, answer count and a short answer summary, the first EDE code, flags (RD, CD, DO, ECS, TC, AD, cached, stale, prefetch) and the ECS prefix. The status, list and rule are what let the dashboard explain an answer in one line.

Storage and retention

TableGranularityKept
query (facts)one row per queryquerylog.retention.raw (720 h) and maxDBBytes (10 GiB)
rollup_1m, _client_1m, _qtype_1m, _upstream_1mminute35 days
rollup_1h, _client_1h, _qtype_1h, _upstream_1h, _domain_1h, _list_1h, _zone_1hhour400 days
rollup_1d, _client_1d, _domain_1ddayforever

Names, clients, upstreams, zones, lists and policies are stored once in dict_* tables and referenced by ID. Charts read the rollups, so a 30-day chart does not scan 30 days of rows. The database is per node: in a multi-node install each node logs what it answered (scatter-gather across nodes is planned).

Metrics

All metric labels are bounded: list names, upstream names and status values, never client addresses or query names.

FamilyExamples
DNSkudurru_dns_requests_total{proto,qtype}, kudurru_dns_responses_total{proto,rcode,status}, kudurru_dns_request_duration_seconds{proto,status_class}, kudurru_dns_dropped_total{reason}
Upstreams and cachesee upstreams and the cache
Store and zonecachekudurru_store_operations_total, kudurru_store_snapshot_age_seconds, kudurru_zonecache_rebuild_duration_seconds, kudurru_zone_serial, kudurru_zone_records
Control planekudurru_leader, kudurru_controller_reconcile_duration_seconds, kudurru_objects_diagnoses
Blockingkudurru_blocking_enabled, kudurru_blockpage_requests_total
Query logkudurru_querylog_events_total, kudurru_querylog_dropped_total{stage,sink}, kudurru_querylog_db_bytes, kudurru_querylog_write_duration_seconds
APIkudurru_api_requests_total, kudurru_api_request_duration_seconds
Edge and VIPkudurru_edge_renders_total, kudurru_edge_dnsdist_restarts_total, kudurru_edge_backend_up, kudurru_vip_master (on the supervisors' own ports)

deploy/prometheus/rules.yaml and deploy/grafana/kudurru-overview.json (also in the chart) are generated and tested against these names.

Health

stateDiagram-v2
    direction LR
    [*] --> Starting
    Starting --> Ready: zonecache initialised,<br/>every readiness check passes
    Ready --> NotReady: a check fails<br/>(objects, zonecache, blocklists,<br/>blocking, dns listeners)
    NotReady --> Ready: it recovers
    Ready --> Lameduck: kudurru lameduck / SIGTERM
    Lameduck --> [*]: drain time passes
    note right of Lameduck
        /healthz/ready 503 and the
        health name answers SERVFAIL,
        so dnsdist and Kubernetes
        both stop sending traffic
    end note
Endpoint (:8081, no auth)Meaning
GET /healthz/startupthe zonecache has initialised (from the store or from local.db)
GET /healthz/readyevery component is ready and the node is not in lameduck
GET /healthz/livethe self-check loop ran in the last 30 s; never load-based
GET /healthzJSON roll-up with every component, for kudurru health and Docker's HEALTHCHECK
GET/POST/DELETE /lameduckread or set lameduck (writes from loopback only)

Upstream reachability is never a readiness condition: a node whose upstreams are down still answers its zones and its cache, and taking it out of rotation would make things worse.

Audit

DriverWhere audit records go
sqlitethe audit table in state.db, hash-chained, written in the same transaction as the change; GET /api/v1/audit reads it; pruned after 400 days by the leader
kubea JSON line on stdout ("log":"audit") for the cluster's log pipeline; the apiserver's own audit log covers CRD writes

Store writes are audited by the driver. The API adds what no store write covers: denied requests, the blocking toggle, cache flushes, query-log purges, NOTIFY and list refreshes.

Source: docs/architecture/observability.md