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