diff --git a/telemetry/server/api/handlers_test.go b/telemetry/server/api/handlers_test.go index d6c10bf1b..80debb53c 100644 --- a/telemetry/server/api/handlers_test.go +++ b/telemetry/server/api/handlers_test.go @@ -2,6 +2,7 @@ package api import ( "bytes" + "encoding/json" "net/http" "net/http/httptest" "testing" @@ -11,6 +12,9 @@ import ( protobuf "google.golang.org/protobuf/proto" ) +// promauto registers on the global registry: one storage per test binary. +var testHandler = NewHandler(storage.NewPrometheusStorage()) + func validReport() *proto.TelemetryData { return &proto.TelemetryData{ TopologyId: "38422678-6a0d-4482-aa33-65b90010ac47", @@ -43,8 +47,7 @@ func marshalReport(t *testing.T, data *proto.TelemetryData) []byte { } func TestCollectTelemetryValidation(t *testing.T) { - // promauto registers on the global registry: one storage per test binary. - h := NewHandler(storage.NewPrometheusStorage()) + h := testHandler t.Run("valid report accepted", func(t *testing.T) { if w := postCollect(t, h, marshalReport(t, validReport()), "application/x-protobuf"); w.Code != http.StatusOK { @@ -96,3 +99,42 @@ func TestCollectTelemetryValidation(t *testing.T) { } }) } + +// The dashboard looks confirmation windows up by the select's string value, +// so the stats JSON must key confirmed_by_days by decimal strings and carry +// unmet thresholds as zeros. +func TestStatsSerializedThresholds(t *testing.T) { + data := validReport() + data.TopologyId = "49533789-7b1e-4593-bb44-76ca1121bd58" + if w := postCollect(t, testHandler, marshalReport(t, data), "application/x-protobuf"); w.Code != http.StatusOK { + t.Fatalf("collect: got %d: %s", w.Code, w.Body.String()) + } + + req := httptest.NewRequest(http.MethodGet, "/api/stats", nil) + w := httptest.NewRecorder() + testHandler.GetStats(w, req) + if w.Code != http.StatusOK { + t.Fatalf("stats: got %d", w.Code) + } + + var stats struct { + ConfirmedByDays map[string]int `json:"confirmed_by_days"` + } + if err := json.Unmarshal(w.Body.Bytes(), &stats); err != nil { + t.Fatalf("decode: %v", err) + } + if len(stats.ConfirmedByDays) != 5 { + t.Fatalf("confirmed_by_days = %v, want the 5 thresholds", stats.ConfirmedByDays) + } + for _, key := range []string{"1", "3", "7", "14", "30"} { + if _, ok := stats.ConfirmedByDays[key]; !ok { + t.Errorf("confirmed_by_days missing %q: %v", key, stats.ConfirmedByDays) + } + } + if stats.ConfirmedByDays["1"] < 1 { + t.Errorf("fresh cluster missing from the 1-day count: %v", stats.ConfirmedByDays) + } + if stats.ConfirmedByDays["30"] != 0 { + t.Errorf("unmet threshold not zero: %v", stats.ConfirmedByDays) + } +} diff --git a/telemetry/server/dashboard/dashboard.go b/telemetry/server/dashboard/dashboard.go index f5d30af00..e049c702e 100644 --- a/telemetry/server/dashboard/dashboard.go +++ b/telemetry/server/dashboard/dashboard.go @@ -57,6 +57,15 @@ func (h *Handler) ServeIndex(w http.ResponseWriter, r *http.Request) { color: #666; margin-top: 5px; } + .stat-label select { + border: none; + background: none; + color: inherit; + font: inherit; + padding: 0; + cursor: pointer; + text-decoration: underline dotted; + } .chart-container { background: white; padding: 20px; @@ -129,7 +138,14 @@ func (h *Handler) ServeIndex(w http.ResponseWriter, r *http.Request) {
-
-
Confirmed Clusters (7+ days)
+
Confirmed Clusters + ( days)
-
@@ -229,14 +245,26 @@ func (h *Handler) ServeIndex(w http.ResponseWriter, r *http.Request) { } } + let latestStats = {}; + function updateStats(stats) { + latestStats = stats; document.getElementById('totalInstances').textContent = stats.total_instances || 0; document.getElementById('activeInstances').textContent = stats.active_instances || 0; - document.getElementById('confirmedInstances').textContent = stats.confirmed_instances || 0; + updateConfirmed(); document.getElementById('totalVersions').textContent = Object.keys(stats.versions || {}).length; document.getElementById('totalOS').textContent = Object.keys(stats.os_distribution || {}).length; } + // Servers from before confirmed_by_days fall back to the fixed + // 7-day count. + function updateConfirmed() { + const days = document.getElementById('confirmDays').value; + const byDays = latestStats.confirmed_by_days || {}; + const count = byDays[days] !== undefined ? byDays[days] : latestStats.confirmed_instances; + document.getElementById('confirmedInstances').textContent = count || 0; + } + function updateCharts(stats) { createPieChart('versionChart', 'Version Distribution', stats.versions || {}); createPieChart('osChart', 'Operating System Distribution', stats.os_distribution || {}); diff --git a/telemetry/server/storage/confirmed_test.go b/telemetry/server/storage/confirmed_test.go index 51e5d2872..e06d04b21 100644 --- a/telemetry/server/storage/confirmed_test.go +++ b/telemetry/server/storage/confirmed_test.go @@ -74,4 +74,14 @@ func TestConfirmedClusters(t *testing.T) { if _, ok := v["9.99"]; ok { t.Errorf("one-shot cluster polluted the distribution: %v", v) } + + // Cluster A meets the 1/3/7-day thresholds, B only the 1-day one, and + // unmet thresholds are present as zero so the dashboard can show them. + byDays := stats["confirmed_by_days"].(map[int]int) + want := map[int]int{1: 2, 3: 1, 7: 1, 14: 0, 30: 0} + for threshold, expected := range want { + if got, ok := byDays[threshold]; !ok || got != expected { + t.Errorf("confirmed_by_days[%d] = %v (present=%v), want %d", threshold, got, ok, expected) + } + } } diff --git a/telemetry/server/storage/history.go b/telemetry/server/storage/history.go index 9a25ba19f..a1ed77930 100644 --- a/telemetry/server/storage/history.go +++ b/telemetry/server/storage/history.go @@ -10,6 +10,10 @@ import ( // on before it counts as confirmed in the aggregated stats. const confirmDays = 7 +// confirmThresholds are the confirmation windows the dashboard lets the +// viewer pick between; confirmDays is the one everything else is built on. +var confirmThresholds = []int{1, 3, 7, 14, 30} + // activeDays is how recently a cluster must have reported to count as active. const activeDays = 7 diff --git a/telemetry/server/storage/prometheus.go b/telemetry/server/storage/prometheus.go index 4cc90fab1..7641a6e0c 100644 --- a/telemetry/server/storage/prometheus.go +++ b/telemetry/server/storage/prometheus.go @@ -207,6 +207,10 @@ func (s *PrometheusStorage) updateStats() { totalInstances := 0 activeInstances := 0 confirmedInstances := 0 + confirmedByDays := make(map[int]int, len(confirmThresholds)) + for _, threshold := range confirmThresholds { + confirmedByDays[threshold] = 0 + } versionsAll := make(map[string]int) osAll := make(map[string]int) versionsConfirmed := make(map[string]int) @@ -223,7 +227,13 @@ func (s *PrometheusStorage) updateStats() { // A cluster is confirmed once seen on confirmDays distinct UTC // days (histories hold one sample per day), so short-lived // clusters can't skew the distributions below. - if len(s.histories[instance.TelemetryData.TopologyId]) >= confirmDays { + daysSeen := len(s.histories[instance.TelemetryData.TopologyId]) + for _, threshold := range confirmThresholds { + if daysSeen >= threshold { + confirmedByDays[threshold]++ + } + } + if daysSeen >= confirmDays { confirmedInstances++ versionsConfirmed[instance.TelemetryData.Version]++ osConfirmed[instance.TelemetryData.Os]++ @@ -249,6 +259,7 @@ func (s *PrometheusStorage) updateStats() { "total_instances": totalInstances, "active_instances": activeInstances, "confirmed_instances": confirmedInstances, + "confirmed_by_days": confirmedByDays, "versions": versions, "os_distribution": osDistribution, }