155 lines
4.1 KiB
Go
155 lines
4.1 KiB
Go
package metrics
|
|
|
|
import (
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Metrics holds all observability metrics for the trip planner service.
|
|
type Metrics struct {
|
|
// Cache metrics per layer
|
|
CacheHits map[string]int64 // per-layer hit counts (city, station, search)
|
|
CacheMisses map[string]int64 // per-layer miss counts
|
|
|
|
// API quota remaining (per key or global)
|
|
APIQuotaRemaining int64
|
|
|
|
// Circuit breaker metrics
|
|
CircuitBreakerTrips int64 // total circuit breaker trips (opened)
|
|
|
|
// Search metrics
|
|
SearchCount int64 // total number of searches
|
|
SearchDuration *histogram // distribution of search durations
|
|
|
|
// Internal counters
|
|
mu sync.Mutex
|
|
layerTTLs map[string]time.Duration
|
|
}
|
|
|
|
// histogram tracks duration values and computes simple stats.
|
|
type histogram struct {
|
|
values []int64 // nanoseconds
|
|
maxValues int
|
|
}
|
|
|
|
// New creates a new Metrics instance with initialized maps.
|
|
func New() *Metrics {
|
|
return &Metrics{
|
|
CacheHits: make(map[string]int64),
|
|
CacheMisses: make(map[string]int64),
|
|
layerTTLs: make(map[string]time.Duration),
|
|
SearchDuration: &histogram{maxValues: 1000},
|
|
}
|
|
}
|
|
|
|
// RecordCacheHit records a cache hit for the given layer.
|
|
func (m *Metrics) RecordCacheHit(layer string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.CacheHits[layer]++
|
|
}
|
|
|
|
// RecordCacheMiss records a cache miss for the given layer.
|
|
func (m *Metrics) RecordCacheMiss(layer string) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.CacheMisses[layer]++
|
|
}
|
|
|
|
// RecordAPIQuota records the remaining API quota.
|
|
func (m *Metrics) RecordAPIQuota(remaining int64) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.APIQuotaRemaining = remaining
|
|
}
|
|
|
|
// RecordCircuitBreakerTrip records a circuit breaker trip.
|
|
func (m *Metrics) RecordCircuitBreakerTrip() {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.CircuitBreakerTrips++
|
|
}
|
|
|
|
// RecordSearch records a completed search with its duration in nanoseconds.
|
|
func (m *Metrics) RecordSearch(durationNS int64) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.SearchCount++
|
|
m.SearchDuration.values = append(m.SearchDuration.values, durationNS)
|
|
// Trim if exceeding max
|
|
if len(m.SearchDuration.values) > m.SearchDuration.maxValues {
|
|
m.SearchDuration.values = m.SearchDuration.values[len(m.SearchDuration.values)-m.SearchDuration.maxValues:]
|
|
}
|
|
}
|
|
|
|
// GetCacheHitRate returns the hit rate (hits / (hits + misses)) for a layer.
|
|
func (m *Metrics) GetCacheHitRate(layer string) float64 {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
hits := m.CacheHits[layer]
|
|
misses := m.CacheMisses[layer]
|
|
total := hits + misses
|
|
if total == 0 {
|
|
return 0
|
|
}
|
|
return float64(hits) / float64(total)
|
|
}
|
|
|
|
// GetMetricsJSON returns all metrics as a JSON-friendly map.
|
|
func (m *Metrics) GetMetricsJSON() map[string]interface{} {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
|
|
avgSearchDuration := 0.0
|
|
if m.SearchCount > 0 && len(m.SearchDuration.values) > 0 {
|
|
var total int64
|
|
for _, v := range m.SearchDuration.values {
|
|
total += v
|
|
}
|
|
avgSearchDuration = float64(total) / float64(len(m.SearchDuration.values)) / 1e6 // convert to milliseconds
|
|
}
|
|
|
|
result := map[string]interface{}{
|
|
"cache_hits": m.CacheHits,
|
|
"cache_misses": m.CacheMisses,
|
|
"cache_hit_rate": m.getOverallHitRate(),
|
|
"api_quota_remaining": m.APIQuotaRemaining,
|
|
"circuit_breaker_trips": m.CircuitBreakerTrips,
|
|
"search_count": m.SearchCount,
|
|
"avg_search_duration_ms": avgSearchDuration,
|
|
"layer_ttls": m.layerTTLs,
|
|
}
|
|
return result
|
|
}
|
|
|
|
// getOverallHitRate calculates overall hit rate across all layers.
|
|
func (m *Metrics) getOverallHitRate() float64 {
|
|
var totalHits, totalMisses int64
|
|
for _, hits := range m.CacheHits {
|
|
totalHits += hits
|
|
}
|
|
for _, misses := range m.CacheMisses {
|
|
totalMisses += misses
|
|
}
|
|
total := totalHits + totalMisses
|
|
if total == 0 {
|
|
return 0
|
|
}
|
|
return float64(totalHits) / float64(total)
|
|
}
|
|
|
|
// SetLayerTTL sets the TTL for a cache layer (for documentation/observability).
|
|
func (m *Metrics) SetLayerTTL(layer string, ttl time.Duration) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
m.layerTTLs[layer] = ttl
|
|
}
|
|
|
|
// GetLayerTTL returns the TTL for a cache layer.
|
|
func (m *Metrics) GetLayerTTL(layer string) (time.Duration, bool) {
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
ttl, ok := m.layerTTLs[layer]
|
|
return ttl, ok
|
|
}
|