admin: add in-memory metrics series store

Bounded ring buffer (240 samples) keyed by source/metric[/labels],
reusing the dashSample ring pattern. No persistence; powers the
upcoming monitoring charts.
This commit is contained in:
Chris Lu committed 2026-09-14 23:59:38 -07:00
1 parent 2f26d5779b
commit 207bc0b75a
1 file changed
+117
+117
View File
@@ -0,0 +1,117 @@
package dash
import (
"sync"
"time"
)
const metricsMaxSamples = 240
type metricsSample struct {
t time.Time
values map[string]float64
}
type metricsSeries struct {
mu sync.Mutex
samples []metricsSample
}
func newMetricsSeries() *metricsSeries {
return &metricsSeries{samples: make([]metricsSample, 0, metricsMaxSamples)}
}
func (s *metricsSeries) record(t time.Time, values map[string]float64) {
s.mu.Lock()
defer s.mu.Unlock()
s.samples = append(s.samples, metricsSample{t: t, values: values})
if len(s.samples) > metricsMaxSamples {
s.samples = s.samples[len(s.samples)-metricsMaxSamples:]
}
}
func (s *metricsSeries) snapshot() []metricsSample {
s.mu.Lock()
defer s.mu.Unlock()
out := make([]metricsSample, len(s.samples))
copy(out, s.samples)
return out
}
type metricsStore struct {
mu sync.Mutex
series map[string]*metricsSeries
}
func newMetricsStore() *metricsStore {
return &metricsStore{series: make(map[string]*metricsSeries)}
}
func (s *metricsStore) record(source, name string, value float64, t time.Time) {
s.mu.Lock()
key := source + "/" + name
ser, ok := s.series[key]
if !ok {
ser = newMetricsSeries()
s.series[key] = ser
}
s.mu.Unlock()
ser.record(t, map[string]float64{"": value})
}
func (s *metricsStore) recordLabeled(source, name string, labels map[string]string, value float64, t time.Time) {
s.mu.Lock()
key := source + "/" + name + "/" + labelKey(labels)
ser, ok := s.series[key]
if !ok {
ser = newMetricsSeries()
s.series[key] = ser
}
s.mu.Unlock()
ser.record(t, map[string]float64{"": value})
}
func (s *metricsStore) get(source, name string) []metricsSample {
s.mu.Lock()
key := source + "/" + name
ser, ok := s.series[key]
s.mu.Unlock()
if !ok {
return nil
}
return ser.snapshot()
}
func (s *metricsStore) getLabeled(source, name string, labels map[string]string) []metricsSample {
s.mu.Lock()
key := source + "/" + name + "/" + labelKey(labels)
ser, ok := s.series[key]
s.mu.Unlock()
if !ok {
return nil
}
return ser.snapshot()
}
func labelKey(labels map[string]string) string {
if len(labels) == 0 {
return ""
}
keys := make([]string, 0, len(labels))
for k := range labels {
keys = append(keys, k)
}
for i := 1; i < len(keys); i++ {
for j := i; j > 0 && keys[j] < keys[j-1]; j-- {
keys[j], keys[j-1] = keys[j-1], keys[j]
}
}
out := ""
for i, k := range keys {
if i > 0 {
out += ","
}
out += k + "=" + labels[k]
}
return out
}