diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 11:19:16 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-05 12:07:37 +0000 |
commit | b485aab7e71c1625cfc27e0f92c9509f42378458 (patch) | |
tree | ae9abe108601079d1679194de237c9a435ae5b55 /src/go/collectors/go.d.plugin/modules/pihole | |
parent | Adding upstream version 1.44.3. (diff) | |
download | netdata-b485aab7e71c1625cfc27e0f92c9509f42378458.tar.xz netdata-b485aab7e71c1625cfc27e0f92c9509f42378458.zip |
Adding upstream version 1.45.3+dfsg.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/go/collectors/go.d.plugin/modules/pihole')
16 files changed, 1746 insertions, 0 deletions
diff --git a/src/go/collectors/go.d.plugin/modules/pihole/README.md b/src/go/collectors/go.d.plugin/modules/pihole/README.md new file mode 120000 index 000000000..b8d3a7b40 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/README.md @@ -0,0 +1 @@ +integrations/pi-hole.md
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/pihole/charts.go b/src/go/collectors/go.d.plugin/modules/pihole/charts.go new file mode 100644 index 000000000..d8e0bd00a --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/charts.go @@ -0,0 +1,166 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package pihole + +import ( + "github.com/netdata/netdata/go/go.d.plugin/agent/module" +) + +const ( + prioDNSQueriesTotal = module.Priority + iota + prioDNSQueries + prioDNSQueriesPerc + prioUniqueClients + prioDomainsOnBlocklist + prioBlocklistLastUpdate + prioUnwantedDomainsBlockingStatus + + prioDNSQueriesTypes + prioDNSQueriesForwardedDestination +) + +var baseCharts = module.Charts{ + chartDNSQueriesTotal.Copy(), + chartDNSQueries.Copy(), + chartDNSQueriesPerc.Copy(), + chartUniqueClients.Copy(), + chartDomainsOnBlocklist.Copy(), + chartBlocklistLastUpdate.Copy(), + chartUnwantedDomainsBlockingStatus.Copy(), +} + +var ( + chartDNSQueriesTotal = module.Chart{ + ID: "dns_queries_total", + Title: "DNS Queries Total (Cached, Blocked and Forwarded)", + Units: "queries", + Fam: "queries", + Ctx: "pihole.dns_queries_total", + Priority: prioDNSQueriesTotal, + Dims: module.Dims{ + {ID: "dns_queries_today", Name: "queries"}, + }, + } + chartDNSQueries = module.Chart{ + ID: "dns_queries", + Title: "DNS Queries", + Units: "queries", + Fam: "queries", + Ctx: "pihole.dns_queries", + Type: module.Stacked, + Priority: prioDNSQueries, + Dims: module.Dims{ + {ID: "queries_cached", Name: "cached"}, + {ID: "ads_blocked_today", Name: "blocked"}, + {ID: "queries_forwarded", Name: "forwarded"}, + }, + } + chartDNSQueriesPerc = module.Chart{ + ID: "dns_queries_percentage", + Title: "DNS Queries Percentage", + Units: "percentage", + Fam: "queries", + Ctx: "pihole.dns_queries_percentage", + Type: module.Stacked, + Priority: prioDNSQueriesPerc, + Dims: module.Dims{ + {ID: "queries_cached_perc", Name: "cached", Div: precision}, + {ID: "ads_blocked_today_perc", Name: "blocked", Div: precision}, + {ID: "queries_forwarded_perc", Name: "forwarded", Div: precision}, + }, + } + chartUniqueClients = module.Chart{ + ID: "unique_clients", + Title: "Unique Clients", + Units: "clients", + Fam: "clients", + Ctx: "pihole.unique_clients", + Priority: prioUniqueClients, + Dims: module.Dims{ + {ID: "unique_clients", Name: "unique"}, + }, + } + chartDomainsOnBlocklist = module.Chart{ + ID: "domains_on_blocklist", + Title: "Domains On Blocklist", + Units: "domains", + Fam: "blocklist", + Ctx: "pihole.domains_on_blocklist", + Priority: prioDomainsOnBlocklist, + Dims: module.Dims{ + {ID: "domains_being_blocked", Name: "blocklist"}, + }, + } + chartBlocklistLastUpdate = module.Chart{ + ID: "blocklist_last_update", + Title: "Blocklist Last Update", + Units: "seconds", + Fam: "blocklist", + Ctx: "pihole.blocklist_last_update", + Priority: prioBlocklistLastUpdate, + Dims: module.Dims{ + {ID: "blocklist_last_update", Name: "ago"}, + }, + } + chartUnwantedDomainsBlockingStatus = module.Chart{ + ID: "unwanted_domains_blocking_status", + Title: "Unwanted Domains Blocking Status", + Units: "status", + Fam: "status", + Ctx: "pihole.unwanted_domains_blocking_status", + Priority: prioUnwantedDomainsBlockingStatus, + Dims: module.Dims{ + {ID: "blocking_status_enabled", Name: "enabled"}, + {ID: "blocking_status_disabled", Name: "disabled"}, + }, + } +) + +var ( + chartDNSQueriesTypes = module.Chart{ + ID: "dns_queries_types", + Title: "DNS Queries Per Type", + Units: "percentage", + Fam: "doQuery types", + Ctx: "pihole.dns_queries_types", + Type: module.Stacked, + Priority: prioDNSQueriesTypes, + Dims: module.Dims{ + {ID: "A", Div: 100}, + {ID: "AAAA", Div: 100}, + {ID: "ANY", Div: 100}, + {ID: "PTR", Div: 100}, + {ID: "SOA", Div: 100}, + {ID: "SRV", Div: 100}, + {ID: "TXT", Div: 100}, + }, + } + chartDNSQueriesForwardedDestination = module.Chart{ + ID: "dns_queries_forwarded_destination", + Title: "DNS Queries Per Destination", + Units: "percentage", + Fam: "queries answered by", + Ctx: "pihole.dns_queries_forwarded_destination", + Type: module.Stacked, + Priority: prioDNSQueriesForwardedDestination, + Dims: module.Dims{ + {ID: "destination_cached", Name: "cached", Div: 100}, + {ID: "destination_blocked", Name: "blocked", Div: 100}, + {ID: "destination_other", Name: "other", Div: 100}, + }, + } +) + +func (p *Pihole) addChartDNSQueriesType() { + chart := chartDNSQueriesTypes.Copy() + if err := p.Charts().Add(chart); err != nil { + p.Warning(err) + } +} + +func (p *Pihole) addChartDNSQueriesForwardedDestinations() { + chart := chartDNSQueriesForwardedDestination.Copy() + if err := p.Charts().Add(chart); err != nil { + p.Warning(err) + } +} diff --git a/src/go/collectors/go.d.plugin/modules/pihole/collect.go b/src/go/collectors/go.d.plugin/modules/pihole/collect.go new file mode 100644 index 000000000..ab0e48ff0 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/collect.go @@ -0,0 +1,274 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package pihole + +import ( + "encoding/json" + "fmt" + "io" + "net/http" + "net/url" + "strings" + "sync" + "time" + + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +const wantAPIVersion = 3 + +const ( + urlPathAPI = "/admin/api.php" + urlQueryKeyAuth = "auth" + urlQueryKeyAPIVersion = "version" + urlQueryKeySummaryRaw = "summaryRaw" + urlQueryKeyGetQueryTypes = "getQueryTypes" // need auth + urlQueryKeyGetForwardDestinations = "getForwardDestinations" // need auth +) + +const ( + precision = 1000 +) + +func (p *Pihole) collect() (map[string]int64, error) { + if p.checkVersion { + ver, err := p.queryAPIVersion() + if err != nil { + return nil, err + } + if ver != wantAPIVersion { + return nil, fmt.Errorf("API version: %d, supported version: %d", ver, wantAPIVersion) + } + p.checkVersion = false + } + + pmx := new(piholeMetrics) + p.queryMetrics(pmx, true) + + if pmx.hasQueryTypes() { + p.addQueriesTypesOnce.Do(p.addChartDNSQueriesType) + } + if pmx.hasForwarders() { + p.addFwsDestinationsOnce.Do(p.addChartDNSQueriesForwardedDestinations) + } + + mx := make(map[string]int64) + p.collectMetrics(mx, pmx) + + return mx, nil +} + +func (p *Pihole) collectMetrics(mx map[string]int64, pmx *piholeMetrics) { + if pmx.hasSummary() { + mx["ads_blocked_today"] = pmx.summary.AdsBlockedToday + mx["ads_percentage_today"] = int64(pmx.summary.AdsPercentageToday * 100) + mx["domains_being_blocked"] = pmx.summary.DomainsBeingBlocked + // GravityLastUpdated.Absolute is <nil> if the file does not exist (deleted/moved) + if pmx.summary.GravityLastUpdated.Absolute != nil { + mx["blocklist_last_update"] = time.Now().Unix() - *pmx.summary.GravityLastUpdated.Absolute + } + mx["dns_queries_today"] = pmx.summary.DNSQueriesToday + mx["queries_forwarded"] = pmx.summary.QueriesForwarded + mx["queries_cached"] = pmx.summary.QueriesCached + mx["unique_clients"] = pmx.summary.UniqueClients + mx["blocking_status_enabled"] = boolToInt(pmx.summary.Status == "enabled") + mx["blocking_status_disabled"] = boolToInt(pmx.summary.Status != "enabled") + + tot := pmx.summary.QueriesCached + pmx.summary.AdsBlockedToday + pmx.summary.QueriesForwarded + mx["queries_cached_perc"] = calcPercentage(pmx.summary.QueriesCached, tot) + mx["ads_blocked_today_perc"] = calcPercentage(pmx.summary.AdsBlockedToday, tot) + mx["queries_forwarded_perc"] = calcPercentage(pmx.summary.QueriesForwarded, tot) + } + + if pmx.hasQueryTypes() { + mx["A"] = int64(pmx.queryTypes.Types.A * 100) + mx["AAAA"] = int64(pmx.queryTypes.Types.AAAA * 100) + mx["ANY"] = int64(pmx.queryTypes.Types.ANY * 100) + mx["PTR"] = int64(pmx.queryTypes.Types.PTR * 100) + mx["SOA"] = int64(pmx.queryTypes.Types.SOA * 100) + mx["SRV"] = int64(pmx.queryTypes.Types.SRV * 100) + mx["TXT"] = int64(pmx.queryTypes.Types.TXT * 100) + } + + if pmx.hasForwarders() { + for k, v := range pmx.forwarders.Destinations { + name := strings.Split(k, "|")[0] + mx["destination_"+name] = int64(v * 100) + } + } +} + +func (p *Pihole) queryMetrics(pmx *piholeMetrics, doConcurrently bool) { + type task func(*piholeMetrics) + + var tasks = []task{p.querySummary} + + if p.Password != "" { + tasks = []task{ + p.querySummary, + p.queryQueryTypes, + p.queryForwardedDestinations, + } + } + + wg := &sync.WaitGroup{} + + wrap := func(call task) task { + return func(metrics *piholeMetrics) { call(metrics); wg.Done() } + } + + for _, task := range tasks { + if doConcurrently { + wg.Add(1) + task = wrap(task) + go task(pmx) + } else { + task(pmx) + } + } + + wg.Wait() +} + +func (p *Pihole) querySummary(pmx *piholeMetrics) { + req, err := web.NewHTTPRequest(p.Request) + if err != nil { + p.Error(err) + return + } + + req.URL.Path = urlPathAPI + req.URL.RawQuery = url.Values{ + urlQueryKeyAuth: []string{p.Password}, + urlQueryKeySummaryRaw: []string{"true"}, + }.Encode() + + var v summaryRawMetrics + if err = p.doWithDecode(&v, req); err != nil { + p.Error(err) + return + } + + pmx.summary = &v +} + +func (p *Pihole) queryQueryTypes(pmx *piholeMetrics) { + req, err := web.NewHTTPRequest(p.Request) + if err != nil { + p.Error(err) + return + } + + req.URL.Path = urlPathAPI + req.URL.RawQuery = url.Values{ + urlQueryKeyAuth: []string{p.Password}, + urlQueryKeyGetQueryTypes: []string{"true"}, + }.Encode() + + var v queryTypesMetrics + err = p.doWithDecode(&v, req) + if err != nil { + p.Error(err) + return + } + + pmx.queryTypes = &v +} + +func (p *Pihole) queryForwardedDestinations(pmx *piholeMetrics) { + req, err := web.NewHTTPRequest(p.Request) + if err != nil { + p.Error(err) + return + } + + req.URL.Path = urlPathAPI + req.URL.RawQuery = url.Values{ + urlQueryKeyAuth: []string{p.Password}, + urlQueryKeyGetForwardDestinations: []string{"true"}, + }.Encode() + + var v forwardDestinations + err = p.doWithDecode(&v, req) + if err != nil { + p.Error(err) + return + } + + pmx.forwarders = &v +} + +func (p *Pihole) queryAPIVersion() (int, error) { + req, err := web.NewHTTPRequest(p.Request) + if err != nil { + return 0, err + } + + req.URL.Path = urlPathAPI + req.URL.RawQuery = url.Values{ + urlQueryKeyAuth: []string{p.Password}, + urlQueryKeyAPIVersion: []string{"true"}, + }.Encode() + + var v piholeAPIVersion + err = p.doWithDecode(&v, req) + if err != nil { + return 0, err + } + + return v.Version, nil +} + +func (p *Pihole) doWithDecode(dst interface{}, req *http.Request) error { + resp, err := p.httpClient.Do(req) + if err != nil { + return err + } + defer closeBody(resp) + + if resp.StatusCode != http.StatusOK { + return fmt.Errorf("%s returned %d status code", req.URL, resp.StatusCode) + } + + content, err := io.ReadAll(resp.Body) + if err != nil { + return fmt.Errorf("error on reading response from %s : %v", req.URL, err) + } + + // empty array if unauthorized query or wrong query + if isEmptyArray(content) { + return fmt.Errorf("unauthorized access to %s", req.URL) + } + + if err := json.Unmarshal(content, dst); err != nil { + return fmt.Errorf("error on parsing response from %s : %v", req.URL, err) + } + + return nil +} + +func isEmptyArray(data []byte) bool { + empty := "[]" + return len(data) == len(empty) && string(data) == empty +} + +func closeBody(resp *http.Response) { + if resp != nil && resp.Body != nil { + _, _ = io.Copy(io.Discard, resp.Body) + _ = resp.Body.Close() + } +} + +func boolToInt(b bool) int64 { + if !b { + return 0 + } + return 1 +} + +func calcPercentage(value, total int64) (v int64) { + if total == 0 { + return 0 + } + return int64(float64(value) * 100 / float64(total) * precision) +} diff --git a/src/go/collectors/go.d.plugin/modules/pihole/config_schema.json b/src/go/collectors/go.d.plugin/modules/pihole/config_schema.json new file mode 100644 index 000000000..72d3edf25 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/config_schema.json @@ -0,0 +1,170 @@ +{ + "jsonSchema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Pi-hole collector configuration.", + "type": "object", + "properties": { + "update_every": { + "title": "Update every", + "description": "Data collection interval, measured in seconds.", + "type": "integer", + "minimum": 1, + "default": 1 + }, + "url": { + "title": "URL", + "description": "The base URL of the Pi-hole instance.", + "type": "string", + "default": "http://127.0.0.1:80", + "format": "uri" + }, + "timeout": { + "title": "Timeout", + "description": "The timeout in seconds for the HTTP request.", + "type": "number", + "minimum": 0.5, + "default": 1 + }, + "not_follow_redirects": { + "title": "Not follow redirects", + "description": "If set, the client will not follow HTTP redirects automatically.", + "type": "boolean" + }, + "setup_vars_path": { + "title": "Path to setupVars.conf", + "description": "This file is used to get the web password.", + "type": "string", + "default": "/etc/pihole/setupVars.conf" + }, + "username": { + "title": "Username", + "description": "The username for basic authentication.", + "type": "string", + "sensitive": true + }, + "password": { + "title": "Password", + "description": "The password for basic authentication.", + "type": "string", + "sensitive": true + }, + "proxy_url": { + "title": "Proxy URL", + "description": "The URL of the proxy server.", + "type": "string" + }, + "proxy_username": { + "title": "Proxy username", + "description": "The username for proxy authentication.", + "type": "string", + "sensitive": true + }, + "proxy_password": { + "title": "Proxy password", + "description": "The password for proxy authentication.", + "type": "string", + "sensitive": true + }, + "headers": { + "title": "Headers", + "description": "Additional HTTP headers to include in the request.", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "tls_skip_verify": { + "title": "Skip TLS verification", + "description": "If set, TLS certificate verification will be skipped.", + "type": "boolean" + }, + "tls_ca": { + "title": "TLS CA", + "description": "The path to the CA certificate file for TLS verification.", + "type": "string", + "pattern": "^$|^/" + }, + "tls_cert": { + "title": "TLS certificate", + "description": "The path to the client certificate file for TLS authentication.", + "type": "string", + "pattern": "^$|^/" + }, + "tls_key": { + "title": "TLS key", + "description": "The path to the client key file for TLS authentication.", + "type": "string", + "pattern": "^$|^/" + } + }, + "required": [ + "url" + ], + "additionalProperties": false, + "patternProperties": { + "^name$": {} + } + }, + "uiSchema": { + "uiOptions": { + "fullPage": true + }, + "timeout": { + "ui:help": "Accepts decimals for precise control (e.g., type 1.5 for 1.5 seconds)." + }, + "password": { + "ui:widget": "password" + }, + "proxy_password": { + "ui:widget": "password" + }, + "ui:flavour": "tabs", + "ui:options": { + "tabs": [ + { + "title": "Base", + "fields": [ + "update_every", + "url", + "timeout", + "not_follow_redirects", + "setup_vars_path" + ] + }, + { + "title": "Auth", + "fields": [ + "username", + "password" + ] + }, + { + "title": "TLS", + "fields": [ + "tls_skip_verify", + "tls_ca", + "tls_cert", + "tls_key" + ] + }, + { + "title": "Proxy", + "fields": [ + "proxy_url", + "proxy_username", + "proxy_password" + ] + }, + { + "title": "Headers", + "fields": [ + "headers" + ] + } + ] + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/pihole/init.go b/src/go/collectors/go.d.plugin/modules/pihole/init.go new file mode 100644 index 000000000..982849452 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/init.go @@ -0,0 +1,78 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package pihole + +import ( + "bufio" + "errors" + "fmt" + "net/http" + "os" + "strings" + + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +func (p *Pihole) validateConfig() error { + if p.URL == "" { + return errors.New("url not set") + } + return nil +} + +func (p *Pihole) initHTTPClient() (*http.Client, error) { + return web.NewHTTPClient(p.Client) +} + +func (p *Pihole) getWebPassword() string { + // do no read setupVarsPath is password is set in the configuration file + if p.Password != "" { + return p.Password + } + if !isLocalHost(p.URL) { + p.Info("abort web password auto detection, host is not localhost") + return "" + } + + p.Infof("starting web password auto detection, reading : %s", p.SetupVarsPath) + pass, err := getWebPassword(p.SetupVarsPath) + if err != nil { + p.Warningf("error during reading '%s' : %v", p.SetupVarsPath, err) + } + + return pass +} + +func getWebPassword(path string) (string, error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer func() { _ = f.Close() }() + + s := bufio.NewScanner(f) + var password string + + for s.Scan() && password == "" { + if strings.HasPrefix(s.Text(), "WEBPASSWORD") { + parts := strings.Split(s.Text(), "=") + if len(parts) != 2 { + return "", fmt.Errorf("unparsable line : %s", s.Text()) + } + password = parts[1] + } + } + + return password, nil +} + +func isLocalHost(u string) bool { + if strings.Contains(u, "127.0.0.1") { + return true + } + if strings.Contains(u, "localhost") { + return true + } + + return false +} diff --git a/src/go/collectors/go.d.plugin/modules/pihole/integrations/pi-hole.md b/src/go/collectors/go.d.plugin/modules/pihole/integrations/pi-hole.md new file mode 100644 index 000000000..9a67e3b51 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/integrations/pi-hole.md @@ -0,0 +1,224 @@ +<!--startmeta +custom_edit_url: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/pihole/README.md" +meta_yaml: "https://github.com/netdata/netdata/edit/master/src/go/collectors/go.d.plugin/modules/pihole/metadata.yaml" +sidebar_label: "Pi-hole" +learn_status: "Published" +learn_rel_path: "Collecting Metrics/DNS and DHCP Servers" +most_popular: False +message: "DO NOT EDIT THIS FILE DIRECTLY, IT IS GENERATED BY THE COLLECTOR'S metadata.yaml FILE" +endmeta--> + +# Pi-hole + + +<img src="https://netdata.cloud/img/pihole.png" width="150"/> + + +Plugin: go.d.plugin +Module: pihole + +<img src="https://img.shields.io/badge/maintained%20by-Netdata-%2300ab44" /> + +## Overview + +This collector monitors Pi-hole instances using [PHP API](https://github.com/pi-hole/AdminLTE). + +The data provided by the API is for the last 24 hours. All collected values refer to this time period and not to the +module's collection interval. + + + + +This collector is supported on all platforms. + +This collector supports collecting metrics from multiple instances of this integration, including remote instances. + + +### Default Behavior + +#### Auto-Detection + +This integration doesn't support auto-detection. + +#### Limits + +The default configuration for this integration does not impose any limits on data collection. + +#### Performance Impact + +The default configuration for this integration is not expected to impose a significant performance impact on the system. + + +## Metrics + +Metrics grouped by *scope*. + +The scope defines the instance that the metric belongs to. An instance is uniquely identified by a set of labels. + + + +### Per Pi-hole instance + +These metrics refer to the entire monitored application. + +This scope has no labels. + +Metrics: + +| Metric | Dimensions | Unit | +|:------|:----------|:----| +| pihole.dns_queries_total | queries | queries | +| pihole.dns_queries | cached, blocked, forwarded | queries | +| pihole.dns_queries_percentage | cached, blocked, forwarded | percentage | +| pihole.unique_clients | unique | clients | +| pihole.domains_on_blocklist | blocklist | domains | +| pihole.blocklist_last_update | ago | seconds | +| pihole.unwanted_domains_blocking_status | enabled, disabled | status | +| pihole.dns_queries_types | a, aaaa, any, ptr, soa, srv, txt | percentage | +| pihole.dns_queries_forwarded_destination | cached, blocked, other | percentage | + + + +## Alerts + + +The following alerts are available: + +| Alert name | On metric | Description | +|:------------|:----------|:------------| +| [ pihole_blocklist_last_update ](https://github.com/netdata/netdata/blob/master/src/health/health.d/pihole.conf) | pihole.blocklist_last_update | gravity.list (blocklist) file last update time | +| [ pihole_status ](https://github.com/netdata/netdata/blob/master/src/health/health.d/pihole.conf) | pihole.unwanted_domains_blocking_status | unwanted domains blocking is disabled | + + +## Setup + +### Prerequisites + +No action required. + +### Configuration + +#### File + +The configuration file name for this integration is `go.d/pihole.conf`. + + +You can edit the configuration file using the `edit-config` script from the +Netdata [config directory](https://github.com/netdata/netdata/blob/master/docs/netdata-agent/configuration.md#the-netdata-config-directory). + +```bash +cd /etc/netdata 2>/dev/null || cd /opt/netdata/etc/netdata +sudo ./edit-config go.d/pihole.conf +``` +#### Options + +The following options can be defined globally: update_every, autodetection_retry. + + +<details><summary>Config options</summary> + +| Name | Description | Default | Required | +|:----|:-----------|:-------|:--------:| +| update_every | Data collection frequency. | 5 | no | +| autodetection_retry | Recheck interval in seconds. Zero means no recheck will be scheduled. | 0 | no | +| url | Server URL. | http://127.0.0.1 | yes | +| setup_vars_path | Path to setupVars.conf. This file is used to get the web password. | /etc/pihole/setupVars.conf | no | +| timeout | HTTP request timeout. | 5 | no | +| username | Username for basic HTTP authentication. | | no | +| password | Password for basic HTTP authentication. | | no | +| proxy_url | Proxy URL. | | no | +| proxy_username | Username for proxy basic HTTP authentication. | | no | +| proxy_password | Password for proxy basic HTTP authentication. | | no | +| method | HTTP request method. | GET | no | +| body | HTTP request body. | | no | +| headers | HTTP request headers. | | no | +| not_follow_redirects | Redirect handling policy. Controls whether the client follows redirects. | no | no | +| tls_skip_verify | Server certificate chain and hostname validation policy. Controls whether the client performs this check. | no | no | +| tls_ca | Certification authority that the client uses when verifying the server's certificates. | | no | +| tls_cert | Client TLS certificate. | | no | +| tls_key | Client TLS key. | | no | + +</details> + +#### Examples + +##### Basic + +A basic example configuration. + +<details><summary>Config</summary> + +```yaml +jobs: + - name: local + url: http://127.0.0.1 + +``` +</details> + +##### HTTPS with self-signed certificate + +Remote instance with enabled HTTPS and self-signed certificate. + +<details><summary>Config</summary> + +```yaml +jobs: + - name: local + url: https://203.0.113.11 + tls_skip_verify: yes + password: 1ebd33f882f9aa5fac26a7cb74704742f91100228eb322e41b7bd6e6aeb8f74b + +``` +</details> + +##### Multi-instance + +> **Note**: When you define multiple jobs, their names must be unique. + +Collecting metrics from local and remote instances. + + +<details><summary>Config</summary> + +```yaml +jobs: + - name: local + url: http://127.0.0.1 + + - name: remote + url: http://203.0.113.10 + password: 1ebd33f882f9aa5fac26a7cb74704742f91100228eb322e41b7bd6e6aeb8f74b + +``` +</details> + + + +## Troubleshooting + +### Debug Mode + +To troubleshoot issues with the `pihole` collector, run the `go.d.plugin` with the debug option enabled. The output +should give you clues as to why the collector isn't working. + +- Navigate to the `plugins.d` directory, usually at `/usr/libexec/netdata/plugins.d/`. If that's not the case on + your system, open `netdata.conf` and look for the `plugins` setting under `[directories]`. + + ```bash + cd /usr/libexec/netdata/plugins.d/ + ``` + +- Switch to the `netdata` user. + + ```bash + sudo -u netdata -s + ``` + +- Run the `go.d.plugin` to debug the collector: + + ```bash + ./go.d.plugin -d -m pihole + ``` + + diff --git a/src/go/collectors/go.d.plugin/modules/pihole/metadata.yaml b/src/go/collectors/go.d.plugin/modules/pihole/metadata.yaml new file mode 100644 index 000000000..b6ef9656f --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/metadata.yaml @@ -0,0 +1,248 @@ +plugin_name: go.d.plugin +modules: + - meta: + id: collector-go.d.plugin-pihole + plugin_name: go.d.plugin + module_name: pihole + monitored_instance: + name: Pi-hole + link: https://pi-hole.net + icon_filename: pihole.png + categories: + - data-collection.dns-and-dhcp-servers + keywords: + - pihole + related_resources: + integrations: + list: [] + info_provided_to_referring_integrations: + description: "" + most_popular: false + overview: + data_collection: + metrics_description: | + This collector monitors Pi-hole instances using [PHP API](https://github.com/pi-hole/AdminLTE). + + The data provided by the API is for the last 24 hours. All collected values refer to this time period and not to the + module's collection interval. + method_description: "" + supported_platforms: + include: [] + exclude: [] + multi_instance: true + additional_permissions: + description: "" + default_behavior: + auto_detection: + description: "" + limits: + description: "" + performance_impact: + description: "" + setup: + prerequisites: + list: [] + configuration: + file: + name: go.d/pihole.conf + options: + description: | + The following options can be defined globally: update_every, autodetection_retry. + folding: + title: Config options + enabled: true + list: + - name: update_every + description: Data collection frequency. + default_value: 5 + required: false + - name: autodetection_retry + description: Recheck interval in seconds. Zero means no recheck will be scheduled. + default_value: 0 + required: false + - name: url + description: Server URL. + default_value: http://127.0.0.1 + required: true + - name: setup_vars_path + description: Path to setupVars.conf. This file is used to get the web password. + default_value: /etc/pihole/setupVars.conf + required: false + - name: timeout + description: HTTP request timeout. + default_value: 5 + required: false + - name: username + description: Username for basic HTTP authentication. + default_value: "" + required: false + - name: password + description: Password for basic HTTP authentication. + default_value: "" + required: false + - name: proxy_url + description: Proxy URL. + default_value: "" + required: false + - name: proxy_username + description: Username for proxy basic HTTP authentication. + default_value: "" + required: false + - name: proxy_password + description: Password for proxy basic HTTP authentication. + default_value: "" + required: false + - name: method + description: HTTP request method. + default_value: GET + required: false + - name: body + description: HTTP request body. + default_value: "" + required: false + - name: headers + description: HTTP request headers. + default_value: "" + required: false + - name: not_follow_redirects + description: Redirect handling policy. Controls whether the client follows redirects. + default_value: false + required: false + - name: tls_skip_verify + description: Server certificate chain and hostname validation policy. Controls whether the client performs this check. + default_value: false + required: false + - name: tls_ca + description: Certification authority that the client uses when verifying the server's certificates. + default_value: "" + required: false + - name: tls_cert + description: Client TLS certificate. + default_value: "" + required: false + - name: tls_key + description: Client TLS key. + default_value: "" + required: false + examples: + folding: + title: Config + enabled: true + list: + - name: Basic + description: A basic example configuration. + config: | + jobs: + - name: local + url: http://127.0.0.1 + - name: HTTPS with self-signed certificate + description: Remote instance with enabled HTTPS and self-signed certificate. + config: | + jobs: + - name: local + url: https://203.0.113.11 + tls_skip_verify: yes + password: 1ebd33f882f9aa5fac26a7cb74704742f91100228eb322e41b7bd6e6aeb8f74b + - name: Multi-instance + description: | + > **Note**: When you define multiple jobs, their names must be unique. + + Collecting metrics from local and remote instances. + config: | + jobs: + - name: local + url: http://127.0.0.1 + + - name: remote + url: http://203.0.113.10 + password: 1ebd33f882f9aa5fac26a7cb74704742f91100228eb322e41b7bd6e6aeb8f74b + troubleshooting: + problems: + list: [] + alerts: + - name: pihole_blocklist_last_update + metric: pihole.blocklist_last_update + info: "gravity.list (blocklist) file last update time" + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/pihole.conf + - name: pihole_status + metric: pihole.unwanted_domains_blocking_status + info: unwanted domains blocking is disabled + link: https://github.com/netdata/netdata/blob/master/src/health/health.d/pihole.conf + metrics: + folding: + title: Metrics + enabled: false + description: "" + availability: [] + scopes: + - name: global + description: These metrics refer to the entire monitored application. + labels: [] + metrics: + - name: pihole.dns_queries_total + description: DNS Queries Total (Cached, Blocked and Forwarded) + unit: queries + chart_type: line + dimensions: + - name: queries + - name: pihole.dns_queries + description: DNS Queries + unit: queries + chart_type: stacked + dimensions: + - name: cached + - name: blocked + - name: forwarded + - name: pihole.dns_queries_percentage + description: DNS Queries Percentage + unit: percentage + chart_type: stacked + dimensions: + - name: cached + - name: blocked + - name: forwarded + - name: pihole.unique_clients + description: Unique Clients + unit: clients + chart_type: line + dimensions: + - name: unique + - name: pihole.domains_on_blocklist + description: Domains On Blocklist + unit: domains + chart_type: line + dimensions: + - name: blocklist + - name: pihole.blocklist_last_update + description: Blocklist Last Update + unit: seconds + chart_type: line + dimensions: + - name: ago + - name: pihole.unwanted_domains_blocking_status + description: Unwanted Domains Blocking Status + unit: status + chart_type: line + dimensions: + - name: enabled + - name: disabled + - name: pihole.dns_queries_types + description: DNS Queries Per Type + unit: percentage + chart_type: stacked + dimensions: + - name: a + - name: aaaa + - name: any + - name: ptr + - name: soa + - name: srv + - name: txt + - name: pihole.dns_queries_forwarded_destination + description: DNS Queries Per Destination + unit: percentage + chart_type: stacked + dimensions: + - name: cached + - name: blocked + - name: other diff --git a/src/go/collectors/go.d.plugin/modules/pihole/metrics.go b/src/go/collectors/go.d.plugin/modules/pihole/metrics.go new file mode 100644 index 000000000..dd4b3b644 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/metrics.go @@ -0,0 +1,84 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package pihole + +type piholeMetrics struct { + summary *summaryRawMetrics // ?summary + queryTypes *queryTypesMetrics // ?getQueryTypes + forwarders *forwardDestinations // ?getForwardedDestinations +} + +func (p piholeMetrics) hasSummary() bool { + return p.summary != nil +} +func (p piholeMetrics) hasQueryTypes() bool { + return p.queryTypes != nil +} +func (p piholeMetrics) hasForwarders() bool { + return p.forwarders != nil && len(p.forwarders.Destinations) > 0 +} + +type piholeAPIVersion struct { + Version int +} + +type summaryRawMetrics struct { + DomainsBeingBlocked int64 `json:"domains_being_blocked"` + DNSQueriesToday int64 `json:"dns_queries_today"` + AdsBlockedToday int64 `json:"ads_blocked_today"` + AdsPercentageToday float64 `json:"ads_percentage_today"` + UniqueDomains int64 `json:"unique_domains"` + QueriesForwarded int64 `json:"queries_forwarded"` + QueriesCached int64 `json:"queries_cached"` + ClientsEverSeen int64 `json:"clients_ever_seen"` + UniqueClients int64 `json:"unique_clients"` + DNSQueriesAllTypes int64 `json:"dns_queries_all_types"` + ReplyNODATA int64 `json:"reply_NODATA"` + ReplyNXDOMAIN int64 `json:"reply_NXDOMAIN"` + ReplyCNAME int64 `json:"reply_CNAME"` + ReplyIP int64 `json:"reply_IP"` + PrivacyLevel int64 `json:"privacy_level"` + Status string `json:"status"` + GravityLastUpdated struct { + // gravity.list has been removed (https://github.com/pi-hole/pi-hole/pull/2871#issuecomment-520251509) + FileExists bool `json:"file_exists"` + Absolute *int64 + } `json:"gravity_last_updated"` +} + +type queryTypesMetrics struct { + Types struct { + A float64 `json:"A (IPv4)"` + AAAA float64 `json:"AAAA (IPv6)"` + ANY float64 + SRV float64 + SOA float64 + PTR float64 + TXT float64 + } `json:"querytypes"` +} + +// https://github.com/pi-hole/FTL/blob/6f69dd5b4ca60f925d68bfff3869350e934a7240/src/api/api.c#L474 +type forwardDestinations struct { + Destinations map[string]float64 `json:"forward_destinations"` +} + +//type ( +// item map[string]int64 +// +// topClients struct { +// Sources item `json:"top_sources"` +// } +// topItems struct { +// TopQueries item `json:"top_queries"` +// TopAds item `json:"top_ads"` +// } +//) +// +//func (i *item) UnmarshalJSON(data []byte) error { +// if isEmptyArray(data) { +// return nil +// } +// type plain *item +// return json.Unmarshal(data, (plain)(i)) +//} diff --git a/src/go/collectors/go.d.plugin/modules/pihole/pihole.go b/src/go/collectors/go.d.plugin/modules/pihole/pihole.go new file mode 100644 index 000000000..a5b103a0c --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/pihole.go @@ -0,0 +1,128 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package pihole + +import ( + _ "embed" + "errors" + "net/http" + "sync" + "time" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" +) + +//go:embed "config_schema.json" +var configSchema string + +func init() { + module.Register("pihole", module.Creator{ + JobConfigSchema: configSchema, + Defaults: module.Defaults{ + UpdateEvery: 5, + }, + Create: func() module.Module { return New() }, + }) +} + +func New() *Pihole { + return &Pihole{ + Config: Config{ + HTTP: web.HTTP{ + Request: web.Request{ + URL: "http://127.0.0.1", + }, + Client: web.Client{ + Timeout: web.Duration(time.Second * 5), + }, + }, + SetupVarsPath: "/etc/pihole/setupVars.conf", + }, + checkVersion: true, + charts: baseCharts.Copy(), + addQueriesTypesOnce: &sync.Once{}, + addFwsDestinationsOnce: &sync.Once{}, + } +} + +type Config struct { + web.HTTP `yaml:",inline" json:""` + UpdateEvery int `yaml:"update_every" json:"update_every"` + SetupVarsPath string `yaml:"setup_vars_path" json:"setup_vars_path"` +} + +type Pihole struct { + module.Base + Config `yaml:",inline" json:""` + + charts *module.Charts + addQueriesTypesOnce *sync.Once + addFwsDestinationsOnce *sync.Once + + httpClient *http.Client + + checkVersion bool +} + +func (p *Pihole) Configuration() any { + return p.Config +} + +func (p *Pihole) Init() error { + if err := p.validateConfig(); err != nil { + p.Errorf("config validation: %v", err) + return err + } + + httpClient, err := p.initHTTPClient() + if err != nil { + p.Errorf("init http client: %v", err) + return err + } + p.httpClient = httpClient + + p.Password = p.getWebPassword() + if p.Password == "" { + p.Warning("no web password, not all metrics available") + } else { + p.Debugf("web password: %s", p.Password) + } + + return nil +} + +func (p *Pihole) Check() error { + mx, err := p.collect() + if err != nil { + p.Error(err) + return err + } + if len(mx) == 0 { + return errors.New("no metrics collected") + } + return nil +} + +func (p *Pihole) Charts() *module.Charts { + return p.charts +} + +func (p *Pihole) Collect() map[string]int64 { + mx, err := p.collect() + if err != nil { + p.Error(err) + } + + if len(mx) == 0 { + return nil + } + + return mx +} + +func (p *Pihole) Cleanup() { + if p.httpClient != nil { + p.httpClient.CloseIdleConnections() + } +} diff --git a/src/go/collectors/go.d.plugin/modules/pihole/pihole_test.go b/src/go/collectors/go.d.plugin/modules/pihole/pihole_test.go new file mode 100644 index 000000000..6af8267f1 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/pihole_test.go @@ -0,0 +1,278 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package pihole + +import ( + "fmt" + "net/http" + "net/http/httptest" + "os" + "testing" + + "github.com/netdata/netdata/go/go.d.plugin/agent/module" + "github.com/netdata/netdata/go/go.d.plugin/pkg/web" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +const ( + pathSetupVarsOK = "testdata/setupVars.conf" + pathSetupVarsWrong = "testdata/wrong.conf" +) + +var ( + dataConfigJSON, _ = os.ReadFile("testdata/config.json") + dataConfigYAML, _ = os.ReadFile("testdata/config.yaml") + + dataEmptyResp = []byte("[]") + dataSummaryRawResp, _ = os.ReadFile("testdata/summaryRaw.json") + dataGetQueryTypesResp, _ = os.ReadFile("testdata/getQueryTypes.json") + dataGetForwardDestinationsResp, _ = os.ReadFile("testdata/getForwardDestinations.json") +) + +func Test_testDataIsValid(t *testing.T) { + for name, data := range map[string][]byte{ + "dataConfigJSON": dataConfigJSON, + "dataConfigYAML": dataConfigYAML, + "dataEmptyResp": dataEmptyResp, + "dataSummaryRawResp": dataSummaryRawResp, + "dataGetQueryTypesResp": dataGetQueryTypesResp, + "dataGetForwardDestinationsResp": dataGetForwardDestinationsResp, + } { + require.NotNil(t, data, name) + } +} + +func TestPihole_ConfigurationSerialize(t *testing.T) { + module.TestConfigurationSerialize(t, &Pihole{}, dataConfigJSON, dataConfigYAML) +} + +func TestPihole_Init(t *testing.T) { + tests := map[string]struct { + wantFail bool + config Config + }{ + "success with default": { + wantFail: false, + config: New().Config, + }, + "fail when URL not set": { + wantFail: true, + config: Config{ + HTTP: web.HTTP{ + Request: web.Request{URL: ""}, + }, + }, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + p := New() + p.Config = test.config + + if test.wantFail { + assert.Error(t, p.Init()) + } else { + assert.NoError(t, p.Init()) + } + }) + } +} + +func TestPihole_Check(t *testing.T) { + tests := map[string]struct { + wantFail bool + prepare func(t *testing.T) (p *Pihole, cleanup func()) + }{ + "success with web password": { + wantFail: false, + prepare: caseSuccessWithWebPassword, + }, + "fail without web password": { + wantFail: true, + prepare: caseFailNoWebPassword, + }, + "fail on unsupported version": { + wantFail: true, + prepare: caseFailUnsupportedVersion, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + p, cleanup := test.prepare(t) + defer cleanup() + + if test.wantFail { + assert.Error(t, p.Check()) + } else { + assert.NoError(t, p.Check()) + } + }) + } +} + +func TestPihole_Charts(t *testing.T) { + assert.NotNil(t, New().Charts()) +} + +func TestPihole_Collect(t *testing.T) { + tests := map[string]struct { + prepare func(t *testing.T) (p *Pihole, cleanup func()) + wantMetrics map[string]int64 + wantNumCharts int + }{ + "success with web password": { + prepare: caseSuccessWithWebPassword, + wantNumCharts: len(baseCharts) + 2, + wantMetrics: map[string]int64{ + "A": 1229, + "AAAA": 1229, + "ANY": 100, + "PTR": 7143, + "SOA": 100, + "SRV": 100, + "TXT": 100, + "ads_blocked_today": 1, + "ads_blocked_today_perc": 33333, + "ads_percentage_today": 100, + "blocking_status_disabled": 0, + "blocking_status_enabled": 1, + "blocklist_last_update": 106273651, + "destination_blocked": 220, + "destination_cached": 8840, + "destination_other": 940, + "dns_queries_today": 1, + "domains_being_blocked": 1, + "queries_cached": 1, + "queries_cached_perc": 33333, + "queries_forwarded": 1, + "queries_forwarded_perc": 33333, + "unique_clients": 1, + }, + }, + "fail without web password": { + prepare: caseFailNoWebPassword, + wantMetrics: nil, + }, + "fail on unsupported version": { + prepare: caseFailUnsupportedVersion, + wantMetrics: nil, + }, + } + + for name, test := range tests { + t.Run(name, func(t *testing.T) { + p, cleanup := test.prepare(t) + defer cleanup() + + mx := p.Collect() + + copyBlockListLastUpdate(mx, test.wantMetrics) + require.Equal(t, test.wantMetrics, mx) + if len(test.wantMetrics) > 0 { + assert.Len(t, *p.Charts(), test.wantNumCharts) + } + }) + } +} + +func caseSuccessWithWebPassword(t *testing.T) (*Pihole, func()) { + p, srv := New(), mockPiholeServer{}.newPiholeHTTPServer() + + p.SetupVarsPath = pathSetupVarsOK + p.URL = srv.URL + + require.NoError(t, p.Init()) + + return p, srv.Close +} + +func caseFailNoWebPassword(t *testing.T) (*Pihole, func()) { + p, srv := New(), mockPiholeServer{}.newPiholeHTTPServer() + + p.SetupVarsPath = pathSetupVarsWrong + p.URL = srv.URL + + require.NoError(t, p.Init()) + + return p, srv.Close +} + +func caseFailUnsupportedVersion(t *testing.T) (*Pihole, func()) { + p, srv := New(), mockPiholeServer{unsupportedVersion: true}.newPiholeHTTPServer() + + p.SetupVarsPath = pathSetupVarsOK + p.URL = srv.URL + + require.NoError(t, p.Init()) + + return p, srv.Close +} + +type mockPiholeServer struct { + unsupportedVersion bool + errOnAPIVersion bool + errOnSummary bool + errOnQueryTypes bool + errOnGetForwardDst bool +} + +func (m mockPiholeServer) newPiholeHTTPServer() *httptest.Server { + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path != urlPathAPI || len(r.URL.Query()) == 0 { + w.WriteHeader(http.StatusBadRequest) + } + + if r.URL.Query().Get(urlQueryKeyAuth) == "" { + _, _ = w.Write(dataEmptyResp) + return + } + + if r.URL.Query().Has(urlQueryKeyAPIVersion) { + if m.errOnAPIVersion { + w.WriteHeader(http.StatusNotFound) + } else if m.unsupportedVersion { + _, _ = w.Write([]byte(fmt.Sprintf(`{"version": %d}`, wantAPIVersion+1))) + } else { + _, _ = w.Write([]byte(fmt.Sprintf(`{"version": %d}`, wantAPIVersion))) + } + return + } + + if r.URL.Query().Has(urlQueryKeySummaryRaw) { + if m.errOnSummary { + w.WriteHeader(http.StatusNotFound) + } else { + _, _ = w.Write(dataSummaryRawResp) + } + return + } + + data := dataEmptyResp + isErr := false + switch { + case r.URL.Query().Has(urlQueryKeyGetQueryTypes): + data, isErr = dataGetQueryTypesResp, m.errOnQueryTypes + case r.URL.Query().Has(urlQueryKeyGetForwardDestinations): + data, isErr = dataGetForwardDestinationsResp, m.errOnGetForwardDst + } + + if isErr { + w.WriteHeader(http.StatusNotFound) + } else { + _, _ = w.Write(data) + } + })) +} + +func copyBlockListLastUpdate(dst, src map[string]int64) { + k := "blocklist_last_update" + if v, ok := src[k]; ok { + if _, ok := dst[k]; ok { + dst[k] = v + } + } +} diff --git a/src/go/collectors/go.d.plugin/modules/pihole/testdata/config.json b/src/go/collectors/go.d.plugin/modules/pihole/testdata/config.json new file mode 100644 index 000000000..2d82443b0 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/testdata/config.json @@ -0,0 +1,21 @@ +{ + "update_every": 123, + "url": "ok", + "body": "ok", + "method": "ok", + "headers": { + "ok": "ok" + }, + "username": "ok", + "password": "ok", + "proxy_url": "ok", + "proxy_username": "ok", + "proxy_password": "ok", + "timeout": 123.123, + "not_follow_redirects": true, + "tls_ca": "ok", + "tls_cert": "ok", + "tls_key": "ok", + "tls_skip_verify": true, + "setup_vars_path": "ok" +} diff --git a/src/go/collectors/go.d.plugin/modules/pihole/testdata/config.yaml b/src/go/collectors/go.d.plugin/modules/pihole/testdata/config.yaml new file mode 100644 index 000000000..a9361246a --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/testdata/config.yaml @@ -0,0 +1,18 @@ +update_every: 123 +url: "ok" +body: "ok" +method: "ok" +headers: + ok: "ok" +username: "ok" +password: "ok" +proxy_url: "ok" +proxy_username: "ok" +proxy_password: "ok" +timeout: 123.123 +not_follow_redirects: yes +tls_ca: "ok" +tls_cert: "ok" +tls_key: "ok" +tls_skip_verify: yes +setup_vars_path: "ok" diff --git a/src/go/collectors/go.d.plugin/modules/pihole/testdata/getForwardDestinations.json b/src/go/collectors/go.d.plugin/modules/pihole/testdata/getForwardDestinations.json new file mode 100644 index 000000000..3bfc646d0 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/testdata/getForwardDestinations.json @@ -0,0 +1,7 @@ +{ + "forward_destinations": { + "blocked|blocked": 2.2, + "cached|cached": 88.4, + "other|other": 9.4 + } +}
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/pihole/testdata/getQueryTypes.json b/src/go/collectors/go.d.plugin/modules/pihole/testdata/getQueryTypes.json new file mode 100644 index 000000000..cf7f19f95 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/testdata/getQueryTypes.json @@ -0,0 +1,11 @@ +{ + "querytypes": { + "A (IPv4)": 12.29, + "AAAA (IPv6)": 12.29, + "ANY": 1, + "SRV": 1, + "SOA": 1, + "PTR": 71.43, + "TXT": 1 + } +}
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/pihole/testdata/setupVars.conf b/src/go/collectors/go.d.plugin/modules/pihole/testdata/setupVars.conf new file mode 100644 index 000000000..97f260297 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/testdata/setupVars.conf @@ -0,0 +1,11 @@ +WEBPASSWORD=1ebd33f882f9aa5fac26a7cb74704742f91100228eb322e41b7bd6e6aeb8f74b +BLOCKING_ENABLED=true +PIHOLE_INTERFACE=enp0s9 +IPV4_ADDRESS=192.168.88.228/24 +IPV6_ADDRESS= +PIHOLE_DNS_1=208.67.222.222 +PIHOLE_DNS_2=208.67.220.220 +QUERY_LOGGING=true +INSTALL_WEB_SERVER=true +INSTALL_WEB_INTERFACE=true +LIGHTTPD_ENABLED=true
\ No newline at end of file diff --git a/src/go/collectors/go.d.plugin/modules/pihole/testdata/summaryRaw.json b/src/go/collectors/go.d.plugin/modules/pihole/testdata/summaryRaw.json new file mode 100644 index 000000000..8a4e59c16 --- /dev/null +++ b/src/go/collectors/go.d.plugin/modules/pihole/testdata/summaryRaw.json @@ -0,0 +1,27 @@ +{ + "domains_being_blocked": 1, + "dns_queries_today": 1, + "ads_blocked_today": 1, + "ads_percentage_today": 1, + "unique_domains": 1, + "queries_forwarded": 1, + "queries_cached": 1, + "clients_ever_seen": 1, + "unique_clients": 1, + "dns_queries_all_types": 1, + "reply_NODATA": 1, + "reply_NXDOMAIN": 1, + "reply_CNAME": 1, + "reply_IP": 1, + "privacy_level": 1, + "status": "enabled", + "gravity_last_updated": { + "file_exists": true, + "absolute": 1560443834, + "relative": { + "days": "3", + "hours": "06", + "minutes": "05" + } + } +}
\ No newline at end of file |