Metrics endpoint
dnswiz exposes your account’s DNS metrics as a Prometheus scrape target. Point your own Prometheus, Grafana Agent, or OpenTelemetry Collector at it and DNS lands on the same dashboard as everything else you run, with your alerting rules, your retention and your on-call routing.
GET https://console.dnswiz.app/api/v1/metrics
Authorization: Bearer <api key>
Set it up
Create an API key with the Metrics endpoint scope and nothing else.
Custom access, metrics: read. A key like that can read these numbers
and cannot touch a zone, a record or a certificate, which is what you
want sitting in a monitoring config.
scrape_configs:
- job_name: dnswiz
scheme: https
metrics_path: /api/v1/metrics
scrape_interval: 60s
static_configs:
- targets: ['console.dnswiz.app']
authorization:
credentials: dnswiz_your_key_here
A minute is a sensible interval. The underlying counters roll up hourly, so scraping faster costs you requests without telling you anything new.
Using it from OpenTelemetry
There is no OTLP endpoint and you do not need one. The OpenTelemetry
Collector’s prometheus receiver scrapes this directly, and everything
downstream of it is ordinary OTLP.
receivers:
prometheus:
config:
scrape_configs:
- job_name: dnswiz
scheme: https
metrics_path: /api/v1/metrics
static_configs:
- targets: ['console.dnswiz.app']
authorization:
credentials: dnswiz_your_key_here
What you get
| Metric | Type | Labels |
|---|---|---|
dnswiz_queries_total |
counter | zone, qtype, rcode |
dnswiz_query_duration_seconds |
histogram | zone |
dnswiz_record_answers_total |
counter | zone, record, endpoint |
dnswiz_endpoint_up |
gauge | endpoint, pool |
dnswiz_zone_records |
gauge | zone |
dnswiz_certificate_expiry_seconds |
gauge | name, issuer |
dnswiz_series_dropped |
gauge | none |
dnswiz_record_answers_total is the one worth knowing about if you use
load balancing. Health tells you a member could be answered with;
this tells you whether it was. A weight that is not doing what you
meant, or a member that is healthy and receiving nothing, both look
correct on a config screen and are obvious here.
Things worth knowing
Labels come from your configuration, never from what was asked for.
There is no label carrying the queried name. That is deliberate: a
typical account has a handful of zones and a few dozen records, but
tens or hundreds of thousands of distinct queried names, almost all
of it subdomain scanning by strangers. A label on the query name would
mean anyone with dig could inflate your metrics bill on demand.
dnswiz_endpoint_up omits endpoints that have never been probed
rather than reporting them as 0. Not measured and down are different
statements, and alerting on the second when you mean the first pages
somebody for nothing.
Counters reset when old data ages out of retention. Prometheus
understands counter resets and rate() handles them correctly. A raw
dnswiz_queries_total graph will show a step down; a rate() graph
will not.
The latency _sum is approximate. The edges keep bucketed counts
rather than a running total, so the sum is estimated from bucket
midpoints and any average derived from it is accurate to within a
bucket width. Quantiles from histogram_quantile() are unaffected,
and are what you want to graph anyway.
dnswiz_series_dropped should be zero. Non-zero means your account
produced more series than one response carries and the scrape you are
looking at is incomplete. Tell us if you see it.
Alerts worth having
groups:
- name: dnswiz
rules:
- alert: DNSZoneWentQuiet
expr: rate(dnswiz_queries_total[15m]) == 0
for: 30m
annotations:
summary: "{{ $labels.zone }} is answering no queries"
description: >
A zone going quiet is how a broken delegation announces
itself. It keeps answering for anyone who reaches it, and
fewer and fewer resolvers do.
- alert: DNSNoDataRate
expr: >
sum by (zone) (rate(dnswiz_queries_total{rcode="NXDOMAIN"}[15m]))
/ sum by (zone) (rate(dnswiz_queries_total[15m])) > 0.8
for: 15m
annotations:
summary: "{{ $labels.zone }} is mostly answering NXDOMAIN"
description: >
Usually somebody walking your zone with a wordlist. Worth
looking at what they found rather than what they tried.
- alert: GSLBMemberStarved
expr: rate(dnswiz_record_answers_total[1h]) == 0
for: 2h
annotations:
summary: "{{ $labels.endpoint }} is getting no traffic for {{ $labels.record }}"
- alert: CertificateExpiringSoon
expr: dnswiz_certificate_expiry_seconds < 14 * 24 * 3600
annotations:
summary: "{{ $labels.name }} expires in under two weeks"