Files
jarl/docs/metrics.md
T
adamandClaude Opus 5.5 88f63ffb0a TLS certificates: Jarl obtains and renews them over ACME
Jarl is now an ACME client (lego's core, no DNS providers). It answers
DNS-01 challenges in a zone it serves, or through Porkbun's DNS API while
Porkbun serves the domain; "auto" decides from the domain's public name
servers at every renewal, and a CNAME on _acme-challenge is followed.
Certificates are used for jarld's HTTPS (served without a restart; each
port speaks HTTPS and plain HTTP, plain HTTP from loopback and trusted
proxies still served) and/or written for named's DNS over TLS/HTTPS, with
rndc reconfig on renewal. Renewal follows the CA's ARI window or two
thirds of the lifetime, with back-off. Certificates you renew yourself
can be used too. Certificates page and dashboard card, jarl cert,
/api/v1/certificates, Prometheus gauges, docs/certificates.md, and an
e2e section against Pebble.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 14:35:53 +01:00

11 KiB

Prometheus metrics

jarld can serve metrics for Prometheus at /metrics:

  • health you can alert on: named up, validation problems, files changed outside Jarl, blocklists that fail to download, backup age, DNSSEC zones waiting for you at the parent, and API tokens about to expire,
  • queries, once query statistics are on: named's counters (result codes, record types, cache, upstream times, DNSSEC validation, rate limiting) and Jarl's own (responses per view and zone, blocked queries per blocklist), plus named's memory, CPU, cache and sockets.

Metrics are off by default. Everything is set in /etc/jarl/jarl.yaml; nothing is stored in the database.

Turning it on

Prometheus on the same server needs only:

metrics:
  enabled: true
sudo systemctl restart jarl
curl -s http://127.0.0.1:8053/metrics | head

/metrics is served on the addresses in listen (and their TLS, if set) and on the local socket. Query counters also need stats.enabled: true (stats.md); without it you get only the health metrics.

Prometheus on another machine

Give /metrics an address of its own and say who may scrape it:

metrics:
  enabled: true
  listen: ["0.0.0.0:9450"]       # serves only /metrics, over plain HTTP
  allow: ["192.168.1.20"]        # the Prometheus server
# prometheus.yml
scrape_configs:
  - job_name: jarl
    static_configs:
      - targets: ["dns.lan:9450"]

Or use a token instead of (or as well as) the address list. Any address that sends it may scrape:

metrics:
  enabled: true
  listen: ["0.0.0.0:9450"]
  token: "…"                     # e.g. from: openssl rand -hex 24
# prometheus.yml
scrape_configs:
  - job_name: jarl
    authorization:
      credentials: "…"           # or credentials_file: /etc/prometheus/jarl.token
    static_configs:
      - targets: ["dns.lan:9450"]

With a token in jarl.yaml, keep the file readable by root and jarld only (chown root:jarl /etc/jarl/jarl.yaml; chmod 640 /etc/jarl/jarl.yaml); jarld warns at start when everyone can read it. On metrics.listen the token travels unencrypted, so use it on a network you trust, or scrape the main listener over HTTPS instead (scheme: https and its port in Prometheus).

Settings

Key Default Meaning
metrics.enabled false Serve /metrics.
metrics.listen [] Extra ADDRESS:PORTs that serve only /metrics, over plain HTTP.
metrics.allow ["127.0.0.1", "::1"] Addresses or prefixes that may scrape without a token. Behind a reverse proxy in trusted_proxies, this is the client's address from X-Forwarded-For. [] with a token: the token is required everywhere.
metrics.token "" A bearer token (at least 16 characters) that lets any address scrape.

A scrape is allowed when it comes from an address in allow, or carries the token, or comes over the local socket from root or jarld's own user. A wrong token is refused (and counts towards the per-address limit on failed logins). Jarl's API tokens don't work for /metrics.

The firewall must let Prometheus reach the port. allow is checked by jarld, not the kernel.

What is exported

