Skip to content

Commit 8b2f9bc

Browse files
authored
Merge pull request #2299 from OneUptime/dna-monitor
DNS monitor
2 parents 2897a93 + fcc6223 commit 8b2f9bc

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1513
-173
lines changed

APIReference/Service/DataTypeDetail.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,19 @@ const dataTypeDetails: Dictionary<DataTypePageData> = {
521521
},
522522
],
523523
},
524+
{
525+
name: "dnsMonitor",
526+
type: "MonitorStepDnsMonitor",
527+
required: false,
528+
description:
529+
"Configuration for DNS monitoring. Required for DNS monitor type. Defines query name (domain), record type, optional DNS server, port, timeout, and retry settings. See MonitorStepDnsMonitor.",
530+
typeLinks: [
531+
{
532+
label: "MonitorStepDnsMonitor",
533+
path: "monitor-step-dns-monitor",
534+
},
535+
],
536+
},
524537
],
525538
values: [],
526539
jsonExample: JSON.stringify(
@@ -2560,6 +2573,31 @@ const dataTypeDetails: Dictionary<DataTypePageData> = {
25602573
description:
25612574
"Whether the SNMP device is reachable. Use with 'True' or 'False'. Applies to: SNMP monitors.",
25622575
},
2576+
{
2577+
value: "DNS Response Time (in ms)",
2578+
description:
2579+
"The DNS query response time in milliseconds. Use with numeric FilterTypes. Applies to: DNS monitors.",
2580+
},
2581+
{
2582+
value: "DNS Is Online",
2583+
description:
2584+
"Whether the DNS resolution succeeded. Use with 'True' or 'False'. Applies to: DNS monitors.",
2585+
},
2586+
{
2587+
value: "DNS Record Value",
2588+
description:
2589+
"The value of a DNS record returned by the query. Use with string FilterTypes (Contains, EqualTo, etc.). Applies to: DNS monitors.",
2590+
},
2591+
{
2592+
value: "DNSSEC Is Valid",
2593+
description:
2594+
"Whether DNSSEC validation passed (AD flag present). Use with 'True' or 'False'. Applies to: DNS monitors.",
2595+
},
2596+
{
2597+
value: "DNS Record Exists",
2598+
description:
2599+
"Whether any DNS records were returned for the query. Use with 'True' or 'False'. Applies to: DNS monitors.",
2600+
},
25632601
{
25642602
value: "JavaScript Expression",
25652603
description:
@@ -3112,6 +3150,93 @@ const dataTypeDetails: Dictionary<DataTypePageData> = {
31123150
2,
31133151
),
31143152
},
3153+
"monitor-step-dns-monitor": {
3154+
title: "MonitorStepDnsMonitor",
3155+
description:
3156+
"Configuration for a DNS monitor step. Defines the domain to query, record type, optional custom DNS server, and timeout settings. Used as the 'dnsMonitor' property on a MonitorStep when the monitor type is 'DNS'. The criteria filters can then use 'DNS Is Online', 'DNS Response Time (in ms)', 'DNS Record Value', 'DNSSEC Is Valid', and 'DNS Record Exists' as CheckOn values.",
3157+
isEnum: false,
3158+
relatedTypes: [
3159+
{
3160+
name: "MonitorStep",
3161+
path: "monitor-step",
3162+
relationship: "Parent that holds this as dnsMonitor property",
3163+
},
3164+
{
3165+
name: "CheckOn",
3166+
path: "check-on",
3167+
relationship: "Use DNS-specific CheckOn values with DNS monitors",
3168+
},
3169+
],
3170+
properties: [
3171+
{
3172+
name: "queryName",
3173+
type: "string",
3174+
required: true,
3175+
description: "The domain name to query (e.g., 'example.com').",
3176+
},
3177+
{
3178+
name: "recordType",
3179+
type: "string (enum)",
3180+
required: true,
3181+
description:
3182+
"The DNS record type to query. Possible values: 'A', 'AAAA', 'CNAME', 'MX', 'NS', 'TXT', 'SOA', 'PTR', 'SRV', 'CAA'.",
3183+
},
3184+
{
3185+
name: "hostname",
3186+
type: "string",
3187+
required: false,
3188+
description:
3189+
"Custom DNS server to use for the query (e.g., '8.8.8.8'). Leave empty to use system default DNS resolver.",
3190+
},
3191+
{
3192+
name: "port",
3193+
type: "number",
3194+
required: false,
3195+
description: "DNS port. Default is 53.",
3196+
},
3197+
{
3198+
name: "timeout",
3199+
type: "number",
3200+
required: false,
3201+
description:
3202+
"Timeout for DNS queries in milliseconds. Default is 5000 (5 seconds).",
3203+
},
3204+
{
3205+
name: "retries",
3206+
type: "number",
3207+
required: false,
3208+
description: "Number of retries for failed DNS queries. Default is 3.",
3209+
},
3210+
],
3211+
values: [],
3212+
jsonExample: JSON.stringify(
3213+
{
3214+
"// Example 1: Basic A record lookup": {
3215+
queryName: "example.com",
3216+
recordType: "A",
3217+
timeout: 5000,
3218+
retries: 3,
3219+
},
3220+
"// Example 2: MX record with custom DNS server": {
3221+
queryName: "example.com",
3222+
recordType: "MX",
3223+
hostname: "8.8.8.8",
3224+
port: 53,
3225+
timeout: 5000,
3226+
retries: 3,
3227+
},
3228+
"// Example 3: TXT record for SPF verification": {
3229+
queryName: "example.com",
3230+
recordType: "TXT",
3231+
hostname: "1.1.1.1",
3232+
timeout: 5000,
3233+
retries: 3,
3234+
},
3235+
},
3236+
null,
3237+
2,
3238+
),
3239+
},
31153240
};
31163241

