@cryptotaxi247 / netdata-1 / commits / c91d2c1d5

fix(go.d/pihole): switch to pihole6 api (#19807)

switch to pihole6 api

Ilya Mashchenko committed Mar 10, 2025 at 13:51 UTC c91d2c1d50011fb99a90c3b50cf8eb693b21f5f6
18 files changed +733 -682
src/go/plugin/go.d/collector/pihole/api.go new
+104
@@ -0,0 +1,104 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package pihole
4 +
5 +const (
6 + // Version 6 (https://ftl.pi-hole.net/master/docs/)
7 + urlPathAPIAuth = "/api/auth"
8 + urlPathAPIStatsSummary = "/api/stats/summary"
9 +)
10 +
11 +type ftlAPIAuthResponse struct {
12 + Session struct {
13 + Valid bool `json:"valid"`
14 + Sid string `json:"sid"`
15 + Csrf string `json:"csrf"`
16 + Validity int64 `json:"validity"`
17 + Message string `json:"message"`
18 + }
19 +}
20 +
21 +// https://github.com/pi-hole/FTL/blob/master/src/api/stats.c#L113
22 +type ftlAPIStatsSummaryResponse struct {
23 + Queries struct {
24 + Total int64 `json:"total" stm:"total"` // Total number of queries
25 + Blocked float64 `json:"blocked" stm:"blocked"` // Number of blocked queries
26 + PercentBlocked float64 `json:"percent_blocked" stm:"percent_blocked,1000,1"` // Percent of blocked queries
27 + UniqueDomains int64 `json:"unique_domains" stm:"unique_domains"` // Number of unique domains FTL knows
28 + Forwarded int64 `json:"forwarded" stm:"forwarded"` // Number of queries that have been forwarded upstream
29 + Cached int64 `json:"cached" stm:"cached"` // Number of queries replied to from cache or local configuration
30 + Frequency float64 `json:"frequency" stm:"frequency,1000,1"` // Average number of queries per second
31 + Types struct {
32 + A int64 `json:"A" stm:"A"` // Type A queries
33 + AAAA int64 `json:"AAAA" stm:"AAAA"` // Type AAAA queries
34 + ANY int64 `json:"ANY" stm:"ANY"` // Type ANY queries
35 + SRV int64 `json:"SRV" stm:"SRV"` // Type SRV queries
36 + SOA int64 `json:"SOA" stm:"SOA"` // Type SOA queries
37 + PTR int64 `json:"PTR" stm:"PTR"` // Type PTR queries
38 + TXT int64 `json:"TXT" stm:"TXT"` // Type TXT queries
39 + NAPTR int64 `json:"NAPTR" stm:"NAPTR"` // Type NAPTR queries
40 + MX int64 `json:"MX" stm:"MX"` // Type MX queries
41 + DS int64 `json:"DS" stm:"DS"` // Type DS queries
42 + RRSIG int64 `json:"RRSIG" stm:"RRSIG"` // Type RRSIG queries
43 + DNSKEY int64 `json:"DNSKEY" stm:"DNSKEY"` // Type DNSKEY queries
44 + NS int64 `json:"NS" stm:"NS"` // Type NS queries
45 + SVCB int64 `json:"SVCB" stm:"SVCB"` // Type SVCB queries
46 + HTTPS int64 `json:"HTTPS" stm:"HTTPS"` // Type HTTPS queries
47 + OTHER int64 `json:"OTHER" stm:"OTHER"` // Queries of remaining types
48 + } `json:"types" stm:"types"` // Number of individual queries
49 + Status struct {
50 + Unknown int64 `json:"UNKNOWN" stm:"UNKNOWN"` // Type UNKNOWN queries
51 + Gravity int64 `json:"GRAVITY" stm:"GRAVITY"` // Type GRAVITY queries
52 + Forwarded int64 `json:"FORWARDED" stm:"FORWARDED"` // Type FORWARDED queries
53 + Cache int64 `json:"CACHE" stm:"CACHE"` // Type CACHE queries
54 + Regex int64 `json:"REGEX" stm:"REGEX"` // Type REGEX queries
55 + DenyList int64 `json:"DENYLIST" stm:"DENYLIST"` // Type DENYLIST queries
56 + ExternalBlockedIP int64 `json:"EXTERNAL_BLOCKED_IP" stm:"EXTERNAL_BLOCKED_IP"` // Type EXTERNAL_BLOCKED_IP queries
57 + ExternalBlockedNull int64 `json:"EXTERNAL_BLOCKED_NULL" stm:"EXTERNAL_BLOCKED_NULL"` // Type EXTERNAL_BLOCKED_NULL queries
58 + ExternalBlockedNxra int64 `json:"EXTERNAL_BLOCKED_NXRA" stm:"EXTERNAL_BLOCKED_NXRA"` // Type EXTERNAL_BLOCKED_NXRA queries
59 + GravityCname int64 `json:"GRAVITY_CNAME" stm:"GRAVITY_CNAME"` // Type GRAVITY_CNAME queries
60 + RegexCname int64 `json:"REGEX_CNAME" stm:"REGEX_CNAME"` // Type REGEX_CNAME queries
61 + DenyListCname int64 `json:"DENYLIST_CNAME" stm:"DENYLIST_CNAME"` // Type DENYLIST_CNAME queries
62 + Retried int64 `json:"RETRIED" stm:"RETRIED"` // Type RETRIED queries
63 + RetriedDnssec int64 `json:"RETRIED_DNSSEC" stm:"RETRIED_DNSSEC"` // Type RETRIED_DNSSEC queries
64 + InProgress int64 `json:"IN_PROGRESS" stm:"IN_PROGRESS"` // Type IN_PROGRESS queries
65 + Dbbusy int64 `json:"DBBUSY" stm:"DBBUSY"` // Type DBBUSY queries
66 + SpecialDomain int64 `json:"SPECIAL_DOMAIN" stm:"SPECIAL_DOMAIN"` // Type SPECIAL_DOMAIN queries
67 + CacheStale int64 `json:"CACHE_STALE" stm:"CACHE_STALE"` // Type CACHE_STALE queries
68 + ExternalBlockedEde15 int64 `json:"EXTERNAL_BLOCKED_EDE15" stm:"EXTERNAL_BLOCKED_EDE15"` // Type EXTERNAL_BLOCKED_EDE15 queries
69 + } `json:"status" stm:"status"` // Number of individual queries (by status)
70 + Replies struct {
71 + UNKNOWN int64 `json:"UNKNOWN" stm:"UNKNOWN"` // Type UNKNOWN replies
72 + NODATA int64 `json:"NODATA" stm:"NODATA"` // Type NODATA replies
73 + NXDOMAIN int64 `json:"NXDOMAIN" stm:"NXDOMAIN"` // Type NXDOMAIN replies
74 + CNAME int64 `json:"CNAME" stm:"CNAME"` // Type CNAME replies
75 + IP int64 `json:"IP" stm:"IP"` // Type IP replies
76 + DOMAIN int64 `json:"DOMAIN" stm:"DOMAIN"` // Type DOMAIN replies
77 + RRNAME int64 `json:"RRNAME" stm:"RRNAME"` // Type RRNAME replies
78 + SERVFAIL int64 `json:"SERVFAIL" stm:"SERVFAIL"` // Type SERVFAIL replies
79 + REFUSED int64 `json:"REFUSED" stm:"REFUSED"` // Type REFUSED replies
80 + NOTIMP int64 `json:"NOTIMP" stm:"NOTIMP"` // Type NOTIMP replies
81 + OTHER int64 `json:"OTHER" stm:"OTHER"` // Type OTHER replies
82 + DNSSEC int64 `json:"DNSSEC" stm:"DNSSEC"` // Type DNSSEC replies
83 + NONE int64 `json:"NONE" stm:"NONE"` // Type NONE replies
84 + BLOB int64 `json:"BLOB" stm:"BLOB"` // Type BLOB replies
85 + } `json:"replies" stm:"replies"` // Number of individual replies
86 + } `json:"queries" stm:"queries"`
87 + Clients struct {
88 + Active int64 `json:"active" stm:"active"` // Number of active clients (seen in the last 24 hours)
89 + Total int64 `json:"total" stm:"total"` // Total number of clients seen by FTL
90 + } `json:"clients" stm:"clients"`
91 + Gravity struct {
92 + DomainsBeingBlocked int64 `json:"domains_being_blocked" stm:"domains_being_blocked"` // Number of domain on your Pi-hole's gravity list
93 + LastUpdate int64 `json:"last_update" stm:"last_update"` // Unix timestamp of last gravity update (may be `0` if unknown)
94 + } `json:"gravity" stm:"gravity"`
95 + Took *float64 `json:"took"` // Time in seconds it took to process the request
96 +}
97 +
98 +type ftlErrorResponse struct {
99 + Error struct {
100 + Key string `json:"key"`
101 + Message string `json:"message"`
102 + } `json:"error"`
103 + Took float64 `json:"took"`
104 +}
src/go/plugin/go.d/collector/pihole/auth.go new
+70
@@ -0,0 +1,70 @@
1 +// SPDX-License-Identifier: GPL-3.0-or-later
2 +
3 +package pihole
4 +
5 +import (
6 + "encoding/json"
7 + "fmt"
8 + "net/http"
9 +
10 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
11 +)
12 +
13 +func (c *Collector) checkAuthSession() error {
14 + if c.auth == nil {
15 + return nil
16 + }
17 +
18 + req, err := web.NewHTTPRequestWithPath(c.RequestConfig, urlPathAPIAuth)
19 + if err != nil {
20 + return err
21 + }
22 + req.Header.Set("X-FTL-SID", c.auth.Session.Sid)
23 + req.Header.Set("X-FTL-CSRF", c.auth.Session.Csrf)
24 +
25 + var resp ftlAPIAuthResponse
26 +
27 + if err := web.DoHTTP(c.httpClient).RequestJSON(req, &resp); err != nil {
28 + if web.IsStatusCode(err, 401) {
29 + c.auth = nil
30 + return nil
31 + }
32 + return err
33 + }
34 +
35 + if !resp.Session.Valid {
36 + c.auth = nil
37 + return nil
38 + }
39 +
40 + return nil
41 +}
42 +
43 +func (c *Collector) getAuthSession() (*ftlAPIAuthResponse, error) {
44 + var pass struct {
45 + Password string `json:"password"`
46 + }
47 + pass.Password = c.Password
48 + bs, _ := json.Marshal(pass)
49 +
50 + cfg := c.RequestConfig.Copy()
51 + cfg.Method = http.MethodPost
52 + cfg.Body = string(bs)
53 +
54 + req, err := web.NewHTTPRequestWithPath(cfg, urlPathAPIAuth)
55 + if err != nil {
56 + return nil, err
57 + }
58 +
59 + var resp ftlAPIAuthResponse
60 +
61 + if err := web.DoHTTP(c.httpClient).RequestJSON(req, &resp); err != nil {
62 + return nil, err
63 + }
64 +
65 + if !resp.Session.Valid {
66 + return nil, fmt.Errorf("invalid auth session (%s)", resp.Session.Message)
67 + }
68 +
69 + return &resp, nil
70 +}
src/go/plugin/go.d/collector/pihole/charts.go
+140 -110
@@ -8,159 +8,189 @@ import (
8
9 const (
10 prioDNSQueriesTotal = module.Priority + iota
11 - prioDNSQueries
12 - prioDNSQueriesPerc
13 - prioUniqueClients
14 - prioDomainsOnBlocklist
15 - prioBlocklistLastUpdate
16 - prioUnwantedDomainsBlockingStatus
11 + prioDNSQueriesBlockedPercent
12 + prioDNSQueriesByDestination
13 + prioDNSQueriesByType
14 + prioDNSQueriesByStatus
15 + prioDNSRepliesByType
16
18 - prioDNSQueriesTypes
19 - prioDNSQueriesForwardedDestination
17 + prioActiveClients
18 +
19 + prioGravityListBlockedDomains
20 + prioGravityListLastUpdateTimeAgo
21 )
22
22 -var baseCharts = module.Charts{
23 +var summaryCharts = module.Charts{
24 chartDNSQueriesTotal.Copy(),
24 - chartDNSQueries.Copy(),
25 - chartDNSQueriesPerc.Copy(),
26 - chartUniqueClients.Copy(),
27 - chartDomainsOnBlocklist.Copy(),
28 - chartBlocklistLastUpdate.Copy(),
29 - chartUnwantedDomainsBlockingStatus.Copy(),
25 + chartDNSQueriesBlockedPercent.Copy(),
26 + chartDNSQueriesByDestination.Copy(),
27 + chartDNSQueriesByType.Copy(),
28 + chartDNSQueriesByStatus.Copy(),
29 +
30 + chartDNSRepliesByType.Copy(),
31 +
32 + chartActiveClients.Copy(),
33 +
34 + chartGravityListBlockedDomains.Copy(),
35 + chartGravityListLastUpdateTimeAgo.Copy(),
36 }
37
38 var (
39 chartDNSQueriesTotal = module.Chart{
40 ID: "dns_queries_total",
35 - Title: "DNS Queries Total (Cached, Blocked and Forwarded)",
36 - Units: "queries",
41 + Title: "Pi-hole DNS Queries Total (Cached, Blocked and Forwarded)",
42 + Units: "queries/s",
43 Fam: "queries",
44 Ctx: "pihole.dns_queries_total",
45 Priority: prioDNSQueriesTotal,
46 Dims: module.Dims{
41 - {ID: "dns_queries_today", Name: "queries"},
47 + {ID: "queries_total", Name: "queries", Algo: module.Incremental},
48 },
49 }
44 - chartDNSQueries = module.Chart{
45 - ID: "dns_queries",
46 - Title: "DNS Queries",
47 - Units: "queries",
50 + chartDNSQueriesBlockedPercent = module.Chart{
51 + ID: "dns_queries_blocked_percent",
52 + Title: "Pi-hole DNS Queries Blocked Percent",
53 + Units: "percent",
54 Fam: "queries",
49 - Ctx: "pihole.dns_queries",
50 - Type: module.Stacked,
51 - Priority: prioDNSQueries,
55 + Ctx: "pihole.dns_queries_blocked_percent",
56 + Priority: prioDNSQueriesBlockedPercent,
57 Dims: module.Dims{
53 - {ID: "queries_cached", Name: "cached"},
54 - {ID: "ads_blocked_today", Name: "blocked"},
55 - {ID: "queries_forwarded", Name: "forwarded"},
58 + {ID: "queries_percent_blocked", Name: "blocked", Div: precision},
59 },
60 }
58 - chartDNSQueriesPerc = module.Chart{
59 - ID: "dns_queries_percentage",
60 - Title: "DNS Queries Percentage",
61 - Units: "percentage",
61 + chartDNSQueriesByDestination = module.Chart{
62 + ID: "dns_queries_by_destination",
63 + Title: "Pi-hole DNS Queries by Destination",
64 + Units: "queries/s",
65 Fam: "queries",
63 - Ctx: "pihole.dns_queries_percentage",
66 + Ctx: "pihole.dns_queries_by_destination",
67 Type: module.Stacked,
65 - Priority: prioDNSQueriesPerc,
68 + Priority: prioDNSQueriesByDestination,
69 Dims: module.Dims{
67 - {ID: "queries_cached_perc", Name: "cached", Div: precision},
68 - {ID: "ads_blocked_today_perc", Name: "blocked", Div: precision},
69 - {ID: "queries_forwarded_perc", Name: "forwarded", Div: precision},
70 + {ID: "queries_cached", Name: "cached", Algo: module.Incremental},
71 + {ID: "queries_blocked", Name: "blocked", Algo: module.Incremental},
72 + {ID: "queries_forwarded", Name: "forwarded", Algo: module.Incremental},
73 },
74 }
72 - chartUniqueClients = module.Chart{
73 - ID: "unique_clients",
74 - Title: "Unique Clients",
75 - Units: "clients",
76 - Fam: "clients",
77 - Ctx: "pihole.unique_clients",
78 - Priority: prioUniqueClients,
75 + chartDNSQueriesByType = module.Chart{
76 + ID: "dns_queries_by_type",
77 + Title: "Pi-hole DNS Queries by Type",
78 + Units: "queries/s",
79 + Fam: "queries",
80 + Ctx: "pihole.dns_queries_by_type",
81 + Type: module.Stacked,
82 + Priority: prioDNSQueriesByType,
83 Dims: module.Dims{
80 - {ID: "unique_clients", Name: "unique"},
84 + {ID: "queries_types_A", Name: "A", Algo: module.Incremental},
85 + {ID: "queries_types_AAAA", Name: "AAAA", Algo: module.Incremental},
86 + {ID: "queries_types_ANY", Name: "ANY", Algo: module.Incremental},
87 + {ID: "queries_types_SRV", Name: "SRV", Algo: module.Incremental},
88 + {ID: "queries_types_SOA", Name: "SOA", Algo: module.Incremental},
89 + {ID: "queries_types_PTR", Name: "PTR", Algo: module.Incremental},
90 + {ID: "queries_types_TXT", Name: "TXT", Algo: module.Incremental},
91 + {ID: "queries_types_NAPTR", Name: "NAPTR", Algo: module.Incremental},
92 + {ID: "queries_types_MX", Name: "MX", Algo: module.Incremental},
93 + {ID: "queries_types_DS", Name: "DS", Algo: module.Incremental},
94 + {ID: "queries_types_RRSIG", Name: "RRSIG", Algo: module.Incremental},
95 + {ID: "queries_types_DNSKEY", Name: "DNSKEY", Algo: module.Incremental},
96 + {ID: "queries_types_NS", Name: "NS", Algo: module.Incremental},
97 + {ID: "queries_types_SVCB", Name: "SVCB", Algo: module.Incremental},
98 + {ID: "queries_types_HTTPS", Name: "HTTPS", Algo: module.Incremental},
99 + {ID: "queries_types_OTHER", Name: "OTHER", Algo: module.Incremental},
100 },
101 }
83 - chartDomainsOnBlocklist = module.Chart{
84 - ID: "domains_on_blocklist",
85 - Title: "Domains On Blocklist",
86 - Units: "domains",
87 - Fam: "blocklist",
88 - Ctx: "pihole.domains_on_blocklist",
89 - Priority: prioDomainsOnBlocklist,
102 + chartDNSQueriesByStatus = module.Chart{
103 + ID: "dns_queries_by_status",
104 + Title: "Pi-hole DNS Queries by Status",
105 + Units: "queries/s",
106 + Fam: "queries",
107 + Ctx: "pihole.dns_queries_by_status",
108 + Type: module.Stacked,
109 + Priority: prioDNSQueriesByStatus,
110 Dims: module.Dims{
91 - {ID: "domains_being_blocked", Name: "blocklist"},
111 + {ID: "queries_status_UNKNOWN", Name: "UNKNOWN", Algo: module.Incremental},
112 + {ID: "queries_status_GRAVITY", Name: "GRAVITY", Algo: module.Incremental},
113 + {ID: "queries_status_FORWARDED", Name: "FORWARDED", Algo: module.Incremental},
114 + {ID: "queries_status_CACHE", Name: "CACHE", Algo: module.Incremental},
115 + {ID: "queries_status_REGEX", Name: "REGEX", Algo: module.Incremental},
116 + {ID: "queries_status_DENYLIST", Name: "DENYLIST", Algo: module.Incremental},
117 + {ID: "queries_status_EXTERNAL_BLOCKED_IP", Name: "EXTERNAL_BLOCKED_IP", Algo: module.Incremental},
118 + {ID: "queries_status_EXTERNAL_BLOCKED_NULL", Name: "EXTERNAL_BLOCKED_NULL", Algo: module.Incremental},
119 + {ID: "queries_status_EXTERNAL_BLOCKED_NXRA", Name: "EXTERNAL_BLOCKED_NXRA", Algo: module.Incremental},
120 + {ID: "queries_status_GRAVITY_CNAME", Name: "GRAVITY_CNAME", Algo: module.Incremental},
121 + {ID: "queries_status_REGEX_CNAME", Name: "REGEX_CNAME", Algo: module.Incremental},
122 + {ID: "queries_status_DENYLIST_CNAME", Name: "DENYLIST_CNAME", Algo: module.Incremental},
123 + {ID: "queries_status_RETRIED", Name: "RETRIED", Algo: module.Incremental},
124 + {ID: "queries_status_RETRIED_DNSSEC", Name: "RETRIED_DNSSEC", Algo: module.Incremental},
125 + {ID: "queries_status_IN_PROGRESS", Name: "IN_PROGRESS", Algo: module.Incremental},
126 + {ID: "queries_status_DBBUSY", Name: "DBBUSY", Algo: module.Incremental},
127 + {ID: "queries_status_SPECIAL_DOMAIN", Name: "SPECIAL_DOMAIN", Algo: module.Incremental},
128 + {ID: "queries_status_CACHE_STALE", Name: "CACHE_STALE", Algo: module.Incremental},
129 + {ID: "queries_status_EXTERNAL_BLOCKED_EDE15", Name: "EXTERNAL_BLOCKED_EDE15", Algo: module.Incremental},
130 },
131 }
94 - chartBlocklistLastUpdate = module.Chart{
95 - ID: "blocklist_last_update",
96 - Title: "Blocklist Last Update",
97 - Units: "seconds",
98 - Fam: "blocklist",
99 - Ctx: "pihole.blocklist_last_update",
100 - Priority: prioBlocklistLastUpdate,
132 +
133 + chartDNSRepliesByType = module.Chart{
134 + ID: "dns_replies_by_type",
135 + Title: "Pi-hole DNS Replies by Type",
136 + Units: "replies/s",
137 + Fam: "replies",
138 + Ctx: "pihole.dns_replies_by_type",
139 + Type: module.Stacked,
140 + Priority: prioDNSRepliesByType,
141 Dims: module.Dims{
102 - {ID: "blocklist_last_update", Name: "ago"},
142 + {ID: "queries_replies_UNKNOWN", Name: "UNKNOWN", Algo: module.Incremental},
143 + {ID: "queries_replies_NODATA", Name: "NODATA", Algo: module.Incremental},
144 + {ID: "queries_replies_NXDOMAIN", Name: "NXDOMAIN", Algo: module.Incremental},
145 + {ID: "queries_replies_CNAME", Name: "CNAME", Algo: module.Incremental},
146 + {ID: "queries_replies_IP", Name: "IP", Algo: module.Incremental},
147 + {ID: "queries_replies_DOMAIN", Name: "DOMAIN", Algo: module.Incremental},
148 + {ID: "queries_replies_RRNAME", Name: "RRNAME", Algo: module.Incremental},
149 + {ID: "queries_replies_SERVFAIL", Name: "SERVFAIL", Algo: module.Incremental},
150 + {ID: "queries_replies_REFUSED", Name: "REFUSED", Algo: module.Incremental},
151 + {ID: "queries_replies_NOTIMP", Name: "NOTIMP", Algo: module.Incremental},
152 + {ID: "queries_replies_DNSSEC", Name: "DNSSEC", Algo: module.Incremental},
153 + {ID: "queries_replies_NONE", Name: "NONE", Algo: module.Incremental},
154 + {ID: "queries_replies_OTHER", Name: "OTHER", Algo: module.Incremental},
155 },
156 }
105 - chartUnwantedDomainsBlockingStatus = module.Chart{
106 - ID: "unwanted_domains_blocking_status",
107 - Title: "Unwanted Domains Blocking Status",
108 - Units: "status",
109 - Fam: "status",
110 - Ctx: "pihole.unwanted_domains_blocking_status",
111 - Priority: prioUnwantedDomainsBlockingStatus,
157 +)
158 +
159 +var (
160 + chartActiveClients = module.Chart{
161 + ID: "active_clients",
162 + Title: "Pi-hole Active Clients (Seen in the Last 24 Hours)",
163 + Units: "clients",
164 + Fam: "clients",
165 + Ctx: "pihole.active_clients",
166 + Priority: prioActiveClients,
167 Dims: module.Dims{
113 - {ID: "blocking_status_enabled", Name: "enabled"},
114 - {ID: "blocking_status_disabled", Name: "disabled"},
168 + {ID: "clients_active", Name: "active"},
169 },
170 }
171 )
172
173 var (
120 - chartDNSQueriesTypes = module.Chart{
121 - ID: "dns_queries_types",
122 - Title: "DNS Queries Per Type",
123 - Units: "percentage",
124 - Fam: "doQuery types",
125 - Ctx: "pihole.dns_queries_types",
126 - Type: module.Stacked,
127 - Priority: prioDNSQueriesTypes,
174 + chartGravityListBlockedDomains = module.Chart{
175 + ID: "gravity_list_blocked_domains",
176 + Title: "Pi-hole Gravity List Blocked Domains",
177 + Units: "domains",
178 + Fam: "blocklist",
179 + Ctx: "pihole.gravity_list_blocked_domains",
180 + Priority: prioGravityListBlockedDomains,
181 Dims: module.Dims{
129 - {ID: "A", Div: 100},
130 - {ID: "AAAA", Div: 100},
131 - {ID: "ANY", Div: 100},
132 - {ID: "PTR", Div: 100},
133 - {ID: "SOA", Div: 100},
134 - {ID: "SRV", Div: 100},
135 - {ID: "TXT", Div: 100},
182 + {ID: "gravity_domains_being_blocked", Name: "blocked"},
183 },
184 }
138 - chartDNSQueriesForwardedDestination = module.Chart{
139 - ID: "dns_queries_forwarded_destination",
140 - Title: "DNS Queries Per Destination",
141 - Units: "percentage",
142 - Fam: "queries answered by",
143 - Ctx: "pihole.dns_queries_forwarded_destination",
144 - Type: module.Stacked,
145 - Priority: prioDNSQueriesForwardedDestination,
185 + chartGravityListLastUpdateTimeAgo = module.Chart{
186 + ID: "gravity_list_last_update_time_ago",
187 + Title: "Pi-hole Gravity List Time Since Last Update",
188 + Units: "seconds",
189 + Fam: "blocklist",
190 + Ctx: "pihole.gravity_list_last_update_time_ago",
191 + Priority: prioGravityListLastUpdateTimeAgo,
192 Dims: module.Dims{
147 - {ID: "destination_cached", Name: "cached", Div: 100},
148 - {ID: "destination_blocked", Name: "blocked", Div: 100},
149 - {ID: "destination_other", Name: "other", Div: 100},
193 + {ID: "gravity_last_update_seconds_ago", Name: "last_update_ago"},
194 },
195 }
196 )
153 -
154 -func (c *Collector) addChartDNSQueriesType() {
155 - chart := chartDNSQueriesTypes.Copy()
156 - if err := c.Charts().Add(chart); err != nil {
157 - c.Warning(err)
158 - }
159 -}
160 -
161 -func (c *Collector) addChartDNSQueriesForwardedDestinations() {
162 - chart := chartDNSQueriesForwardedDestination.Copy()
163 - if err := c.Charts().Add(chart); err != nil {
164 - c.Warning(err)
165 - }
166 -}
src/go/plugin/go.d/collector/pihole/collect.go
+30 -206
@@ -3,248 +3,72 @@
3 package pihole
4
5 import (
6 - "encoding/json"
6 "errors"
7 "fmt"
9 - "io"
10 - "net/http"
11 - "net/url"
12 - "strings"
13 - "sync"
8 "time"
9
16 - "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/metrix"
10 + "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/stm"
11 "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
12 )
13
20 -const wantAPIVersion = 3
21 -
22 -const (
23 - urlPathAPI = "/admin/api.php"
24 - urlQueryKeyAuth = "auth"
25 - urlQueryKeyAPIVersion = "version"
26 - urlQueryKeySummaryRaw = "summaryRaw"
27 - urlQueryKeyGetQueryTypes = "getQueryTypes" // need auth
28 - urlQueryKeyGetForwardDestinations = "getForwardDestinations" // need auth
29 -)
30 -
14 const (
15 precision = 1000
16 )
17
18 func (c *Collector) collect() (map[string]int64, error) {
36 - if c.checkVersion {
37 - ver, err := c.queryAPIVersion()
19 + if err := c.checkAuthSession(); err != nil {
20 + return nil, err
21 + }
22 + if c.auth == nil {
23 + auth, err := c.getAuthSession()
24 if err != nil {
25 return nil, err
26 }
41 - if ver != wantAPIVersion {
42 - return nil, fmt.Errorf("API version: %d, supported version: %d", ver, wantAPIVersion)
43 - }
44 - c.checkVersion = false
45 - }
46 -
47 - pmx := new(piholeMetrics)
48 - c.queryMetrics(pmx, true)
49 -
50 - if pmx.hasQueryTypes() {
51 - c.addQueriesTypesOnce.Do(c.addChartDNSQueriesType)
52 - }
53 - if pmx.hasForwarders() {
54 - c.addFwsDestinationsOnce.Do(c.addChartDNSQueriesForwardedDestinations)
27 + c.auth = auth
28 }
29
30 mx := make(map[string]int64)
58 - c.collectMetrics(mx, pmx)
59 -
60 - return mx, nil
61 -}
62 -
63 -func (c *Collector) collectMetrics(mx map[string]int64, pmx *piholeMetrics) {
64 - if pmx.hasSummary() {
65 - mx["ads_blocked_today"] = pmx.summary.AdsBlockedToday
66 - mx["ads_percentage_today"] = int64(pmx.summary.AdsPercentageToday * 100)
67 - mx["domains_being_blocked"] = pmx.summary.DomainsBeingBlocked
68 - // GravityLastUpdated.Absolute is <nil> if the file does not exist (deleted/moved)
69 - if pmx.summary.GravityLastUpdated.Absolute != nil {
70 - mx["blocklist_last_update"] = time.Now().Unix() - *pmx.summary.GravityLastUpdated.Absolute
71 - }
72 - mx["dns_queries_today"] = pmx.summary.DNSQueriesToday
73 - mx["queries_forwarded"] = pmx.summary.QueriesForwarded
74 - mx["queries_cached"] = pmx.summary.QueriesCached
75 - mx["unique_clients"] = pmx.summary.UniqueClients
76 - mx["blocking_status_enabled"] = metrix.Bool(pmx.summary.Status == "enabled")
77 - mx["blocking_status_disabled"] = metrix.Bool(pmx.summary.Status != "enabled")
78 -
79 - tot := pmx.summary.QueriesCached + pmx.summary.AdsBlockedToday + pmx.summary.QueriesForwarded
80 - mx["queries_cached_perc"] = calcPercentage(pmx.summary.QueriesCached, tot)
81 - mx["ads_blocked_today_perc"] = calcPercentage(pmx.summary.AdsBlockedToday, tot)
82 - mx["queries_forwarded_perc"] = calcPercentage(pmx.summary.QueriesForwarded, tot)
83 - }
31
85 - if pmx.hasQueryTypes() {
86 - mx["A"] = int64(pmx.queryTypes.Types.A * 100)
87 - mx["AAAA"] = int64(pmx.queryTypes.Types.AAAA * 100)
88 - mx["ANY"] = int64(pmx.queryTypes.Types.ANY * 100)
89 - mx["PTR"] = int64(pmx.queryTypes.Types.PTR * 100)
90 - mx["SOA"] = int64(pmx.queryTypes.Types.SOA * 100)
91 - mx["SRV"] = int64(pmx.queryTypes.Types.SRV * 100)
92 - mx["TXT"] = int64(pmx.queryTypes.Types.TXT * 100)
93 - }
94 -
95 - if pmx.hasForwarders() {
96 - for k, v := range pmx.forwarders.Destinations {
97 - name := strings.Split(k, "|")[0]
98 - mx["destination_"+name] = int64(v * 100)
99 - }
100 - }
101 -}
102 -
103 -func (c *Collector) queryMetrics(pmx *piholeMetrics, doConcurrently bool) {
104 - type task func(*piholeMetrics)
105 -
106 - var tasks = []task{c.querySummary}
107 -
108 - if c.Password != "" {
109 - tasks = []task{
110 - c.querySummary,
111 - c.queryQueryTypes,
112 - c.queryForwardedDestinations,
32 + if err := c.collectMetrics(mx); err != nil {
33 + if web.IsStatusCode(err, 401) {
34 + c.auth = nil
35 }
36 + return nil, err
37 }
38
116 - wg := &sync.WaitGroup{}
117 -
118 - wrap := func(call task) task {
119 - return func(metrics *piholeMetrics) { call(metrics); wg.Done() }
120 - }
121 -
122 - for _, task := range tasks {
123 - if doConcurrently {
124 - wg.Add(1)
125 - task = wrap(task)
126 - go task(pmx)
127 - } else {
128 - task(pmx)
129 - }
130 - }
131 -
132 - wg.Wait()
133 -}
134 -
135 -func (c *Collector) querySummary(pmx *piholeMetrics) {
136 - req, err := web.NewHTTPRequestWithPath(c.RequestConfig, urlPathAPI)
137 - if err != nil {
138 - c.Error(err)
139 - return
140 - }
141 -
142 - req.URL.RawQuery = url.Values{
143 - urlQueryKeyAuth: []string{c.Password},
144 - urlQueryKeySummaryRaw: []string{"true"},
145 - }.Encode()
146 -
147 - var v summaryRawMetrics
148 - if err = c.doHTTP(req, &v); err != nil {
149 - c.Error(err)
150 - return
151 - }
152 -
153 - pmx.summary = &v
39 + return mx, nil
40 }
41
156 -func (c *Collector) queryQueryTypes(pmx *piholeMetrics) {
157 - req, err := web.NewHTTPRequestWithPath(c.RequestConfig, urlPathAPI)
158 - if err != nil {
159 - c.Error(err)
160 - return
42 +func (c *Collector) collectMetrics(mx map[string]int64) error {
43 + if c.auth == nil {
44 + return errors.New("no auth session")
45 }
46
163 - req.URL.RawQuery = url.Values{
164 - urlQueryKeyAuth: []string{c.Password},
165 - urlQueryKeyGetQueryTypes: []string{"true"},
166 - }.Encode()
167 -
168 - var v queryTypesMetrics
169 - err = c.doHTTP(req, &v)
47 + req, err := web.NewHTTPRequestWithPath(c.RequestConfig, urlPathAPIStatsSummary)
48 if err != nil {
171 - c.Error(err)
172 - return
49 + return err
50 }
51 + req.Header.Set("X-FTL-SID", c.auth.Session.Sid)
52 + req.Header.Set("X-FTL-CSRF", c.auth.Session.Csrf)
53
175 - pmx.queryTypes = &v
176 -}
54 + var resp ftlAPIStatsSummaryResponse
55
178 -func (c *Collector) queryForwardedDestinations(pmx *piholeMetrics) {
179 - req, err := web.NewHTTPRequestWithPath(c.RequestConfig, urlPathAPI)
180 - if err != nil {
181 - c.Error(err)
182 - return
56 + if err := web.DoHTTP(c.httpClient).RequestJSON(req, &resp); err != nil {
57 + return err
58 }
59
185 - req.URL.RawQuery = url.Values{
186 - urlQueryKeyAuth: []string{c.Password},
187 - urlQueryKeyGetForwardDestinations: []string{"true"},
188 - }.Encode()
189 -
190 - var v forwardDestinations
191 - err = c.doHTTP(req, &v)
192 - if err != nil {
193 - c.Error(err)
194 - return
60 + if resp.Took == nil {
61 + return fmt.Errorf("unexpected response from %s", req.URL)
62 }
63
197 - pmx.forwarders = &v
198 -}
199 -
200 -func (c *Collector) queryAPIVersion() (int, error) {
201 - req, err := web.NewHTTPRequestWithPath(c.RequestConfig, urlPathAPI)
202 - if err != nil {
203 - return 0, err
64 + for k, v := range stm.ToMap(resp) {
65 + mx[k] = v
66 }
67
206 - req.URL.RawQuery = url.Values{
207 - urlQueryKeyAuth: []string{c.Password},
208 - urlQueryKeyAPIVersion: []string{"true"},
209 - }.Encode()
210 -
211 - var v piholeAPIVersion
212 - err = c.doHTTP(req, &v)
213 - if err != nil {
214 - return 0, err
68 + // 0 if unknown
69 + if resp.Gravity.LastUpdate != 0 {
70 + mx["gravity_last_update_seconds_ago"] = int64(time.Since(time.Unix(resp.Gravity.LastUpdate, 0)).Seconds())
71 }
72
217 - return v.Version, nil
218 -}
219 -
220 -func (c *Collector) doHTTP(req *http.Request, dst any) error {
221 - return web.DoHTTP(c.httpClient).Request(req, func(body io.Reader) error {
222 - content, err := io.ReadAll(body)
223 - if err != nil {
224 - return fmt.Errorf("failed to read response: %v", err)
225 - }
226 -
227 - // empty array if unauthorized query or wrong query
228 - if isEmptyArray(content) {
229 - return errors.New("unauthorized access")
230 - }
231 -
232 - if err := json.Unmarshal(content, dst); err != nil {
233 - return fmt.Errorf("failed to decode JSON response: %v", err)
234 - }
235 -
236 - return nil
237 - })
238 -}
239 -
240 -func isEmptyArray(data []byte) bool {
241 - empty := "[]"
242 - return len(data) == len(empty) && string(data) == empty
243 -}
244 -
245 -func calcPercentage(value, total int64) (v int64) {
246 - if total == 0 {
247 - return 0
248 - }
249 - return int64(float64(value) * 100 / float64(total) * precision)
73 + return nil
74 }
src/go/plugin/go.d/collector/pihole/collector.go
+13 -25
@@ -8,7 +8,6 @@ import (
8 "errors"
9 "fmt"
10 "net/http"
11 - "sync"
11 "time"
12
13 "github.com/netdata/netdata/go/plugins/plugin/go.d/agent/module"
@@ -23,7 +22,7 @@ func init() {
22 module.Register("pihole", module.Creator{
23 JobConfigSchema: configSchema,
24 Defaults: module.Defaults{
26 - UpdateEvery: 5,
25 + UpdateEvery: 1,
26 },
27 Create: func() module.Module { return New() },
28 Config: func() any { return &Config{} },
@@ -35,18 +34,15 @@ func New() *Collector {
34 Config: Config{
35 HTTPConfig: web.HTTPConfig{
36 RequestConfig: web.RequestConfig{
38 - URL: "http://127.0.0.1",
37 + URL: "http://127.0.0.1",
38 + Password: "",
39 },
40 ClientConfig: web.ClientConfig{
41 - Timeout: confopt.Duration(time.Second * 5),
41 + Timeout: confopt.Duration(time.Second * 1),
42 },
43 },
44 - SetupVarsPath: "/etc/pihole/setupVars.conf",
44 },
46 - checkVersion: true,
47 - charts: baseCharts.Copy(),
48 - addQueriesTypesOnce: &sync.Once{},
49 - addFwsDestinationsOnce: &sync.Once{},
45 + charts: summaryCharts.Copy(),
46 }
47 }
48
@@ -54,20 +50,16 @@ type Config struct {
50 Vnode string `yaml:"vnode,omitempty" json:"vnode"`
51 UpdateEvery int `yaml:"update_every,omitempty" json:"update_every"`
52 web.HTTPConfig `yaml:",inline" json:""`
57 - SetupVarsPath string `yaml:"setup_vars_path" json:"setup_vars_path"`
53 }
54
55 type Collector struct {
56 module.Base
57 Config `yaml:",inline" json:""`
58
64 - charts *module.Charts
65 - addQueriesTypesOnce *sync.Once
66 - addFwsDestinationsOnce *sync.Once
59 + charts *module.Charts
60
61 httpClient *http.Client
69 -
70 - checkVersion bool
62 + auth *ftlAPIAuthResponse
63 }
64
65 func (c *Collector) Configuration() any {
@@ -75,23 +67,19 @@ func (c *Collector) Configuration() any {
67 }
68
69 func (c *Collector) Init(context.Context) error {
78 - if err := c.validateConfig(); err != nil {
79 - return fmt.Errorf("config validation: %v", err)
70 + if c.URL == "" {
71 + return errors.New("url not set")
72 + }
73 + if c.Password == "" {
74 + return errors.New("password not set")
75 }
76
82 - httpClient, err := c.initHTTPClient()
77 + httpClient, err := web.NewHTTPClient(c.ClientConfig)
78 if err != nil {
79 return fmt.Errorf("init http client: %v", err)
80 }
81 c.httpClient = httpClient
82
88 - c.Password = c.getWebPassword()
89 - if c.Password == "" {
90 - c.Warning("no web password, not all metrics available")
91 - } else {
92 - c.Debugf("web password: %s", c.Password)
93 - }
94 -
83 return nil
84 }
85
src/go/plugin/go.d/collector/pihole/collector_test.go
+181 -115
@@ -4,7 +4,8 @@ package pihole
4
5 import (
6 "context"
7 - "fmt"
7 + "encoding/json"
8 + "io"
9 "net/http"
10 "net/http/httptest"
11 "os"
@@ -17,29 +18,18 @@ import (
18 "github.com/stretchr/testify/require"
19 )
20
20 -const (
21 - pathSetupVarsOK = "testdata/setupVars.conf"
22 - pathSetupVarsWrong = "testdata/wrong.conf"
23 -)
24 -
21 var (
22 dataConfigJSON, _ = os.ReadFile("testdata/config.json")
23 dataConfigYAML, _ = os.ReadFile("testdata/config.yaml")
24
29 - dataEmptyResp = []byte("[]")
30 - dataSummaryRawResp, _ = os.ReadFile("testdata/summaryRaw.json")
31 - dataGetQueryTypesResp, _ = os.ReadFile("testdata/getQueryTypes.json")
32 - dataGetForwardDestinationsResp, _ = os.ReadFile("testdata/getForwardDestinations.json")
25 + dataStatsSummary, _ = os.ReadFile("testdata/v6.0.5/stats_summary.json")
26 )
27
28 func Test_testDataIsValid(t *testing.T) {
29 for name, data := range map[string][]byte{
37 - "dataConfigJSON": dataConfigJSON,
38 - "dataConfigYAML": dataConfigYAML,
39 - "dataEmptyResp": dataEmptyResp,
40 - "dataSummaryRawResp": dataSummaryRawResp,
41 - "dataGetQueryTypesResp": dataGetQueryTypesResp,
42 - "dataGetForwardDestinationsResp": dataGetForwardDestinationsResp,
30 + "dataConfigJSON": dataConfigJSON,
31 + "dataConfigYAML": dataConfigYAML,
32 + "dataStatsSummary": dataStatsSummary,
33 } {
34 require.NotNil(t, data, name)
35 }
@@ -54,8 +44,8 @@ func TestCollector_Init(t *testing.T) {
44 wantFail bool
45 config Config
46 }{
57 - "success with default": {
58 - wantFail: false,
47 + "fails with default": {
48 + wantFail: true,
49 config: New().Config,
50 },
51 "fail when URL not set": {
@@ -66,6 +56,14 @@ func TestCollector_Init(t *testing.T) {
56 },
57 },
58 },
59 + "fail when password not set": {
60 + wantFail: true,
61 + config: Config{
62 + HTTPConfig: web.HTTPConfig{
63 + RequestConfig: web.RequestConfig{URL: "http://127.0.0.1", Password: ""},
64 + },
65 + },
66 + },
67 }
68
69 for name, test := range tests {
@@ -87,17 +85,17 @@ func TestCollector_Check(t *testing.T) {
85 wantFail bool
86 prepare func(t *testing.T) (collr *Collector, cleanup func())
87 }{
90 - "success with web password": {
88 + "case success": {
89 wantFail: false,
92 - prepare: caseSuccessWithWebPassword,
90 + prepare: caseSuccess,
91 },
94 - "fail without web password": {
92 + "case wrong password": {
93 wantFail: true,
96 - prepare: caseFailNoWebPassword,
94 + prepare: caseWrongPassword,
95 },
98 - "fail on unsupported version": {
96 + "case error on stats summary": {
97 wantFail: true,
100 - prepare: caseFailUnsupportedVersion,
98 + prepare: caseErrOnStatsSummary,
99 },
100 }
101
@@ -125,42 +123,80 @@ func TestCollector_Collect(t *testing.T) {
123 wantMetrics map[string]int64
124 wantNumCharts int
125 }{
128 - "success with web password": {
129 - prepare: caseSuccessWithWebPassword,
130 - wantNumCharts: len(baseCharts) + 2,
126 + "case success": {
127 + prepare: caseSuccess,
128 + wantNumCharts: len(summaryCharts),
129 wantMetrics: map[string]int64{
132 - "A": 1229,
133 - "AAAA": 1229,
134 - "ANY": 100,
135 - "PTR": 7143,
136 - "SOA": 100,
137 - "SRV": 100,
138 - "TXT": 100,
139 - "ads_blocked_today": 1,
140 - "ads_blocked_today_perc": 33333,
141 - "ads_percentage_today": 100,
142 - "blocking_status_disabled": 0,
143 - "blocking_status_enabled": 1,
144 - "blocklist_last_update": 106273651,
145 - "destination_blocked": 220,
146 - "destination_cached": 8840,
147 - "destination_other": 940,
148 - "dns_queries_today": 1,
149 - "domains_being_blocked": 1,
150 - "queries_cached": 1,
151 - "queries_cached_perc": 33333,
152 - "queries_forwarded": 1,
153 - "queries_forwarded_perc": 33333,
154 - "unique_clients": 1,
130 + "clients_active": 2,
131 + "clients_total": 2,
132 + "gravity_domains_being_blocked": 131270,
133 + "gravity_last_update": 1741494842,
134 + "gravity_last_update_seconds_ago": 107202,
135 + "queries_blocked": 1,
136 + "queries_cached": 204,
137 + "queries_forwarded": 45,
138 + "queries_frequency": 0,
139 + "queries_percent_blocked": 1100,
140 + "queries_replies_BLOB": 1,
141 + "queries_replies_CNAME": 1,
142 + "queries_replies_DNSSEC": 1,
143 + "queries_replies_DOMAIN": 72,
144 + "queries_replies_IP": 124,
145 + "queries_replies_NODATA": 49,
146 + "queries_replies_NONE": 1,
147 + "queries_replies_NOTIMP": 1,
148 + "queries_replies_NXDOMAIN": 4,
149 + "queries_replies_OTHER": 1,
150 + "queries_replies_REFUSED": 1,
151 + "queries_replies_RRNAME": 1,
152 + "queries_replies_SERVFAIL": 1,
153 + "queries_replies_UNKNOWN": 1,
154 + "queries_status_CACHE": 121,
155 + "queries_status_CACHE_STALE": 83,
156 + "queries_status_DBBUSY": 1,
157 + "queries_status_DENYLIST": 1,
158 + "queries_status_DENYLIST_CNAME": 1,
159 + "queries_status_EXTERNAL_BLOCKED_EDE15": 1,
160 + "queries_status_EXTERNAL_BLOCKED_IP": 1,
161 + "queries_status_EXTERNAL_BLOCKED_NULL": 1,
162 + "queries_status_EXTERNAL_BLOCKED_NXRA": 1,
163 + "queries_status_FORWARDED": 45,
164 + "queries_status_GRAVITY": 1,
165 + "queries_status_GRAVITY_CNAME": 1,
166 + "queries_status_IN_PROGRESS": 1,
167 + "queries_status_REGEX": 1,
168 + "queries_status_REGEX_CNAME": 1,
169 + "queries_status_RETRIED": 1,
170 + "queries_status_RETRIED_DNSSEC": 1,
171 + "queries_status_SPECIAL_DOMAIN": 1,
172 + "queries_status_UNKNOWN": 1,
173 + "queries_total": 249,
174 + "queries_types_A": 84,
175 + "queries_types_AAAA": 84,
176 + "queries_types_ANY": 1,
177 + "queries_types_DNSKEY": 1,
178 + "queries_types_DS": 1,
179 + "queries_types_HTTPS": 1,
180 + "queries_types_MX": 1,
181 + "queries_types_NAPTR": 1,
182 + "queries_types_NS": 1,
183 + "queries_types_OTHER": 1,
184 + "queries_types_PTR": 73,
185 + "queries_types_RRSIG": 1,
186 + "queries_types_SOA": 1,
187 + "queries_types_SRV": 8,
188 + "queries_types_SVCB": 1,
189 + "queries_types_TXT": 1,
190 + "queries_unique_domains": 29,
191 },
192 },
157 - "fail without web password": {
158 - prepare: caseFailNoWebPassword,
159 - wantMetrics: nil,
193 + "case wrong password": {
194 + prepare: caseWrongPassword,
195 + wantNumCharts: len(summaryCharts),
196 },
161 - "fail on unsupported version": {
162 - prepare: caseFailUnsupportedVersion,
163 - wantMetrics: nil,
197 + "case error on stats summary": {
198 + prepare: caseErrOnStatsSummary,
199 + wantNumCharts: len(summaryCharts),
200 },
201 }
202
@@ -172,41 +208,44 @@ func TestCollector_Collect(t *testing.T) {
208 mx := collr.Collect(context.Background())
209
210 copyBlockListLastUpdate(mx, test.wantMetrics)
211 +
212 require.Equal(t, test.wantMetrics, mx)
213 +
214 + assert.Len(t, *collr.Charts(), test.wantNumCharts)
215 if len(test.wantMetrics) > 0 {
177 - assert.Len(t, *collr.Charts(), test.wantNumCharts)
216 + module.TestMetricsHasAllChartsDims(t, collr.Charts(), mx)
217 }
218 })
219 }
220 }
221
183 -func caseSuccessWithWebPassword(t *testing.T) (*Collector, func()) {
184 - collr, srv := New(), mockPiholeServer{}.newPiholeHTTPServer()
185 -
186 - collr.SetupVarsPath = pathSetupVarsOK
222 +func caseSuccess(t *testing.T) (collr *Collector, cleanup func()) {
223 + collr, mock := New(), mockPiholeServer{password: "secret"}
224 + srv := mock.newPiholeHTTPServer()
225 collr.URL = srv.URL
226 + collr.Password = mock.password
227
228 require.NoError(t, collr.Init(context.Background()))
229
230 return collr, srv.Close
231 }
232
194 -func caseFailNoWebPassword(t *testing.T) (*Collector, func()) {
195 - collr, srv := New(), mockPiholeServer{}.newPiholeHTTPServer()
196 -
197 - collr.SetupVarsPath = pathSetupVarsWrong
233 +func caseWrongPassword(t *testing.T) (collr *Collector, cleanup func()) {
234 + collr, mock := New(), mockPiholeServer{password: "secret"}
235 + srv := mock.newPiholeHTTPServer()
236 collr.URL = srv.URL
237 + collr.Password = mock.password + "!"
238
239 require.NoError(t, collr.Init(context.Background()))
240
241 return collr, srv.Close
242 }
243
205 -func caseFailUnsupportedVersion(t *testing.T) (*Collector, func()) {
206 - collr, srv := New(), mockPiholeServer{unsupportedVersion: true}.newPiholeHTTPServer()
207 -
208 - collr.SetupVarsPath = pathSetupVarsOK
244 +func caseErrOnStatsSummary(t *testing.T) (collr *Collector, cleanup func()) {
245 + collr, mock := New(), mockPiholeServer{password: "secret", errOnStatsSummary: true}
246 + srv := mock.newPiholeHTTPServer()
247 collr.URL = srv.URL
248 + collr.Password = mock.password
249
250 require.NoError(t, collr.Init(context.Background()))
251
@@ -214,63 +253,90 @@ func caseFailUnsupportedVersion(t *testing.T) (*Collector, func()) {
253 }
254
255 type mockPiholeServer struct {
217 - unsupportedVersion bool
218 - errOnAPIVersion bool
219 - errOnSummary bool
220 - errOnQueryTypes bool
221 - errOnGetForwardDst bool
256 + password string
257 + errOnStatsSummary bool
258 }
259
260 func (m mockPiholeServer) newPiholeHTTPServer() *httptest.Server {
225 - return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
226 - if r.URL.Path != urlPathAPI || len(r.URL.Query()) == 0 {
227 - w.WriteHeader(http.StatusBadRequest)
228 - }
229 -
230 - if r.URL.Query().Get(urlQueryKeyAuth) == "" {
231 - _, _ = w.Write(dataEmptyResp)
232 - return
233 - }
261 + const (
262 + ftlSid = "ftl-sid"
263 + ftlCsrf = "ftl-csrf"
264 + )
265
235 - if r.URL.Query().Has(urlQueryKeyAPIVersion) {
236 - if m.errOnAPIVersion {
237 - w.WriteHeader(http.StatusNotFound)
238 - } else if m.unsupportedVersion {
239 - _, _ = w.Write([]byte(fmt.Sprintf(`{"version": %d}`, wantAPIVersion+1)))
240 - } else {
241 - _, _ = w.Write([]byte(fmt.Sprintf(`{"version": %d}`, wantAPIVersion)))
266 + return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
267 + switch r.URL.Path {
268 + case urlPathAPIAuth:
269 + switch r.Method {
270 + case http.MethodGet:
271 + if r.Header.Get("X-FTL-SID") != ftlSid || r.Header.Get("X-FTL-CSRF") != ftlCsrf {
272 + var resp ftlErrorResponse
273 + resp.Error.Key = "unauthorized"
274 + resp.Error.Message = "Unauthorized"
275 + w.WriteHeader(http.StatusUnauthorized)
276 + bs, _ := json.Marshal(resp)
277 + _, _ = w.Write(bs)
278 + return
279 + }
280 +
281 + var resp ftlAPIAuthResponse
282 + resp.Session.Valid = true
283 + resp.Session.Sid = ftlSid
284 + resp.Session.Csrf = ftlCsrf
285 + bs, _ := json.Marshal(resp)
286 + _, _ = w.Write(bs)
287 + case http.MethodPost:
288 + bs, err := io.ReadAll(r.Body)
289 + if err != nil {
290 + w.WriteHeader(http.StatusBadRequest)
291 + return
292 + }
293 +
294 + var pass struct {
295 + Password string `json:"password"`
296 + }
297 + if err := json.Unmarshal(bs, &pass); err != nil {
298 + w.WriteHeader(http.StatusBadRequest)
299 + return
300 + }
301 +
302 + if pass.Password != m.password {
303 + var resp ftlAPIAuthResponse
304 + w.WriteHeader(http.StatusUnauthorized)
305 + bs, _ := json.Marshal(resp)
306 + _, _ = w.Write(bs)
307 + return
308 + }
309 +
310 + var resp ftlAPIAuthResponse
311 + resp.Session.Valid = true
312 + resp.Session.Sid = ftlSid
313 + resp.Session.Csrf = ftlCsrf
314 + bs, _ = json.Marshal(resp)
315 + _, _ = w.Write(bs)
316 }
243 - return
244 - }
245 -
246 - if r.URL.Query().Has(urlQueryKeySummaryRaw) {
247 - if m.errOnSummary {
248 - w.WriteHeader(http.StatusNotFound)
249 - } else {
250 - _, _ = w.Write(dataSummaryRawResp)
317 + case urlPathAPIStatsSummary:
318 + if m.errOnStatsSummary {
319 + w.WriteHeader(http.StatusBadRequest)
320 + return
321 }
252 - return
253 - }
254 -
255 - data := dataEmptyResp
256 - isErr := false
257 - switch {
258 - case r.URL.Query().Has(urlQueryKeyGetQueryTypes):
259 - data, isErr = dataGetQueryTypesResp, m.errOnQueryTypes
260 - case r.URL.Query().Has(urlQueryKeyGetForwardDestinations):
261 - data, isErr = dataGetForwardDestinationsResp, m.errOnGetForwardDst
262 - }
263 -
264 - if isErr {
265 - w.WriteHeader(http.StatusNotFound)
266 - } else {
267 - _, _ = w.Write(data)
322 + if r.Header.Get("X-FTL-SID") != ftlSid || r.Header.Get("X-FTL-CSRF") != ftlCsrf {
323 + var resp ftlErrorResponse
324 + resp.Error.Key = "unauthorized"
325 + resp.Error.Message = "Unauthorized"
326 + w.WriteHeader(http.StatusUnauthorized)
327 + bs, _ := json.Marshal(resp)
328 + _, _ = w.Write(bs)
329 + return
330 + }
331 + _, _ = w.Write(dataStatsSummary)
332 + default:
333 + w.WriteHeader(http.StatusBadRequest)
334 }
335 }))
336 }
337
338 func copyBlockListLastUpdate(dst, src map[string]int64) {
273 - k := "blocklist_last_update"
339 + k := "gravity_last_update_seconds_ago"
340 if v, ok := src[k]; ok {
341 if _, ok := dst[k]; ok {
342 dst[k] = v
src/go/plugin/go.d/collector/pihole/init.go deleted
-78
@@ -1,78 +0,0 @@
1 -// SPDX-License-Identifier: GPL-3.0-or-later
2 -
3 -package pihole
4 -
5 -import (
6 - "bufio"
7 - "errors"
8 - "fmt"
9 - "net/http"
10 - "os"
11 - "strings"
12 -
13 - "github.com/netdata/netdata/go/plugins/plugin/go.d/pkg/web"
14 -)
15 -
16 -func (c *Collector) validateConfig() error {
17 - if c.URL == "" {
18 - return errors.New("url not set")
19 - }
20 - return nil
21 -}
22 -
23 -func (c *Collector) initHTTPClient() (*http.Client, error) {
24 - return web.NewHTTPClient(c.ClientConfig)
25 -}
26 -
27 -func (c *Collector) getWebPassword() string {
28 - // do no read setupVarsPath is password is set in the configuration file
29 - if c.Password != "" {
30 - return c.Password
31 - }
32 - if !isLocalHost(c.URL) {
33 - c.Info("abort web password auto detection, host is not localhost")
34 - return ""
35 - }
36 -
37 - c.Infof("starting web password auto detection, reading : %s", c.SetupVarsPath)
38 - pass, err := getWebPassword(c.SetupVarsPath)
39 - if err != nil {
40 - c.Warningf("error during reading '%s' : %v", c.SetupVarsPath, err)
41 - }
42 -
43 - return pass
44 -}
45 -
46 -func getWebPassword(path string) (string, error) {
47 - f, err := os.Open(path)
48 - if err != nil {
49 - return "", err
50 - }
51 - defer func() { _ = f.Close() }()
52 -
53 - s := bufio.NewScanner(f)
54 - var password string
55 -
56 - for s.Scan() && password == "" {
57 - if strings.HasPrefix(s.Text(), "WEBPASSWORD") {
58 - parts := strings.Split(s.Text(), "=")
59 - if len(parts) != 2 {
60 - return "", fmt.Errorf("unparsable line : %s", s.Text())
61 - }
62 - password = parts[1]
63 - }
64 - }
65 -
66 - return password, nil
67 -}
68 -
69 -func isLocalHost(u string) bool {
70 - if strings.Contains(u, "127.0.0.1") {
71 - return true
72 - }
73 - if strings.Contains(u, "localhost") {
74 - return true
75 - }
76 -
77 - return false
78 -}
src/go/plugin/go.d/collector/pihole/metadata.yaml
+109 -67
@@ -21,10 +21,11 @@ modules:
21 overview:
22 data_collection:
23 metrics_description: |
24 - This collector monitors Pi-hole instances using [PHP API](https://github.com/pi-hole/AdminLTE).
24 + This collector monitors Pi-hole instances using [Pi-hole API 6.0](https://ftl.pi-hole.net/master/docs/).
25
26 - The data provided by the API is for the last 24 hours. All collected values refer to this time period and not to the
27 - module's collection interval.
26 + It collects DNS query statistics including total queries, blocked domains, query types, resolution status, and client information.
27 +
28 + **Note**: This collector is not compatible with Pi-hole versions earlier than v6.0.
29 method_description: ""
30 supported_platforms:
31 include: []
@@ -34,14 +35,27 @@ modules:
35 description: ""
36 default_behavior:
37 auto_detection:
37 - description: ""
38 + description: |
39 + By default, it detects Pi-hole instances running on:
40 +
41 + - localhost that are listening on port 80
42 + - within Docker containers
43 +
44 + > **Note that the Pi-hole API 6.0 requires a password**.
45 + > While Netdata can automatically detect Pi-hole instances and create data collection jobs, these jobs will fail unless you provide the necessary credentials.
46 limits:
47 description: ""
48 performance_impact:
49 description: ""
50 setup:
51 prerequisites:
44 - list: []
52 + list:
53 + - title: Pi-hole v6.0 or newer
54 + description: |
55 + This collector requires Pi-hole v6.0 or newer as it uses the [Pi-hole API 6.0](https://ftl.pi-hole.net/master/docs/).
56 + - title: Authentication credentials
57 + description: |
58 + Pi-hole administrator password is required for API authentication. Make sure to configure this in the collector settings even when using auto-detection.
59 configuration:
60 file:
61 name: go.d/pihole.conf
@@ -54,7 +68,7 @@ modules:
68 list:
69 - name: update_every
70 description: Data collection frequency.
57 - default_value: 5
71 + default_value: 1
72 required: false
73 - name: autodetection_retry
74 description: Recheck interval in seconds. Zero means no recheck will be scheduled.
@@ -64,13 +78,9 @@ modules:
78 description: Server URL.
79 default_value: http://127.0.0.1
80 required: true
67 - - name: setup_vars_path
68 - description: Path to setupVars.conf. This file is used to get the web password.
69 - default_value: /etc/pihole/setupVars.conf
70 - required: false
81 - name: timeout
82 description: HTTP request timeout.
73 - default_value: 5
83 + default_value: 1
84 required: false
85 - name: username
86 description: Username for basic HTTP authentication.
@@ -79,7 +89,7 @@ modules:
89 - name: password
90 description: Password for basic HTTP authentication.
91 default_value: ""
82 - required: false
92 + required: true
93 - name: proxy_url
94 description: Proxy URL.
95 default_value: ""
@@ -135,6 +145,7 @@ modules:
145 jobs:
146 - name: local
147 url: http://127.0.0.1
148 + password: Gv7#pQm9Xy
149 - name: HTTPS with self-signed certificate
150 description: Remote instance with enabled HTTPS and self-signed certificate.
151 config: |
@@ -142,7 +153,7 @@ modules:
153 - name: local
154 url: https://203.0.113.11
155 tls_skip_verify: yes
145 - password: 1ebd33f882f9aa5fac26a7cb74704742f91100228eb322e41b7bd6e6aeb8f74b
156 + password: bT4@zK1wVr
157 - name: Multi-instance
158 description: |
159 > **Note**: When you define multiple jobs, their names must be unique.
@@ -152,22 +163,19 @@ modules:
163 jobs:
164 - name: local
165 url: http://127.0.0.1
166 + password: Gv7#pQm9Xy
167
168 - name: remote
169 url: http://203.0.113.10
158 - password: 1ebd33f882f9aa5fac26a7cb74704742f91100228eb322e41b7bd6e6aeb8f74b
170 + password: bT4@zK1wVr
171 troubleshooting:
172 problems:
173 list: []
174 alerts:
163 - - name: pihole_blocklist_last_update
164 - metric: pihole.blocklist_last_update
175 + - name: pihole_gravity_list_last_update
176 + metric: pihole.gravity_list_last_update_time_ago
177 info: "gravity.list (blocklist) file last update time"
178 link: https://github.com/netdata/netdata/blob/master/src/health/health.d/pihole.conf
167 - - name: pihole_status
168 - metric: pihole.unwanted_domains_blocking_status
169 - info: unwanted domains blocking is disabled
170 - link: https://github.com/netdata/netdata/blob/master/src/health/health.d/pihole.conf
179 metrics:
180 folding:
181 title: Metrics
@@ -180,69 +188,103 @@ modules:
188 labels: []
189 metrics:
190 - name: pihole.dns_queries_total
183 - description: DNS Queries Total (Cached, Blocked and Forwarded)
184 - unit: queries
191 + description: Pi-hole DNS Queries Total (Cached, Blocked and Forwarded)
192 + unit: queries/s
193 chart_type: line
194 dimensions:
195 - name: queries
188 - - name: pihole.dns_queries
189 - description: DNS Queries
190 - unit: queries
191 - chart_type: stacked
196 + - name: pihole.dns_queries_blocked_percent
197 + description: Pi-hole DNS Queries Blocked Percent
198 + unit: percent
199 + chart_type: line
200 dimensions:
193 - - name: cached
201 - name: blocked
195 - - name: forwarded
196 - - name: pihole.dns_queries_percentage
197 - description: DNS Queries Percentage
198 - unit: percentage
202 + - name: pihole.dns_queries_by_destination
203 + description: Pi-hole DNS Queries by Destination
204 + unit: queries/s
205 chart_type: stacked
206 dimensions:
207 - name: cached
208 - name: blocked
209 - name: forwarded
204 - - name: pihole.unique_clients
205 - description: Unique Clients
210 + - name: pihole.dns_queries_by_type
211 + description: Pi-hole DNS Queries by Type
212 + unit: queries/s
213 + chart_type: stacked
214 + dimensions:
215 + - name: A
216 + - name: AAA
217 + - name: ANY
218 + - name: SRV
219 + - name: SOA
220 + - name: PTR
221 + - name: TXT
222 + - name: NAPTR
223 + - name: MX
224 + - name: DS
225 + - name: RRSIG
226 + - name: DNSKEY
227 + - name: NS
228 + - name: SVCB
229 + - name: HTTPS
230 + - name: OTHER
231 + - name: pihole.dns_queries_by_status
232 + description: Pi-hole DNS Queries by Status
233 + unit: queries/s
234 + chart_type: stacked
235 + dimensions:
236 + - name: UNKNOWN
237 + - name: GRAVITY
238 + - name: FORWARDED
239 + - name: CACHE
240 + - name: REGEX
241 + - name: DENYLIST
242 + - name: EXTERNAL_BLOCKED_IP
243 + - name: EXTERNAL_BLOCKED_NULL
244 + - name: EXTERNAL_BLOCKED_NXRA
245 + - name: GRAVITY_CNAME
246 + - name: REGEX_CNAME
247 + - name: DENYLIST_CNAME
248 + - name: RETRIED
249 + - name: RETRIED_DNSSEC
250 + - name: IN_PROGRESS
251 + - name: DBBUSY
252 + - name: SPECIAL_DOMAIN
253 + - name: CACHE_STALE
254 + - name: EXTERNAL_BLOCKED_EDE15
255 + - name: pihole.dns_replies_by_status
256 + description: Pi-hole DNS Replies by Status
257 + unit: replies/s
258 + chart_type: stacked
259 + dimensions:
260 + - name: UNKNOWN
261 + - name: NODATA
262 + - name: NXDOMAIN
263 + - name: CNAME
264 + - name: IP
265 + - name: DOMAIN
266 + - name: RRNAME
267 + - name: SERVFAIL
268 + - name: REFUSED
269 + - name: NOTIMP
270 + - name: DNSSEC
271 + - name: NONE
272 + - name: OTHER
273 + - name: pihole.active_clients
274 + description: Pi-hole Active Clients (Seen in the Last 24 Hours)
275 unit: clients
276 chart_type: line
277 dimensions:
209 - - name: unique
210 - - name: pihole.domains_on_blocklist
211 - description: Domains On Blocklist
278 + - name: active
279 + - name: pihole.gravity_list_blocked_domains
280 + description: Pi-hole Gravity List Blocked Domains
281 unit: domains
282 chart_type: line
283 dimensions:
215 - - name: blocklist
216 - - name: pihole.blocklist_last_update
217 - description: Blocklist Last Update
284 + - name: blocked
285 + - name: pihole.gravity_list_last_update_time_ago
286 + description: Pi-hole Gravity List Time Since Last Update
287 unit: seconds
288 chart_type: line
289 dimensions:
221 - - name: ago
222 - - name: pihole.unwanted_domains_blocking_status
223 - description: Unwanted Domains Blocking Status
224 - unit: status
225 - chart_type: line
226 - dimensions:
227 - - name: enabled
228 - - name: disabled
229 - - name: pihole.dns_queries_types
230 - description: DNS Queries Per Type
231 - unit: percentage
232 - chart_type: stacked
233 - dimensions:
234 - - name: a
235 - - name: aaaa
236 - - name: any
237 - - name: ptr
238 - - name: soa
239 - - name: srv
240 - - name: txt
241 - - name: pihole.dns_queries_forwarded_destination
242 - description: DNS Queries Per Destination
243 - unit: percentage
244 - chart_type: stacked
245 - dimensions:
246 - - name: cached
247 - - name: blocked
248 - - name: other
290 + - name: last_update_ago
src/go/plugin/go.d/collector/pihole/testdata/config.json
+1 -2
@@ -18,6 +18,5 @@
18 "tls_cert": "ok",
19 "tls_key": "ok",
20 "tls_skip_verify": true,
21 - "force_http2": true,
22 - "setup_vars_path": "ok"
21 + "force_http2": true
22 }
src/go/plugin/go.d/collector/pihole/testdata/config.yaml
-1
@@ -17,4 +17,3 @@ tls_cert: "ok"
17 tls_key: "ok"
18 tls_skip_verify: yes
19 force_http2: yes
20 -setup_vars_path: "ok"
src/go/plugin/go.d/collector/pihole/testdata/getForwardDestinations.json deleted
-7
@@ -1,7 +0,0 @@
1 -{
2 - "forward_destinations": {
3 - "blocked|blocked": 2.2,
4 - "cached|cached": 88.4,
5 - "other|other": 9.4
6 - }
7 -}
\ No newline at end of file
src/go/plugin/go.d/collector/pihole/testdata/getQueryTypes.json deleted
-11
@@ -1,11 +0,0 @@
1 -{
2 - "querytypes": {
3 - "A (IPv4)": 12.29,
4 - "AAAA (IPv6)": 12.29,
5 - "ANY": 1,
6 - "SRV": 1,
7 - "SOA": 1,
8 - "PTR": 71.43,
9 - "TXT": 1
10 - }
11 -}
\ No newline at end of file
src/go/plugin/go.d/collector/pihole/testdata/setupVars.conf deleted
-11
@@ -1,11 +0,0 @@
1 -WEBPASSWORD=1ebd33f882f9aa5fac26a7cb74704742f91100228eb322e41b7bd6e6aeb8f74b
2 -BLOCKING_ENABLED=true
3 -PIHOLE_INTERFACE=enp0s9
4 -IPV4_ADDRESS=192.168.88.228/24
5 -IPV6_ADDRESS=
6 -PIHOLE_DNS_1=208.67.222.222
7 -PIHOLE_DNS_2=208.67.220.220
8 -QUERY_LOGGING=true
9 -INSTALL_WEB_SERVER=true
10 -INSTALL_WEB_INTERFACE=true
11 -LIGHTTPD_ENABLED=true
\ No newline at end of file
src/go/plugin/go.d/collector/pihole/testdata/summaryRaw.json deleted
-27
@@ -1,27 +0,0 @@
1 -{
2 - "domains_being_blocked": 1,
3 - "dns_queries_today": 1,
4 - "ads_blocked_today": 1,
5 - "ads_percentage_today": 1,
6 - "unique_domains": 1,
7 - "queries_forwarded": 1,
8 - "queries_cached": 1,
9 - "clients_ever_seen": 1,
10 - "unique_clients": 1,
11 - "dns_queries_all_types": 1,
12 - "reply_NODATA": 1,
13 - "reply_NXDOMAIN": 1,
14 - "reply_CNAME": 1,
15 - "reply_IP": 1,
16 - "privacy_level": 1,
17 - "status": "enabled",
18 - "gravity_last_updated": {
19 - "file_exists": true,
20 - "absolute": 1560443834,
21 - "relative": {
22 - "days": "3",
23 - "hours": "06",
24 - "minutes": "05"
25 - }
26 - }
27 -}
\ No newline at end of file
src/go/plugin/go.d/collector/pihole/testdata/v6.0.5/stats_summary.json new
+75
@@ -0,0 +1,75 @@
1 +{
2 + "queries": {
3 + "total": 249,
4 + "blocked": 1,
5 + "percent_blocked": 1.1,
6 + "unique_domains": 29,
7 + "forwarded": 45,
8 + "cached": 204,
9 + "frequency": 0,
10 + "types": {
11 + "A": 84,
12 + "AAAA": 84,
13 + "ANY": 1,
14 + "SRV": 8,
15 + "SOA": 1,
16 + "PTR": 73,
17 + "TXT": 1,
18 + "NAPTR": 1,
19 + "MX": 1,
20 + "DS": 1,
21 + "RRSIG": 1,
22 + "DNSKEY": 1,
23 + "NS": 1,
24 + "SVCB": 1,
25 + "HTTPS": 1,
26 + "OTHER": 1
27 + },
28 + "status": {
29 + "UNKNOWN": 1,
30 + "GRAVITY": 1,
31 + "FORWARDED": 45,
32 + "CACHE": 121,
33 + "REGEX": 1,
34 + "DENYLIST": 1,
35 + "EXTERNAL_BLOCKED_IP": 1,
36 + "EXTERNAL_BLOCKED_NULL": 1,
37 + "EXTERNAL_BLOCKED_NXRA": 1,
38 + "GRAVITY_CNAME": 1,
39 + "REGEX_CNAME": 1,
40 + "DENYLIST_CNAME": 1,
41 + "RETRIED": 1,
42 + "RETRIED_DNSSEC": 1,
43 + "IN_PROGRESS": 1,
44 + "DBBUSY": 1,
45 + "SPECIAL_DOMAIN": 1,
46 + "CACHE_STALE": 83,
47 + "EXTERNAL_BLOCKED_EDE15": 1
48 + },
49 + "replies": {
50 + "UNKNOWN": 1,
51 + "NODATA": 49,
52 + "NXDOMAIN": 4,
53 + "CNAME": 1,
54 + "IP": 124,
55 + "DOMAIN": 72,
56 + "RRNAME": 1,
57 + "SERVFAIL": 1,
58 + "REFUSED": 1,
59 + "NOTIMP": 1,
60 + "OTHER": 1,
61 + "DNSSEC": 1,
62 + "NONE": 1,
63 + "BLOB": 1
64 + }
65 + },
66 + "clients": {
67 + "active": 2,
68 + "total": 2
69 + },
70 + "gravity": {
71 + "domains_being_blocked": 131270,
72 + "last_update": 1741494842
73 + },
74 + "took": 7.43865966796875e-05
75 +}
src/go/plugin/go.d/config/go.d/sd/net_listeners.conf
+1 -1
@@ -109,7 +109,7 @@ classify:
109 - tags: "pgbouncer"
110 expr: '{{ or (eq .Port "6432") (eq .Comm "pgbouncer") }}'
111 - tags: "pihole"
112 - expr: '{{ and (eq .Port "53") (eq .Comm "pihole-FTL") }}'
112 + expr: '{{ and (eq .Port "80") (eq .Comm "pihole-FTL") }}'
113 - tags: "pika"
114 expr: '{{ and (eq .Port "9221") (eq .Comm "pika") }}'
115 - tags: "postgres"
src/go/plugin/go.d/pkg/web/client.go
+7 -2
@@ -8,6 +8,7 @@ import (
8 "fmt"
9 "io"
10 "net/http"
11 + "strings"
12 )
13
14 type Client struct {
@@ -69,13 +70,13 @@ func (c *Client) handleNokCode(req *http.Request, resp *http.Response) error {
70 if c.onNokCode != nil {
71 handled, err := c.onNokCode(resp)
72 if err != nil {
72 - return fmt.Errorf("'%s' returned HTTP status code: %d (%w)", req.URL, resp.StatusCode, err)
73 + return fmt.Errorf("%s '%s' returned HTTP status code: %d (%w)", req.Method, req.URL, resp.StatusCode, err)
74 }
75 if handled {
76 return nil
77 }
78 }
78 - return fmt.Errorf("'%s' returned HTTP status code: %d", req.URL, resp.StatusCode)
79 + return fmt.Errorf("%s '%s' returned HTTP status code: %d", req.Method, req.URL, resp.StatusCode)
80 }
81
82 func CloseBody(resp *http.Response) {
@@ -84,3 +85,7 @@ func CloseBody(resp *http.Response) {
85 _ = resp.Body.Close()
86 }
87 }
88 +
89 +func IsStatusCode(err error, code int) bool {
90 + return err != nil && strings.Contains(err.Error(), fmt.Sprintf("code: %d", code))
91 +}
src/health/health.d/pihole.conf
+2 -19
@@ -2,8 +2,8 @@
2 # Blocklist last update time.
3 # Default update interval is a week.
4
5 - template: pihole_blocklist_last_update
6 - on: pihole.blocklist_last_update
5 + template: pihole_gravity_list_last_update
6 + on: pihole.gravity_list_last_update_time_ago
7 class: Errors
8 type: Ad Filtering
9 component: Pi-hole
@@ -14,20 +14,3 @@ component: Pi-hole
14 summary: Pi-hole blocklist last update
15 info: gravity.list (blocklist) file last update time
16 to: sysadmin
17 -
18 -# Pi-hole's ability to block unwanted domains.
19 -# Should be enabled. The whole point of Pi-hole!
20 -
21 - template: pihole_status
22 - on: pihole.unwanted_domains_blocking_status
23 - class: Errors
24 - type: Ad Filtering
25 -component: Pi-hole
26 - every: 10s
27 - units: status
28 - calc: $disabled
29 - warn: $this != nan AND $this == 1
30 - delay: up 2m down 5m
31 - summary: Pi-hole domains blocking status
32 - info: Unwanted domains blocking is disabled
33 - to: sysadmin