diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 30dba94..1a85d49 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -7,6 +7,8 @@ import ( "golang.org/x/exp/constraints" ) +const StatsTotal = "total" + var ( l = log.GetLogger("metrics") @@ -155,14 +157,11 @@ func initMetric(name string, metric *prometheus.GaugeVec) { } func Update(ims ...InstanceMetrics) { - s := OverallStats{} for _, im := range ims { update(im) - s[im.HostName] = im.Stats + stats[im.HostName] = im.Stats } - stats = s - l.Debug("updated") } @@ -241,12 +240,13 @@ type InstanceMetrics struct { type OverallStats map[string]*model.Stats -func (s OverallStats) consolidate() *model.Stats { - total := model.NewStats() - for _, stats := range s { - total.Add(stats) +func (os OverallStats) consolidate() OverallStats { + consolidated := OverallStats{StatsTotal: model.NewStats()} + for host, stats := range os { + consolidated[host] = stats + consolidated[StatsTotal].Add(stats) } - return total + return consolidated } func safeMetric[T Number](v *T) float64 { @@ -260,6 +260,10 @@ type Number interface { constraints.Float | constraints.Integer } -func GetStats() *model.Stats { +func GetStats() OverallStats { return stats.consolidate() } + +func (os OverallStats) Total() *model.Stats { + return os[StatsTotal] +} diff --git a/pkg/sync/http.go b/pkg/sync/http.go index a541afe..65c39ea 100644 --- a/pkg/sync/http.go +++ b/pkg/sync/http.go @@ -33,6 +33,8 @@ func (w *worker) handleSync(c *gin.Context) { } func (w *worker) handleRoot(c *gin.Context) { + total, dns, blocked, malware, adult := statsGraph() + c.HTML(http.StatusOK, "index.html", map[string]interface{}{ "DarkMode": w.cfg.API.DarkMode, "Metrics": w.cfg.API.Metrics.Enabled, @@ -41,18 +43,18 @@ func (w *worker) handleRoot(c *gin.Context) { "SyncStatus": w.status(), "Stats": map[string]interface{}{ "Labels": getLast24Hours(), - "DNS": metrics.GetStats().DnsQueries, - "Blocked": metrics.GetStats().BlockedFiltering, - "BlockedPercentage": fmt.Sprintf("%.2f", (float64(*metrics.GetStats().NumBlockedFiltering)*100.0)/float64(*metrics.GetStats().NumDnsQueries)), - "Malware": metrics.GetStats().ReplacedSafebrowsing, - "MalwarePercentage": fmt.Sprintf("%.2f", (float64(*metrics.GetStats().NumReplacedSafebrowsing)*100.0)/float64(*metrics.GetStats().NumDnsQueries)), - "Adult": metrics.GetStats().ReplacedParental, - "AdultPercentage": fmt.Sprintf("%.2f", (float64(*metrics.GetStats().NumReplacedParental)*100.0)/float64(*metrics.GetStats().NumDnsQueries)), + "DNS": dns, + "Blocked": blocked, + "BlockedPercentage": fmt.Sprintf("%.2f", (float64(*total.NumBlockedFiltering)*100.0)/float64(*total.NumDnsQueries)), + "Malware": malware, + "MalwarePercentage": fmt.Sprintf("%.2f", (float64(*total.NumReplacedSafebrowsing)*100.0)/float64(*total.NumDnsQueries)), + "Adult": adult, + "AdultPercentage": fmt.Sprintf("%.2f", (float64(*total.NumReplacedParental)*100.0)/float64(*total.NumDnsQueries)), - "TotalDNS": metrics.GetStats().NumDnsQueries, - "TotalBlocked": metrics.GetStats().NumBlockedFiltering, - "TotalMalware": metrics.GetStats().NumReplacedSafebrowsing, - "TotalAdult": metrics.GetStats().NumReplacedParental, + "TotalDNS": total.NumDnsQueries, + "TotalBlocked": total.NumBlockedFiltering, + "TotalMalware": total.NumReplacedSafebrowsing, + "TotalAdult": total.NumReplacedParental, }, }, ) diff --git a/pkg/sync/index.html b/pkg/sync/index.html index 6179678..fd993c2 100644 --- a/pkg/sync/index.html +++ b/pkg/sync/index.html @@ -181,20 +181,27 @@ {{- end }} diff --git a/pkg/sync/stats.go b/pkg/sync/stats.go new file mode 100644 index 0000000..031fb63 --- /dev/null +++ b/pkg/sync/stats.go @@ -0,0 +1,116 @@ +package sync + +import ( + "strings" + + "github.com/bakito/adguardhome-sync/pkg/client/model" + "github.com/bakito/adguardhome-sync/pkg/metrics" + "golang.org/x/exp/slices" +) + +var ( + blue = []int{78, 141, 245} + blueAlternatives = [][]int{ + {44, 95, 163}, + {122, 166, 247}, + {30, 61, 92}, + {93, 158, 255}, + {58, 123, 213}, + } + + red = []int{255, 94, 94} + redAlternatives = [][]int{ + {204, 59, 59}, + {255, 127, 127}, + {140, 36, 36}, + {255, 153, 153}, + {255, 66, 66}, + } + + yellow = []int{232, 198, 78} + yellowAlternatives = [][]int{ + {196, 163, 60}, + {255, 220, 110}, + {140, 114, 36}, + {250, 233, 156}, + {212, 180, 84}, + } + + green = []int{110, 224, 122} + greenAlternatives = [][]int{ + {68, 160, 80}, + {142, 255, 158}, + {44, 140, 63}, + {163, 255, 192}, + {85, 198, 102}, + } +) + +func statsGraph() (*model.Stats, []line, []line, []line, []line) { + s := metrics.GetStats() + t := s.Total() + dns := graphLines(t, s, blue, blueAlternatives, func(s *model.Stats) []int { + return *s.DnsQueries + }) + blocked := graphLines(t, s, red, redAlternatives, func(s *model.Stats) []int { + return *s.BlockedFiltering + }) + malware := graphLines(t, s, green, greenAlternatives, func(s *model.Stats) []int { + return *s.ReplacedSafebrowsing + }) + adult := graphLines(t, s, yellow, yellowAlternatives, func(s *model.Stats) []int { + return *s.ReplacedParental + }) + + return t, dns, blocked, malware, adult +} + +func graphLines(t *model.Stats, s metrics.OverallStats, baseColor []int, altColors [][]int, dataCB func(s *model.Stats) []int) []line { + g := &graph{ + total: line{ + Fill: true, + Title: "Total", + Data: dataCB(t), + R: baseColor[0], + G: baseColor[1], + B: baseColor[2], + }, + } + + var i int + for name, data := range s { + if name != metrics.StatsTotal { + g.replicas = append(g.replicas, line{ + Fill: false, + Title: name, + Data: dataCB(data), + R: altColors[i%len(altColors)][0], + G: altColors[i%len(altColors)][1], + B: altColors[i%len(altColors)][2], + }) + i++ + } + } + + lines := []line{g.total} + + slices.SortFunc(g.replicas, func(a, b line) int { + return strings.Compare(a.Title, b.Title) + }) + lines = append(lines, g.replicas...) + return lines +} + +type graph struct { + total line + replicas []line +} + +type line struct { + Data []int `json:"data"` + R int `json:"r"` + G int `json:"g"` + B int `json:"b"` + Title string `json:"title"` + Fill bool `json:"fill"` +}