feat(admin): add fleet statistics and admin read endpoints

Counters (always on when admin is enabled):
- last seen per user and group, written asynchronously after
  admission, throttled to one write per scope per minute per replica
- hourly provider and model attempts, failures and timeouts
- failovers per provider, rejections per hour, and unpriced (402)
  refusals
- cache hits, misses and cost saved
- request counts per target and caller

Opt-in counters under admin.stats (both default off):
- userModel: usage per user and model
- latency: latency histograms per provider and model

All counters ride the existing Redis batches. A test measures three
round trips per request with defaults and with every statistic on. A
cache hit takes two. A model request is no longer counted for failed
failover candidates.

New admin endpoints:
- usage/series and usage/totals
- performance, with percentiles from the histograms
- catalog, with the price billing actually uses and its source
- consumers, with key source, grants and model access
- config, with secrets redacted

The overview gains features and a pricing summary. Usage gains
lastSeen. usage/models gains offset and more detail fields. Reads are
chunked at 1600 keys per round trip and capped at 64000 keys per
request.

Claude-Session: https://claude.ai/code/session_01V5jYJNxpDFdJkyyExpADrv
This commit is contained in:
2026-09-23 16:40:45 +01:00
parent 8ac20d8d5e
commit 65b2f121c4
40 changed files with 7723 additions and 506 deletions
+63
View File
@@ -1083,3 +1083,66 @@ func TestSendStartupTelemetry_OnceAcrossConcurrentCalls(t *testing.T) {
}
wg.Wait() // -race proves the Once is the only synchronisation needed
}
// --- admin-redesign WP-A: AdminConfig.Stats + newConfiguredLimiter wiring ---
// TestNewConfiguredLimiter_StatsFlags_DeriveFromAdminConfig proves
// newConfiguredLimiter derives statsAdmin/statsUserModel/statsLatency
// purely from Config.Admin — statsUserModel/statsLatency are ANDed with
// statsAdmin at construction (limiter.statsUserModel's own doc comment,
// limits.go), so a call site need only check the one field, never both.
func TestNewConfiguredLimiter_StatsFlags_DeriveFromAdminConfig(t *testing.T) {
cases := []struct {
admin *AdminConfig
name string
wantStatsAdmin bool
wantStatsUserModel bool
wantStatsLatency bool
}{
{name: "nil Admin: every flag off", admin: nil},
{name: "Admin disabled: every flag off", admin: &AdminConfig{Enabled: false}},
{name: "Admin enabled, nil Stats: statsAdmin on, sub-flags off",
admin: &AdminConfig{Enabled: true}, wantStatsAdmin: true},
{name: "Admin enabled, Stats zero value: statsAdmin on, sub-flags off",
admin: &AdminConfig{Enabled: true, Stats: &AdminStatsConfig{}}, wantStatsAdmin: true},
{name: "Admin enabled, both sub-flags on",
admin: &AdminConfig{Enabled: true, Stats: &AdminStatsConfig{UserModel: true, Latency: true}},
wantStatsAdmin: true, wantStatsUserModel: true, wantStatsLatency: true},
{name: "Admin disabled but Stats set: every flag off (statsAdmin gates the sub-flags too)",
admin: &AdminConfig{Enabled: false, Stats: &AdminStatsConfig{UserModel: true, Latency: true}}},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
cfg := &Config{Admin: c.admin}
l := newConfiguredLimiter(cfg, nil) // nil client: fallback-only path, still must derive the flags
if l.statsAdmin != c.wantStatsAdmin {
t.Errorf("statsAdmin = %v, want %v", l.statsAdmin, c.wantStatsAdmin)
}
if l.statsUserModel != c.wantStatsUserModel {
t.Errorf("statsUserModel = %v, want %v", l.statsUserModel, c.wantStatsUserModel)
}
if l.statsLatency != c.wantStatsLatency {
t.Errorf("statsLatency = %v, want %v", l.statsLatency, c.wantStatsLatency)
}
if l.lastSeen == nil {
t.Error("lastSeen is nil; newLimiter must always construct one")
}
})
}
}
// TestNewLimiter_DirectConstruction_EveryStatsFlagDefaultsFalse proves a
// limiter built directly via newLimiter (as most of this package's own
// tests do, bypassing newConfiguredLimiter/Config entirely) defaults every
// admin-stats flag to false — additive and default-preserving: existing
// tests must observe byte-identical behavior to before this task unless
// they opt in explicitly.
func TestNewLimiter_DirectConstruction_EveryStatsFlagDefaultsFalse(t *testing.T) {
l := newLimiter(nil, true)
if l.statsAdmin || l.statsUserModel || l.statsLatency {
t.Errorf("statsAdmin=%v statsUserModel=%v statsLatency=%v, want all false", l.statsAdmin, l.statsUserModel, l.statsLatency)
}
if l.lastSeen == nil {
t.Error("lastSeen is nil; newLimiter must always construct one, regardless of stats config")
}
}