aboutsummaryrefslogtreecommitdiff
path: root/internal/monitor/dns.go
diff options
context:
space:
mode:
authorFuwn <[email protected]>2026-01-19 19:41:22 -0800
committerFuwn <[email protected]>2026-01-19 19:41:22 -0800
commit5cb6f53da0f5927edad01e854432eb0b70371b89 (patch)
tree133824d444dc38134996eadb30a34ed63b44cfd1 /internal/monitor/dns.go
parentfeat: Add disable_ping_tooltips option to hide ping hover details (diff)
downloadkaze-5cb6f53da0f5927edad01e854432eb0b70371b89.tar.xz
kaze-5cb6f53da0f5927edad01e854432eb0b70371b89.zip
feat: Add ICMP, DNS, and GraphQL monitor types
Add three new monitor types with full support: - ICMP: Ping monitoring with configurable packet count, tracks packet loss and average RTT. Marks degraded on partial packet loss. - DNS: DNS resolution monitoring supporting A, AAAA, CNAME, MX, and TXT records. Optional custom DNS server and validation of expected IPs/CNAME. - GraphQL: GraphQL endpoint monitoring with query execution, variable support, error detection, and content validation. All new monitors include retry support, response time tracking, and integrate with existing display options (round_response_time, etc). GraphQL monitors also support SSL certificate tracking.
Diffstat (limited to 'internal/monitor/dns.go')
-rw-r--r--internal/monitor/dns.go241
1 files changed, 241 insertions, 0 deletions
diff --git a/internal/monitor/dns.go b/internal/monitor/dns.go
new file mode 100644
index 0000000..4f4f099
--- /dev/null
+++ b/internal/monitor/dns.go
@@ -0,0 +1,241 @@
+package monitor
+
+import (
+ "context"
+ "fmt"
+ "net"
+ "strings"
+ "time"
+
+ "github.com/Fuwn/kaze/internal/config"
+)
+
+// DNSMonitor monitors DNS resolution
+type DNSMonitor struct {
+ name string
+ target string // Domain to resolve
+ interval time.Duration
+ timeout time.Duration
+ retries int
+ roundResponseTime bool
+ roundUptime bool
+ dnsServer string // Optional DNS server
+ expectedIPs []string // Expected IP addresses
+ expectedCNAME string // Expected CNAME
+ recordType string // DNS record type (A, AAAA, CNAME, MX, TXT, etc.)
+}
+
+// NewDNSMonitor creates a new DNS monitor
+func NewDNSMonitor(cfg config.MonitorConfig) (*DNSMonitor, error) {
+ // Default to A record if not specified
+ recordType := "A"
+ if cfg.RecordType != "" {
+ recordType = strings.ToUpper(cfg.RecordType)
+ }
+
+ return &DNSMonitor{
+ name: cfg.Name,
+ target: cfg.Target,
+ interval: cfg.Interval.Duration,
+ timeout: cfg.Timeout.Duration,
+ retries: cfg.Retries,
+ roundResponseTime: cfg.RoundResponseTime,
+ roundUptime: cfg.RoundUptime,
+ dnsServer: cfg.DNSServer,
+ expectedIPs: cfg.ExpectedIPs,
+ expectedCNAME: cfg.ExpectedCNAME,
+ recordType: recordType,
+ }, nil
+}
+
+// Name returns the monitor's name
+func (m *DNSMonitor) Name() string {
+ return m.name
+}
+
+// Type returns the monitor type
+func (m *DNSMonitor) Type() string {
+ return "dns"
+}
+
+// Target returns the monitor target
+func (m *DNSMonitor) Target() string {
+ return m.target
+}
+
+// Interval returns the check interval
+func (m *DNSMonitor) Interval() time.Duration {
+ return m.interval
+}
+
+// Retries returns the number of retry attempts
+func (m *DNSMonitor) Retries() int {
+ return m.retries
+}
+
+// HideSSLDays returns whether to hide SSL days from display
+func (m *DNSMonitor) HideSSLDays() bool {
+ return false // DNS doesn't use SSL
+}
+
+// RoundResponseTime returns whether to round response time
+func (m *DNSMonitor) RoundResponseTime() bool {
+ return m.roundResponseTime
+}
+
+// RoundUptime returns whether to round uptime percentage
+func (m *DNSMonitor) RoundUptime() bool {
+ return m.roundUptime
+}
+
+// Check performs the DNS resolution check
+func (m *DNSMonitor) Check(ctx context.Context) *Result {
+ result := &Result{
+ MonitorName: m.name,
+ Timestamp: time.Now(),
+ }
+
+ // Create resolver
+ resolver := &net.Resolver{}
+ if m.dnsServer != "" {
+ // Use custom DNS server
+ resolver = &net.Resolver{
+ PreferGo: true,
+ Dial: func(ctx context.Context, network, address string) (net.Conn, error) {
+ d := net.Dialer{Timeout: m.timeout}
+ return d.DialContext(ctx, "udp", m.dnsServer)
+ },
+ }
+ }
+
+ // Create timeout context
+ timeoutCtx, cancel := context.WithTimeout(ctx, m.timeout)
+ defer cancel()
+
+ start := time.Now()
+
+ switch m.recordType {
+ case "A", "AAAA":
+ ips, err := resolver.LookupIP(timeoutCtx, "ip", m.target)
+ result.ResponseTime = time.Since(start)
+
+ if err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("DNS lookup failed: %w", err)
+ return result
+ }
+
+ if len(ips) == 0 {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("no IP addresses returned")
+ return result
+ }
+
+ // Check if expected IPs match
+ if len(m.expectedIPs) > 0 {
+ found := false
+ for _, expectedIP := range m.expectedIPs {
+ for _, ip := range ips {
+ if ip.String() == expectedIP {
+ found = true
+ break
+ }
+ }
+ if found {
+ break
+ }
+ }
+ if !found {
+ result.Status = StatusDegraded
+ result.Error = fmt.Errorf("resolved IPs don't match expected: got %v, expected %v", ips, m.expectedIPs)
+ return result
+ }
+ }
+
+ result.Status = StatusUp
+
+ case "CNAME":
+ cname, err := resolver.LookupCNAME(timeoutCtx, m.target)
+ result.ResponseTime = time.Since(start)
+
+ if err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("CNAME lookup failed: %w", err)
+ return result
+ }
+
+ // Check if expected CNAME matches
+ if m.expectedCNAME != "" && cname != m.expectedCNAME {
+ result.Status = StatusDegraded
+ result.Error = fmt.Errorf("CNAME mismatch: got %s, expected %s", cname, m.expectedCNAME)
+ return result
+ }
+
+ result.Status = StatusUp
+
+ case "MX":
+ mxRecords, err := resolver.LookupMX(timeoutCtx, m.target)
+ result.ResponseTime = time.Since(start)
+
+ if err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("MX lookup failed: %w", err)
+ return result
+ }
+
+ if len(mxRecords) == 0 {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("no MX records found")
+ return result
+ }
+
+ result.Status = StatusUp
+
+ case "TXT":
+ txtRecords, err := resolver.LookupTXT(timeoutCtx, m.target)
+ result.ResponseTime = time.Since(start)
+
+ if err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("TXT lookup failed: %w", err)
+ return result
+ }
+
+ if len(txtRecords) == 0 {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("no TXT records found")
+ return result
+ }
+
+ result.Status = StatusUp
+
+ default:
+ // Fallback to generic IP lookup
+ ips, err := resolver.LookupIP(timeoutCtx, "ip", m.target)
+ result.ResponseTime = time.Since(start)
+
+ if err != nil {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("DNS lookup failed: %w", err)
+ return result
+ }
+
+ if len(ips) == 0 {
+ result.Status = StatusDown
+ result.Error = fmt.Errorf("no IP addresses returned")
+ return result
+ }
+
+ result.Status = StatusUp
+ }
+
+ // Check for slow DNS resolution (degraded if > 1 second)
+ if result.Status == StatusUp && result.ResponseTime > 1*time.Second {
+ result.Status = StatusDegraded
+ if result.Error == nil {
+ result.Error = fmt.Errorf("slow DNS resolution: %v", result.ResponseTime)
+ }
+ }
+
+ return result
+}