summaryrefslogtreecommitdiffstats
path: root/src/go/collectors/go.d.plugin/modules/pihole/collect.go
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/go/collectors/go.d.plugin/modules/pihole/collect.go274
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)
+}