Everything is prefixed jarl_. The text format is checked with promtool check metrics.

Health (always)

These are read at most every 15 seconds; a scrape in between gets the same values.

Metric Meaning
jarl_build_info{version} Always 1.
jarl_start_time_seconds When jarld started.
jarl_metrics_state_ok 0 when Jarl's state couldn't be read (see jarld's log).
jarl_adopted 1 when named loads Jarl's configuration.
jarl_named_up 1 when named answers rndc status (missing with bind.no_reload).
jarl_named_recursive_clients, …_soft_limit, …_limit Recursive clients now, and named's limits (rndc status).
jarl_named_tcp_clients, …_limit TCP clients now, and the limit.
jarl_open_changesets Changesets with staged changes not applied yet.
jarl_last_apply_timestamp_seconds When the last changeset was applied.
jarl_problems{severity} Validation problems (error, warning, info) as on the dashboard, including named's own warnings.
jarl_drifted_files Managed files changed or removed outside Jarl.
jarl_objects{kind} Views, zones, records, ACLs, TSIG keys and options.
jarl_backup_last_success_timestamp_seconds, jarl_backup_last_size_bytes The newest backup by jarld backup.
jarl_backup_last_failure_timestamp_seconds A failed backup newer than the last good one.
jarl_backup_stale 1 when the newest backup is older than backup.warn_after.
jarl_blocking_paused 1 while blocking is paused (only with blocklists or rules).
jarl_blocking_paused_until_timestamp_seconds When a timed pause ends.
jarl_blocklist_enabled{list} 1 for lists that are switched on.
jarl_blocklist_entries{list} Domains in the last good download.
jarl_blocklist_failing{list} 1 when the last download or check of an enabled list failed.
jarl_blocklist_last_check_timestamp_seconds{list}, jarl_blocklist_last_change_timestamp_seconds{list} When the list was last checked, and last changed.
jarl_dnssec_phase{zone,view,phase} Always 1, one per signed zone; phase is one of the DNSSEC phases. ready means: add the DS at the parent. removing_ds: remove it.
jarl_token_expiry_timestamp_seconds{id,user,token} When each API token with an expiry date expires.
jarl_certificate_expiry_timestamp_seconds{certificate,source} When each certificate Jarl manages expires (certificates.md).
jarl_certificate_failing{certificate,source} 1 when obtaining (or reading) the certificate failed the last time, or it has expired.
jarl_certificate_renewal_failures{certificate} Failed attempts to obtain a certificate since its last success.

Queries (with stats.enabled)

The counters start at zero when jarld starts (Prometheus handles that). named's counters move once a minute, when Jarl polls named's statistics channel. Counters from the response log (responses, views, zones, blocking) move as lines are read and need stats.query_log. Clients in stats.exclude_clients are left out of those. Names and clients are not exported, because there would be too many of them. Use the Statistics page for them.

Metric Meaning
jarl_queries_total Queries named received.
jarl_responses_by_rcode_total{rcode} Responses by result code (NOERROR, NXDOMAIN, SERVFAIL, REFUSED, …).
jarl_queries_by_type_total{type} Queries by record type.
jarl_queries_by_transport_total{transport} udp and tcp.
jarl_answers_total{source} authoritative or recursive.
jarl_recursion_queries_total, jarl_dropped_queries_total, jarl_duplicate_queries_total Queries that caused recursion, that named dropped, and duplicates.
jarl_ratelimit_responses_total{action} Responses rate limiting dropped or slipped (named's counters; they don't move in log-only mode).
jarl_ratelimit_logged_total{action} Rate limited responses from named's log: drop, slip, and in log-only mode would-drop, would-slip (exposure.md).
jarl_upstream_responses_by_rtt_total{rtt} Upstream answers during recursion by round-trip time (<10ms … >1600ms).
jarl_dnssec_validations_total{result} valid, valid-negative and failed.
jarl_cache_lookups_total{result} hits and misses.
jarl_logged_responses_total Responses read from the response log.
jarl_responses_by_view_total{view} Logged responses per view (_default without views).
jarl_responses_by_zone_total{zone} Logged responses per local zone; - for names in no local zone.
jarl_blocked_queries_total Queries blocking answered.
jarl_blocked_by_list_total{list} Blocked queries per blocklist; rules for your own rules.
jarl_stats_poll_ok, jarl_stats_last_poll_timestamp_seconds Whether the last poll of named's statistics channel worked, and when it was.
jarl_stats_log_lines_total, jarl_stats_last_log_line_timestamp_seconds Response log lines read, and the time of the last one.
jarl_stats_warnings Warnings on the Statistics page (e.g. no log lines while named answers queries).
jarl_named_start_time_seconds, jarl_named_config_time_seconds When named started, and last loaded its configuration.
jarl_named_resident_memory_bytes, jarl_named_threads, jarl_named_cpu_seconds_total named's process (/proc).
jarl_named_memory_in_use_bytes Memory named counts as in use.
jarl_named_cache_rrsets, jarl_named_cache_memory_bytes named's caches, all views together.
jarl_named_sockets{proto} Open UDP and TCP sockets.
jarl_named_recursive_clients_highwater, jarl_named_tcp_clients_highwater The most clients at once since named started.

Host metrics (load, memory, disks) are left to node_exporter.

Example queries

# Queries per second, and the share that failed
rate(jarl_queries_total[5m])
rate(jarl_responses_by_rcode_total{rcode="SERVFAIL"}[5m]) / ignoring(rcode) rate(jarl_queries_total[5m])

# Blocked share of logged responses, per list
rate(jarl_blocked_by_list_total[1h]) / ignoring(list) group_left rate(jarl_logged_responses_total[1h])

# Busiest zones
topk(5, rate(jarl_responses_by_zone_total{zone!="-"}[1h]))

# Cache hit rate
rate(jarl_cache_lookups_total{result="hits"}[5m]) / ignoring(result) sum without(result) (rate(jarl_cache_lookups_total[5m]))

Example alerts

groups:
  - name: jarl
    rules:
      - alert: JarlDown
        expr: up{job="jarl"} == 0
        for: 5m
      - alert: NamedDown
        expr: jarl_named_up == 0
        for: 2m
      - alert: JarlConfigErrors
        expr: jarl_problems{severity="error"} > 0
        for: 15m
      - alert: JarlDrift
        expr: jarl_drifted_files > 0
        for: 15m
      - alert: BlocklistFailing
        expr: jarl_blocklist_failing == 1
        for: 6h
      - alert: BackupStale
        expr: jarl_backup_stale == 1
      - alert: DNSSECWaitingForYou
        expr: jarl_dnssec_phase{phase=~"ready|removing_ds|unsigned"} == 1
        for: 1h
        annotations:
          summary: "{{ $labels.zone }}: DNSSEC is waiting for a step at the parent ({{ $labels.phase }})"
      - alert: TokenExpiresSoon
        expr: jarl_token_expiry_timestamp_seconds - time() < 7 * 86400
        annotations:
          summary: "API token {{ $labels.token }} of {{ $labels.user }} expires within a week"
      - alert: CertificateFailing
        expr: jarl_certificate_failing == 1
        for: 12h
        annotations:
          summary: "Certificate {{ $labels.certificate }} can't be renewed (see the Certificates page)"
      - alert: CertificateExpiresSoon
        expr: jarl_certificate_expiry_timestamp_seconds - time() < 10 * 86400
        annotations:
          summary: "Certificate {{ $labels.certificate }} expires within 10 days"
      - alert: BlockingPausedForLong
        expr: jarl_blocking_paused == 1
        for: 24h
      - alert: NamedRecursiveClientsHigh
        expr: jarl_named_recursive_clients / jarl_named_recursive_clients_soft_limit > 0.8
        for: 5m
      - alert: StatsNotCollecting
        expr: jarl_stats_poll_ok == 0 or jarl_stats_warnings > 0
        for: 15m