diff --git a/README.md b/README.md index 979e3e1..ab9e352 100644 --- a/README.md +++ b/README.md @@ -12,20 +12,6 @@ This is a Prometheus exporter for [PI-Hole](https://pi-hole.net/)'s Raspberry PI ## Installation -### From sources - -First, retrieve the project: -```bash -$ go get -u github.com/eko/pihole-exporter -# or -$ git clone https://github.com/eko/pihole-exporter.git -``` - -Then, build the binary (here, an example to run on Raspberry PI ARM architecture): -```bash -$ GOOS=linux GOARCH=arm GOARM=7 go build -o pihole_exporter . -``` - ### Download binary You can also download the latest version of the binary built for your architecture here: @@ -44,6 +30,20 @@ You can also download the latest version of the binary built for your architectu [Linux](https://github.com/eko/pihole-exporter/releases/latest/download/pihole_exporter-linux-arm) ] +### From sources + +First, you have to retrieve the project sources by using one of the following way: +```bash +$ go get -u github.com/eko/pihole-exporter +# or +$ git clone https://github.com/eko/pihole-exporter.git +``` + +Then, build the binary (here, an example to run on Raspberry PI ARM architecture): +```bash +$ GOOS=linux GOARCH=arm GOARM=7 go build -o pihole_exporter . +``` + ## Usage In order to run the exporter, type the following command (arguments are optional): @@ -51,29 +51,37 @@ In order to run the exporter, type the following command (arguments are optional ```bash $ ./pihole_exporter -pihole_hostname 192.168.1.10 -pihole_password azerty -2019/05/09 09:32:19 ------------------------------------ -2019/05/09 09:32:19 - PI-Hole exporter configuration - -2019/05/09 09:32:19 ------------------------------------ -2019/05/09 09:32:19 PIHoleHostname : 192.168.1.10 -2019/05/09 09:32:19 PIHolePassword : azerty -2019/05/09 09:32:19 Port : 9311 -2019/05/09 09:32:19 Interval : 10s -2019/05/09 09:32:19 ------------------------------------ -2019/05/09 09:32:19 New prometheus metric registered: Desc{fqName: "pihole_domains_being_blocked", help: "This represent the number of domains being blocked", constLabels: {}, variableLabels: []} -2019/05/09 09:32:19 New prometheus metric registered: Desc{fqName: "pihole_dns_queries_today", help: "This represent the number of DNS queries made over the current day", constLabels: {}, variableLabels: []} -2019/05/09 09:32:19 New prometheus metric registered: Desc{fqName: "pihole_ads_blocked_today", help: "This represent the number of ads blocked over the current day", constLabels: {}, variableLabels: []} -2019/05/09 09:32:19 New prometheus metric registered: Desc{fqName: "pihole_ads_percentage_today", help: "This represent the percentage of ads blocked over the current day", constLabels: {}, variableLabels: []} -2019/05/09 09:32:19 New prometheus metric registered: Desc{fqName: "pihole_unique_domains", help: "This represent the number of unique domains seen", constLabels: {}, variableLabels: []} -2019/05/09 09:32:19 New prometheus metric registered: Desc{fqName: "pihole_queries_forwarded", help: "This represent the number of queries forwarded", constLabels: {}, variableLabels: []} -2019/05/09 09:32:19 New prometheus metric registered: Desc{fqName: "pihole_queries_cached", help: "This represent the number of queries cached", constLabels: {}, variableLabels: []} -2019/05/09 09:32:19 New prometheus metric registered: Desc{fqName: "pihole_clients_ever_seen", help: "This represent the number of clients ever seen", constLabels: {}, variableLabels: []} -2019/05/09 09:32:19 New prometheus metric registered: Desc{fqName: "pihole_unique_clients", help: "This represent the number of unique clients seen", constLabels: {}, variableLabels: []} -2019/05/09 09:32:19 New prometheus metric registered: Desc{fqName: "pihole_dns_queries_all_types", help: "This represent the number of DNS queries made for all types", constLabels: {}, variableLabels: []} -2019/05/09 09:32:19 Starting HTTP server +2019/05/09 20:19:52 ------------------------------------ +2019/05/09 20:19:52 - PI-Hole exporter configuration - +2019/05/09 20:19:52 ------------------------------------ +2019/05/09 20:19:52 PIHoleHostname : 192.168.1.10 +2019/05/09 20:19:52 PIHolePassword : azerty +2019/05/09 20:19:52 Port : 9311 +2019/05/09 20:19:52 Interval : 10s +2019/05/09 20:19:52 ------------------------------------ +2019/05/09 20:19:52 New Prometheus metric registered: domains_blocked +2019/05/09 20:19:52 New Prometheus metric registered: dns_queries_today +2019/05/09 20:19:52 New Prometheus metric registered: ads_blocked_today +2019/05/09 20:19:52 New Prometheus metric registered: ads_percentag_today +2019/05/09 20:19:52 New Prometheus metric registered: unique_domains +2019/05/09 20:19:52 New Prometheus metric registered: queries_forwarded +2019/05/09 20:19:52 New Prometheus metric registered: queries_cached +2019/05/09 20:19:52 New Prometheus metric registered: clients_ever_seen +2019/05/09 20:19:52 New Prometheus metric registered: unique_clients +2019/05/09 20:19:52 New Prometheus metric registered: dns_queries_all_types +2019/05/09 20:19:52 New Prometheus metric registered: reply +2019/05/09 20:19:52 New Prometheus metric registered: top_queries +2019/05/09 20:19:52 New Prometheus metric registered: top_ads +2019/05/09 20:19:52 New Prometheus metric registered: top_sources +2019/05/09 20:19:52 New Prometheus metric registered: forward_destinations +2019/05/09 20:19:52 New Prometheus metric registered: querytypes +2019/05/09 20:19:52 New Prometheus metric registered: status +2019/05/09 20:19:52 Starting HTTP server +2019/05/09 20:19:54 New tick of statistics: 648 ads blocked / 66796 total DNS querie ... ``` -## Available options +## Available CLI options ```bash # Interval of time the exporter will fetch data from PI-Hole -interval duration (optional) (default 10s) @@ -87,3 +95,25 @@ $ ./pihole_exporter -pihole_hostname 192.168.1.10 -pihole_password azerty # Port to be used for the exporter -port string (optional) (default "9311") ``` + +## Available Prometheus metrics + +| Metric name | Description | +|:----------------------------:|-------------------------------------------------------------------------------------------| +| pihole_domains_being_blocked | This represent the number of domains being blocked | +| pihole_dns_queries_today | This represent the number of DNS queries made over the current day | +| pihole_ads_blocked_today | This represent the number of ads blocked over the current day | +| pihole_ads_percentage_today | This represent the percentage of ads blocked over the current day | +| pihole_unique_domains | This represent the number of unique domains seen | +| pihole_queries_forwarded | This represent the number of queries forwarded | +| pihole_queries_cached | This represent the number of queries cached | +| pihole_clients_ever_seen | This represent the number of clients ever seen | +| pihole_unique_clients | This represent the number of unique clients seen | +| pihole_dns_queries_all_types | This represent the number of DNS queries made for all types | +| pihole_reply | This represent the number of replies made for all types | +| pihole_top_queries | This represent the number of top queries made by PI-Hole by domain | +| pihole_top_ads | This represent the number of top ads made by PI-Hole by domain | +| pihole_top_sources | This represent the number of top sources requests made by PI-Hole by source host | +| pihole_forward_destinations | This represent the number of forward destinations requests made by PI-Hole by destination | +| pihole_querytypes | This represent the number of queries made by PI-Hole by type | +| pihole_status | This represent if PI-Hole is enabled | diff --git a/config/configuration.go b/config/configuration.go index dbf3c99..19f2861 100644 --- a/config/configuration.go +++ b/config/configuration.go @@ -13,6 +13,7 @@ import ( "github.com/heetch/confita/backend/flags" ) +// Config is the exporter CLI configuration. type Config struct { PIHoleHostname string `config:"pihole_hostname"` PIHolePassword string `config:"pihole_password"` @@ -31,6 +32,7 @@ func getDefaultConfig() *Config { } } +// Load method loads the configuration by using both flag or environment variables. func Load() *Config { loaders := []backend.Backend{ env.NewBackend(), diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index a93d5df..3a50a3a 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -8,111 +8,198 @@ import ( var ( // DomainsBlocked - The number of domains being blocked by PI-Hole. - DomainsBlocked = prometheus.NewGauge( + DomainsBlocked = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "domains_being_blocked", Namespace: "pihole", Help: "This represent the number of domains being blocked", }, + []string{"hostname"}, ) // DNSQueriesToday - The number of DNS requests made over PI-Hole over the current day. - DNSQueriesToday = prometheus.NewGauge( + DNSQueriesToday = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "dns_queries_today", Namespace: "pihole", Help: "This represent the number of DNS queries made over the current day", }, + []string{"hostname"}, ) // AdsBlockedToday - The number of ads blocked by PI-Hole over the current day. - AdsBlockedToday = prometheus.NewGauge( + AdsBlockedToday = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "ads_blocked_today", Namespace: "pihole", Help: "This represent the number of ads blocked over the current day", }, + []string{"hostname"}, ) // AdsPercentageToday - The percentage of ads blocked by PI-Hole over the current day. - AdsPercentageToday = prometheus.NewGauge( + AdsPercentageToday = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "ads_percentage_today", Namespace: "pihole", Help: "This represent the percentage of ads blocked over the current day", }, + []string{"hostname"}, ) // UniqueDomains - The number of unique domains seen by PI-Hole. - UniqueDomains = prometheus.NewGauge( + UniqueDomains = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "unique_domains", Namespace: "pihole", Help: "This represent the number of unique domains seen", }, + []string{"hostname"}, ) // QueriesForwarded - The number of queries forwarded by PI-Hole. - QueriesForwarded = prometheus.NewGauge( + QueriesForwarded = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "queries_forwarded", Namespace: "pihole", Help: "This represent the number of queries forwarded", }, + []string{"hostname"}, ) // QueriesCached - The number of queries cached by PI-Hole. - QueriesCached = prometheus.NewGauge( + QueriesCached = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "queries_cached", Namespace: "pihole", Help: "This represent the number of queries cached", }, + []string{"hostname"}, ) // ClientsEverSeen - The number of clients ever seen by PI-Hole. - ClientsEverSeen = prometheus.NewGauge( + ClientsEverSeen = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "clients_ever_seen", Namespace: "pihole", Help: "This represent the number of clients ever seen", }, + []string{"hostname"}, ) // UniqueClients - The number of unique clients seen by PI-Hole. - UniqueClients = prometheus.NewGauge( + UniqueClients = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "unique_clients", Namespace: "pihole", Help: "This represent the number of unique clients seen", }, + []string{"hostname"}, ) - // DnsQueriesAllTypes - The number of DNS queries made for all types by PI-Hole. - DnsQueriesAllTypes = prometheus.NewGauge( + // DNSQueriesAllTypes - The number of DNS queries made for all types by PI-Hole. + DNSQueriesAllTypes = prometheus.NewGaugeVec( prometheus.GaugeOpts{ Name: "dns_queries_all_types", Namespace: "pihole", Help: "This represent the number of DNS queries made for all types", }, + []string{"hostname"}, + ) + + // Reply - The number of replies made for every types by PI-Hole. + Reply = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "reply", + Namespace: "pihole", + Help: "This represent the number of replies made for all types", + }, + []string{"hostname", "type"}, + ) + + // TopQueries - The number of top queries made by PI-Hole by domain. + TopQueries = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "top_queries", + Namespace: "pihole", + Help: "This represent the number of top queries made by PI-Hole by domain", + }, + []string{"hostname", "domain"}, + ) + + // TopAds - The number of top ads made by PI-Hole by domain. + TopAds = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "top_ads", + Namespace: "pihole", + Help: "This represent the number of top ads made by PI-Hole by domain", + }, + []string{"hostname", "domain"}, + ) + + // TopSources - The number of top sources requests made by PI-Hole by source host. + TopSources = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "top_sources", + Namespace: "pihole", + Help: "This represent the number of top sources requests made by PI-Hole by source host", + }, + []string{"hostname", "source"}, + ) + + // ForwardDestinations - The number of forward destinations requests made by PI-Hole by destination. + ForwardDestinations = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "forward_destinations", + Namespace: "pihole", + Help: "This represent the number of forward destinations requests made by PI-Hole by destination", + }, + []string{"hostname", "destination"}, + ) + + // QueryTypes - The number of queries made by PI-Hole by type. + QueryTypes = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "querytypes", + Namespace: "pihole", + Help: "This represent the number of queries made by PI-Hole by type", + }, + []string{"hostname", "type"}, + ) + + // Status - Is PI-Hole enabled? + Status = prometheus.NewGaugeVec( + prometheus.GaugeOpts{ + Name: "status", + Namespace: "pihole", + Help: "This if PI-Hole is enabled", + }, + []string{"hostname"}, ) ) -// Init initializes Prometheus metrics +// Init initializes all Prometheus metrics made available by PI-Hole exporter. func Init() { - initMetric(DomainsBlocked) - initMetric(DNSQueriesToday) - initMetric(AdsBlockedToday) - initMetric(AdsPercentageToday) - initMetric(UniqueDomains) - initMetric(QueriesForwarded) - initMetric(QueriesCached) - initMetric(ClientsEverSeen) - initMetric(UniqueClients) - initMetric(DnsQueriesAllTypes) + initMetric("domains_blocked", DomainsBlocked) + initMetric("dns_queries_today", DNSQueriesToday) + initMetric("ads_blocked_today", AdsBlockedToday) + initMetric("ads_percentag_today", AdsPercentageToday) + initMetric("unique_domains", UniqueDomains) + initMetric("queries_forwarded", QueriesForwarded) + initMetric("queries_cached", QueriesCached) + initMetric("clients_ever_seen", ClientsEverSeen) + initMetric("unique_clients", UniqueClients) + initMetric("dns_queries_all_types", DNSQueriesAllTypes) + initMetric("reply", Reply) + initMetric("top_queries", TopQueries) + initMetric("top_ads", TopAds) + initMetric("top_sources", TopSources) + initMetric("forward_destinations", ForwardDestinations) + initMetric("querytypes", QueryTypes) + initMetric("status", Status) } -func initMetric(metric prometheus.Gauge) { +func initMetric(name string, metric *prometheus.GaugeVec) { prometheus.MustRegister(metric) - log.Printf("New prometheus metric registered: %s", metric.Desc().String()) + log.Printf("New Prometheus metric registered: %s", name) } diff --git a/internal/pihole/client.go b/internal/pihole/client.go index 2261682..8d7d0cd 100644 --- a/internal/pihole/client.go +++ b/internal/pihole/client.go @@ -20,6 +20,7 @@ var ( statsURLPattern = "http://%s/admin/api.php?summaryRaw&overTimeData&topItems&recentItems&getQueryTypes&getForwardDestinations&getQuerySources&jsonForceObject" ) +// Client struct is a PI-Hole client to request an instance of a PI-Hole ad blocker. type Client struct { hostname string password string @@ -27,6 +28,7 @@ type Client struct { httpClient http.Client } +// NewClient method initializes a new PI-Hole client. func NewClient(hostname, password string, interval time.Duration) *Client { return &Client{ hostname: hostname, @@ -40,7 +42,9 @@ func NewClient(hostname, password string, interval time.Duration) *Client { } } -func (c *Client) Fetch() { +// Scrape method logins and retrieves statistics from PI-Hole JSON API +// and then pass them as Prometheus metrics. +func (c *Client) Scrape() { for range time.Tick(c.interval) { sessionID := c.getPHPSessionID() if sessionID == nil { @@ -49,19 +53,53 @@ func (c *Client) Fetch() { } stats := c.getStatistics(*sessionID) + c.setMetrics(stats) - log.Println("New tick of statistics", stats) + log.Printf("New tick of statistics: %s", stats.ToString()) + } +} - metrics.DomainsBlocked.Set(float64(stats.DomainsBeingBlocked)) - metrics.DNSQueriesToday.Set(float64(stats.DNSQueriesToday)) - metrics.AdsBlockedToday.Set(float64(stats.AdsBlockedToday)) - metrics.AdsPercentageToday.Set(float64(stats.AdsPercentageToday)) - metrics.UniqueDomains.Set(float64(stats.UniqueDomains)) - metrics.QueriesForwarded.Set(float64(stats.QueriesForwarded)) - metrics.QueriesCached.Set(float64(stats.QueriesCached)) - metrics.ClientsEverSeen.Set(float64(stats.ClientsEverSeen)) - metrics.UniqueClients.Set(float64(stats.UniqueClients)) - metrics.DnsQueriesAllTypes.Set(float64(stats.DnsQueriesAllTypes)) +func (c *Client) setMetrics(stats *Stats) { + metrics.DomainsBlocked.WithLabelValues(c.hostname).Set(float64(stats.DomainsBeingBlocked)) + metrics.DNSQueriesToday.WithLabelValues(c.hostname).Set(float64(stats.DNSQueriesToday)) + metrics.AdsBlockedToday.WithLabelValues(c.hostname).Set(float64(stats.AdsBlockedToday)) + metrics.AdsPercentageToday.WithLabelValues(c.hostname).Set(float64(stats.AdsPercentageToday)) + metrics.UniqueDomains.WithLabelValues(c.hostname).Set(float64(stats.UniqueDomains)) + metrics.QueriesForwarded.WithLabelValues(c.hostname).Set(float64(stats.QueriesForwarded)) + metrics.QueriesCached.WithLabelValues(c.hostname).Set(float64(stats.QueriesCached)) + metrics.ClientsEverSeen.WithLabelValues(c.hostname).Set(float64(stats.ClientsEverSeen)) + metrics.UniqueClients.WithLabelValues(c.hostname).Set(float64(stats.UniqueClients)) + metrics.DNSQueriesAllTypes.WithLabelValues(c.hostname).Set(float64(stats.DNSQueriesAllTypes)) + + metrics.Reply.WithLabelValues(c.hostname, "no_data").Set(float64(stats.ReplyNoData)) + metrics.Reply.WithLabelValues(c.hostname, "nx_domain").Set(float64(stats.ReplyNxDomain)) + metrics.Reply.WithLabelValues(c.hostname, "cname").Set(float64(stats.ReplyCname)) + metrics.Reply.WithLabelValues(c.hostname, "ip").Set(float64(stats.ReplyIP)) + + var isEnabled int = 0 + if stats.Status == enabledStatus { + isEnabled = 1 + } + metrics.Status.WithLabelValues(c.hostname).Set(float64(isEnabled)) + + for domain, value := range stats.TopQueries { + metrics.TopQueries.WithLabelValues(c.hostname, domain).Set(float64(value)) + } + + for domain, value := range stats.TopAds { + metrics.TopAds.WithLabelValues(c.hostname, domain).Set(float64(value)) + } + + for source, value := range stats.TopSources { + metrics.TopSources.WithLabelValues(c.hostname, source).Set(float64(value)) + } + + for destination, value := range stats.ForwardDestinations { + metrics.ForwardDestinations.WithLabelValues(c.hostname, destination).Set(value) + } + + for queryType, value := range stats.QueryTypes { + metrics.QueryTypes.WithLabelValues(c.hostname, queryType).Set(value) } } diff --git a/internal/pihole/model.go b/internal/pihole/model.go index e676612..d800d0b 100644 --- a/internal/pihole/model.go +++ b/internal/pihole/model.go @@ -1,15 +1,36 @@ package pihole -// Stats is the PI-Hole statistics JSON API corresponding model +import "fmt" + +const ( + enabledStatus = "enabled" +) + +// Stats struct is the PI-Hole statistics JSON API corresponding model. type Stats struct { - DomainsBeingBlocked int `json:"domains_being_blocked"` - DNSQueriesToday int `json:"dns_queries_today"` - AdsBlockedToday int `json:"ads_blocked_today"` - AdsPercentageToday float64 `json:"ads_percentage_today"` - UniqueDomains int `json:"unique_domains"` - QueriesForwarded int `json:"queries_forwarded"` - QueriesCached int `json:"queries_cached"` - ClientsEverSeen int `json:"clients_ever_seen"` - UniqueClients int `json:"unique_clients"` - DnsQueriesAllTypes int `json:"dns_queries_all_types"` + DomainsBeingBlocked int `json:"domains_being_blocked"` + DNSQueriesToday int `json:"dns_queries_today"` + AdsBlockedToday int `json:"ads_blocked_today"` + AdsPercentageToday float64 `json:"ads_percentage_today"` + UniqueDomains int `json:"unique_domains"` + QueriesForwarded int `json:"queries_forwarded"` + QueriesCached int `json:"queries_cached"` + ClientsEverSeen int `json:"clients_ever_seen"` + UniqueClients int `json:"unique_clients"` + DNSQueriesAllTypes int `json:"dns_queries_all_types"` + ReplyNoData int `json:"reply_NODATA"` + ReplyNxDomain int `json:"reply_NXDOMAIN"` + ReplyCname int `json:"reply_CNAME"` + ReplyIP int `json:"reply_IP"` + TopQueries map[string]int `json:"top_queries"` + TopAds map[string]int `json:"top_ads"` + TopSources map[string]int `json:"top_sources"` + ForwardDestinations map[string]float64 `json:"forward_destinations"` + QueryTypes map[string]float64 `json:"querytypes"` + Status string `json:"status"` +} + +// ToString method returns a string of the current statistics struct. +func (s *Stats) ToString() string { + return fmt.Sprintf("%d ads blocked / %d total DNS queries", s.AdsBlockedToday, s.DNSQueriesAllTypes) } diff --git a/internal/server/server.go b/internal/server/server.go index e7adf94..dd20015 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -9,10 +9,13 @@ import ( "golang.org/x/net/context" ) +// Server is the struct for the HTTP server. type Server struct { httpServer *http.Server } +// NewServer method initializes a new HTTP server instance and associates +// the different routes that will be used by Prometheus (metrics) or for monitoring (readiness, liveness). func NewServer(port string) *Server { mux := http.NewServeMux() httpServer := &http.Server{Addr: ":" + port, Handler: mux} @@ -28,6 +31,7 @@ func NewServer(port string) *Server { return s } +// ListenAndServe method serves HTTP requests. func (s *Server) ListenAndServe() { log.Println("Starting HTTP server") @@ -37,6 +41,7 @@ func (s *Server) ListenAndServe() { } } +// Stop method stops the HTTP server (so the exporter become unavailable). func (s *Server) Stop() { ctx, cancel := context.WithTimeout(context.Background(), time.Second) defer cancel() diff --git a/main.go b/main.go index 4842770..89a5b3c 100644 --- a/main.go +++ b/main.go @@ -34,7 +34,7 @@ func main() { func initPiholeClient(hostname, password string, interval time.Duration) { client := pihole.NewClient(hostname, password, interval) - go client.Fetch() + go client.Scrape() } func initHttpServer(port string) {