Watch
1
0
Fork
You've already forked pkg-proxy
1
mirror of https://github.com/git-pkgs/proxy.git synced 2026-11-09 15:49:16 -05:00
pkg-proxy/internal/server/analytics_runtime_test.go
Simon Chrzanowski 92b1fb20a6 Follow npm and Packagist change feeds to revalidate changed metadata
Within metadata_ttl cached npm and Composer metadata is served without
asking upstream, so a new release can take that long to show up. Both
registries publish a feed of the packages that changed.

upstream.npm_change_feed and upstream.composer_change_feed (off by
default, require cache_metadata) start a reader per feed that runs every
minute and clears fetched_at on the cached rows of listed packages that
were fetched before the read. The rows keep their document and ETag, so
the next request revalidates them upstream. Routed packages are cached
under other keys and never match. A Packagist resync expires all cached
Composer metadata. Failed reads back off up to 15 minutes. The feed
position is kept in memory, so after a restart the reader starts from the
feed's current position.
2026-10-27 16:31:47 +01:00

288 lines
9.7 KiB
Go

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")
}
}