099e5347d5
- Updated the Makefile to include a new `clean-disk` target for removing disk cache, and modified the `run-validation` target to clean the disk cache before starting. - Enhanced the `check-review-labels` target to include additional file types in the search for temporary review labels, improving code hygiene checks. - Refined the README.md to clarify the hardening section and improve the description of the `prefill` command. - Removed the obsolete `test_cache/.gitkeep` file to clean up the repository.
302 lines
9.7 KiB
Go
302 lines
9.7 KiB
Go
// 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())
|
|
}
|