diff --git a/go.mod b/go.mod index a0c6ff7fdf9..13744e5fcf4 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,8 @@ require ( howett.net/plist v1.0.1 ) +require github.com/prometheus/client_golang v1.22.0 + require ( cloud.google.com/go v0.121.3 // indirect cloud.google.com/go/ai v0.12.1 // indirect @@ -51,7 +53,9 @@ require ( github.com/BurntSushi/toml v1.5.0 // indirect github.com/ameshkov/dnsstamps v1.0.3 // indirect github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 // indirect + github.com/beorn7/perks v1.0.1 // indirect github.com/ccojocar/zxcvbn-go v1.0.4 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fzipp/gocyclo v0.6.0 // indirect @@ -67,11 +71,16 @@ require ( github.com/josharian/native v1.1.0 // indirect github.com/jstemmer/go-junit-report/v2 v2.1.0 // indirect github.com/kisielk/errcheck v1.9.0 // indirect + github.com/kylelemons/godebug v1.1.0 // indirect github.com/mdlayher/socket v0.5.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/patrickmn/go-cache v2.1.0+incompatible // indirect github.com/pierrec/lz4/v4 v4.1.22 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.1 // indirect + github.com/prometheus/common v0.62.0 // indirect + github.com/prometheus/procfs v0.15.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect github.com/robfig/cron/v3 v3.0.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect diff --git a/go.sum b/go.sum index 942b62ec667..7abc1577749 100644 --- a/go.sum +++ b/go.sum @@ -26,12 +26,16 @@ github.com/ameshkov/dnsstamps v1.0.3 h1:Srzik+J9mivH1alRACTbys2xOxs0lRH9qnTA7Y1O github.com/ameshkov/dnsstamps v1.0.3/go.mod h1:Ii3eUu73dx4Vw5O4wjzmT5+lkCwovjzaEZZ4gKyIH5A= github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0 h1:0b2vaepXIfMsG++IsjHiI2p4bxALD1Y2nQKGMR5zDQM= github.com/beefsack/go-rate v0.0.0-20220214233405-116f4ca011a0/go.mod h1:6YNgTHLutezwnBvyneBbwvB8C82y3dcoOj5EQJIdGXA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw= github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0= github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500 h1:6lhrsTEnloDPXyeZBvSYvQf8u86jbKehZPVDDlkgDl4= github.com/c2h5oh/datasize v0.0.0-20231215233829-aa82cc1e6500/go.mod h1:S/7n9copUssQ56c7aAgHqftWO4LTf4xY6CGWt8Bc+3M= github.com/ccojocar/zxcvbn-go v1.0.4 h1:FWnCIRMXPj43ukfX000kvBZvV6raSxakYr1nzyNrUcc= github.com/ccojocar/zxcvbn-go v1.0.4/go.mod h1:3GxGX+rHmueTUMvm5ium7irpyjmm7ikxYFOSJB21Das= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -105,10 +109,14 @@ github.com/kardianos/service v1.2.2 h1:ZvePhAHfvo0A7Mftk/tEzqEZ7Q4lgnR8sGz4xu1YX github.com/kardianos/service v1.2.2/go.mod h1:CIMRFEJVL+0DS1a3Nx06NaMn4Dz63Ng6O7dl0qH0zVM= github.com/kisielk/errcheck v1.9.0 h1:9xt1zI9EBfcYBvdU1nVrzMzzUPUtPKs9bVSIM3TAb3M= github.com/kisielk/errcheck v1.9.0/go.mod h1:kQxWMMVZgIkDq7U8xtG/n2juOjbLgZtedi0D+/VL/i8= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/mdlayher/ethernet v0.0.0-20220221185849-529eae5b6118 h1:2oDp6OOhLxQ9JBoUuysVz9UZ9uI6oLUbvAZu0x8o+vE= @@ -126,6 +134,8 @@ github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus= github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8= github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= @@ -141,6 +151,14 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= +github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= +github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= +github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= +github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= +github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= +github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= +github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/quic-go v0.53.0 h1:QHX46sISpG2S03dPeZBgVIZp8dGagIaiu2FiVYvpCZI= diff --git a/internal/home/control.go b/internal/home/control.go index 4516880511c..bf76e56d0f6 100644 --- a/internal/home/control.go +++ b/internal/home/control.go @@ -9,14 +9,15 @@ import ( "strings" "time" - "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" - "github.com/AdguardTeam/AdGuardHome/internal/aghnet" - "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" - "github.com/AdguardTeam/AdGuardHome/internal/version" "github.com/AdguardTeam/golibs/httphdr" "github.com/AdguardTeam/golibs/netutil" "github.com/AdguardTeam/golibs/netutil/urlutil" "github.com/NYTimes/gziphandler" + + "github.com/AdguardTeam/AdGuardHome/internal/aghhttp" + "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/dnsforward" + "github.com/AdguardTeam/AdGuardHome/internal/version" ) // appendDNSAddrs is a convenient helper for appending a formatted form of DNS @@ -184,6 +185,10 @@ func registerControlHandlers(web *webAPI) { globalContext.mux.HandleFunc("/apple/doh.mobileconfig", postInstall(handleMobileConfigDoH)) globalContext.mux.HandleFunc("/apple/dot.mobileconfig", postInstall(handleMobileConfigDoT)) RegisterAuthHandlers(web) + + // Register metrics endpoint without control prefix, similar to /dns-query + // Use empty method to bypass auth/gzip middleware like dns-query does + httpRegister("", "/metrics", web.metricsHandler.ServeHTTP) } // httpRegister registers an HTTP handler. diff --git a/internal/home/web.go b/internal/home/web.go index 96d4852fa28..95f3f63d747 100644 --- a/internal/home/web.go +++ b/internal/home/web.go @@ -12,7 +12,6 @@ import ( "sync" "time" - "github.com/AdguardTeam/AdGuardHome/internal/updater" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/logutil/slogutil" "github.com/AdguardTeam/golibs/netutil" @@ -20,9 +19,15 @@ import ( "github.com/AdguardTeam/golibs/netutil/urlutil" "github.com/AdguardTeam/golibs/osutil" "github.com/NYTimes/gziphandler" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/collectors" + "github.com/prometheus/client_golang/prometheus/promhttp" "github.com/quic-go/quic-go/http3" "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" + + "github.com/AdguardTeam/AdGuardHome/internal/metrics" + "github.com/AdguardTeam/AdGuardHome/internal/updater" ) // TODO(a.garipov): Make configurable. @@ -124,6 +129,12 @@ type webAPI struct { // httpsServer is the server that handles HTTPS traffic. If it is not nil, // [Web.http3Server] must also not be nil. httpsServer httpsServer + + // metricsRegistry is the Prometheus registry for metrics collection. + metricsRegistry *prometheus.Registry + + // metricsHandler is the HTTP handler for serving metrics. + metricsHandler http.Handler } // newWebAPI creates a new instance of the web UI and API server. conf must be @@ -133,12 +144,22 @@ type webAPI struct { func newWebAPI(ctx context.Context, conf *webConfig) (w *webAPI) { conf.logger.InfoContext(ctx, "initializing") + // Initialize Prometheus metrics + metricsRegistry := prometheus.NewRegistry() + metricsRegistry.MustRegister(collectors.NewGoCollector()) + metricsRegistry.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) + + // Register DNS metrics + metrics.RegisterDNSMetrics(metricsRegistry) + w = &webAPI{ - conf: conf, - logger: conf.logger, - baseLogger: conf.baseLogger, - tlsManager: conf.tlsManager, - auth: conf.auth, + conf: conf, + logger: conf.logger, + baseLogger: conf.baseLogger, + tlsManager: conf.tlsManager, + auth: conf.auth, + metricsRegistry: metricsRegistry, + metricsHandler: promhttp.HandlerFor(metricsRegistry, promhttp.HandlerOpts{}), } clientFS := http.FileServer(http.FS(conf.clientFS)) diff --git a/internal/metrics/dns.go b/internal/metrics/dns.go new file mode 100644 index 00000000000..20b523d7ff0 --- /dev/null +++ b/internal/metrics/dns.go @@ -0,0 +1,46 @@ +package metrics + +import ( + "time" + + "github.com/prometheus/client_golang/prometheus" +) + +// DNS query result types matching internal/stats package +const ( + ResultNotFiltered = "not_filtered" + ResultFiltered = "filtered" + ResultSafeBrowsing = "safe_browsing" + ResultSafeSearch = "safe_search" + ResultParental = "parental" + ResultUnknown = "unknown" +) + +// DNSQueries tracks DNS queries by their processing result +var DNSQueries = prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "adguard_dns_queries_total", + Help: "Total number of DNS queries by processing result", +}, []string{"result"}) + +// DNSResponseTime tracks DNS query response times using native exponential histogram +var DNSResponseTime = prometheus.NewHistogramVec(prometheus.HistogramOpts{ + Name: "adguard_dns_response_time_seconds", + Help: "DNS query response time in seconds", + NativeHistogramBucketFactor: 1.1, +}, []string{"result"}) + +// RegisterDNSMetrics registers all DNS-related metrics with the provided registry +func RegisterDNSMetrics(registry *prometheus.Registry) { + registry.MustRegister(DNSQueries) + registry.MustRegister(DNSResponseTime) +} + +// IncrementDNSQueryByResult increments counters for a specific query result type +func IncrementDNSQueryByResult(result string) { + DNSQueries.WithLabelValues(result).Inc() +} + +// ObserveDNSResponseTime records a DNS query response time +func ObserveDNSResponseTime(result string, duration time.Duration) { + DNSResponseTime.WithLabelValues(result).Observe(duration.Seconds()) +} diff --git a/internal/metrics/dns_test.go b/internal/metrics/dns_test.go new file mode 100644 index 00000000000..fd368ff8595 --- /dev/null +++ b/internal/metrics/dns_test.go @@ -0,0 +1,53 @@ +package metrics + +import ( + "testing" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/testutil" +) + +func TestDNSMetrics(t *testing.T) { + // Create a new counter for isolated testing + testCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ + Name: "test_dns_queries_by_result_total", + Help: "Test counter for DNS queries by processing result", + }, []string{"result"}) + + // Test incrementing queries by result + testCounter.WithLabelValues(ResultFiltered).Inc() + testCounter.WithLabelValues(ResultNotFiltered).Inc() + testCounter.WithLabelValues(ResultSafeBrowsing).Inc() + testCounter.WithLabelValues(ResultNotFiltered).Inc() // Add another not filtered + + // Verify result counters + filteredValue := testutil.ToFloat64(testCounter.WithLabelValues(ResultFiltered)) + if filteredValue != 1 { + t.Errorf("Expected filtered queries to be 1, got %f", filteredValue) + } + + notFilteredValue := testutil.ToFloat64(testCounter.WithLabelValues(ResultNotFiltered)) + if notFilteredValue != 2 { + t.Errorf("Expected not filtered queries to be 2, got %f", notFilteredValue) + } + + safeBrowsingValue := testutil.ToFloat64(testCounter.WithLabelValues(ResultSafeBrowsing)) + if safeBrowsingValue != 1 { + t.Errorf("Expected safe browsing queries to be 1, got %f", safeBrowsingValue) + } +} + +func TestRegisterDNSMetrics(t *testing.T) { + registry := prometheus.NewRegistry() + + // This should not panic + RegisterDNSMetrics(registry) + + // Registering again should panic due to duplicate registration + defer func() { + if r := recover(); r == nil { + t.Error("Expected panic when registering metrics twice") + } + }() + RegisterDNSMetrics(registry) +} diff --git a/internal/stats/unit.go b/internal/stats/unit.go index a8d7a3d6266..12a6908657b 100644 --- a/internal/stats/unit.go +++ b/internal/stats/unit.go @@ -9,11 +9,13 @@ import ( "slices" "time" - "github.com/AdguardTeam/AdGuardHome/internal/aghnet" "github.com/AdguardTeam/dnsproxy/proxy" "github.com/AdguardTeam/golibs/errors" "github.com/AdguardTeam/golibs/logutil/slogutil" "go.etcd.io/bbolt" + + "github.com/AdguardTeam/AdGuardHome/internal/aghnet" + "github.com/AdguardTeam/AdGuardHome/internal/metrics" ) const ( @@ -328,6 +330,26 @@ func (u *unit) add(e *Entry) { u.timeSum += pt u.nTotal++ + // Update Prometheus metrics + // Map Result constants to metrics labels + var resultLabel string + switch e.Result { + case RNotFiltered: + resultLabel = metrics.ResultNotFiltered + case RFiltered: + resultLabel = metrics.ResultFiltered + case RSafeBrowsing: + resultLabel = metrics.ResultSafeBrowsing + case RSafeSearch: + resultLabel = metrics.ResultSafeSearch + case RParental: + resultLabel = metrics.ResultParental + default: + resultLabel = metrics.ResultUnknown + } + metrics.IncrementDNSQueryByResult(resultLabel) + metrics.ObserveDNSResponseTime(resultLabel, e.ProcessingTime) + for _, s := range e.UpstreamStats { if s.IsCached || s.Error != nil { continue