From ac7f3805d47cf9d5b4d1b28495226034b50b1e8b Mon Sep 17 00:00:00 2001
From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com>
Date: Sat, 28 Dec 2024 14:17:48 +0000
Subject: [PATCH] Add custom unmarshalling for pihole queries stats
+ hide-graph and hide-top-domains options for DNS stats widget
---
docs/configuration.md | 8 ++++
internal/glance/templates/dns-stats.html | 7 +++-
internal/glance/widget-dns-stats.go | 53 ++++++++++++++++++------
3 files changed, 54 insertions(+), 14 deletions(-)
diff --git a/docs/configuration.md b/docs/configuration.md
index 1224ba7..c818e52 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -1358,6 +1358,8 @@ Preview:
| username | string | when service is `adguard` | |
| password | string | when service is `adguard` | |
| token | string | when service is `pihole` | |
+| hide-graph | bool | no | false |
+| hide-top-domains | bool | no | false |
| hour-format | string | no | 12h |
##### `service`
@@ -1378,6 +1380,12 @@ Only required when using AdGuard Home. The password used to log into the admin d
##### `token`
Only required when using Pi-hole. The API token which can be found in `Settings -> API -> Show API token`. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`.
+##### `hide-graph`
+Whether to hide the graph showing the number of queries over time.
+
+##### `hide-top-domains`
+Whether to hide the list of top blocked domains.
+
##### `hour-format`
Whether to display the relative time in the graph in `12h` or `24h` format.
diff --git a/internal/glance/templates/dns-stats.html b/internal/glance/templates/dns-stats.html
index 8447ce1..7938038 100644
--- a/internal/glance/templates/dns-stats.html
+++ b/internal/glance/templates/dns-stats.html
@@ -24,6 +24,8 @@
{{ end }}
+ {{ $showGraph := not (or .HideGraph (eq (len .Stats.Series) 0)) }}
+ {{ if $showGraph }}
+ {{ end }}
- {{ if .Stats.TopBlockedDomains }}
-
+ {{ if and (not .HideTopDomains) .Stats.TopBlockedDomains }}
+ Top blocked domains
{{ range .Stats.TopBlockedDomains }}
diff --git a/internal/glance/widget-dns-stats.go b/internal/glance/widget-dns-stats.go
index f6bc4b3..833a80d 100644
--- a/internal/glance/widget-dns-stats.go
+++ b/internal/glance/widget-dns-stats.go
@@ -20,13 +20,15 @@ type dnsStatsWidget struct {
TimeLabels [8]string `yaml:"-"`
Stats *dnsStats `yaml:"-"`
- HourFormat string `yaml:"hour-format"`
- Service string `yaml:"service"`
- AllowInsecure bool `yaml:"allow-insecure"`
- URL string `yaml:"url"`
- Token string `yaml:"token"`
- Username string `yaml:"username"`
- Password string `yaml:"password"`
+ HourFormat string `yaml:"hour-format"`
+ HideGraph bool `yaml:"hide-graph"`
+ HideTopDomains bool `yaml:"hide-top-domains"`
+ Service string `yaml:"service"`
+ AllowInsecure bool `yaml:"allow-insecure"`
+ URL string `yaml:"url"`
+ Token string `yaml:"token"`
+ Username string `yaml:"username"`
+ Password string `yaml:"password"`
}
func makeDNSWidgetTimeLabels(format string) [8]string {
@@ -58,9 +60,9 @@ func (widget *dnsStatsWidget) update(ctx context.Context) {
var err error
if widget.Service == "adguard" {
- stats, err = fetchAdguardStats(string(widget.URL), widget.AllowInsecure, string(widget.Username), string(widget.Password))
+ stats, err = fetchAdguardStats(widget.URL, widget.AllowInsecure, widget.Username, widget.Password, widget.HideGraph)
} else {
- stats, err = fetchPiholeStats(string(widget.URL), widget.AllowInsecure, string(widget.Token))
+ stats, err = fetchPiholeStats(widget.URL, widget.AllowInsecure, widget.Token, widget.HideGraph)
}
if !widget.canContinueUpdateAfterHandlingErr(err) {
@@ -111,7 +113,7 @@ type adguardStatsResponse struct {
TopBlockedDomains []map[string]int `json:"top_blocked_domains"`
}
-func fetchAdguardStats(instanceURL string, allowInsecure bool, username, password string) (*dnsStats, error) {
+func fetchAdguardStats(instanceURL string, allowInsecure bool, username, password string, noGraph bool) (*dnsStats, error) {
requestURL := strings.TrimRight(instanceURL, "/") + "/control/stats"
request, err := http.NewRequest("GET", requestURL, nil)
@@ -170,6 +172,10 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor
}
}
+ if noGraph {
+ return stats, nil
+ }
+
queriesSeries := responseJson.QueriesSeries
blockedSeries := responseJson.BlockedSeries
@@ -223,7 +229,7 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor
type piholeStatsResponse struct {
TotalQueries int `json:"dns_queries_today"`
- QueriesSeries map[int64]int `json:"domains_over_time"`
+ QueriesSeries piholeQueriesSeries `json:"domains_over_time"`
BlockedQueries int `json:"ads_blocked_today"`
BlockedSeries map[int64]int `json:"ads_over_time"`
BlockedPercentage float64 `json:"ads_percentage_today"`
@@ -231,6 +237,25 @@ type piholeStatsResponse struct {
DomainsBlocked int `json:"domains_being_blocked"`
}
+// If the user has query logging disabled it's possible for domains_over_time to be returned as an
+// empty array rather than a map which will prevent unmashalling the rest of the data so we use
+// custom unmarshal behavior to fallback to an empty map.
+// See https://github.com/glanceapp/glance/issues/289
+type piholeQueriesSeries map[int64]int
+
+func (p *piholeQueriesSeries) UnmarshalJSON(data []byte) error {
+ temp := make(map[int64]int)
+
+ err := json.Unmarshal(data, &temp)
+ if err != nil {
+ *p = make(piholeQueriesSeries)
+ } else {
+ *p = temp
+ }
+
+ return nil
+}
+
// If user has some level of privacy enabled on Pihole, `json:"top_ads"` is an empty array
// Use custom unmarshal behavior to avoid not getting the rest of the valid data when unmarshalling
type piholeTopBlockedDomains map[string]int
@@ -250,7 +275,7 @@ func (p *piholeTopBlockedDomains) UnmarshalJSON(data []byte) error {
return nil
}
-func fetchPiholeStats(instanceURL string, allowInsecure bool, token string) (*dnsStats, error) {
+func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGraph bool) (*dnsStats, error) {
if token == "" {
return nil, errors.New("missing API token")
}
@@ -299,6 +324,10 @@ func fetchPiholeStats(instanceURL string, allowInsecure bool, token string) (*dn
stats.TopBlockedDomains = domains[:min(len(domains), 5)]
}
+ if noGraph {
+ return stats, nil
+ }
+
// Pihole _should_ return data for the last 24 hours in a 10 minute interval, 6*24 = 144
if len(responseJson.QueriesSeries) != 144 || len(responseJson.BlockedSeries) != 144 {
slog.Warn(