diff options
Diffstat (limited to '')
-rw-r--r-- | src/go/collectors/go.d.plugin/modules/pihole/collect.go | 274 |
1 files changed, 274 insertions, 0 deletions
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) +} |