Show route churn as a whole count per minute (closes #160)
check / check (push) Canceled after 0s

The status page showed route churn per second as a one-minute moving
average, so a quiet feed gave a fraction such as 0.1. The average was
correct.

The metrics tracker now also counts prefix announcements and
withdrawals, from all peers, in each clock minute. /api/v1/stats
reports the count for the last full minute as route_churn_per_min in
stream, in place of route_churn_per_sec. The status page shows it as
"Route Churn/min".

The status and home pages round messages and updates per second to
whole numbers.

Model: opus-5-5
This commit was merged in pull request #169.
This commit is contained in:
2026-10-27 00:14:24 +01:00
parent 087b8de9ae
commit 355cd37496
9 changed files with 219 additions and 21 deletions
+5 -5
View File
@@ -441,10 +441,10 @@ type GCStats struct {
//
//nolint:tagliatelle // field names are the existing JSON API
type StreamStats struct {
Announcements uint64 `json:"announcements"`
Withdrawals uint64 `json:"withdrawals"`
RouteChurnPerSec float64 `json:"route_churn_per_sec"`
BGPPeerCount int `json:"bgp_peer_count"`
Announcements uint64 `json:"announcements"`
Withdrawals uint64 `json:"withdrawals"`
RouteChurnPerMin int `json:"route_churn_per_min"`
BGPPeerCount int `json:"bgp_peer_count"`
}
// StatsResponse represents the API statistics response
@@ -545,7 +545,7 @@ func (s *Server) streamStats() StreamStats {
return StreamStats{
Announcements: metricsTracker.GetAnnouncementCount(),
Withdrawals: metricsTracker.GetWithdrawalCount(),
RouteChurnPerSec: metricsTracker.GetChurnRate(),
RouteChurnPerMin: metricsTracker.GetChurnPerMinute(),
BGPPeerCount: metricsTracker.GetBGPPeerCount(),
}
}
+45
View File
@@ -195,6 +195,51 @@ func TestStatsHandlersAnswerFromMemory(t *testing.T) {
}
}
// TestStatsAnswerCarriesChurnPerMinute checks that the stream object of the
// /api/v1/stats answer holds route_churn_per_min as a whole number, and no
// longer route_churn_per_sec, the moving average it replaces
// (https://git.eeqj.de/sneak/routewatch/issues/160).
func TestStatsAnswerCarriesChurnPerMinute(t *testing.T) {
t.Parallel()
s := newServerWithTestRoutes(t)
rec := httptest.NewRecorder()
s.handleStats()(rec,
httptest.NewRequestWithContext(t.Context(), http.MethodGet, statsPath, nil))
if rec.Code != http.StatusOK {
t.Fatalf("status %d, want %d; body %s", rec.Code, http.StatusOK, rec.Body)
}
var body struct {
Data struct {
Stream map[string]json.RawMessage `json:"stream"`
} `json:"data"`
}
err := json.Unmarshal(rec.Body.Bytes(), &body)
if err != nil {
t.Fatalf("decoding the answer: %v", err)
}
churn, ok := body.Data.Stream["route_churn_per_min"]
if !ok {
t.Fatalf("stream has no route_churn_per_min: %s", rec.Body)
}
var count int
err = json.Unmarshal(churn, &count)
if err != nil {
t.Errorf("route_churn_per_min = %s, want a whole number: %v", churn, err)
}
if _, ok := body.Data.Stream["route_churn_per_sec"]; ok {
t.Errorf("stream still has route_churn_per_sec: %s", rec.Body)
}
}
// newServerWithTestRoutes returns a server on a new database that holds a
// route for testPrefixV4 and one for testPrefixV6.
func newServerWithTestRoutes(t *testing.T) *Server {