diff --git a/go.mod b/go.mod index c92b7c9..39bda48 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/joho/godotenv v1.5.1 github.com/labstack/echo-contrib v0.16.0 github.com/labstack/echo/v4 v4.11.4 + github.com/mitchellh/mapstructure v1.5.0 github.com/prometheus/client_golang v1.19.0 github.com/sethvargo/go-envconfig v1.0.1 ) diff --git a/go.sum b/go.sum index 816f289..c1920b8 100644 --- a/go.sum +++ b/go.sum @@ -21,6 +21,8 @@ github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovk github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= diff --git a/internal/adguard/client.go b/internal/adguard/client.go index fd8b258..b352baa 100644 --- a/internal/adguard/client.go +++ b/internal/adguard/client.go @@ -9,8 +9,10 @@ import ( "net/http" "net/url" "slices" + "strconv" "github.com/henrywhitaker3/adguard-exporter/internal/config" + "github.com/mitchellh/mapstructure" ) type Client struct { @@ -91,6 +93,31 @@ func (c *Client) GetDhcp(ctx context.Context) (*DhcpStatus, error) { return out, nil } +func (c *Client) GetQueryTypes(ctx context.Context) (map[string]int, error) { + log := &queryLog{} + err := c.do(ctx, http.MethodGet, "/control/querylog?limit=1000&response_status=all", log) + if err != nil { + return nil, err + } + + out := map[string]int{} + for _, d := range log.Log { + if d.Answer != nil && len(d.Answer) > 0 { + for i := range d.Answer { + switch v := d.Answer[i].Value.(type) { + case string: + out[d.Answer[i].Type]++ + case map[string]any: + dns65 := &type65{} + mapstructure.Decode(v, dns65) + out["TYPE"+strconv.Itoa(dns65.Hdr.Rrtype)]++ + } + } + } + } + return out, nil +} + func (c *Client) Url() string { return c.conf.Url } diff --git a/internal/adguard/types.go b/internal/adguard/types.go index c62f546..43c0181 100644 --- a/internal/adguard/types.go +++ b/internal/adguard/types.go @@ -43,3 +43,46 @@ type DhcpStatus struct { StaticLeases []DhcpLease `json:"static_leases"` Leases []DhcpLease } + +type query struct { + Class string `json:"class"` + Host string `json:"host"` + Type string `json:"type"` +} + +type answer struct { + Type string `json:"type"` + TTL float64 `json:"ttl"` + Value any `json:"value"` +} + +type dnsHeader struct { + Name string `json:"Name"` + Rrtype int `json:"Rrtype"` + Class int `json:"Class"` + TTL int `json:"Ttl"` + Rdlength int `json:"Rdlength"` +} + +type type65 struct { + Hdr dnsHeader `json:"Hdr"` + RData string `json:"Rdata"` +} + +type logEntry struct { + Answer []answer `json:"answer"` + DNSSec Bool `json:"answer_dnssec"` + Client string `json:"client"` + ClientProto string `json:"client_proto"` + Elapsed string `json:"elapsed_ms"` + Question query `json:"question"` + Reason string `json:"reason"` + Status string `json:"status"` + Time string `json:"time"` + Upstream string `json:"upstream"` +} + +type queryLog struct { + Log []logEntry `json:"data"` + Oldest string `json:"oldest"` +} diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 05aa44a..752803e 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -82,6 +82,11 @@ var ( Namespace: "adguard", Help: "The average response time for each of the top upstream servers", }, []string{"server", "upstream"}) + QueryTypes = prometheus.NewGaugeVec(prometheus.GaugeOpts{ + Name: "query_types", + Namespace: "adguard", + Help: "The number of queries for a specific type", + }, []string{"server", "type"}) // DHCP DhcpEnabled = prometheus.NewGaugeVec(prometheus.GaugeOpts{ @@ -151,6 +156,7 @@ func Init() { prometheus.MustRegister(TopQueriedDomains) prometheus.MustRegister(TopUpstreams) prometheus.MustRegister(TopUpstreamsAvgTimes) + prometheus.MustRegister(QueryTypes) // Status prometheus.MustRegister(Running) diff --git a/internal/worker/worker.go b/internal/worker/worker.go index 0962e94..ddbb6af 100644 --- a/internal/worker/worker.go +++ b/internal/worker/worker.go @@ -38,6 +38,7 @@ func collect(ctx context.Context, client *adguard.Client) error { go collectStats(ctx, client) go collectStatus(ctx, client) go collectDhcp(ctx, client) + go collectQueryTypeStats(ctx, client) return nil } @@ -104,3 +105,16 @@ func collectDhcp(ctx context.Context, client *adguard.Client) { metrics.DhcpEnabled.WithLabelValues(client.Url()).Set(float64(dhcp.Enabled.Int())) metrics.DhcpLeases.Record(client.Url(), dhcp.Leases) } + +func collectQueryTypeStats(ctx context.Context, client *adguard.Client) { + stats, err := client.GetQueryTypes(ctx) + if err != nil { + log.Printf("ERROR - could not get query type stats: %v\n", err) + metrics.ScrapeErrors.WithLabelValues(client.Url()).Inc() + return + } + + for t, v := range stats { + metrics.QueryTypes.WithLabelValues(client.Url(), t).Set(float64(v)) + } +}