Files
elmprodvpn/selective-vpn-api/app/resolver/dns_metrics.go

159 lines
3.5 KiB
Go

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, "; ")
}