From 86c12b3921b4d529c63063325ac8b643d92fe812 Mon Sep 17 00:00:00 2001 From: Eldwan Brianne Date: Mon, 2 Nov 2020 18:18:08 +0100 Subject: [PATCH] Added more metrics. HTTP protocol only supported --- internal/adguard/client.go | 144 +++++++++++++++++++----------------- internal/adguard/model.go | 14 +++- internal/metrics/metrics.go | 106 ++++++++++++++++++++++---- main.go | 61 ++++++++------- 4 files changed, 217 insertions(+), 108 deletions(-) diff --git a/internal/adguard/client.go b/internal/adguard/client.go index 1615f16..9ac7e95 100644 --- a/internal/adguard/client.go +++ b/internal/adguard/client.go @@ -1,113 +1,123 @@ package adguard import ( - "encoding/json" - "fmt" - "io/ioutil" - "log" - "net/http" - "os" - "time" - b64 "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "os" + "time" - "github.com/ebrianne/adguard-exporter/internal/metrics" + "github.com/ebrianne/adguard-exporter/internal/metrics" ) var ( - statsURLPattern = "%s://%s:%d/control/stats" + statsURLPattern = "%s://%s:%d/control/stats" ) // Client struct is a AdGuard client to request an instance of a AdGuard ad blocker. type Client struct { - httpClient http.Client - interval time.Duration - protocol string - hostname string - port uint16 - username string - password string - sessionID string + httpClient http.Client + interval time.Duration + protocol string + hostname string + port uint16 + b64password string } // NewClient method initializes a new AdGuard client. -func NewClient(protocol, hostname string, port uint16, username, password string, interval time.Duration) *Client { - if protocol != "http" && protocol != "https" { - log.Printf("protocol %s is invalid. Must be http or https.", protocol) - os.Exit(1) - } +func NewClient(protocol, hostname string, port uint16, b64password string, interval time.Duration) *Client { + if protocol != "http" { + log.Printf("protocol %s is invalid. Must be http.", protocol) + os.Exit(1) + } - return &Client{ - protocol: protocol, - hostname: hostname, - port: port, - username: username, - password: password, - interval: interval, - httpClient: http.Client{ - CheckRedirect: func(req *http.Request, via []*http.Request) error { - return http.ErrUseLastResponse - }, - }, - } + return &Client{ + protocol: protocol, + hostname: hostname, + port: port, + b64password: b64password, + interval: interval, + httpClient: http.Client{}, + } } // Scrape method authenticates and retrieves statistics from AdGuard JSON API // and then pass them as Prometheus metrics. func (c *Client) Scrape() { - for range time.Tick(c.interval) { - stats := c.getStatistics() + for range time.Tick(c.interval) { + stats := c.getStatistics() - c.setMetrics(stats) + c.setMetrics(stats) - log.Printf("New tick of statistics: %s", stats.ToString()) - } + log.Printf("New tick of statistics: %s", stats.ToString()) + } } func (c *Client) setMetrics(stats *Stats) { - metrics.AvgProcessingTime.WithLabelValues(c.hostname).Set(float64(stats.AvgProcessingTime)) + metrics.AvgProcessingTime.WithLabelValues(c.hostname).Set(float64(stats.AvgProcessingTime)) + metrics.DnsQueries.WithLabelValues(c.hostname).Set(float64(stats.DnsQueries)) + metrics.BlockedFiltering.WithLabelValues(c.hostname).Set(float64(stats.BlockedFiltering)) + metrics.ParentalFiltering.WithLabelValues(c.hostname).Set(float64(stats.ParentalFiltering)) + metrics.SafeBrowsingFiltering.WithLabelValues(c.hostname).Set(float64(stats.SafeBrowsingFiltering)) + metrics.SafeSearchFiltering.WithLabelValues(c.hostname).Set(float64(stats.SafeSearchFiltering)) + + for l := range stats.TopQueries { + for domain, value := range stats.TopQueries[l] { + metrics.TopQueries.WithLabelValues(c.hostname, domain).Set(float64(value)) + } + } + + for l := range stats.TopBlocked { + for domain, value := range stats.TopBlocked[l] { + metrics.TopBlocked.WithLabelValues(c.hostname, domain).Set(float64(value)) + } + } + + for l := range stats.TopClients { + for source, value := range stats.TopClients[l] { + metrics.TopClients.WithLabelValues(c.hostname, source).Set(float64(value)) + } + } } func (c *Client) getStatistics() *Stats { - var stats Stats + var stats Stats - statsURL := fmt.Sprintf(statsURLPattern, c.protocol, c.hostname, c.port) + statsURL := fmt.Sprintf(statsURLPattern, c.protocol, c.hostname, c.port) - req, err := http.NewRequest("GET", statsURL, nil) - if err != nil { - log.Fatal("An error has occured when creating HTTP statistics request", err) - } + req, err := http.NewRequest("GET", statsURL, nil) + if err != nil { + log.Fatal("An error has occured when creating HTTP statistics request ", err) + } - if c.isUsingPassword() { - c.authenticateRequest(req) - } + if c.isUsingPassword() { + c.authenticateRequest(req) + } resp, err := c.httpClient.Do(req) if err != nil { log.Printf("An error has occured during login to Adguard: %v", err) } - body, err := ioutil.ReadAll(resp.Body) - if err != nil { - log.Println("Unable to read Adguard statistics HTTP response", err) - } + body, err := ioutil.ReadAll(resp.Body) + if err != nil { + log.Println("Unable to read Adguard statistics HTTP response", err) + } - err = json.Unmarshal(body, &stats) - if err != nil { - log.Println("Unable to unmarshal Adguard statistics to statistics struct model", err) - } + err = json.Unmarshal(body, &stats) + if err != nil { + log.Println("Unable to unmarshal Adguard statistics to statistics struct model", err) + } - return &stats -} - -func (c *Client) basicAuth() string { - auth := c.username + ":" + c.password - return base64.StdEncoding.EncodeToString([]byte(auth)) + return &stats } func (c *Client) isUsingPassword() bool { - return len(c.password) > 0 + return len(c.b64password) > 0 } func (c *Client) authenticateRequest(req *http.Request) { - req.Header.Add("Authorization", "Basic " + c.basicAuth()) + req.Header.Add("Authorization", "Basic " + c.b64password) } diff --git a/internal/adguard/model.go b/internal/adguard/model.go index 136b2c8..d935042 100644 --- a/internal/adguard/model.go +++ b/internal/adguard/model.go @@ -3,14 +3,22 @@ package adguard import "fmt" const ( - enabledStatus = "enabled" + enabledStatus = "enabled" ) type Stats struct { - AvgProcessingTime float64 `json:"avg_processing_time"` + AvgProcessingTime float64 `json:"avg_processing_time"` + DnsQueries int `json:"num_dns_queries"` + BlockedFiltering int `json:"num_blocked_filtering"` + ParentalFiltering int `json:"num_replaced_parental"` + SafeBrowsingFiltering int `json:"num_replaced_safebrowsing"` + SafeSearchFiltering int `json:"num_replaced_safesearch"` + TopQueries []map[string]int `json:"top_queried_domains"` + TopBlocked []map[string]int `json:"top_blocked_domains"` + TopClients []map[string]int `json:"top_clients"` } // ToString method returns a string of the current statistics struct. func (s *Stats) ToString() string { - return fmt.Sprintf("Average Processing Time %d s", s.AvgProcessingTime) + return fmt.Sprintf("%d ads blocked / %d total DNS queries", s.BlockedFiltering, s.DnsQueries) } diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 89c3099..a13ecbc 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -1,28 +1,108 @@ package metrics import ( - "log" + "log" - "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus" ) var ( - AvgProcessingTime = prometheus.NewGaugeVec( - prometheus.GaugeOpts{ - Name: "avg_processing_time", - Namespace: "adguard", - Help: "This represent the average processing time for a DNS query in s", - }, - []string{"hostname"}, - ) + AvgProcessingTime = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "avg_processing_time", + Namespace: "adguard", + Help: "This represent the average processing time for a DNS query in s", + }, + []string{"hostname"}, + ) + + DnsQueries = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "num_dns_queries", + Namespace: "adguard", + Help: "Number of DNS queries", + }, + []string{"hostname"}, + ) + + BlockedFiltering = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "num_blocked_filtering", + Namespace: "adguard", + Help: "This represent the number of domains blocked", + }, + []string{"hostname"}, + ) + + ParentalFiltering = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "num_replaced_parental", + Namespace: "adguard", + Help: "This represent the number of domains blocked (parental)", + }, + []string{"hostname"}, + ) + + SafeBrowsingFiltering = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "num_replaced_safebrowsing", + Namespace: "adguard", + Help: "This represent the number of domains blocked (safe browsing)", + }, + []string{"hostname"}, + ) + + SafeSearchFiltering = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "num_replaced_safesearch", + Namespace: "adguard", + Help: "This represent the number of domains blocked (safe search)", + }, + []string{"hostname"}, + ) + + TopQueries = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "top_queried_domains", + Namespace: "adguard", + Help: "This represent the top queried domains", + }, + []string{"hostname", "domain"}, + ) + + TopBlocked = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "top_blocked_domains", + Namespace: "adguard", + Help: "This represent the top bloacked domains", + }, + []string{"hostname", "domain"}, + ) + + TopClients = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "top_clients", + Namespace: "adguard", + Help: "This represent the top clients", + }, + []string{"hostname", "client"}, + ) ) // Init initializes all Prometheus metrics made available by AdGuard exporter. func Init() { - initMetric("avg_processing_time", AvgProcessingTime) + initMetric("avg_processing_time", AvgProcessingTime) + initMetric("num_dns_queries", DnsQueries) + initMetric("num_blocked_filtering", BlockedFiltering) + initMetric("num_replaced_parental", ParentalFiltering) + initMetric("num_replaced_safebrowsing", SafeBrowsingFiltering) + initMetric("num_replaced_safesearch", SafeSearchFiltering) + initMetric("top_queried_domains", TopQueries) + initMetric("top_blocked_domains", TopBlocked) + initMetric("top_clients", TopClients) } func initMetric(name string, metric *prometheus.GaugeVec) { - prometheus.MustRegister(metric) - log.Printf("New Prometheus metric registered: %s", name) + prometheus.MustRegister(metric) + log.Printf("New Prometheus metric registered: %s", name) } diff --git a/main.go b/main.go index f98fe12..fa32fbc 100644 --- a/main.go +++ b/main.go @@ -1,53 +1,64 @@ package main import ( - "fmt" - "os" - "os/signal" - "syscall" - "time" + "fmt" + "os" + "os/signal" + "syscall" + "time" + "encoding/base64" - "github.com/ebrianne/adguard-exporter/config" - "github.com/ebrianne/adguard-exporter/internal/metrics" - "github.com/ebrianne/adguard-exporter/internal/adguard" - "github.com/ebrianne/adguard-exporter/internal/server" + "github.com/ebrianne/adguard-exporter/config" + "github.com/ebrianne/adguard-exporter/internal/metrics" + "github.com/ebrianne/adguard-exporter/internal/adguard" + "github.com/ebrianne/adguard-exporter/internal/server" ) const ( - name = "adguard-exporter" + name = "adguard-exporter" ) var ( - s *server.Server + s *server.Server ) func main() { - conf := config.Load() + conf := config.Load() - metrics.Init() + metrics.Init() - initAdguardClient(conf.AdguardProtocol, conf.AdguardHostname, conf.AdguardPort, conf.AdguardUsername, conf.AdguardPassword, conf.Interval) - initHttpServer(conf.Port) + initAdguardClient(conf.AdguardProtocol, conf.AdguardHostname, conf.AdguardPort, conf.AdguardUsername, conf.AdguardPassword, conf.Interval) + initHttpServer(conf.Port) - handleExitSignal() + handleExitSignal() +} + +func basicAuth(username, password string) string { + auth := username + ":" + password + return base64.StdEncoding.EncodeToString([]byte(auth)) } func initAdguardClient(protocol, hostname string, port uint16, username, password string, interval time.Duration) { - client := adguard.NewClient(protocol, hostname, port, username, password, interval) - go client.Scrape() + b64password := "" + if len(username) > 0 && len(password) > 0 { + b64password = basicAuth(username, password) + } + + client := adguard.NewClient(protocol, hostname, port, b64password, interval) + go client.Scrape() } func initHttpServer(port string) { - s = server.NewServer(port) - go s.ListenAndServe() + s = server.NewServer(port) + go s.ListenAndServe() } func handleExitSignal() { - stop := make(chan os.Signal, 1) - signal.Notify(stop, os.Interrupt, syscall.SIGTERM) + stop := make(chan os.Signal, 1) + signal.Notify(stop, os.Interrupt, syscall.SIGTERM) - <-stop + <-stop - s.Stop() - fmt.Println(fmt.Sprintf("\n%s HTTP server stopped", name)) + s.Stop() + fmt.Println(fmt.Sprintf("\n%s HTTP server stopped", name)) }