github.com/netdata/go.d.plugin@v0.58.1/modules/pihole/collect.go (about) 1 // SPDX-License-Identifier: GPL-3.0-or-later 2 3 package pihole 4 5 import ( 6 "encoding/json" 7 "fmt" 8 "io" 9 "net/http" 10 "net/url" 11 "strings" 12 "sync" 13 "time" 14 15 "github.com/netdata/go.d.plugin/pkg/web" 16 ) 17 18 const wantAPIVersion = 3 19 20 const ( 21 urlPathAPI = "/admin/api.php" 22 urlQueryKeyAuth = "auth" 23 urlQueryKeyAPIVersion = "version" 24 urlQueryKeySummaryRaw = "summaryRaw" 25 urlQueryKeyGetQueryTypes = "getQueryTypes" // need auth 26 urlQueryKeyGetForwardDestinations = "getForwardDestinations" // need auth 27 ) 28 29 const ( 30 precision = 1000 31 ) 32 33 func (p *Pihole) collect() (map[string]int64, error) { 34 if p.checkVersion { 35 ver, err := p.queryAPIVersion() 36 if err != nil { 37 return nil, err 38 } 39 if ver != wantAPIVersion { 40 return nil, fmt.Errorf("API version: %d, supported version: %d", ver, wantAPIVersion) 41 } 42 p.checkVersion = false 43 } 44 45 pmx := new(piholeMetrics) 46 p.queryMetrics(pmx, true) 47 48 if pmx.hasQueryTypes() { 49 p.addQueriesTypesOnce.Do(p.addChartDNSQueriesType) 50 } 51 if pmx.hasForwarders() { 52 p.addFwsDestinationsOnce.Do(p.addChartDNSQueriesForwardedDestinations) 53 } 54 55 mx := make(map[string]int64) 56 p.collectMetrics(mx, pmx) 57 58 return mx, nil 59 } 60 61 func (p *Pihole) collectMetrics(mx map[string]int64, pmx *piholeMetrics) { 62 if pmx.hasSummary() { 63 mx["ads_blocked_today"] = pmx.summary.AdsBlockedToday 64 mx["ads_percentage_today"] = int64(pmx.summary.AdsPercentageToday * 100) 65 mx["domains_being_blocked"] = pmx.summary.DomainsBeingBlocked 66 // GravityLastUpdated.Absolute is <nil> if the file does not exist (deleted/moved) 67 if pmx.summary.GravityLastUpdated.Absolute != nil { 68 mx["blocklist_last_update"] = time.Now().Unix() - *pmx.summary.GravityLastUpdated.Absolute 69 } 70 mx["dns_queries_today"] = pmx.summary.DNSQueriesToday 71 mx["queries_forwarded"] = pmx.summary.QueriesForwarded 72 mx["queries_cached"] = pmx.summary.QueriesCached 73 mx["unique_clients"] = pmx.summary.UniqueClients 74 mx["blocking_status_enabled"] = boolToInt(pmx.summary.Status == "enabled") 75 mx["blocking_status_disabled"] = boolToInt(pmx.summary.Status != "enabled") 76 77 tot := pmx.summary.QueriesCached + pmx.summary.AdsBlockedToday + pmx.summary.QueriesForwarded 78 mx["queries_cached_perc"] = calcPercentage(pmx.summary.QueriesCached, tot) 79 mx["ads_blocked_today_perc"] = calcPercentage(pmx.summary.AdsBlockedToday, tot) 80 mx["queries_forwarded_perc"] = calcPercentage(pmx.summary.QueriesForwarded, tot) 81 } 82 83 if pmx.hasQueryTypes() { 84 mx["A"] = int64(pmx.queryTypes.Types.A * 100) 85 mx["AAAA"] = int64(pmx.queryTypes.Types.AAAA * 100) 86 mx["ANY"] = int64(pmx.queryTypes.Types.ANY * 100) 87 mx["PTR"] = int64(pmx.queryTypes.Types.PTR * 100) 88 mx["SOA"] = int64(pmx.queryTypes.Types.SOA * 100) 89 mx["SRV"] = int64(pmx.queryTypes.Types.SRV * 100) 90 mx["TXT"] = int64(pmx.queryTypes.Types.TXT * 100) 91 } 92 93 if pmx.hasForwarders() { 94 for k, v := range pmx.forwarders.Destinations { 95 name := strings.Split(k, "|")[0] 96 mx["destination_"+name] = int64(v * 100) 97 } 98 } 99 } 100 101 func (p *Pihole) queryMetrics(pmx *piholeMetrics, doConcurrently bool) { 102 type task func(*piholeMetrics) 103 104 var tasks = []task{p.querySummary} 105 106 if p.Password != "" { 107 tasks = []task{ 108 p.querySummary, 109 p.queryQueryTypes, 110 p.queryForwardedDestinations, 111 } 112 } 113 114 wg := &sync.WaitGroup{} 115 116 wrap := func(call task) task { 117 return func(metrics *piholeMetrics) { call(metrics); wg.Done() } 118 } 119 120 for _, task := range tasks { 121 if doConcurrently { 122 wg.Add(1) 123 task = wrap(task) 124 go task(pmx) 125 } else { 126 task(pmx) 127 } 128 } 129 130 wg.Wait() 131 } 132 133 func (p *Pihole) querySummary(pmx *piholeMetrics) { 134 req, err := web.NewHTTPRequest(p.Request) 135 if err != nil { 136 p.Error(err) 137 return 138 } 139 140 req.URL.Path = urlPathAPI 141 req.URL.RawQuery = url.Values{ 142 urlQueryKeyAuth: []string{p.Password}, 143 urlQueryKeySummaryRaw: []string{"true"}, 144 }.Encode() 145 146 var v summaryRawMetrics 147 if err = p.doWithDecode(&v, req); err != nil { 148 p.Error(err) 149 return 150 } 151 152 pmx.summary = &v 153 } 154 155 func (p *Pihole) queryQueryTypes(pmx *piholeMetrics) { 156 req, err := web.NewHTTPRequest(p.Request) 157 if err != nil { 158 p.Error(err) 159 return 160 } 161 162 req.URL.Path = urlPathAPI 163 req.URL.RawQuery = url.Values{ 164 urlQueryKeyAuth: []string{p.Password}, 165 urlQueryKeyGetQueryTypes: []string{"true"}, 166 }.Encode() 167 168 var v queryTypesMetrics 169 err = p.doWithDecode(&v, req) 170 if err != nil { 171 p.Error(err) 172 return 173 } 174 175 pmx.queryTypes = &v 176 } 177 178 func (p *Pihole) queryForwardedDestinations(pmx *piholeMetrics) { 179 req, err := web.NewHTTPRequest(p.Request) 180 if err != nil { 181 p.Error(err) 182 return 183 } 184 185 req.URL.Path = urlPathAPI 186 req.URL.RawQuery = url.Values{ 187 urlQueryKeyAuth: []string{p.Password}, 188 urlQueryKeyGetForwardDestinations: []string{"true"}, 189 }.Encode() 190 191 var v forwardDestinations 192 err = p.doWithDecode(&v, req) 193 if err != nil { 194 p.Error(err) 195 return 196 } 197 198 pmx.forwarders = &v 199 } 200 201 func (p *Pihole) queryAPIVersion() (int, error) { 202 req, err := web.NewHTTPRequest(p.Request) 203 if err != nil { 204 return 0, err 205 } 206 207 req.URL.Path = urlPathAPI 208 req.URL.RawQuery = url.Values{ 209 urlQueryKeyAuth: []string{p.Password}, 210 urlQueryKeyAPIVersion: []string{"true"}, 211 }.Encode() 212 213 var v piholeAPIVersion 214 err = p.doWithDecode(&v, req) 215 if err != nil { 216 return 0, err 217 } 218 219 return v.Version, nil 220 } 221 222 func (p *Pihole) doWithDecode(dst interface{}, req *http.Request) error { 223 resp, err := p.httpClient.Do(req) 224 if err != nil { 225 return err 226 } 227 defer closeBody(resp) 228 229 if resp.StatusCode != http.StatusOK { 230 return fmt.Errorf("%s returned %d status code", req.URL, resp.StatusCode) 231 } 232 233 content, err := io.ReadAll(resp.Body) 234 if err != nil { 235 return fmt.Errorf("error on reading response from %s : %v", req.URL, err) 236 } 237 238 // empty array if unauthorized query or wrong query 239 if isEmptyArray(content) { 240 return fmt.Errorf("unauthorized access to %s", req.URL) 241 } 242 243 if err := json.Unmarshal(content, dst); err != nil { 244 return fmt.Errorf("error on parsing response from %s : %v", req.URL, err) 245 } 246 247 return nil 248 } 249 250 func isEmptyArray(data []byte) bool { 251 empty := "[]" 252 return len(data) == len(empty) && string(data) == empty 253 } 254 255 func closeBody(resp *http.Response) { 256 if resp != nil && resp.Body != nil { 257 _, _ = io.Copy(io.Discard, resp.Body) 258 _ = resp.Body.Close() 259 } 260 } 261 262 func boolToInt(b bool) int64 { 263 if !b { 264 return 0 265 } 266 return 1 267 } 268 269 func calcPercentage(value, total int64) (v int64) { 270 if total == 0 { 271 return 0 272 } 273 return int64(float64(value) * 100 / float64(total) * precision) 274 }