Files
CosmicClash/server/observability/metrics.go
T
2026-09-01 17:06:38 +01:00

87 lines
2.3 KiB
Go

package observability
import (
"fmt"
"io"
"sort"
"sync"
"time"
)
// Metrics is a bounded in-process collector for API request health. Operation
// names are normalized to a fixed vocabulary before storage.
type Metrics struct {
mu sync.Mutex
counts map[metricKey]uint64
sums map[metricKey]time.Duration
}
type metricKey struct{ operation, status string }
func NewMetrics() *Metrics {
return &Metrics{counts: make(map[metricKey]uint64), sums: make(map[metricKey]time.Duration)}
}
func (m *Metrics) ObserveAPI(operation string, statusCode int, duration time.Duration) {
if m == nil {
return
}
if duration < 0 {
duration = 0
}
key := metricKey{normalizeOperation(operation), statusClass(statusCode)}
m.mu.Lock()
m.counts[key]++
m.sums[key] += duration
m.mu.Unlock()
}
func (m *Metrics) WritePrometheus(w io.Writer) error {
if m == nil {
return nil
}
m.mu.Lock()
keys := make([]metricKey, 0, len(m.counts))
for key := range m.counts {
keys = append(keys, key)
}
sort.Slice(keys, func(i, j int) bool {
if keys[i].operation != keys[j].operation {
return keys[i].operation < keys[j].operation
}
return keys[i].status < keys[j].status
})
counts := make(map[metricKey]uint64, len(keys))
sums := make(map[metricKey]time.Duration, len(keys))
for _, key := range keys {
counts[key], sums[key] = m.counts[key], m.sums[key]
}
m.mu.Unlock()
if _, err := io.WriteString(w, "# TYPE cosmic_clash_api_requests_total counter\n# TYPE cosmic_clash_api_latency_seconds summary\n"); err != nil {
return err
}
for _, key := range keys {
labels := fmt.Sprintf(`operation="%s",status="%s"`, key.operation, key.status)
if _, err := fmt.Fprintf(w, "cosmic_clash_api_requests_total{%s} %d\ncosmic_clash_api_latency_seconds_count{%s} %d\ncosmic_clash_api_latency_seconds_sum{%s} %.9f\n", labels, counts[key], labels, counts[key], labels, sums[key].Seconds()); err != nil {
return err
}
}
return nil
}
func normalizeOperation(operation string) string {
for _, allowed := range []string{"queue", "proposal", "assignment", "profile", "ranked_profile", "server", "events", "session", "probe"} {
if operation == allowed {
return allowed
}
}
return "other"
}
func statusClass(code int) string {
if code < 100 || code > 599 {
return "unknown"
}
return fmt.Sprintf("%dxx", code/100)
}