31173242
export default class ServiceHandler {

Common/Server/Services/MonitorService.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,14 @@ export class Service extends DatabaseService<Model> {
127127
monitorDestination = `${monitorDestination}:${port}`;
128128
}
129129
}
130+
131+
// For DNS monitors, use the queryName from dnsMonitor config
132+
if (monitorType === MonitorType.DNS && firstStep?.data?.dnsMonitor) {
133+
monitorDestination = firstStep.data.dnsMonitor.queryName || "";
134+
if (firstStep.data.dnsMonitor.hostname) {
135+
monitorDestination = `${monitorDestination} @${firstStep.data.dnsMonitor.hostname}`;
136+
}
137+
}
130138
}
131139
}
132140

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import DataToProcess from "../DataToProcess";
2+
import CompareCriteria from "./CompareCriteria";
3+
import {
4+
CheckOn,
5+
CriteriaFilter,
6+
FilterType,
7+
} from "../../../../Types/Monitor/CriteriaFilter";
8+
import DnsMonitorResponse from "../../../../Types/Monitor/DnsMonitor/DnsMonitorResponse";
9+
import ProbeMonitorResponse from "../../../../Types/Probe/ProbeMonitorResponse";
10+
import EvaluateOverTime from "./EvaluateOverTime";
11+
import CaptureSpan from "../../Telemetry/CaptureSpan";
12+
import logger from "../../Logger";
13+
14+
export default class DnsMonitorCriteria {
15+
@CaptureSpan()
16+
public static async isMonitorInstanceCriteriaFilterMet(input: {
17+
dataToProcess: DataToProcess;
18+
criteriaFilter: CriteriaFilter;
19+
}): Promise<string | null> {
20+
let threshold: number | string | undefined | null =
21+
input.criteriaFilter.value;
22+
23+
const dataToProcess: ProbeMonitorResponse =
24+
input.dataToProcess as ProbeMonitorResponse;
25+
26+
const dnsResponse: DnsMonitorResponse | undefined =
27+
dataToProcess.dnsResponse;
28+
29+
let overTimeValue: Array<number | boolean> | number | boolean | undefined =
30+
undefined;
31+
32+
if (
33+
input.criteriaFilter.evaluateOverTime &&
34+
input.criteriaFilter.evaluateOverTimeOptions
35+
) {
36+
try {
37+
overTimeValue = await EvaluateOverTime.getValueOverTime({
38+
projectId: (input.dataToProcess as ProbeMonitorResponse).projectId,
39+
monitorId: input.dataToProcess.monitorId!,
40+
evaluateOverTimeOptions: input.criteriaFilter.evaluateOverTimeOptions,
41+
metricType: input.criteriaFilter.checkOn,
42+
});
43+
44+
if (Array.isArray(overTimeValue) && overTimeValue.length === 0) {
45+
overTimeValue = undefined;
46+
}
47+
} catch (err) {
48+
logger.error(
49+
`Error in getting over time value for ${input.criteriaFilter.checkOn}`,
50+
);
51+
logger.error(err);
52+
overTimeValue = undefined;
53+
}
54+
}
55+
56+
// Check if DNS is online
57+
if (input.criteriaFilter.checkOn === CheckOn.DnsIsOnline) {
58+
const currentIsOnline: boolean | Array<boolean> =
59+
(overTimeValue as Array<boolean>) ||
60+
(input.dataToProcess as ProbeMonitorResponse).isOnline;
61+
62+
return CompareCriteria.compareCriteriaBoolean({
63+
value: currentIsOnline,
64+
criteriaFilter: input.criteriaFilter,
65+
});
66+
}
67+
68+
// Check DNS response time
69+
if (input.criteriaFilter.checkOn === CheckOn.DnsResponseTime) {
70+
threshold = CompareCriteria.convertToNumber(threshold);
71+
72+
if (threshold === null || threshold === undefined) {
73+
return null;
74+
}
75+
76+
const currentResponseTime: number | Array<number> =
77+
(overTimeValue as Array<number>) ||
78+
dnsResponse?.responseTimeInMs ||
79+
(input.dataToProcess as ProbeMonitorResponse).responseTimeInMs;
80+
81+
if (currentResponseTime === null || currentResponseTime === undefined) {
82+
return null;
83+
}
84+
85+
return CompareCriteria.compareCriteriaNumbers({
86+
value: currentResponseTime,
87+
threshold: threshold as number,
88+
criteriaFilter: input.criteriaFilter,
89+
});
90+
}
91+
92+
// Check if DNS record exists
93+
if (input.criteriaFilter.checkOn === CheckOn.DnsRecordExists) {
94+
const exists: boolean = Boolean(
95+
dnsResponse?.records && dnsResponse.records.length > 0,
96+
);
97+
98+
const isTrue: boolean =
99+
input.criteriaFilter.filterType === FilterType.True;
100+
const isFalse: boolean =
101+
input.criteriaFilter.filterType === FilterType.False;
102+
103+
if (exists && isTrue) {
104+
return `DNS records exist for the query.`;
105+
}
106+
107+
if (!exists && isFalse) {
108+
return `No DNS records found for the query.`;
109+
}
110+
111+
return null;
112+
}
113+
114+
// Check DNSSEC validity
115+
if (input.criteriaFilter.checkOn === CheckOn.DnssecIsValid) {
116+
const isTrue: boolean =
117+
input.criteriaFilter.filterType === FilterType.True;
118+
const isFalse: boolean =
119+
input.criteriaFilter.filterType === FilterType.False;
120+
121+
if (dnsResponse?.isDnssecValid === undefined) {
122+
return null;
123+
}
124+
125+
if (dnsResponse.isDnssecValid && isTrue) {
126+
return `DNSSEC is valid.`;
127+
}
128+
129+
if (!dnsResponse.isDnssecValid && isFalse) {
130+
return `DNSSEC is not valid.`;
131+
}
132+
133+
return null;
134+
}
135+
136+
// Check DNS record value
137+
if (input.criteriaFilter.checkOn === CheckOn.DnsRecordValue) {
138+
if (!dnsResponse?.records || dnsResponse.records.length === 0) {
139+
return null;
140+
}
141+
142+
// Check if any record value matches the criteria
143+
for (const record of dnsResponse.records) {
144+
const recordValue: string = record.value;
145+
146+
// Try numeric comparison first
147+
if (
148+
typeof threshold === "number" ||
149+
(typeof threshold === "string" && !isNaN(Number(threshold)))
150+
) {
151+
const numericThreshold: number | null =
152+
CompareCriteria.convertToNumber(threshold);
153+
154+
if (numericThreshold !== null && !isNaN(Number(recordValue))) {
155+
const result: string | null =
156+
CompareCriteria.compareCriteriaNumbers({
157+
value: Number(recordValue),
158+
threshold: numericThreshold,
159+
criteriaFilter: input.criteriaFilter,
160+
});
161+
162+
if (result) {
163+
return `DNS record (${record.type}): ${result}`;
164+
}
165+
}
166+
}
167+
168+
// String comparison
169+
const result: string | null = CompareCriteria.compareCriteriaStrings({
170+
value: recordValue,
171+
threshold: String(threshold),
172+
criteriaFilter: input.criteriaFilter,
173+
});
174+
175+
if (result) {
176+
return `DNS record (${record.type}): ${result}`;
177+
}
178+
}
179+
}
180+
181+
return null;
182+
}
183+
}

