Guide
Monitoring and alerts
Most of what TelltaleDNS can show you works from the first start. This guide covers what to add: Prometheus and Grafana, objectives, alerts that reach you, and exports to the tools you already run. For what each feature does, see Monitoring and alerts.
- What you get without setting anything up
- Scrape metrics and add the Grafana dashboard
- Set service-level objectives
- Get alerts somewhere
- Send events and traces elsewhere
- Keep names and devices out of the log
- Review and acknowledge anomalies
- Check your listeners
- Which tool for which question
Monitoring never slows DNS. A slow disk, a collector that's down, or an unreachable mail server loses detail, never answers.
What you get without setting anything up
Dashboard
Queries, blocks, cache hits, latency, and top lists. History is saved to <data_dir>/rollups.db (per minute for 7 days, per hour for 400 days, per day after that), so it survives restarts and upgrades. The Service level card is in the Advanced view.
Query log
Every query on disk for 30 days (up to 2 GiB). Search it on the Query log page, where Why? explains each decision, or from the shell with telltale qlog search.
Health icon
At the bottom of the menu: a quiet check when healthy, an amber triangle when something needs a look, a red octagon when DNS is failing for some devices. Click it for every reason and a link to the page to look at.
Anomalies
Each device's usual behavior is learned over its first 7 days. The Anomalies page then lists spikes, phone-home patterns, failed-lookup bursts, and generated-looking domains, with the evidence.
The machine
The Cluster page shows a card per node (a standalone node too): memory, CPU, disk, and temperature, with the last hour under each bar and a warning when something looks wrong.
Listener and upstream checks
Each node asks its own listeners a question every 30 seconds. The Upstreams page's Answer quality card shows each upstream's DNSSEC verdicts and the error codes it attaches.
The same health answer is at GET /api/v1/system/health. Details: Health, Query log, Host resources.
Scrape metrics and add the Grafana dashboard
Native, Docker, or a Pi
- Every node serves
/metricson its metrics listener,0.0.0.0:9153by default. Move it with[telemetry.metrics] listen(a[telemetry]change waits for a restart). The same listener serves/livez,/healthz, and/readyz. - It needs no sign-in, and it answers only clients inside
[access] allowed_networks, so make sure your Prometheus server's address is in there. The API port also serves/metricsto signed-in users (a viewer token, or HTTP Basic for scrapers that can't send one). - Add a scrape job for each node. If you'll load the objective rules (below), name the job
telltale-metrics:# prometheus.yml scrape_configs: - job_name: telltale-metrics static_configs: - targets: ["<node>:9153"] - In Grafana, import
deploy/grafana/telltale-dashboard.json, then pick the Prometheus data source, job, and instance at the top.
Kubernetes (Helm)
Three values turn on the monitoring pieces:
serviceMonitor:
enabled: true # Prometheus scrapes the controller and every resolver pod
prometheusRule:
enabled: true # alert rules, plus the SLO recording rules and burn-rate alerts
grafanaDashboard:
enabled: true # the dashboard as a ConfigMap, labelled grafana_dashboard: "1"
- The alert rules cover the server down, all upstreams down, the SERVFAIL rate, stale lists, dropped telemetry, masked client IPs, device anomalies, the host (
TelltaleDNSDataDiskLow,TelltaleDNSMemoryPressure,TelltaleDNSOutOfMemoryKill,TelltaleDNSCpuThrottled,TelltaleDNSRunningHot), and the listeners (TelltaleDNSListenerNotAnswering,TelltaleDNSListenerCertExpiring). - In
mode: scaled, the ServiceMonitor also scrapes the resolver pods (through a headless<release>-resolver-metricsService), so the query, SERVFAIL, and latency metrics cover all the traffic. The alert and SLO rules watch both jobs, so an alert can fire for a resolver pod too. - With kube-prometheus-stack, Prometheus only loads rules that carry its release label. Add it, or the rules are created but never evaluated:
prometheusRule: enabled: true labels: release: prometheus # your kube-prometheus-stack release name
Every key and default: Helm values. The rest of the chart: Run on Kubernetes.
From a latency spike to the query behind it. Each bucket of telltale_query_duration_seconds carries an exemplar. Start Prometheus with --enable-feature=exemplar-storage, and the bundled dashboard shows them as dots. In Grafana's Prometheus data source, under Exemplars, add a link with label name trace_id and URL https://<your TelltaleDNS>/#/queries?trace=${__value.raw}. Exemplars and traces.
The main metric names are in the Monitoring docs.
Set service-level objectives
Two promises, each with an error budget over a window: answers that aren't SERVFAIL, and answers sent within a time limit. They work out of the box with the defaults below. Change them if your network needs different numbers.
Config file
[slo]
enabled = true
availability_target = 99.9 # percent of answers that aren't SERVFAIL
latency_target = 99 # percent of answers sent within latency_ms
latency_ms = 250 # 1, 5, 10, 25, 50, 100, 250, 500, 1000, 2500, or 5000
window_days = 30 # 1 to 90
For Prometheus without Kubernetes, load deploy/prometheus/telltale-slo-rules.yaml with rule_files. It holds the same recording rules and burn-rate alerts, written for the default targets and a scrape job named telltale-metrics.
Helm
With prometheusRule.enabled, the chart adds the recording rules and burn-rate alerts. Keep prometheusRule.slo equal to the [slo] section the nodes use, since the web UI and API read that one:
prometheusRule:
enabled: true
slo:
availabilityTarget: 99.9
latencyTarget: 99
latencyMs: 250
- When it warns: a fast burn (14.4× over 1 hour and 5 minutes) or a slow burn (6× over 6 hours and 30 minutes) turns the health icon amber and fires
slo_burnalert rules. A pair needs at least 50 answers in its short window and 10 bad ones in its long window, so one SERVFAIL on a quiet night stays quiet. - Where to look: the dashboard's Service level card (Advanced view), the Grafana dashboard's Service level row,
GET /api/v1/stats/slo, and the MCP toolslo_status.
What counts and how burn rates work: Service-level objectives.
Get alerts somewhere
Alerts have two parts: destinations (where messages go: email, ntfy, gotify, slack, or webhook) and rules (what to watch, and which destinations to tell).
Web UI
- Open Alerts (under System). It shows what's firing now and how the last delivery to each destination went.
- Add a destination. Passwords and tokens stay in files on the node; you give their paths.
- Press Send test on it. You see the result, including a wrong password or an unreachable server.
- Add rules that send to it. Each change gets a dry run first; on a Git-managed cluster, the page says what to add to Git.
Config file
[alerts]
interval_secs = 30 # how often rules are checked
[[alerts.destination]]
name = "phone"
type = "ntfy" # webhook | ntfy | gotify | slack | email
url = "https://ntfy.sh/my-home-dns"
# token_file = "/run/secrets/ntfy-token" # an access token, kept out of the config
[[alerts.rule]]
name = "Upstream down"
when = "upstream_down" # an upstream's circuit breaker is open
for_secs = 60 # only if it lasts a minute
to = ["phone"]
Check it with telltale config check, then apply it as in Config file only.
API
GET /api/v1/alerts: firing alerts and deliveries.PUT/DELETE /api/v1/alerts/destinations/{name}and/api/v1/alerts/rules/{name}: the same fields as the configuration, with?dryRun=true,If-Match, andIdempotency-Key.POST /api/v1/alerts/destinations/{name}/test: send a test message.
Agent tokens need config:write:alerts. Only file paths for secrets go through the API.
Rules worth starting with: upstream_down, servfail_rate, list_failing, disk_full, slo_burn, probe_failing, and cert_expiring if you serve encrypted DNS. In a cluster add node_down, and sync_lag with for_secs around 300 so a normal sync stays quiet. Every condition, and what each sends one alert per, is in the Alerts docs.
- A condition must hold for
for_secs(60 by default) before it alerts, and a "Resolved" message follows when it clears. Anomalies, updates, new devices, pending plans, and failed rollouts go out once each. - In a cluster the primary checks the rules for the whole cluster, so you get one message, not one per node. Nodes in maintenance are left out of per-node rules.
Any mail account that allows SMTP sign-in works; you don't need your own mail server.
[[alerts.destination]]
name = "inbox"
type = "email"
url = "smtp://smtp.gmail.com:587" # STARTTLS; smtps://host:465 for TLS from the start
from = "you@gmail.com"
to = ["you@gmail.com"] # one or more
username = "you@gmail.com"
password_file = "/run/secrets/smtp-password"
- Gmail: turn on 2-Step Verification, make an app password (Google Account → Security → App passwords), and put it in
password_file. Most other providers work the same way with their SMTP host and an app password. - Encryption is required whenever a password is sent; there's no fallback to plain. For a private mail server, give its CA in
tls_ca. - The subject starts with
[TelltaleDNS], for easy mail filters. In Kubernetes, keep the password in a Secret mounted as a file, never in values or Git.
Send events and traces elsewhere
All of these are config-file settings, read at startup: restart after changing them. Event sinks and traces follow the query log's privacy level (names hashed at 1 and above, no client addresses at 2 and above).
| You want | Use | Goes to |
|---|---|---|
| Metrics pushed instead of (or as well as) scraped | [telemetry.otlp] endpoint | An OTLP/HTTP collector, at <endpoint>/v1/metrics |
| Sampled and slow queries as traces | traces_sample_every, traces_slow_ms under [telemetry.otlp] | Tempo, Jaeger, and the like, at <endpoint>/v1/traces |
| Every query in your log pipeline | [[telemetry.sink]], type = file, syslog, or webhook | Loki, Elastic, Splunk, Graylog, a SIEM |
| Query events as OpenTelemetry logs | A webhook sink with format = "otlp_logs" | <collector>/v1/logs |
| The DNS messages themselves | [telemetry.dnstap] | A dnstap reader on a Unix socket or TCP |
[telemetry.otlp]
endpoint = "http://otel-collector:4318" # OTLP/HTTP
interval_secs = 60
traces_sample_every = 1000 # 1 query in 1,000 (0: none, the default)
traces_slow_ms = 250 # and every query that took at least 250 ms (0: none)
[[telemetry.sink]]
name = "siem"
type = "syslog" # RFC 5424
address = "udp://192.168.1.20:514" # or tcp://, or tls://host:6514
statuses = ["blocked"] # only blocks; empty means every event
A destination that's slow or down drops events and counts them (telltale_sink_dropped_total, telltale_dnstap_dropped_total, telltale_traces_total{result="dropped"}). Webhook sinks can keep refused batches on disk with spill_max_bytes. Every option: OpenTelemetry, Event sinks, dnstap.
Keep names and devices out of the log
Phones checking their connection, time servers, and a monitoring probe asking every few seconds drown out what matters. Excluded queries are still answered and still counted in Prometheus, but left out of the query log, the live view, the dashboard and top lists, device pages, anomalies, event sinks, OTLP traces, and exemplars.
Web UI
Open Settings → Query handling → Kept out of the log. Add names and addresses, use the toggle to turn the lists on or off, and the change is checked before it's applied. Revert to the file brings back the file's version. Under a Git-managed configuration, the UI shows the TOML to commit.
Config file
[exclusions]
enabled = true # off keeps the lists, logs everything again
names = ["connectivitycheck.gstatic.com", "ntp.org"] # each with its subdomains
clients = ["192.168.1.10", "10.20.0.0/24"] # addresses or networks
API
PUT /api/v1/exclusions/default. AI agents use the MCP tool plan_set_exclusions.
- It applies from the next query on, on every node of a cluster (
[exclusions]is shared). Queries logged before stay. - The dashboard's totals leave excluded queries out, so they'll be lower than Prometheus's.
telltale_queries_excluded_totalcounts them.
Hiding names or devices for privacy instead? Use [telemetry.qlog] privacy_level: 1 hashes names, 2 also drops client addresses, 3 keeps no per-query log. Counts, graphs, and latency keep working, but anomaly detection and new-device alerts are off at 1 and above. Privacy levels.
Review and acknowledge anomalies
- Open the Anomalies page. Each finding shows what was seen, the usual value, the threshold, and the window. In a cluster, Found by names the nodes.
- Look at the device's queries, and move it to a stricter group if needed. Findings never block anything themselves.
- Press Acknowledge on the card (or Acknowledge all). It stops counting as new: the sidebar badge and anomaly alerts skip it. Show acknowledged shows it again with who acknowledged it, when, and the note; Undo acknowledge takes it back.
It needs the operator role and is recorded in the audit log. API: POST /api/v1/analytics/anomalies/acknowledge with the finding's id; MCP: acknowledge_anomalies. An acknowledgement on any node reaches every node within seconds.
Too noisy, or too quiet? Tune detection, or ignore domains you expect:
[telemetry.anomaly]
enabled = true
learning_days = 7
sensitivity = "normal" # low | normal | high
ignore_domains = ["apple.com"] # never reported (e.g. expected connectivity checks)
To hear about findings, add an alert rule with when = "anomaly". Details: Device anomalies.
Check your listeners
A listener can be bound and still not answer: a stuck TLS stack, a firewall rule, a load balancer in front. So every node asks each of its own listeners a question over that listener's protocol, every 30 seconds, and reads each certificate's expiry.
- See the results in Settings → System → Listener checks, at
GET /api/v1/system/probes(every node), or with the MCP toolprobe_status. - Add your load balancer's address (or another IP) as an extra target:
[probe]
cert_warn_days = 14 # a certificate expiring sooner degrades the health level
targets = ["udp://192.168.5.112:53"] # extra: by IP, e.g. a load balancer's address
- Targets take
udp://,tcp://,tls://,https://…/dns-query,h3://, andquic://with an IP address. A listener withproxy_protocol = trueis skipped. - Two failures in a row turn the health icon amber; an expired certificate turns it red. Alert on it with
probe_failingandcert_expiringrules. - Probe queries stay out of the query log, top lists, device analytics, and event sinks.
Details: Listener checks.
Which tool for which question
| Question | Look here |
|---|---|
| Is anything wrong right now? | The health icon; click it for reasons |
| Why was this name blocked (or not)? | Query log → Why?, or the Explain page |
| What did one device ask, and when? | Query log filtered by client, or telltale qlog search --client |
| Are we meeting our promises this month? | The dashboard's Service level card, or the Grafana Service level row |
| Which upstream is slow or failing? | The Upstreams page, or the Grafana upstream panels |
| Is an upstream filtering or tampering? | Upstreams → Answer quality |
| Is a device behaving oddly? | The Anomalies page |
| Is the disk, memory, or CPU running out? | The Cluster page's node cards, or telltale_host_* metrics |
| Do encrypted clients still get through? | Settings → System → Listener checks |
| What happened over the last months? | The dashboard's 7- and 30-day views, or Grafana |
| How do I tell someone at 3 a.m.? | Alerts, or Prometheus alert rules |