// steamcache/metrics/metrics.go package metrics import ( "fmt" "net/http" "sync" "sync/atomic" "time" ) // 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 MemoryCacheHits int64 DiskCacheHits int64 Promotions int64 Evictions int64 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) } // 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) } // 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 } 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: atomic.LoadInt64(&m.MemoryCacheSize), DiskCacheSize: atomic.LoadInt64(&m.DiskCacheSize), 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), ServiceRequests: serviceRequests, UpstreamErrors: atomic.LoadInt64(&m.UpstreamErrors), CacheWriteFailures: atomic.LoadInt64(&m.CacheWriteFailures), ServiceErrors: serviceErrors, Uptime: time.Since(m.StartTime), LastResetTime: m.LastResetTime, } } // 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.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 DiskTierReady int64 MemoryCacheHits int64 DiskCacheHits int64 Promotions int64 Evictions int64 UpstreamErrors int64 CacheWriteFailures int64 ServiceErrors map[string]int64 ServiceRequests map[string]int64 Uptime time.Duration LastResetTime time.Time } // WriteText emits the Prometheus-style text metrics to the ResponseWriter. // Promoted from internal handler per Phase 3 for better package ownership. // 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) { _, _ = fmt.Fprintf(w, "# SteamCache2 Metrics\n") _, _ = fmt.Fprintf(w, "total_requests %d\n", stats.TotalRequests) _, _ = fmt.Fprintf(w, "cache_hits %d\n", stats.CacheHits) _, _ = fmt.Fprintf(w, "cache_misses %d\n", stats.CacheMisses) _, _ = fmt.Fprintf(w, "negative_cache_hits %d\n", stats.NegativeCacheHits) _, _ = fmt.Fprintf(w, "cache_coalesced %d\n", stats.CacheCoalesced) _, _ = fmt.Fprintf(w, "range_cache %d\n", stats.RangeCache) _, _ = fmt.Fprintf(w, "range_upstream %d\n", stats.RangeUpstream) _, _ = fmt.Fprintf(w, "errors %d\n", stats.Errors) _, _ = fmt.Fprintf(w, "rate_limited %d\n", stats.RateLimited) _, _ = fmt.Fprintf(w, "upstream_errors %d\n", stats.UpstreamErrors) _, _ = fmt.Fprintf(w, "cache_write_failures %d\n", stats.CacheWriteFailures) _, _ = fmt.Fprintf(w, "memory_cache_hits %d\n", stats.MemoryCacheHits) _, _ = fmt.Fprintf(w, "disk_cache_hits %d\n", stats.DiskCacheHits) _, _ = fmt.Fprintf(w, "promotions %d\n", stats.Promotions) _, _ = fmt.Fprintf(w, "evictions %d\n", stats.Evictions) for svc, cnt := range stats.ServiceErrors { _, _ = fmt.Fprintf(w, "service_errors{service=%q} %d\n", svc, cnt) } for svc, cnt := range stats.ServiceRequests { _, _ = fmt.Fprintf(w, "service_requests{service=%q} %d\n", svc, cnt) } _, _ = fmt.Fprintf(w, "hit_rate %.4f\n", stats.HitRate) _, _ = fmt.Fprintf(w, "avg_response_time_ms %.2f\n", float64(stats.AvgResponseTime.Nanoseconds())/1e6) _, _ = fmt.Fprintf(w, "total_bytes_served %d\n", stats.TotalBytesServed) _, _ = fmt.Fprintf(w, "total_bytes_saved %d\n", stats.TotalBytesSaved) _, _ = fmt.Fprintf(w, "memory_cache_size %d\n", stats.MemoryCacheSize) _, _ = fmt.Fprintf(w, "disk_cache_size %d\n", stats.DiskCacheSize) _, _ = fmt.Fprintf(w, "disk_tier_ready %d\n", stats.DiskTierReady) _, _ = fmt.Fprintf(w, "uptime_seconds %.2f\n", stats.Uptime.Seconds()) }