// 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 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 // 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) } // IncrementCacheCoalesced increments the coalesced request counter func (m *Metrics) IncrementCacheCoalesced() { atomic.AddInt64(&m.CacheCoalesced, 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) } // 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), 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), 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.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 Errors int64 RateLimited int64 HitRate float64 AvgResponseTime time.Duration TotalBytesServed int64 TotalBytesSaved int64 MemoryCacheSize int64 DiskCacheSize 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, "cache_coalesced %d\n", stats.CacheCoalesced) _, _ = 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, "uptime_seconds %.2f\n", stats.Uptime.Seconds()) }