diff options
| author | Fuwn <[email protected]> | 2026-01-19 19:41:22 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-01-19 19:41:22 -0800 |
| commit | 5cb6f53da0f5927edad01e854432eb0b70371b89 (patch) | |
| tree | 133824d444dc38134996eadb30a34ed63b44cfd1 /internal/monitor/dns.go | |
| parent | feat: Add disable_ping_tooltips option to hide ping hover details (diff) | |
| download | kaze-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.go | 241 |
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 +} |