DNS monitors
Before you start...
Make sure that you carefully read the common documentation about managing the monitors!
Management methods
If you navigate to the Web UI of Kuvasz, you can create a new monitor on the Dashboard, or on the DNS monitors page, by clicking the "+ New Monitor" button in the page header.
dns-monitors:
- name: "My DNS Monitor" # (1)!
host: "example.com" # (2)!
uptime-check-interval: 60 # (3)!
resolver-host: "1.1.1.1" # (4)!
resolver-port: 53 # (5)!
transport: "UDP" # (6)!
record-matchers: # (7)!
- record-type: "A"
match-type: "EXACT"
value: "93.184.216.34"
- record-type: "TXT"
match-type: "CONTAINS"
value: "v=spf1"
expected-response-code: "NOERROR" # (8)!
drift-detection-enabled: false # (9)!
drift-record-types: # (10)!
- "NS"
- "MX"
timeout-ms: 5000 # (11)!
latency-threshold-ms: 1000 # (12)!
failure-count-threshold: 1 # (13)!
enabled: true # (14)!
metrics-history-enabled: true # (15)!
integrations: # (16)!
- "slack:devops_channel"
# ... other monitors
- Name: The name of the monitor, which must be unique across all DNS monitors.
- Host: The domain name to query (e.g.
example.com). - Uptime check interval: The interval in seconds at which the uptime checks will be performed. The minimum value is 5 seconds.
- Resolver host: Optional. The nameserver to send the queries to. If unset, the system resolver is used.
- Resolver port: The port of the nameserver. Must be between 1 and 65535. Defaults to 53.
- Transport:
UDP(the default, with an automatic TCP fallback on truncated responses) orTCP(forced for every query). - Record matchers: The assertions to evaluate against the resolved records. Each matcher has a
record-type, amatch-type(EXACT,CONTAINSorREGEX, defaults toCONTAINS) and avalue. All matchers must pass, and a single matcher passes if any record of its type satisfies it. If you leave it empty, the check falls back to a plainAlookup and the up/down status is decided by the response code alone. - Expected response code: The response code the resolver is expected to return:
NOERROR(default),NXDOMAIN,SERVFAILorREFUSED. Anything other thanNOERRORrequires the record matchers to be empty. - Drift detection enabled: Whether a dedicated notification is sent when the resolved records change between two checks, without affecting the up/down status. Defaults to false.
- Drift record types: Optional. The record types drift detection watches. When empty, it watches exactly the types your record matchers cover; naming types here replaces that default with the listed ones. Ignored when drift detection is disabled.
- Timeout (ms): The timeout of a whole check in milliseconds, shared as a budget by every query it makes. Must be between 1 and 30000. Defaults to 5000.
- Latency threshold (ms): Optional. If set, the check is considered DOWN when resolving the name takes longer than this value. Leave it unset to only alert on failed resolutions.
- Failure count threshold: The number of consecutive failures that should occur before the monitor is considered down. Defaults to 1.
- Enabled: Whether the monitor is enabled or not. If it's disabled, it won't be checked, and no events will be recorded for it.
- Metrics history enabled: Whether metrics history (resolution latency) is recorded for the monitor. Defaults to true.
- Integrations: A list of integrations to assign to the monitor. The format is
"{integration-type}:{integration-name}", whereintegration-typeis the type of the integration (e.g.email,slack, etc.), andintegration-nameis the name of the integration as defined in theintegrationssection of your YAML file. Example:email:my-email-integration.
This section won't go into details about the API or about exact API calls, since it's well documented and must be self-explanatory. You can find more information about the available endpoints and their usage in the API documentation.
However, here are few of the most important endpoints:
GET /api/v2/dns-monitors- List all DNS monitorsGET /api/v2/dns-monitors/{id}- Get a specific DNS monitor by its IDPOST /api/v2/dns-monitors- Create a new DNS monitorPATCH /api/v2/dns-monitors/{id}- Update an existing DNS monitorDELETE /api/v2/dns-monitors/{id}- Delete a DNS monitor
Settings
Name
4.2.0
string
name
The name of the monitor, which must be unique across all DNS monitors.
Host
4.2.0
string
host
The domain name to query (e.g. example.com). This is the name the queries are sent for, not the nameserver that answers them - see the custom resolver for that.
Uptime check interval
4.2.0
number
uptime-check-interval
The interval in seconds at which the uptime checks will be performed. The minimum value is 5 seconds.
Enabled
4.2.0
true
boolean
enabled
Whether the monitor is enabled or not. If it's disabled, it won't be checked, and no events will be recorded for it.
Failure count threshold
4.2.0
1
number
failure-count-threshold
The number of consecutive failures that should occur before the monitor is considered down. Defaults to 1, which means that the monitor will be considered down after the first failure. If you set it to a higher value, for example 3, the monitor will be considered down only after 3 consecutive failures, which can help to reduce false positives in case of temporary network issues or other transient problems.
Metrics history enabled
4.2.0
true
boolean
metrics-history-enabled
Whether the metrics history (resolution latency over time) is enabled or not. If it's disabled, the monitor won't record the measured metrics. If you disable it on a monitor that has already recorded metrics history, the existing history will be deleted.
Integrations
4.2.0
empty
list
integrations
A list of integrations to assign to the monitor.
If you're using YAML, or the API, the format is "{type}:{name}", where type is the alias of the integration (e.g. email, slack, etc.), and name is the name of the integration as defined in the integrations section of your YAML file. Example: email:my-email-integration.
Tip
You can add/keep disabled integrations in the list, but they will not be used for the monitor. This is useful if you want to enable them later without modifying the monitor's configuration.
Global integrations can be explicitly added too, which is handy if you're about to make them non-global later, but you want to make sure that they will be assigned to certain monitors even after the change.
Resolver settings
Custom resolver
4.2.0
empty
string
resolver-host
The nameserver to send the queries to, either as a hostname or as an IP address (e.g. 1.1.1.1). If you leave it unset, the system resolver of the container is used, which is the right choice when you want to monitor the name the way your infrastructure sees it.
Setting it explicitly is useful when you want to verify a specific nameserver: your authoritative servers, your internal DNS, or a public resolver like 1.1.1.1 or 8.8.8.8 to see what the outside world gets.
Monitoring multiple resolvers
Since a monitor queries exactly one resolver, the way to compare resolvers is to create one monitor per resolver with the same host and the same matchers. That also gives you separate incidents, latency history and notifications per resolver.
Prefer an IP address over a hostname
A hostname has to be resolved first, before the check can send a single query to it - and that lookup goes through the system resolver, on every check. Two consequences worth knowing about:
- It is not covered by the timeout. The timeout budget applies to the queries the check sends to your nameserver, not to finding that nameserver in the first place. If the system resolver is slow or unreachable, the check can take considerably longer than
timeout-msbefore it gives up. - When it does give up, the monitor goes DOWN with a timeout or network error from the queries, not with one naming the resolver - so a broken
resolver-hostlooks like a broken monitored name.
Giving an IP address (1.1.1.1, 10.0.0.53, ...) skips the lookup entirely and keeps the whole check inside its budget. Use a hostname only when the address genuinely isn't stable, and keep in mind that you are then also monitoring the system resolver's ability to resolve it.
Resolver port
4.2.0
53
number
resolver-port
The port of the nameserver. Must be between 1 and 65535. You only need to change it if your nameserver listens on a non-standard port (e.g. a local resolver on 5353).
Transport
4.2.0
UDP
enum
transport
The transport used for the queries:
| Value | Behavior |
|---|---|
UDP |
Queries are sent over UDP, and are automatically retried over TCP when the response comes back truncated. The default, and what a normal resolver does. |
TCP |
Every query is sent over TCP. Useful when you want to verify that your nameserver accepts TCP queries at all - which it is required to, even though it is often overlooked. |
Encrypted transports
DNS over TLS/HTTPS/QUIC (DoT / DoH / DoQ) is not supported yet.
Timeout
4.2.0
5000
number
timeout-ms
The timeout of a whole check in milliseconds. Must be between 1 and 30000. If the resolver doesn't answer within this time, the check is considered a failure.
Note that this is a budget shared by every query the check makes, not a per-query limit. A monitor with matchers on three different record types sends three queries, and each of them gets whatever the previous ones left over - so asserting on more record types never extends the time a check may take. The lookups made only for drift detection draw from the same budget, and are made last: a slow assertion phase starves them, which skips the drift comparison for that round instead of letting the check run longer.
The budget covers the queries sent to your nameserver. It does not cover looking up the nameserver itself when resolver-host is given as a hostname rather than an IP address - see the warning there.
Latency threshold
4.2.0
empty
number
latency-threshold-ms
An optional resolution-latency threshold in milliseconds. If set, the check is considered DOWN when resolving the name takes longer than this value, even if the answer itself is correct. Leave it unset to only alert when the resolution fails.
This works together with the timeout, not instead of it. The timeout is the hard ceiling that decides whether the name resolves at all, while the latency threshold is a lower bar that flags resolutions which succeed but are too slow. For example, with timeout-ms: 5000 and latency-threshold-ms: 500, a resolution that takes 800 ms still returns a valid answer, but it breaches your latency SLA, so the monitor is marked DOWN with a dedicated latency error.
The measured latency is the total wall-clock time of the check's assertion queries: a monitor asserting on three record types measures all three lookups together, so adding a matcher on a type you didn't query before raises the reading even though nothing got slower. Revisit the threshold when you add matchers. Lookups performed only for drift detection are deliberately excluded from the reading, so widening your drift watch list can never push a monitor over its latency threshold.
Keep it well below the timeout
Since the timeout is a budget for the whole check, a check can never measure more latency than the timeout allows: once the budget is gone, the next query fails and the monitor goes DOWN with a timeout error rather than a latency error.
A latency-threshold-ms set at or above timeout-ms therefore never fires. Leave the threshold a comfortable margin below the timeout - and remember that the margin is shared by every record type you assert on.
Evaluation settings
Record matchers
4.2.0
empty
list
record-matchers
The assertions to evaluate against the resolved records. Each matcher consists of three fields:
| Field | Values | Description |
|---|---|---|
record-type |
A, AAAA, CNAME, MX, NS, TXT, SOA, SRV, CAA, PTR |
The record type the matcher is evaluated against |
match-type |
EXACT, CONTAINS (default), REGEX |
How the value is compared to the records |
value |
any non-blank string | The expected value. For REGEX it must be a valid Java / Kotlin regex |
The evaluation rules are the following:
- Every matcher must pass - they are ANDed.
- A single matcher passes if at least one record of its type satisfies it. This is what makes multi-value answers (round-robin
Arecords, multipleMXorNSentries) work naturally: asserting on one of them doesn't require you to enumerate the rest. - A matcher whose record type returned no records at all fails, and the resulting DOWN event names the failing matchers.
- Only the record types your matchers cover are actually queried, one query per distinct type. (An explicit drift watch list can add further lookups.)
- If the list is empty, the check falls back to a plain
Alookup, and the up/down status is decided by the response code alone.
An empty answer is not a failure on its own
With no matchers, only the response code is evaluated. A name that exists but has no A record answers NOERROR with an empty answer section, so such a monitor is UP even though nothing was resolved. A name that doesn't exist answers NXDOMAIN and is correctly marked DOWN.
If you want to require that the answer actually contains something, add a matcher that any record satisfies - a REGEX matcher with the value . is the shortest way, since it matches any non-empty record and fails when there are none.
Requiring all of several values
Because a matcher passes when any record satisfies it, "the answer must contain both 1.2.3.4 and 5.6.7.8" is expressed as two EXACT matchers, one per required value. Each of them has to find its own record, and since all matchers are ANDed, both values must be present.
This does not assert that nothing else is in the answer - use drift detection if you want to know about records you didn't expect.
Normalization
The returned records and the values of your EXACT and CONTAINS matchers go through the same normalization before they are compared, so you can paste dig output verbatim without worrying about formatting details. (A REGEX pattern is deliberately left untouched - see regex patterns.) The normalization:
- Records are rendered in their canonical
digpresentation format, which for the multi-field types means:MX→{priority} {exchange}, e.g.10 mail.example.comSRV→{priority} {weight} {port} {target}, e.g.10 5 5060 sip.example.comCAA→{flags} {tag} {value}, e.g.0 issue letsencrypt.orgSOA→{mname} {rname} {serial} {refresh} {retry} {expire} {minimum}
- A trailing dot is stripped from the end of the value, so
mail.example.com.andmail.example.comare the same thing. Note that this only applies to the very end: in a multi-field type where a name is not the last field - themnameandrnameof anSOA- the dot after the name stays, so match those withCONTAINSrather than spelling out the whole record. - Everything is lowercased, since DNS names are case-insensitive.
- Double quotes are removed and internal whitespace is collapsed to single spaces.
TXTrecords are rejoined from their 255-byte chunks into the single logical value they represent. This matters for long records: a DKIM key or a long SPF record is split into chunks on the wire, and comparing them chunk by chunk would make any assertion on them impossible. In Kuvasz you simply assert on the whole value.
Regex patterns
REGEX matchers use the Java / Kotlin regular expression syntax (java.util.regex.Pattern), which is the Perl-style flavor you know from most languages. Everything that dialect offers is available, including:
- shorthand classes (
\d,\w,\s,\b) and Unicode properties (\p{L}) - greedy, lazy and possessive quantifiers (
a+,a+?,a++) and atomic groups ((?>…)) - alternation, grouping, backreferences and named groups (
(?<label>…)) - lookahead and lookbehind (
(?=…),(?<!…))
Two dialects it is not:
- It is not POSIX, and this one bites silently: a bracket expression like
[[:alpha:]]does not fail, it is read as a character class containing the literal characters:,a,l,pandh. Write\p{Alpha}or[a-z]instead. - It is not PCRE. PCRE-only extensions such as recursion (
(?R)) or\Kdon't exist and are rejected by the server when you save the monitor.
The way a pattern is evaluated:
- It is matched against the normalized record, but the pattern itself is used as-is - so write it against the normalized form: no trailing dot, no surrounding quotes, single spaces between fields.
- It only has to match somewhere inside the record, like a "contains" match. Anchor it with
^and$when you mean the whole record. Multiline mode is off, so those anchors refer to the beginning and the end of the whole record value - which is a single line anyway, since normalization collapses every whitespace character into a space. - It is always compiled case-insensitively, so there is no need for an inline
(?i)flag. Records are lowercased by the normalization anyway.
Escaping backslashes
A regex is a plain string in YAML and JSON, so the quoting style decides whether your backslashes survive:
- single-quoted YAML (recommended) passes them through as-is:
'^[a-z0-9-]+\.example\.com$' - double-quoted YAML and JSON (the REST API) process escape sequences, so every backslash must be doubled:
"^[a-z0-9-]+\\.example\\.com$"
An unescaped \. in a double-quoted string is not a literal dot - it may be rejected as an invalid escape or silently become something else.
Invalid patterns are rejected when you save
A pattern that doesn't compile is refused by every write path (Web UI, REST API, YAML import and YAML bootstrap config), so a typo can never silently turn into an assertion that matches nothing.
The Web UI additionally pre-checks your pattern in the browser, using the JavaScript regex engine, to give you an inline error while you type. The two dialects are nearly identical, but they disagree at the edges in both directions:
- Java-only constructs - possessive quantifiers like
a*+, atomic groups(?>…)- are valid on the server while the in-browser check rejects them. Configure those through the API or YAML if you need them. - Patterns that JavaScript accepts but Java doesn't - an empty character class
[], or a PCRE-ism like\K- pass the in-browser check and are then rejected by the server, so you get the error on save rather than while typing.
Examples
record-matchers:
- record-type: "A"
match-type: "EXACT"
value: "93.184.216.34"
- record-type: "A"
match-type: "EXACT"
value: "93.184.216.35"
record-matchers:
- record-type: "MX"
match-type: "EXACT"
value: "10 mail.example.com"
- record-type: "TXT"
match-type: "CONTAINS"
value: "v=spf1"
record-matchers:
- record-type: "CNAME"
match-type: "REGEX"
# Single-quoted, so the backslashes are taken literally. Anchored, so the
# whole record must match, not just a part of it.
value: '^[a-z0-9-]+\.our-cdn\.net$'
record-matchers:
- record-type: "CNAME"
match-type: "REGEX"
value: "^[a-z0-9-]+\\.our-cdn\\.net$"
record-matchers:
- record-type: "TXT"
match-type: "REGEX"
value: 'v=dkim1;.*p=[a-z0-9+/]{100,}={0,2}'
Expected response code
4.2.0
NOERROR
enum
expected-response-code
The DNS response code the resolver is expected to return. The monitor is DOWN whenever the observed code differs from it.
| Value | Meaning |
|---|---|
NOERROR |
The query was answered successfully. The default. |
NXDOMAIN |
The name does not exist |
SERVFAIL |
The nameserver failed to process the query |
REFUSED |
The nameserver refused to answer the query |
Other response codes
Any response code not listed above (NOTIMP, FORMERR, etc.) is reported as SERVFAIL, since from a monitor's point of view they are all the same kind of server-side failure.
Non-NOERROR codes require empty matchers
Setting this to anything other than NOERROR requires the record matchers to be empty, and the monitor will be rejected otherwise: you cannot assert on the records of a name that you expect not to resolve.
This is what makes negative monitoring possible: point a monitor at a name that must not exist (a decommissioned host, or a wildcard that shouldn't be there) with expected-response-code: "NXDOMAIN", and you'll be notified as soon as it starts resolving.
Drift detection
Drift detection enabled
4.2.0
false
boolean
drift-detection-enabled
Whether Kuvasz should notify you when the resolved records change between two checks.
When enabled, the monitor keeps the last resolved answer set as a baseline, and whenever a check returns a different one, a dedicated DNS_RECORDS_CHANGED notification is sent to the assigned integrations, and the baseline is replaced with the new answer. The stored baseline is also shown on the monitor's detail page in the Web UI, under Resolved records.
Some important properties of this mechanism:
- Drift is evaluated independently of the up/down status. A monitor can be perfectly UP and still report drift - which is exactly the point, since a hijacked delegation or an accidental record change resolves just fine.
- Drift never marks a monitor DOWN and never creates an incident. It is a notification-only event, similar in spirit to the HTTP redirect notification.
- The first successful check is silent: it only seeds the baseline, since there is nothing to compare against yet.
- A check that failed or returned a non-
NOERRORresponse is never compared, because an empty answer section would otherwise look like every watched record having disappeared at once. This means drift detection is effectively inert on a monitor whose expected response code is notNOERROR: such a monitor is UP exactly when there are no records to compare. - Record order doesn't matter: answer sets are sorted before comparison, so a resolver rotating its round-robin answers won't trigger a notification.
Why it's off by default?
Plenty of answer sets legitimately rotate - CDN endpoints, short-TTL load balancers, cloud provider IPs. Enabling drift detection on those would only produce noise, so it's an explicit, per-monitor decision.
Drift record types
4.2.0
empty
list
drift-record-types
The record types drift detection watches. Ignored unless drift detection is enabled.
- When empty (the default), drift detection watches exactly the record types your record matchers cover (or
A, when there are no matchers). This costs no extra lookups, since those types are queried anyway. - When you name types explicitly, they replace that default: exactly the listed types are watched, whether or not your matchers cover them. This is how a monitor watches something it doesn't assert on - watching
NSandMX, for example, gets you a notification when your delegation or your mail routing changes, without making either of them a reason to mark the monitor DOWN.
Each named type that isn't already covered by your matchers adds one query per check. These extra lookups are not counted towards the measured latency or the latency threshold, and if one of them fails, only the drift comparison is skipped for that round - the up/down evaluation is unaffected.
They do, however, draw from the check's timeout budget, and they run after the assertion queries. A watch list wide enough to exhaust what the assertions left over doesn't slow the check down or endanger its status - it simply means the round ends without a drift comparison. If you see drift notifications go quiet on a monitor with many watched types, raise the timeout.
Common operations
Toggling a monitor
You can enable or disable a monitor at any time, which is useful if you want to temporarily stop monitoring a specific name without deleting it.
Disabled monitors won't be counted in the cumulated metrics, like uptime ratio.
Deleting a monitor
If you delete a monitor, it will be removed from the database, and all of its recorded events and metrics (i.e. metrics history, uptime checks, the stored drift baseline, etc.) will be deleted as well. This is a destructive operation, so make sure you really want to delete the monitor.
Look for the delete button with the sign next to the monitor you want to delete.
Remove the monitor from your YAML file, and then restart Kuvasz to apply the changes.
Use the DELETE /api/v2/dns-monitors/{id} endpoint to delete the monitor by its ID.
Modifying the assigned integrations
You can modify the assigned integrations of a monitor by clicking on the configure button with the sign on the monitor's detail page (look for the Integrations block), where you can add or remove integrations as needed.
Modify the integrations property of your affected monitor, by adding or removing list items, and then restart Kuvasz to apply the changes.