mirror of
https://github.com/glanceapp/glance.git
synced 2025-08-19 12:24:45 +02:00
Merge pull request #339 from KevinFumbles/dev
Added Technitium Service Option to DNS-Stats Widget
This commit is contained in:
@@ -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. |
|
| glance.parent | The ID of the parent container. Used to group containers under a single parent. |
|
||||||
|
|
||||||
### DNS Stats
|
### 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:
|
Example:
|
||||||
|
|
||||||
@@ -1755,7 +1755,7 @@ Preview:
|
|||||||
|
|
||||||
> [!NOTE]
|
> [!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
|
#### Properties
|
||||||
|
|
||||||
@@ -1772,22 +1772,22 @@ Preview:
|
|||||||
| hour-format | string | no | 12h |
|
| hour-format | string | no | 12h |
|
||||||
|
|
||||||
##### `service`
|
##### `service`
|
||||||
Either `adguard` or `pihole`.
|
Either `adguard`, `pihole`, or `technitium`.
|
||||||
|
|
||||||
##### `allow-insecure`
|
##### `allow-insecure`
|
||||||
Whether to allow invalid/self-signed certificates when making the request to the service.
|
Whether to allow invalid/self-signed certificates when making the request to the service.
|
||||||
|
|
||||||
##### `url`
|
##### `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`
|
##### `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`
|
##### `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`
|
##### `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`.
|
||||||
|
|
||||||
##### `hide-graph`
|
##### `hide-graph`
|
||||||
Whether to hide the graph showing the number of queries over time.
|
Whether to hide the graph showing the number of queries over time.
|
||||||
|
@@ -14,6 +14,12 @@ import (
|
|||||||
|
|
||||||
var dnsStatsWidgetTemplate = mustParseTemplate("dns-stats.html", "widget-base.html")
|
var dnsStatsWidgetTemplate = mustParseTemplate("dns-stats.html", "widget-base.html")
|
||||||
|
|
||||||
|
const (
|
||||||
|
dnsStatsBars = 8
|
||||||
|
dnsStatsHoursSpan = 24
|
||||||
|
dnsStatsHoursPerBar int = dnsStatsHoursSpan / dnsStatsBars
|
||||||
|
)
|
||||||
|
|
||||||
type dnsStatsWidget struct {
|
type dnsStatsWidget struct {
|
||||||
widgetBase `yaml:",inline"`
|
widgetBase `yaml:",inline"`
|
||||||
|
|
||||||
@@ -48,8 +54,12 @@ func (widget *dnsStatsWidget) initialize() error {
|
|||||||
withTitleURL(string(widget.URL)).
|
withTitleURL(string(widget.URL)).
|
||||||
withCacheDuration(10 * time.Minute)
|
withCacheDuration(10 * time.Minute)
|
||||||
|
|
||||||
if widget.Service != "adguard" && widget.Service != "pihole" {
|
switch widget.Service {
|
||||||
return errors.New("service must be either 'adguard' or 'pihole'")
|
case "adguard":
|
||||||
|
case "pihole":
|
||||||
|
case "technitium":
|
||||||
|
default:
|
||||||
|
return errors.New("service must be either 'adguard', 'pihole', or 'technitium'")
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -59,10 +69,13 @@ func (widget *dnsStatsWidget) update(ctx context.Context) {
|
|||||||
var stats *dnsStats
|
var stats *dnsStats
|
||||||
var err error
|
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)
|
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)
|
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) {
|
if !widget.canContinueUpdateAfterHandlingErr(err) {
|
||||||
@@ -179,31 +192,27 @@ func fetchAdguardStats(instanceURL string, allowInsecure bool, username, passwor
|
|||||||
queriesSeries := responseJson.QueriesSeries
|
queriesSeries := responseJson.QueriesSeries
|
||||||
blockedSeries := responseJson.BlockedSeries
|
blockedSeries := responseJson.BlockedSeries
|
||||||
|
|
||||||
const bars = 8
|
if len(queriesSeries) > dnsStatsHoursSpan {
|
||||||
const hoursSpan = 24
|
queriesSeries = queriesSeries[len(queriesSeries)-dnsStatsHoursSpan:]
|
||||||
const hoursPerBar int = hoursSpan / bars
|
} else if len(queriesSeries) < dnsStatsHoursSpan {
|
||||||
|
queriesSeries = append(make([]int, dnsStatsHoursSpan-len(queriesSeries)), queriesSeries...)
|
||||||
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 {
|
if len(blockedSeries) > dnsStatsHoursSpan {
|
||||||
blockedSeries = blockedSeries[len(blockedSeries)-hoursSpan:]
|
blockedSeries = blockedSeries[len(blockedSeries)-dnsStatsHoursSpan:]
|
||||||
} else if len(blockedSeries) < hoursSpan {
|
} else if len(blockedSeries) < dnsStatsHoursSpan {
|
||||||
blockedSeries = append(make([]int, hoursSpan-len(blockedSeries)), blockedSeries...)
|
blockedSeries = append(make([]int, dnsStatsHoursSpan-len(blockedSeries)), blockedSeries...)
|
||||||
}
|
}
|
||||||
|
|
||||||
maxQueriesInSeries := 0
|
maxQueriesInSeries := 0
|
||||||
|
|
||||||
for i := 0; i < bars; i++ {
|
for i := 0; i < dnsStatsBars; i++ {
|
||||||
queries := 0
|
queries := 0
|
||||||
blocked := 0
|
blocked := 0
|
||||||
|
|
||||||
for j := 0; j < hoursPerBar; j++ {
|
for j := 0; j < dnsStatsHoursPerBar; j++ {
|
||||||
queries += queriesSeries[i*hoursPerBar+j]
|
queries += queriesSeries[i*dnsStatsHoursPerBar+j]
|
||||||
blocked += blockedSeries[i*hoursPerBar+j]
|
blocked += blockedSeries[i*dnsStatsHoursPerBar+j]
|
||||||
}
|
}
|
||||||
|
|
||||||
stats.Series[i] = dnsStatsSeries{
|
stats.Series[i] = dnsStatsSeries{
|
||||||
@@ -220,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)
|
stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,3 +388,139 @@ func fetchPiholeStats(instanceURL string, allowInsecure bool, token string, noGr
|
|||||||
|
|
||||||
return stats, nil
|
return stats, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type technitiumStatsResponse struct {
|
||||||
|
Response 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 {
|
||||||
|
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),
|
||||||
|
DomainsBlocked: responseJson.Response.Stats.BlockedZones + responseJson.Response.Stats.BlockListZones,
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) > 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 < dnsStatsBars; i++ {
|
||||||
|
queries := 0
|
||||||
|
blocked := 0
|
||||||
|
|
||||||
|
for j := 0; j < dnsStatsHoursPerBar; j++ {
|
||||||
|
queries += queriesSeries[i*dnsStatsHoursPerBar+j]
|
||||||
|
blocked += blockedSeries[i*dnsStatsHoursPerBar+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 < dnsStatsBars; i++ {
|
||||||
|
stats.Series[i].PercentTotal = int(float64(stats.Series[i].Queries) / float64(maxQueriesInSeries) * 100)
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user