package server import ( "testing" "github.com/git-pkgs/proxy/internal/metrics" "github.com/prometheus/client_golang/prometheus" ) func TestFormatDuration(t *testing.T) { tests := []struct { seconds float64 want string }{ {0, "-"}, {-1, "-"}, {0.0000004, "400 ns"}, {0.00041, "410 µs"}, {0.0081, "8.1 ms"}, {1.25, "1.25 s"}, } for _, tc := range tests { if got := formatDuration(tc.seconds); got != tc.want { t.Errorf("formatDuration(%v) = %q, want %q", tc.seconds, got, tc.want) } } } func TestRuntimeViewNilSnapshot(t *testing.T) { if v := runtimeView(nil, false); v.Available { t.Error("a nil snapshot must not report itself as available") } } func TestRuntimeViewShapesRegistry(t *testing.T) { reg := prometheus.NewRegistry() requests := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_requests_total", Help: "t"}, []string{"ecosystem", "status"}) scanErrors := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_scan_errors_total", Help: "t"}, []string{"ecosystem", "scanner", "error_type"}) hits := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_cache_hits_total", Help: "t"}, []string{"ecosystem", "kind"}) misses := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_cache_misses_total", Help: "t"}, []string{"ecosystem", "kind"}) breaker := prometheus.NewGaugeVec( prometheus.GaugeOpts{Name: "proxy_circuit_breaker_state", Help: "t"}, []string{"registry"}) storage := prometheus.NewHistogramVec( prometheus.HistogramOpts{Name: "proxy_storage_operation_duration_seconds", Help: "t"}, []string{"operation"}) reg.MustRegister(requests, scanErrors, hits, misses, breaker, storage) requests.WithLabelValues("npm", "200").Add(90) requests.WithLabelValues("npm", "304").Add(5) requests.WithLabelValues("npm", "500").Add(5) scanErrors.WithLabelValues("npm", "clamav", "timeout").Add(3) hits.WithLabelValues("npm", metrics.CacheKindArtifact).Add(80) misses.WithLabelValues("npm", metrics.CacheKindArtifact).Add(20) breaker.WithLabelValues("registry.npmjs.org").Set(0) breaker.WithLabelValues("static.crates.io").Set(2) storage.WithLabelValues("get").Observe(0.002) snap, err := metrics.GatherFrom(reg) if err != nil { t.Fatalf("GatherFrom: %v", err) } v := runtimeView(snap, false) if !v.Available { t.Fatal("expected the view to be available") } if v.Requests != "100" { t.Errorf("Requests = %q, want %q", v.Requests, "100") } if a := v.ArtifactCache; a.HitRatio != "80.0" || !a.HasTraffic { t.Errorf("ArtifactCache = %+v, want an 80.0%% hit rate", a) } // Status codes collapse to classes; 5xx is flagged as a failure row. wantClasses := map[string]struct { count string bad bool }{"2xx": {"90", false}, "3xx": {"5", false}, "5xx": {"5", true}} if len(v.StatusClasses) != len(wantClasses) { t.Fatalf("got %d status classes, want %d: %+v", len(v.StatusClasses), len(wantClasses), v.StatusClasses) } for _, row := range v.StatusClasses { want, ok := wantClasses[row.Label] if !ok { t.Errorf("unexpected status class %q", row.Label) continue } if row.Count != want.count || row.Bad != want.bad { t.Errorf("%s = %q (bad=%v), want %q (bad=%v)", row.Label, row.Count, row.Bad, want.count, want.bad) } } // The metric that prompted this: it must reach the page with its labels joined. if len(v.ScanErrors) != 1 { t.Fatalf("expected 1 scan error row, got %+v", v.ScanErrors) } if v.ScanErrors[0].Label != "npm · clamav · timeout" || v.ScanErrors[0].Count != "3" { t.Errorf("scan error row = %+v, want npm · clamav · timeout = 3", v.ScanErrors[0]) } if !v.ScanningOn { t.Error("scanning should read as on once a scan metric carries a value") } if len(v.Breakers) != 2 { t.Fatalf("expected 2 breakers, got %+v", v.Breakers) } var open, closed int for _, b := range v.Breakers { if b.Open { open++ if b.State != "open" { t.Errorf("open breaker state = %q, want %q", b.State, "open") } } else { closed++ } } if open != 1 || closed != 1 { t.Errorf("breakers: %d open / %d closed, want 1 / 1", open, closed) } if len(v.StorageOps) != 1 || v.StorageOps[0].Label != "get" || v.StorageOps[0].Mean != "2.0 ms" { t.Errorf("StorageOps = %+v, want one 'get' row with a 2.0 ms mean", v.StorageOps) } } // Evictions are routine housekeeping, not failures: they reach the page by // reason and ecosystem without the failure tint. func TestRuntimeViewShowsEvictions(t *testing.T) { reg := prometheus.NewRegistry() evicted := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_artifacts_evicted_total", Help: "t"}, []string{"reason", "ecosystem"}) reg.MustRegister(evicted) evicted.WithLabelValues("retention", "npm").Add(4) evicted.WithLabelValues("lru", "cargo") snap, err := metrics.GatherFrom(reg) if err != nil { t.Fatalf("GatherFrom: %v", err) } v := runtimeView(snap, false) if len(v.Evictions) != 1 { t.Fatalf("Evictions = %+v, want only the non-zero row", v.Evictions) } if got := v.Evictions[0]; got.Label != "retention · npm" || got.Count != "4" || got.Bad { t.Errorf("eviction row = %+v, want retention · npm = 4 without the failure tint", got) } } func TestRuntimeViewShowsMetadataEvictions(t *testing.T) { reg := prometheus.NewRegistry() evicted := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_metadata_evicted_total", Help: "t"}, []string{"ecosystem"}) reg.MustRegister(evicted) evicted.WithLabelValues("golang").Add(3) snap, err := metrics.GatherFrom(reg) if err != nil { t.Fatalf("GatherFrom: %v", err) } v := runtimeView(snap, false) if len(v.MetadataEvictions) != 1 || v.MetadataEvictions[0].Label != "golang" || v.MetadataEvictions[0].Count != "3" { t.Errorf("MetadataEvictions = %+v, want golang = 3", v.MetadataEvictions) } } func TestRuntimeViewShowsChangeFeedExpiries(t *testing.T) { reg := prometheus.NewRegistry() expired := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_metadata_expired_by_change_feed_total", Help: "t"}, []string{"ecosystem"}) reg.MustRegister(expired) expired.WithLabelValues("packagist").Add(2) snap, err := metrics.GatherFrom(reg) if err != nil { t.Fatalf("GatherFrom: %v", err) } v := runtimeView(snap, false) if len(v.ChangeFeedExpired) != 1 || v.ChangeFeedExpired[0].Label != "packagist" || v.ChangeFeedExpired[0].Count != "2" { t.Errorf("ChangeFeedExpired = %+v, want packagist = 2", v.ChangeFeedExpired) } } // A counter sitting at zero carries no information and would bury the rows // that do, so it is left out. func TestRuntimeViewDropsZeroCounters(t *testing.T) { reg := prometheus.NewRegistry() errs := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_storage_errors_total", Help: "t"}, []string{"operation"}) reg.MustRegister(errs) // Touching a label creates the series at zero. errs.WithLabelValues("get") errs.WithLabelValues("put").Add(2) snap, err := metrics.GatherFrom(reg) if err != nil { t.Fatalf("GatherFrom: %v", err) } v := runtimeView(snap, false) if len(v.StorageErrors) != 1 { t.Fatalf("expected only the non-zero row, got %+v", v.StorageErrors) } if v.StorageErrors[0].Label != "put" { t.Errorf("kept row = %q, want %q", v.StorageErrors[0].Label, "put") } } // Artifact and metadata lookups are separate caches, so the view reports each // on its own rather than pooling them into one hit rate. func TestRuntimeViewSplitsCacheKinds(t *testing.T) { reg := prometheus.NewRegistry() hits := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_cache_hits_total", Help: "t"}, []string{"ecosystem", "kind"}) misses := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_cache_misses_total", Help: "t"}, []string{"ecosystem", "kind"}) served := prometheus.NewCounterVec( prometheus.CounterOpts{Name: "proxy_metadata_cache_served_bytes_total", Help: "t"}, []string{"ecosystem"}) reg.MustRegister(hits, misses, served) hits.WithLabelValues("npm", metrics.CacheKindArtifact).Add(80) misses.WithLabelValues("npm", metrics.CacheKindArtifact).Add(20) hits.WithLabelValues("pypi", metrics.CacheKindMetadata).Add(20) hits.WithLabelValues("npm", metrics.CacheKindMetadata).Add(10) misses.WithLabelValues("pypi", metrics.CacheKindMetadata).Add(10) served.WithLabelValues("pypi").Add(2048) snap, err := metrics.GatherFrom(reg) if err != nil { t.Fatalf("GatherFrom: %v", err) } v := runtimeView(snap, false) want := map[string]struct { got CacheLookups want CacheLookups }{ "artifact": {v.ArtifactCache, CacheLookups{Hits: "80", Misses: "20", HitRatio: "80.0", HasTraffic: true}}, "metadata": {v.MetadataCache, CacheLookups{Hits: "30", Misses: "10", HitRatio: "75.0", HasTraffic: true}}, } for kind, c := range want { if c.got != c.want { t.Errorf("%s lookups = %+v, want %+v", kind, c.got, c.want) } } if v.MetadataServed != "2.0 KB" { t.Errorf("MetadataServed = %q, want %q", v.MetadataServed, "2.0 KB") } } // An empty registry must still render, reporting nothing rather than dividing // by zero or showing a hit rate of NaN. func TestRuntimeViewEmptyRegistry(t *testing.T) { snap, err := metrics.GatherFrom(prometheus.NewRegistry()) if err != nil { t.Fatalf("GatherFrom: %v", err) } v := runtimeView(snap, false) if !v.Available { t.Error("an empty registry is still a valid snapshot") } for name, c := range map[string]CacheLookups{"artifact": v.ArtifactCache, "metadata": v.MetadataCache} { if c.HasTraffic { t.Errorf("%s: no traffic recorded, so HasTraffic must be false", name) } if c.HitRatio != "" { t.Errorf("%s: HitRatio = %q, want empty when nothing was recorded", name, c.HitRatio) } } if v.Requests != "0" { t.Errorf("Requests = %q, want %q", v.Requests, "0") } if v.RequestMean != "-" { t.Errorf("RequestMean = %q, want a dash", v.RequestMean) } if v.ScanningOn { t.Error("scanning must read as off with no scan metrics") } }