Browse Source

Merge pull request #339 from KevinFumbles/dev

Added Technitium Service Option to DNS-Stats Widget
Svilen Markov 4 tháng trước cách đây
mục cha
commit
e355c643f4
2 tập tin đã thay đổi với 173 bổ sung28 xóa
  1. 7 7
      docs/configuration.md
  2. 166 21
      internal/glance/widget-dns-stats.go

+ 7 - 7
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,22 +1772,22 @@ 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.
 
 ##### `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. 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`
 Whether to hide the graph showing the number of queries over time.

+ 166 - 21
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"`
 
@@ -48,8 +54,12 @@ func (widget *dnsStatsWidget) initialize() error {
 		withTitleURL(string(widget.URL)).
 		withCacheDuration(10 * time.Minute)
 
-	if widget.Service != "adguard" && widget.Service != "pihole" {
-		return errors.New("service must be either 'adguard' or 'pihole'")
+	switch widget.Service {
+	case "adguard":
+	case "pihole":
+	case "technitium":
+	default:
+		return errors.New("service must be either 'adguard', 'pihole', or 'technitium'")
 	}
 
 	return nil
@@ -59,10 +69,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) {
@@ -179,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{
@@ -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)
 	}
 
@@ -379,3 +388,139 @@ 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"`
+			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
+}