Files
steamcache2/steamcache/metrics/metrics_test.go
T
pike dd72668c2d
CI / vulncheck (pull_request) Successful in 14s
CI / check-and-test (pull_request) Successful in 42s
metrics: Prometheus text exposition for /metrics
/metrics was Prometheus-ish name/value text with a custom banner and
text/plain Content-Type, so strict scrapers could fail.

Emit Prometheus text 0.0.4 (# HELP, # TYPE, counter|gauge) with stable
metric names, and set Content-Type to text/plain; version=0.0.4; charset=utf-8.
2026-09-08 19:53:58 +00:00

180 lines
5.6 KiB
Go

package metrics
import (
"bytes"
"errors"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestCapacityPressureEventsWriteTextAndReset(t *testing.T) {
t.Parallel()
m := NewMetrics()
if got := m.GetStats().CapacityPressureEvents; got != 0 {
t.Fatalf("initial CapacityPressureEvents=%d, want 0", got)
}
NoteSoftEviction(m, "memory", 0)
if got := m.GetStats().CapacityPressureEvents; got != 0 {
t.Fatalf("zero-byte eviction counted: %d", got)
}
NoteSoftEviction(m, "memory", 128)
st := m.GetStats()
if st.CapacityPressureEvents != 1 {
t.Fatalf("after memory eviction, CapacityPressureEvents=%d, want 1", st.CapacityPressureEvents)
}
if st.Evictions != 1 {
t.Fatalf("after memory eviction, Evictions=%d, want 1 (existing counter kept)", st.Evictions)
}
NoteSoftEviction(m, "disk", 64)
NoteNoSpace(m, errors.New("no space left on device"))
st = m.GetStats()
if st.CapacityPressureEvents != 3 {
t.Fatalf("after disk eviction + ENOSPC, CapacityPressureEvents=%d, want 3", st.CapacityPressureEvents)
}
if st.Evictions != 2 {
t.Fatalf("ENOSPC must not increment evictions; Evictions=%d, want 2", st.Evictions)
}
rec := httptest.NewRecorder()
WriteText(rec, st)
body := rec.Body.Bytes()
if !bytes.Contains(body, []byte("capacity_pressure_events 3")) {
t.Errorf("WriteText missing capacity_pressure_events 3: %q", rec.Body.String())
}
if !bytes.Contains(body, []byte("evictions 2")) {
t.Errorf("WriteText missing evictions 2: %q", rec.Body.String())
}
if !bytes.Contains(body, []byte("# HELP capacity_pressure_events")) {
t.Errorf("WriteText missing # HELP capacity_pressure_events: %q", rec.Body.String())
}
if !bytes.Contains(body, []byte("# TYPE capacity_pressure_events counter")) {
t.Errorf("WriteText missing # TYPE capacity_pressure_events counter: %q", rec.Body.String())
}
if !bytes.Contains(body, []byte("# TYPE evictions counter")) {
t.Errorf("WriteText missing # TYPE evictions counter: %q", rec.Body.String())
}
m.Reset()
st = m.GetStats()
if st.CapacityPressureEvents != 0 || st.Evictions != 0 {
t.Errorf("after Reset, CapacityPressureEvents=%d Evictions=%d, want 0", st.CapacityPressureEvents, st.Evictions)
}
}
func TestNoteSoftEvictionNilMetrics(t *testing.T) {
t.Parallel()
// Must not panic when metrics are not wired (unit tests / early init).
NoteSoftEviction(nil, "memory", 10)
NoteNoSpace(nil, errors.New("ENOSPC"))
}
func TestWriteTextPrometheusExposition(t *testing.T) {
t.Parallel()
st := &Stats{
TotalRequests: 10,
CacheHits: 4,
CacheMisses: 6,
NegativeCacheHits: 1,
CacheCoalesced: 2,
RangeCache: 3,
RangeUpstream: 5,
Errors: 1,
RateLimited: 1,
UpstreamErrors: 1,
CacheWriteFailures: 1,
MemoryCacheHits: 2,
DiskCacheHits: 2,
Promotions: 1,
Evictions: 1,
CapacityPressureEvents: 1,
ServiceErrors: map[string]int64{"steam": 2},
ServiceRequests: map[string]int64{"steam": 7},
HitRate: 0.4,
AvgResponseTime: 2 * time.Millisecond,
TotalBytesServed: 100,
TotalBytesSaved: 50,
MemoryCacheSize: 8,
DiskCacheSize: 16,
DiskTierReady: 1,
Uptime: 3 * time.Second,
}
rec := httptest.NewRecorder()
WriteText(rec, st)
body := rec.Body.String()
if strings.Contains(body, "# SteamCache2 Metrics") {
t.Error("non-standard # SteamCache2 Metrics banner must not be present")
}
counters := []string{
"total_requests", "cache_hits", "cache_misses", "negative_cache_hits",
"cache_coalesced", "range_cache", "range_upstream", "errors", "rate_limited",
"upstream_errors", "cache_write_failures", "memory_cache_hits", "disk_cache_hits",
"promotions", "evictions", "capacity_pressure_events", "service_errors",
"service_requests", "total_bytes_served", "total_bytes_saved",
}
gauges := []string{
"hit_rate", "avg_response_time_ms", "memory_cache_size", "disk_cache_size",
"disk_tier_ready", "uptime_seconds",
}
for _, name := range counters {
assertHelpType(t, body, name, "counter")
}
for _, name := range gauges {
assertHelpType(t, body, name, "gauge")
}
if !strings.Contains(body, `service_errors{service="steam"} 2`) {
t.Errorf("missing labeled service_errors sample: %q", body)
}
if !strings.Contains(body, `service_requests{service="steam"} 7`) {
t.Errorf("missing labeled service_requests sample: %q", body)
}
if !strings.Contains(body, "disk_tier_ready 1\n") {
t.Errorf("missing disk_tier_ready 1 sample: %q", body)
}
if !strings.Contains(body, "range_cache 3\n") {
t.Errorf("missing range_cache 3 sample: %q", body)
}
if !strings.Contains(body, "negative_cache_hits 1\n") {
t.Errorf("missing negative_cache_hits 1 sample: %q", body)
}
}
func assertHelpType(t *testing.T, body, name, typ string) {
t.Helper()
help := "# HELP " + name + " "
typeLine := "# TYPE " + name + " " + typ
iHelp := strings.Index(body, help)
if iHelp < 0 {
t.Errorf("missing %q", help)
return
}
iType := strings.Index(body[iHelp:], typeLine)
if iType < 0 {
t.Errorf("missing %q after HELP for %s", typeLine, name)
return
}
afterType := body[iHelp+iType+len(typeLine):]
if !strings.HasPrefix(afterType, "\n") {
t.Errorf("# TYPE %s not followed by newline", name)
return
}
sample := afterType[1:]
if !strings.HasPrefix(sample, name+" ") && !strings.HasPrefix(sample, name+"{") {
t.Errorf("sample for %s does not follow TYPE; next line starts %q", name, firstLine(sample))
}
}
func firstLine(s string) string {
if i := strings.IndexByte(s, '\n'); i >= 0 {
return s[:i]
}
return s
}