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.
This commit is contained in:
@@ -4,7 +4,9 @@ import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestCapacityPressureEventsWriteTextAndReset(t *testing.T) {
|
||||
@@ -47,6 +49,15 @@ func TestCapacityPressureEventsWriteTextAndReset(t *testing.T) {
|
||||
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()
|
||||
@@ -61,3 +72,108 @@ func TestNoteSoftEvictionNilMetrics(t *testing.T) {
|
||||
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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user