Common/Server/Utils/Monitor/MonitorCriteriaEvaluator.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import MetricMonitorCriteria from "./Criteria/MetricMonitorCriteria";
1212
import TraceMonitorCriteria from "./Criteria/TraceMonitorCriteria";
1313
import ExceptionMonitorCriteria from "./Criteria/ExceptionMonitorCriteria";
1414
import SnmpMonitorCriteria from "./Criteria/SnmpMonitorCriteria";
15+
import DnsMonitorCriteria from "./Criteria/DnsMonitorCriteria";
1516
import MonitorCriteriaMessageBuilder from "./MonitorCriteriaMessageBuilder";
1617
import MonitorCriteriaDataExtractor from "./MonitorCriteriaDataExtractor";
1718
import MonitorCriteriaMessageFormatter from "./MonitorCriteriaMessageFormatter";
@@ -493,6 +494,18 @@ ${contextBlock}
493494
}
494495
}
495496

497+
if (input.monitor.monitorType === MonitorType.DNS) {
498+
const dnsMonitorResult: string | null =
499+
await DnsMonitorCriteria.isMonitorInstanceCriteriaFilterMet({
500+
dataToProcess: input.dataToProcess,
501+
criteriaFilter: input.criteriaFilter,
502+
});
503+
504+
if (dnsMonitorResult) {
505+
return dnsMonitorResult;
506+
}
507+
}
508+
496509
return null;
497510
}
498511

0 commit comments

Comments
 (0)