package resolver import ( "fmt" "sort" "strings" ) type DNSErrorKind string const ( DNSErrorNXDomain DNSErrorKind = "nxdomain" DNSErrorTimeout DNSErrorKind = "timeout" DNSErrorTemporary DNSErrorKind = "temporary" DNSErrorOther DNSErrorKind = "other" ) type DNSUpstreamMetrics struct { Attempts int OK int NXDomain int Timeout int Temporary int Other int Skipped int } type DNSMetrics struct { Attempts int OK int NXDomain int Timeout int Temporary int Other int Skipped int PerUpstream map[string]*DNSUpstreamMetrics } func (m *DNSMetrics) EnsureUpstream(upstream string) *DNSUpstreamMetrics { if m.PerUpstream == nil { m.PerUpstream = map[string]*DNSUpstreamMetrics{} } if us, ok := m.PerUpstream[upstream]; ok { return us } us := &DNSUpstreamMetrics{} m.PerUpstream[upstream] = us return us } func (m *DNSMetrics) AddSuccess(upstream string) { m.Attempts++ m.OK++ us := m.EnsureUpstream(upstream) us.Attempts++ us.OK++ } func (m *DNSMetrics) AddError(upstream string, kind DNSErrorKind) { m.Attempts++ us := m.EnsureUpstream(upstream) us.Attempts++ switch kind { case DNSErrorNXDomain: m.NXDomain++ us.NXDomain++ case DNSErrorTimeout: m.Timeout++ us.Timeout++ case DNSErrorTemporary: m.Temporary++ us.Temporary++ default: m.Other++ us.Other++ } } func (m *DNSMetrics) AddCooldownSkip(upstream string) { m.Skipped++ us := m.EnsureUpstream(upstream) us.Skipped++ } func (m *DNSMetrics) Merge(other DNSMetrics) { m.Attempts += other.Attempts m.OK += other.OK m.NXDomain += other.NXDomain m.Timeout += other.Timeout m.Temporary += other.Temporary m.Other += other.Other m.Skipped += other.Skipped for upstream, src := range other.PerUpstream { dst := m.EnsureUpstream(upstream) dst.Attempts += src.Attempts dst.OK += src.OK dst.NXDomain += src.NXDomain dst.Timeout += src.Timeout dst.Temporary += src.Temporary dst.Other += src.Other dst.Skipped += src.Skipped } } func (m DNSMetrics) TotalErrors() int { return m.NXDomain + m.Timeout + m.Temporary + m.Other } func (m DNSMetrics) FormatPerUpstream() string { if len(m.PerUpstream) == 0 { return "" } keys := make([]string, 0, len(m.PerUpstream)) for k := range m.PerUpstream { keys = append(keys, k) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, k := range keys { v := m.PerUpstream[k] parts = append(parts, fmt.Sprintf("%s{attempts=%d ok=%d nxdomain=%d timeout=%d temporary=%d other=%d skipped=%d}", k, v.Attempts, v.OK, v.NXDomain, v.Timeout, v.Temporary, v.Other, v.Skipped)) } return strings.Join(parts, "; ") } func (m DNSMetrics) FormatResolverHealth() string { if len(m.PerUpstream) == 0 { return "" } keys := make([]string, 0, len(m.PerUpstream)) for k := range m.PerUpstream { keys = append(keys, k) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, k := range keys { v := m.PerUpstream[k] if v == nil || v.Attempts <= 0 { continue } okRate := float64(v.OK) / float64(v.Attempts) timeoutRate := float64(v.Timeout) / float64(v.Attempts) score := okRate*100.0 - timeoutRate*50.0 state := "bad" switch { case score >= 70 && timeoutRate <= 0.05: state = "good" case score >= 35: state = "degraded" default: state = "bad" } parts = append(parts, fmt.Sprintf("%s{score=%.1f state=%s attempts=%d ok=%d timeout=%d nxdomain=%d temporary=%d other=%d skipped=%d}", k, score, state, v.Attempts, v.OK, v.Timeout, v.NXDomain, v.Temporary, v.Other, v.Skipped)) } return strings.Join(parts, "; ") }