// steamcache/metrics/metrics.go package metrics import ( "fmt" "net/http" "sync" "sync/atomic" "time" "s1d3sw1ped/steamcache2/steamcache/logger" ) // Metrics tracks various performance and operational metrics type Metrics struct { // Request metrics TotalRequests int64 CacheHits int64 CacheMisses int64 CacheCoalesced int64 NegativeCacheHits int64 // 404/410 served from a still-valid negative cache entry RangeCache int64 // Range requests served as 206 from an already-cached object (HIT) RangeUpstream int64 // Range requests that required an upstream fetch (full object), served as 206 Errors int64 RateLimited int64 // Performance metrics TotalResponseTime int64 // in nanoseconds TotalBytesServed int64 TotalBytesSaved int64 // bytes served from cache instead of being re-downloaded from upstream // Cache metrics MemoryCacheSize int64 DiskCacheSize int64 MemoryCacheCapacity int64 // configured memory capacity (bytes) DiskCacheCapacity int64 // configured disk capacity (bytes); 0 when no disk MemoryCacheHits int64 DiskCacheHits int64 Promotions int64 Evictions int64 CapacityPressureEvents int64 // soft eviction under cap and/or disk ENOSPC DiskTierReady int64 // 0=pending (or unset), 1=ready or no-disk (N/A) // Expanded observability (upstream breakdowns, cache write failures, per-service errors) UpstreamErrors int64 CacheWriteFailures int64 ServiceErrors map[string]int64 serviceErrorsMutex sync.RWMutex // Service metrics ServiceRequests map[string]int64 serviceMutex sync.RWMutex // Time tracking StartTime time.Time LastResetTime time.Time } // NewMetrics creates a new metrics instance func NewMetrics() *Metrics { now := time.Now() return &Metrics{ ServiceRequests: make(map[string]int64), ServiceErrors: make(map[string]int64), StartTime: now, LastResetTime: now, } } // IncrementTotalRequests increments the total request counter func (m *Metrics) IncrementTotalRequests() { atomic.AddInt64(&m.TotalRequests, 1) } // IncrementCacheHits increments the cache hit counter func (m *Metrics) IncrementCacheHits() { atomic.AddInt64(&m.CacheHits, 1) } // IncrementCacheMisses increments the cache miss counter func (m *Metrics) IncrementCacheMisses() { atomic.AddInt64(&m.CacheMisses, 1) } // IncrementNegativeCacheHits increments hits served from a 404/410 negative entry. func (m *Metrics) IncrementNegativeCacheHits() { atomic.AddInt64(&m.NegativeCacheHits, 1) } // IncrementCacheCoalesced increments the coalesced request counter func (m *Metrics) IncrementCacheCoalesced() { atomic.AddInt64(&m.CacheCoalesced, 1) } // IncrementRangeCache increments the Range-from-cache counter (HIT served as 206) func (m *Metrics) IncrementRangeCache() { atomic.AddInt64(&m.RangeCache, 1) } // IncrementRangeUpstream increments the Range-from-upstream counter (full object // fetched upstream, requested slice served as 206) func (m *Metrics) IncrementRangeUpstream() { atomic.AddInt64(&m.RangeUpstream, 1) } // IncrementErrors increments the error counter func (m *Metrics) IncrementErrors() { atomic.AddInt64(&m.Errors, 1) } // IncrementRateLimited increments the rate limited counter func (m *Metrics) IncrementRateLimited() { atomic.AddInt64(&m.RateLimited, 1) } // AddResponseTime adds response time to the total func (m *Metrics) AddResponseTime(duration time.Duration) { atomic.AddInt64(&m.TotalResponseTime, int64(duration)) } // AddBytesServed adds bytes served to the total func (m *Metrics) AddBytesServed(bytes int64) { atomic.AddInt64(&m.TotalBytesServed, bytes) } // AddBytesSaved records bytes that were served from the cache instead of being // fetched again from the upstream (the main value metric for a cache). func (m *Metrics) AddBytesSaved(bytes int64) { atomic.AddInt64(&m.TotalBytesSaved, bytes) } // SetMemoryCacheSize sets the current memory cache size func (m *Metrics) SetMemoryCacheSize(size int64) { atomic.StoreInt64(&m.MemoryCacheSize, size) } // SetDiskCacheSize sets the current disk cache size func (m *Metrics) SetDiskCacheSize(size int64) { atomic.StoreInt64(&m.DiskCacheSize, size) } // SetMemoryCacheCapacity sets the configured memory cache capacity in bytes. func (m *Metrics) SetMemoryCacheCapacity(capacity int64) { atomic.StoreInt64(&m.MemoryCacheCapacity, capacity) } // SetDiskCacheCapacity sets the configured disk cache capacity in bytes // (0 when no disk is configured). func (m *Metrics) SetDiskCacheCapacity(capacity int64) { atomic.StoreInt64(&m.DiskCacheCapacity, capacity) } // SetDiskTierReady sets whether the disk slow tier is attached (1) or still pending (0). // Memory-only (no disk) also uses 1 — meaning "not waiting on disk attach". Reset does not clear this. func (m *Metrics) SetDiskTierReady(ready int64) { atomic.StoreInt64(&m.DiskTierReady, ready) } // GetDiskTierReady returns 1 if disk tier is ready (or no disk configured), else 0 while attach pending. func (m *Metrics) GetDiskTierReady() int64 { return atomic.LoadInt64(&m.DiskTierReady) } // IncrementMemoryCacheHits increments memory cache hits func (m *Metrics) IncrementMemoryCacheHits() { atomic.AddInt64(&m.MemoryCacheHits, 1) } // IncrementDiskCacheHits increments disk cache hits func (m *Metrics) IncrementDiskCacheHits() { atomic.AddInt64(&m.DiskCacheHits, 1) } // IncrementServiceRequests increments requests for a specific service func (m *Metrics) IncrementServiceRequests(service string) { m.serviceMutex.Lock() defer m.serviceMutex.Unlock() m.ServiceRequests[service]++ } // GetServiceRequests returns the number of requests for a service func (m *Metrics) GetServiceRequests(service string) int64 { m.serviceMutex.RLock() defer m.serviceMutex.RUnlock() return m.ServiceRequests[service] } func (m *Metrics) IncrementPromotions() { atomic.AddInt64(&m.Promotions, 1) } func (m *Metrics) IncrementEvictions() { atomic.AddInt64(&m.Evictions, 1) } func (m *Metrics) IncrementCapacityPressureEvents() { atomic.AddInt64(&m.CapacityPressureEvents, 1) } // NoteSoftEviction records one cap-pressure eviction batch that freed bytes. // Keeps the existing evictions counter and also increments capacity_pressure_events. // Nil m is safe: the log still fires so ops can grep without metrics wired. func NoteSoftEviction(m *Metrics, tier string, evicted uint) { if evicted == 0 { return } if m != nil { m.IncrementEvictions() m.IncrementCapacityPressureEvents() } logger.Logger.Info(). Str("tier", tier). Str("reason", "eviction"). Uint("bytes_evicted", evicted). Msg("cache capacity pressure") } // NoteNoSpace records a disk Create/Write/Mkdir ENOSPC (or equivalent) event. func NoteNoSpace(m *Metrics, err error) { if m != nil { m.IncrementCapacityPressureEvents() } logger.Logger.Warn(). Str("tier", "disk"). Str("reason", "enospc"). Err(err). Msg("cache capacity pressure") } // Additional observability counters func (m *Metrics) IncrementUpstreamErrors() { atomic.AddInt64(&m.UpstreamErrors, 1) } func (m *Metrics) IncrementCacheWriteFailures() { atomic.AddInt64(&m.CacheWriteFailures, 1) } func (m *Metrics) IncrementServiceError(service string) { m.serviceErrorsMutex.Lock() defer m.serviceErrorsMutex.Unlock() if m.ServiceErrors == nil { m.ServiceErrors = make(map[string]int64) } m.ServiceErrors[service]++ } // GetStats returns a snapshot of current metrics func (m *Metrics) GetStats() *Stats { totalRequests := atomic.LoadInt64(&m.TotalRequests) cacheHits := atomic.LoadInt64(&m.CacheHits) cacheMisses := atomic.LoadInt64(&m.CacheMisses) var hitRate float64 if totalRequests > 0 { hitRate = float64(cacheHits) / float64(totalRequests) } var avgResponseTime time.Duration if totalRequests > 0 { avgResponseTime = time.Duration(atomic.LoadInt64(&m.TotalResponseTime) / totalRequests) } m.serviceMutex.RLock() serviceRequests := make(map[string]int64) for k, v := range m.ServiceRequests { serviceRequests[k] = v } m.serviceMutex.RUnlock() serviceErrors := make(map[string]int64) m.serviceErrorsMutex.RLock() defer m.serviceErrorsMutex.RUnlock() for k, v := range m.ServiceErrors { serviceErrors[k] = v } memoryCacheSize := atomic.LoadInt64(&m.MemoryCacheSize) diskCacheSize := atomic.LoadInt64(&m.DiskCacheSize) memoryCacheCapacity := atomic.LoadInt64(&m.MemoryCacheCapacity) diskCacheCapacity := atomic.LoadInt64(&m.DiskCacheCapacity) return &Stats{ TotalRequests: totalRequests, CacheHits: cacheHits, CacheMisses: cacheMisses, CacheCoalesced: atomic.LoadInt64(&m.CacheCoalesced), NegativeCacheHits: atomic.LoadInt64(&m.NegativeCacheHits), RangeCache: atomic.LoadInt64(&m.RangeCache), RangeUpstream: atomic.LoadInt64(&m.RangeUpstream), Errors: atomic.LoadInt64(&m.Errors), RateLimited: atomic.LoadInt64(&m.RateLimited), HitRate: hitRate, AvgResponseTime: avgResponseTime, TotalBytesServed: atomic.LoadInt64(&m.TotalBytesServed), TotalBytesSaved: atomic.LoadInt64(&m.TotalBytesSaved), MemoryCacheSize: memoryCacheSize, DiskCacheSize: diskCacheSize, MemoryCacheCapacity: memoryCacheCapacity, DiskCacheCapacity: diskCacheCapacity, DiskCacheFullRatio: diskFullRatio(diskCacheSize, diskCacheCapacity), DiskTierReady: atomic.LoadInt64(&m.DiskTierReady), MemoryCacheHits: atomic.LoadInt64(&m.MemoryCacheHits), DiskCacheHits: atomic.LoadInt64(&m.DiskCacheHits), Promotions: atomic.LoadInt64(&m.Promotions), Evictions: atomic.LoadInt64(&m.Evictions), CapacityPressureEvents: atomic.LoadInt64(&m.CapacityPressureEvents), ServiceRequests: serviceRequests, UpstreamErrors: atomic.LoadInt64(&m.UpstreamErrors), CacheWriteFailures: atomic.LoadInt64(&m.CacheWriteFailures), ServiceErrors: serviceErrors, Uptime: time.Since(m.StartTime), LastResetTime: m.LastResetTime, } } // diskFullRatio is size / capacity clamped to [0,1]. // It is 0 when no disk is configured or the capacity is 0. func diskFullRatio(size, capacity int64) float64 { if size <= 0 || capacity <= 0 { return 0 } ratio := float64(size) / float64(capacity) if ratio > 1 { return 1 } return ratio } // Reset resets all metrics to zero func (m *Metrics) Reset() { atomic.StoreInt64(&m.TotalRequests, 0) atomic.StoreInt64(&m.CacheHits, 0) atomic.StoreInt64(&m.CacheMisses, 0) atomic.StoreInt64(&m.CacheCoalesced, 0) atomic.StoreInt64(&m.NegativeCacheHits, 0) atomic.StoreInt64(&m.RangeCache, 0) atomic.StoreInt64(&m.RangeUpstream, 0) atomic.StoreInt64(&m.Errors, 0) atomic.StoreInt64(&m.RateLimited, 0) atomic.StoreInt64(&m.TotalResponseTime, 0) atomic.StoreInt64(&m.TotalBytesServed, 0) atomic.StoreInt64(&m.TotalBytesSaved, 0) atomic.StoreInt64(&m.MemoryCacheHits, 0) atomic.StoreInt64(&m.DiskCacheHits, 0) atomic.StoreInt64(&m.Promotions, 0) atomic.StoreInt64(&m.Evictions, 0) atomic.StoreInt64(&m.CapacityPressureEvents, 0) atomic.StoreInt64(&m.UpstreamErrors, 0) atomic.StoreInt64(&m.CacheWriteFailures, 0) m.serviceMutex.Lock() m.ServiceRequests = make(map[string]int64) m.serviceMutex.Unlock() m.serviceErrorsMutex.Lock() defer m.serviceErrorsMutex.Unlock() m.ServiceErrors = make(map[string]int64) m.LastResetTime = time.Now() } // Stats represents a snapshot of metrics type Stats struct { TotalRequests int64 CacheHits int64 CacheMisses int64 CacheCoalesced int64 NegativeCacheHits int64 RangeCache int64 RangeUpstream int64 Errors int64 RateLimited int64 HitRate float64 AvgResponseTime time.Duration TotalBytesServed int64 TotalBytesSaved int64 MemoryCacheSize int64 DiskCacheSize int64 MemoryCacheCapacity int64 // configured memory capacity (bytes) DiskCacheCapacity int64 // configured disk capacity (bytes); 0 when no disk DiskCacheFullRatio float64 // disk_cache_size / disk_cache_capacity, clamped to [0,1]; 0 when no disk or capacity is 0 DiskTierReady int64 MemoryCacheHits int64 DiskCacheHits int64 Promotions int64 Evictions int64 CapacityPressureEvents int64 UpstreamErrors int64 CacheWriteFailures int64 ServiceErrors map[string]int64 ServiceRequests map[string]int64 Uptime time.Duration LastResetTime time.Time } // WriteText emits Prometheus text exposition format 0.0.4 to the ResponseWriter. // Each metric family is # HELP, then # TYPE, then one or more sample lines. // Metric names are stable; labeled series keep service=%q (Prometheus-valid quotes). // All fmt.Fprintf errors are intentionally discarded via _ = : this is a best-effort // read-only debug endpoint; client disconnects or write errors during metrics dump // are not actionable (do not affect cache correctness or require retries). func WriteText(w http.ResponseWriter, stats *Stats) { writeInt(w, "total_requests", "Total HTTP requests handled.", "counter", stats.TotalRequests) writeInt(w, "cache_hits", "Requests served from cache.", "counter", stats.CacheHits) writeInt(w, "cache_misses", "Requests not found in cache.", "counter", stats.CacheMisses) writeInt(w, "negative_cache_hits", "404/410 served from a still-valid negative cache entry.", "counter", stats.NegativeCacheHits) writeInt(w, "cache_coalesced", "Requests coalesced onto an in-flight upstream fetch.", "counter", stats.CacheCoalesced) writeInt(w, "range_cache", "Range requests served as 206 from an already-cached object.", "counter", stats.RangeCache) writeInt(w, "range_upstream", "Range requests that required an upstream fetch, served as 206.", "counter", stats.RangeUpstream) writeInt(w, "errors", "Request errors.", "counter", stats.Errors) writeInt(w, "rate_limited", "Requests rejected by rate limiting.", "counter", stats.RateLimited) writeInt(w, "upstream_errors", "Errors talking to upstream.", "counter", stats.UpstreamErrors) writeInt(w, "cache_write_failures", "Failures writing objects into cache.", "counter", stats.CacheWriteFailures) writeInt(w, "memory_cache_hits", "Hits served from the memory tier.", "counter", stats.MemoryCacheHits) writeInt(w, "disk_cache_hits", "Hits served from the disk tier.", "counter", stats.DiskCacheHits) writeInt(w, "promotions", "Objects promoted from disk to memory.", "counter", stats.Promotions) writeInt(w, "evictions", "Objects evicted from cache.", "counter", stats.Evictions) writeInt(w, "capacity_pressure_events", "Soft eviction under the memory or disk cap, and/or disk ENOSPC.", "counter", stats.CapacityPressureEvents) writeHelpType(w, "service_errors", "Errors attributed to a named service.", "counter") for svc, cnt := range stats.ServiceErrors { _, _ = fmt.Fprintf(w, "service_errors{service=%q} %d\n", svc, cnt) } writeHelpType(w, "service_requests", "Requests attributed to a named service.", "counter") for svc, cnt := range stats.ServiceRequests { _, _ = fmt.Fprintf(w, "service_requests{service=%q} %d\n", svc, cnt) } writeFloat(w, "hit_rate", "Cache hits divided by total requests.", "gauge", "%.4f", stats.HitRate) writeFloat(w, "avg_response_time_ms", "Average response time in milliseconds.", "gauge", "%.2f", float64(stats.AvgResponseTime.Nanoseconds())/1e6) writeInt(w, "total_bytes_served", "Total bytes sent to clients.", "counter", stats.TotalBytesServed) writeInt(w, "total_bytes_saved", "Bytes served from cache instead of being re-downloaded from upstream.", "counter", stats.TotalBytesSaved) writeInt(w, "memory_cache_size", "Current memory cache size in bytes.", "gauge", stats.MemoryCacheSize) writeInt(w, "memory_cache_capacity", "Configured memory cache capacity in bytes.", "gauge", stats.MemoryCacheCapacity) writeInt(w, "disk_cache_size", "Current disk cache size in bytes.", "gauge", stats.DiskCacheSize) writeInt(w, "disk_cache_capacity", "Configured disk cache capacity in bytes; 0 when no disk is configured.", "gauge", stats.DiskCacheCapacity) writeFloat(w, "disk_cache_full_ratio", "disk_cache_size / disk_cache_capacity in [0,1]; 0 when no disk or capacity is 0.", "gauge", "%.4f", stats.DiskCacheFullRatio) writeInt(w, "disk_tier_ready", "1 if the disk tier is attached or no disk is configured; 0 while attach is pending.", "gauge", stats.DiskTierReady) writeFloat(w, "uptime_seconds", "Process uptime in seconds.", "gauge", "%.2f", stats.Uptime.Seconds()) } func writeHelpType(w http.ResponseWriter, name, help, typ string) { _, _ = fmt.Fprintf(w, "# HELP %s %s\n# TYPE %s %s\n", name, help, name, typ) } func writeInt(w http.ResponseWriter, name, help, typ string, v int64) { writeHelpType(w, name, help, typ) _, _ = fmt.Fprintf(w, "%s %d\n", name, v) } func writeFloat(w http.ResponseWriter, name, help, typ, valFmt string, v float64) { writeHelpType(w, name, help, typ) _, _ = fmt.Fprintf(w, "%s "+valFmt+"\n", name, v) }