From 0c8358beaa95730caa9deffd60db67d10793ad0e Mon Sep 17 00:00:00 2001 From: Kevin <15876989+KevinFumbles@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:58:05 -0500 Subject: [PATCH 1/6] Added Technitium as a valid service for dns-stats widget --- internal/glance/widget-dns-stats.go | 152 +++++++++++++++++++++++++++- 1 file changed, 149 insertions(+), 3 deletions(-) diff --git a/internal/glance/widget-dns-stats.go b/internal/glance/widget-dns-stats.go index 833a80d..6ba506d 100644 --- a/internal/glance/widget-dns-stats.go +++ b/internal/glance/widget-dns-stats.go @@ -48,7 +48,11 @@ func (widget *dnsStatsWidget) initialize() error { withTitleURL(string(widget.URL)). withCacheDuration(10 * time.Minute) - if widget.Service != "adguard" && widget.Service != "pihole" { + switch widget.Service { + case "adguard": + case "pihole": + case "technitium": + default: return errors.New("service must be either 'adguard' or 'pihole'") } @@ -59,10 +63,13 @@ func (widget *dnsStatsWidget) update(ctx context.Context) { var stats *dnsStats var err error - if widget.Service == "adguard" { + switch widget.Service { + case "adguard": stats, err = fetchAdguardStats(widget.URL, widget.AllowInsecure, widget.Username, widget.Password, widget.HideGraph) - } else { + case "pihole": stats, err = fetchPiholeStats(widget.URL, widget.AllowInsecure, widget.Token, widget.HideGraph) + case "technitium": + stats, err = fetchTechnitiumStats(widget.URL, widget.AllowInsecure, widget.Token, widget.HideGraph) } if !widget.canContinueUpdateAfterHandlingErr(err) { @@ -379,3 +386,142 @@ func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGr return stats, nil } + +type technitiumStatsResponse struct { + Response struct { + Stats struct { + TotalQueries int `json:"totalQueries"` + BlockedQueries int `json:"totalBlocked"` + } `json:"stats"` + MainChartData struct { + Datasets []struct { + Label string `json:"label"` + Data []int `json:"data"` + } `json:"datasets"` + } `json:"mainChartData"` + TopBlockedDomains []struct { + Domain string `json:"name"` + Count int `json:"hits"` + } + } `json:"response"` +} + +func fetchTechnitiumStats(instanceUrl string, allowInsecure bool, token string, noGraph bool) (*dnsStats, error) { + + if token == "" { + return nil, errors.New("missing API token") + } + + requestURL := strings.TrimRight(instanceUrl, "/") + "/api/dashboard/stats/get?token=" + token + "&type=LastDay" + + request, err := http.NewRequest("GET", requestURL, nil) + if err != nil { + return nil, err + } + + var client requestDoer + if !allowInsecure { + client = defaultHTTPClient + } else { + client = defaultInsecureHTTPClient + } + + responseJson, err := decodeJsonFromRequest[technitiumStatsResponse](client, request) + if err != nil { + return nil, err + } + + var topBlockedDomainsCount = min(len(responseJson.Response.TopBlockedDomains), 5) + + stats := &dnsStats{ + TotalQueries: responseJson.Response.Stats.TotalQueries, + BlockedQueries: responseJson.Response.Stats.BlockedQueries, + TopBlockedDomains: make([]dnsStatsBlockedDomain, 0, topBlockedDomainsCount), + } + + if stats.TotalQueries <= 0 { + return stats, nil + } + + stats.BlockedPercent = int(float64(responseJson.Response.Stats.BlockedQueries) / float64(responseJson.Response.Stats.TotalQueries) * 100) + + for i := 0; i < topBlockedDomainsCount; i++ { + domain := responseJson.Response.TopBlockedDomains[i] + firstDomain := domain.Domain + + if firstDomain == "" { + continue + } + + stats.TopBlockedDomains = append(stats.TopBlockedDomains, dnsStatsBlockedDomain{ + Domain: firstDomain, + }) + + if stats.BlockedQueries > 0 { + stats.TopBlockedDomains[i].PercentBlocked = int(float64(domain.Count) / float64(responseJson.Response.Stats.BlockedQueries) * 100) + } + } + + if noGraph { + return stats, nil + } + + var queriesSeries, blockedSeries []int + + for _, label := range responseJson.Response.MainChartData.Datasets { + switch label.Label { + case "Total": + queriesSeries = label.Data + case "Blocked": + blockedSeries = label.Data + } + } + + const bars = 8 + const hoursSpan = 24 + const hoursPerBar int = hoursSpan / bars + + if len(queriesSeries) > hoursSpan { + queriesSeries = queriesSeries[len(queriesSeries)-hoursSpan:] + } else if len(queriesSeries) < hoursSpan { + queriesSeries = append(make([]int, hoursSpan-len(queriesSeries)), queriesSeries...) + } + + if len(blockedSeries) > hoursSpan { + blockedSeries = blockedSeries[len(blockedSeries)-hoursSpan:] + } else if len(blockedSeries) < hoursSpan { + blockedSeries = append(make([]int, hoursSpan-len(blockedSeries)), blockedSeries...) + } + + maxQueriesInSeries := 0 + + for i := 0; i < bars; i++ { + queries := 0 + blocked := 0 + + for j := 0; j < hoursPerBar; j++ { + queries += queriesSeries[i*hoursPerBar+j] + blocked += blockedSeries[i*hoursPerBar+j] + } + + stats.Series[i] = dnsStatsSeries{ + Queries: queries, + Blocked: blocked, + } + + if queries > 0 { + stats.Series[i].PercentBlocked = int(float64(blocked) / float64(queries) * 100) + } + + if queries > maxQueriesInSeries { + maxQueriesInSeries = queries + } + } + + for i := 0; i < bars; i++ { + stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100) + } + + return stats, nil + +} From baee94ed1de8ef43413f93553177be18bc276722 Mon Sep 17 00:00:00 2001 From: Kevin <15876989+KevinFumbles@users.noreply.github.com> Date: Wed, 12 Feb 2025 15:58:21 -0500 Subject: [PATCH 2/6] Added configuration documentation for Technitium dns-stats service --- docs/configuration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index 0cee1ec..a1f3523 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1737,7 +1737,7 @@ The path to the Docker socket. | glance.parent | The ID of the parent container. Used to group containers under a single parent. | ### DNS Stats -Display statistics from a self-hosted ad-blocking DNS resolver such as AdGuard Home or Pi-hole. +Display statistics from a self-hosted ad-blocking DNS resolver such as AdGuard Home, Pi-hole, or Technitium. Example: @@ -1755,7 +1755,7 @@ Preview: > [!NOTE] > -> When using AdGuard Home the 3rd statistic on top will be the average latency and when using Pi-hole it will be the total number of blocked domains from all adlists. +> When using AdGuard Home the 3rd statistic on top will be the average latency and when using Pi-hole or Technitium it will be the total number of blocked domains from all adlists. #### Properties @@ -1772,7 +1772,7 @@ Preview: | hour-format | string | no | 12h | ##### `service` -Either `adguard` or `pihole`. +Either `adguard`, `pihole`, or `technitium`. ##### `allow-insecure` Whether to allow invalid/self-signed certificates when making the request to the service. @@ -1787,7 +1787,7 @@ Only required when using AdGuard Home. The username used to log into the admin d Only required when using AdGuard Home. The password used to log into the admin dashboard. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. ##### `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}`. +Only required when using Pi-hole or Technitium. For Pi-hole, the API token which can be found in `Settings -> API -> Show API token`; for Technitium, an API token can be generated at `Administration -> Sessions -> Create 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. From 94806ed45dbdb15c32b9792980344a94e320d9ff Mon Sep 17 00:00:00 2001 From: Kevin <15876989+KevinFumbles@users.noreply.github.com> Date: Wed, 12 Feb 2025 16:05:38 -0500 Subject: [PATCH 3/6] Added blocked domains count for Technitium --- internal/glance/widget-dns-stats.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/glance/widget-dns-stats.go b/internal/glance/widget-dns-stats.go index 6ba506d..a137228 100644 --- a/internal/glance/widget-dns-stats.go +++ b/internal/glance/widget-dns-stats.go @@ -392,6 +392,8 @@ type technitiumStatsResponse struct { Stats struct { TotalQueries int `json:"totalQueries"` BlockedQueries int `json:"totalBlocked"` + BlockedZones int `json:"blockedZones"` + BlockListZones int `json:"blockListZones"` } `json:"stats"` MainChartData struct { Datasets []struct { @@ -437,6 +439,7 @@ func fetchTechnitiumStats(instanceUrl string, allowInsecure bool, token string, TotalQueries: responseJson.Response.Stats.TotalQueries, BlockedQueries: responseJson.Response.Stats.BlockedQueries, TopBlockedDomains: make([]dnsStatsBlockedDomain, 0, topBlockedDomainsCount), + DomainsBlocked: responseJson.Response.Stats.BlockedZones + responseJson.Response.Stats.BlockListZones, } if stats.TotalQueries <= 0 { From facbf6f5294d7764b2c6c0c7ff1bf30fc691f1f9 Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:08:36 +0000 Subject: [PATCH 4/6] Remove mention of env variable syntax --- docs/configuration.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/configuration.md b/docs/configuration.md index a1f3523..1bd4a26 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1778,16 +1778,16 @@ Either `adguard`, `pihole`, or `technitium`. Whether to allow invalid/self-signed certificates when making the request to the service. ##### `url` -The base URL of the service. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. +The base URL of the service. ##### `username` -Only required when using AdGuard Home. The username used to log into the admin dashboard. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. +Only required when using AdGuard Home. The username used to log into the admin dashboard. ##### `password` -Only required when using AdGuard Home. The password used to log into the admin dashboard. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. +Only required when using AdGuard Home. The password used to log into the admin dashboard. ##### `token` -Only required when using Pi-hole or Technitium. For Pi-hole, the API token which can be found in `Settings -> API -> Show API token`; for Technitium, an API token can be generated at `Administration -> Sessions -> Create Token`. Can be specified from an environment variable using the syntax `${VARIABLE_NAME}`. +Only required when using Pi-hole or Technitium. For Pi-hole, the API token which can be found in `Settings -> API -> Show API token`; for Technitium, an API token can be generated at `Administration -> Sessions -> Create Token`. ##### `hide-graph` Whether to hide the graph showing the number of queries over time. From f9209406fbfa55cec8209caff14299f5d4dc1a7d Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:18:27 +0000 Subject: [PATCH 5/6] Reduce duplication of constants --- internal/glance/widget-dns-stats.go | 68 ++++++++++++++--------------- 1 file changed, 32 insertions(+), 36 deletions(-) diff --git a/internal/glance/widget-dns-stats.go b/internal/glance/widget-dns-stats.go index a137228..34f1dfe 100644 --- a/internal/glance/widget-dns-stats.go +++ b/internal/glance/widget-dns-stats.go @@ -14,6 +14,12 @@ import ( var dnsStatsWidgetTemplate = mustParseTemplate("dns-stats.html", "widget-base.html") +const ( + dnsStatsBars = 8 + dnsStatsHoursSpan = 24 + dnsStatsHoursPerBar int = dnsStatsHoursSpan / dnsStatsBars +) + type dnsStatsWidget struct { widgetBase `yaml:",inline"` @@ -186,31 +192,27 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor queriesSeries := responseJson.QueriesSeries blockedSeries := responseJson.BlockedSeries - const bars = 8 - const hoursSpan = 24 - const hoursPerBar int = hoursSpan / bars - - if len(queriesSeries) > hoursSpan { - queriesSeries = queriesSeries[len(queriesSeries)-hoursSpan:] - } else if len(queriesSeries) < hoursSpan { - queriesSeries = append(make([]int, hoursSpan-len(queriesSeries)), queriesSeries...) + if len(queriesSeries) > dnsStatsHoursSpan { + queriesSeries = queriesSeries[len(queriesSeries)-dnsStatsHoursSpan:] + } else if len(queriesSeries) < dnsStatsHoursSpan { + queriesSeries = append(make([]int, dnsStatsHoursSpan-len(queriesSeries)), queriesSeries...) } - if len(blockedSeries) > hoursSpan { - blockedSeries = blockedSeries[len(blockedSeries)-hoursSpan:] - } else if len(blockedSeries) < hoursSpan { - blockedSeries = append(make([]int, hoursSpan-len(blockedSeries)), blockedSeries...) + if len(blockedSeries) > dnsStatsHoursSpan { + blockedSeries = blockedSeries[len(blockedSeries)-dnsStatsHoursSpan:] + } else if len(blockedSeries) < dnsStatsHoursSpan { + blockedSeries = append(make([]int, dnsStatsHoursSpan-len(blockedSeries)), blockedSeries...) } maxQueriesInSeries := 0 - for i := 0; i < bars; i++ { + for i := 0; i < dnsStatsBars; i++ { queries := 0 blocked := 0 - for j := 0; j < hoursPerBar; j++ { - queries += queriesSeries[i*hoursPerBar+j] - blocked += blockedSeries[i*hoursPerBar+j] + for j := 0; j < dnsStatsHoursPerBar; j++ { + queries += queriesSeries[i*dnsStatsHoursPerBar+j] + blocked += blockedSeries[i*dnsStatsHoursPerBar+j] } stats.Series[i] = dnsStatsSeries{ @@ -227,7 +229,7 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor } } - for i := 0; i < bars; i++ { + for i := 0; i < dnsStatsBars; i++ { stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100) } @@ -409,7 +411,6 @@ type technitiumStatsResponse struct { } func fetchTechnitiumStats(instanceUrl string, allowInsecure bool, token string, noGraph bool) (*dnsStats, error) { - if token == "" { return nil, errors.New("missing API token") } @@ -480,31 +481,27 @@ func fetchTechnitiumStats(instanceUrl string, allowInsecure bool, token string, } } - const bars = 8 - const hoursSpan = 24 - const hoursPerBar int = hoursSpan / bars - - if len(queriesSeries) > hoursSpan { - queriesSeries = queriesSeries[len(queriesSeries)-hoursSpan:] - } else if len(queriesSeries) < hoursSpan { - queriesSeries = append(make([]int, hoursSpan-len(queriesSeries)), queriesSeries...) + if len(queriesSeries) > dnsStatsHoursSpan { + queriesSeries = queriesSeries[len(queriesSeries)-dnsStatsHoursSpan:] + } else if len(queriesSeries) < dnsStatsHoursSpan { + queriesSeries = append(make([]int, dnsStatsHoursSpan-len(queriesSeries)), queriesSeries...) } - if len(blockedSeries) > hoursSpan { - blockedSeries = blockedSeries[len(blockedSeries)-hoursSpan:] - } else if len(blockedSeries) < hoursSpan { - blockedSeries = append(make([]int, hoursSpan-len(blockedSeries)), blockedSeries...) + if len(blockedSeries) > dnsStatsHoursSpan { + blockedSeries = blockedSeries[len(blockedSeries)-dnsStatsHoursSpan:] + } else if len(blockedSeries) < dnsStatsHoursSpan { + blockedSeries = append(make([]int, dnsStatsHoursSpan-len(blockedSeries)), blockedSeries...) } maxQueriesInSeries := 0 - for i := 0; i < bars; i++ { + for i := 0; i < dnsStatsBars; i++ { queries := 0 blocked := 0 - for j := 0; j < hoursPerBar; j++ { - queries += queriesSeries[i*hoursPerBar+j] - blocked += blockedSeries[i*hoursPerBar+j] + for j := 0; j < dnsStatsHoursPerBar; j++ { + queries += queriesSeries[i*dnsStatsHoursPerBar+j] + blocked += blockedSeries[i*dnsStatsHoursPerBar+j] } stats.Series[i] = dnsStatsSeries{ @@ -521,10 +518,9 @@ func fetchTechnitiumStats(instanceUrl string, allowInsecure bool, token string, } } - for i := 0; i < bars; i++ { + for i := 0; i < dnsStatsBars; i++ { stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100) } return stats, nil - } From fcccb7eb387b9fb5480fe80992a4a3be407133ff Mon Sep 17 00:00:00 2001 From: Svilen Markov <7613769+svilenmarkov@users.noreply.github.com> Date: Mon, 17 Feb 2025 23:20:38 +0000 Subject: [PATCH 6/6] Update error message --- internal/glance/widget-dns-stats.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/glance/widget-dns-stats.go b/internal/glance/widget-dns-stats.go index 34f1dfe..68b6ac2 100644 --- a/internal/glance/widget-dns-stats.go +++ b/internal/glance/widget-dns-stats.go @@ -59,7 +59,7 @@ func (widget *dnsStatsWidget) initialize() error { case "pihole": case "technitium": default: - return errors.New("service must be either 'adguard' or 'pihole'") + return errors.New("service must be either 'adguard', 'pihole', or 'technitium'") } return nil