Test the eviction, source cache, upstream and transform metrics
check / check (push) Canceled after 0s

The tests check the metrics on a variant hit, on misses that fetch or use
the cached source, on a failed fetch, and on both kinds of eviction. They
fail until the metrics exist.

Model: opus-5-5
This commit is contained in:
sneak committed 2026-11-11 14:00:04 +00:00
1 parent e34463acf8
commit 88b4f49815
2 files changed
+249

No files matched your search

@@ -0,0 +1,112 @@
package imageproxy
import (
"testing"
"sneak.berlin/go/pixa/internal/imagerequest"
)
// metricValues holds the metrics of a proxy and its cache that
// TestProxy_Get_CountsSourceCacheFetchesAndTransforms checks. A histogram
// is given by its count of observations.
type metricValues struct {
sourceCacheHits uint64
sourceCacheMisses uint64
fetchDurations uint64
fetchErrors uint64
transformDuration uint64
inProgress float64
}
// readMetricValues reads the metrics of svc and its cache that
// metricValues holds from their metrics registry.
func readMetricValues(t *testing.T, svc *testProxy) metricValues {
t.Helper()
families, err := svc.registry.Gather()
if err != nil {
t.Fatalf("failed to gather the metrics: %v", err)
}
var values metricValues
for _, family := range families {
metric := family.GetMetric()[0]
switch family.GetName() {
case "pixa_source_cache_hits_total":
values.sourceCacheHits = uint64(metric.GetCounter().GetValue())
case "pixa_source_cache_misses_total":
values.sourceCacheMisses = uint64(metric.GetCounter().GetValue())
case "pixa_upstream_fetch_duration_seconds":
values.fetchDurations = metric.GetHistogram().GetSampleCount()
case "pixa_upstream_fetch_errors_total":
values.fetchErrors = uint64(metric.GetCounter().GetValue())
case "pixa_transform_duration_seconds":
values.transformDuration = metric.GetHistogram().GetSampleCount()
case "pixa_transforms_in_progress":
values.inProgress = metric.GetGauge().GetValue()
}
}
return values
}
// TestProxy_Get_CountsSourceCacheFetchesAndTransforms walks Get through a
// miss that fetches the source, a hit, a miss that uses the cached source
// and a miss whose source is not found upstream, checking the source cache,
// upstream fetch and transform metrics after each.
func TestProxy_Get_CountsSourceCacheFetchesAndTransforms(t *testing.T) {
t.Parallel()
svc, fixtures := setupTestProxy(t)
// want is source cache hits, source cache misses, fetch durations
// observed, fetch errors, transform durations observed, transforms in
// progress.
steps := []struct {
name string
path string
size int
wantErr bool
want metricValues
}{
{"miss that fetches the source", testPathPhoto, 50, false,
metricValues{0, 1, 1, 0, 1, 0}},
{"hit", testPathPhoto, 50, false,
metricValues{0, 1, 1, 0, 1, 0}},
{"miss that uses the cached source", testPathPhoto, 25, false,
metricValues{1, 1, 1, 0, 2, 0}},
{"miss whose source is not found", "/images/nonexistent.jpg", 50, true,
metricValues{1, 2, 1, 1, 2, 0}},
}
for _, step := range steps {
resp, err := svc.Get(t.Context(), &imagerequest.Request{
Source: imagerequest.Source{
Host: fixtures.GoodHost,
Path: step.path,
},
Transform: imagerequest.Transform{
Size: imagerequest.Size{Width: step.size, Height: step.size},
Format: imagerequest.FormatJPEG,
Quality: 85,
FitMode: imagerequest.FitCover,
},
})
if (err != nil) != step.wantErr {
t.Fatalf("%s: Get() error = %v, want error %t", step.name, err, step.wantErr)
}
if err == nil {
_ = resp.Content.Close()
}
got := readMetricValues(t, svc)
t.Logf("after the %s: metrics = %+v", step.name, got)
if got != step.want {
t.Fatalf("after the %s: metrics = %+v, want %+v", step.name, got, step.want)
}
}
}
+137
View File
@@ -0,0 +1,137 @@
package imgcache
import (
"bytes"
"testing"
"time"
"github.com/prometheus/client_golang/prometheus"
"sneak.berlin/go/pixa/internal/imagerequest"
)
// gatheredValue returns the value of the metric name on registry: a
// counter's or a gauge's value.
func gatheredValue(t *testing.T, registry *prometheus.Registry, name string) float64 {
t.Helper()
families, err := registry.Gather()
if err != nil {
t.Fatalf("failed to gather the metrics: %v", err)
}
for _, family := range families {
if family.GetName() != name {
continue
}
metric := family.GetMetric()[0]
if metric.GetGauge() != nil {
return metric.GetGauge().GetValue()
}
return metric.GetCounter().GetValue()
}
t.Fatalf("the metric %s is not on the registry", name)
return 0
}
// TestEvictToLimit_CountsEvictionsAndUsage stores four variants of 1000
// bytes in a cache limited to 3000 bytes. EvictToLimit must count the one
// eviction and its bytes, and set the usage and limit gauges.
func TestEvictToLimit_CountsEvictionsAndUsage(t *testing.T) {
t.Parallel()
const limit = 3000
registry := prometheus.NewRegistry()
cache, err := NewCache(evictionTestDB(t),
Config{StateDir: t.TempDir(), MaxBytes: limit}, registry)
if err != nil {
t.Fatalf("NewCache() error = %v", err)
}
keys := []imagerequest.VariantKey{
testVariantKeyOne, testVariantKeyTwo,
testVariantKeyThree, testVariantKeyFour,
}
for i, key := range keys {
storeEvictionTestVariant(t, cache, key, bytes.Repeat([]byte{0x01}, 1000))
setVariantLastAccessed(t, cache, key,
time.Now().Add(-time.Duration(len(keys)-i)*time.Hour))
}
err = cache.EvictToLimit(t.Context())
if err != nil {
t.Fatalf("EvictToLimit() error = %v", err)
}
want := map[string]float64{
"pixa_cache_evictions_total": 1,
"pixa_cache_evicted_bytes_total": 1000,
"pixa_cache_usage_bytes": 3000,
"pixa_cache_max_bytes": limit,
}
for name, value := range want {
got := gatheredValue(t, registry, name)
t.Logf("%s = %v", name, got)
if got != value {
t.Errorf("%s = %v, want %v", name, got, value)
}
}
}
// TestEvictNotCheckedRecently_CountsEvictions stores a source of 6 bytes and
// a variant of 7 bytes last checked with upstream 31 days ago.
// EvictNotCheckedRecently must count both evictions and their 13 bytes.
func TestEvictNotCheckedRecently_CountsEvictions(t *testing.T) {
t.Parallel()
registry := prometheus.NewRegistry()
cache, err := NewCache(evictionTestDB(t),
Config{StateDir: t.TempDir(), MaxBytes: 1 << 30}, registry)
if err != nil {
t.Fatalf("NewCache() error = %v", err)
}
old := time.Now().Add(-31 * 24 * time.Hour)
_, err = cache.PutSource(t.Context(),
imagerequest.Source{Host: testHostCDN, Path: "/old.jpg"},
bytes.NewReader([]byte("source")),
SourceInfo{FetchedAt: old, CheckedAt: old})
if err != nil {
t.Fatalf("PutSource() error = %v", err)
}
err = cache.PutVariant(t.Context(), testVariantKeyOne,
SourceInfo{CheckedAt: old}, bytes.NewReader([]byte("variant")))
if err != nil {
t.Fatalf("PutVariant() error = %v", err)
}
err = cache.EvictNotCheckedRecently(t.Context())
if err != nil {
t.Fatalf("EvictNotCheckedRecently() error = %v", err)
}
want := map[string]float64{
"pixa_cache_evictions_total": 2,
"pixa_cache_evicted_bytes_total": 13,
}
for name, value := range want {
got := gatheredValue(t, registry, name)
t.Logf("%s = %v", name, got)
if got != value {
t.Errorf("%s = %v, want %v", name, got, value)
}
}
}