From 87c87e9855703ca42c55bc9473e6ef8c854a3c33 Mon Sep 17 00:00:00 2001 From: Alexis Montagne Date: Tue, 25 Mar 2025 16:00:19 -0700 Subject: [PATCH] .: Add a multi incarnation scope wrapper --- internal/hash/hash.go | 12 ++- multi_incarnation_scope.go | 184 ++++++++++++++++++++++++++++++++ multi_incarnation_scope_test.go | 43 ++++++++ 3 files changed, 236 insertions(+), 3 deletions(-) create mode 100644 multi_incarnation_scope.go create mode 100644 multi_incarnation_scope_test.go diff --git a/internal/hash/hash.go b/internal/hash/hash.go index 178a2e3..5d57359 100644 --- a/internal/hash/hash.go +++ b/internal/hash/hash.go @@ -14,13 +14,19 @@ func New() uint64 { // Add adds a string to a fnv64a hash value, returning the updated hash. func Add(h uint64, s string) uint64 { + h = AddNoScramble(h, s) + + h ^= magicRune + h *= prime64 + + return h +} + +func AddNoScramble(h uint64, s string) uint64 { for i := 0; i < len(s); i++ { h ^= uint64(s[i]) h *= prime64 } - h ^= magicRune - h *= prime64 - return h } diff --git a/multi_incarnation_scope.go b/multi_incarnation_scope.go new file mode 100644 index 0000000..1d708cb --- /dev/null +++ b/multi_incarnation_scope.go @@ -0,0 +1,184 @@ +package stats + +import ( + "fmt" + "strconv" + "sync" + + "github.com/upfluence/stats/internal/hash" +) + +var globalIncarnationRegistry = &incarnationRegistry{key: "incarnation", counters: make(map[counterKey]uint)} + +type incarnationRegistry struct { + countersMu sync.Mutex + counters map[counterKey]uint + key string +} + +func (ir *incarnationRegistry) next(ck counterKey) uint { + ir.countersMu.Lock() + defer ir.countersMu.Unlock() + + current, ok := ir.counters[ck] + + next := current + 1 + + if !ok { + next-- + } + + ir.counters[ck] = next + + return next +} + +type counterKey struct { + keySum uint64 + tagsSum uint64 +} + +func newCounterKey() counterKey { + return counterKey{keySum: hash.New()} +} + +func (ck counterKey) add(key string, tags map[string]string) counterKey { + if key != "" { + if ck.keySum != hash.New() { + key = "_" + key + } + + ck.keySum = hash.AddNoScramble(ck.keySum, key) + } + + for k, v := range tags { + ck.tagsSum |= hash.Add(hash.Add(hash.New(), k), v) + } + + return ck +} + +func (ck counterKey) appendTag(k, v string) counterKey { + ck.tagsSum |= hash.Add(hash.Add(hash.New(), k), v) + + return ck +} + +type multiIncarnationScope struct { + scope Scope + + currentKey counterKey + registry *incarnationRegistry +} + +func newMultiIncarnationScope(sc Scope, r *incarnationRegistry) *multiIncarnationScope { + return &multiIncarnationScope{ + scope: sc, + currentKey: newCounterKey().add(sc.namespace(), sc.tags()), + registry: r, + } +} + +func GlobalIncarnationScope(sc Scope) Scope { + return newMultiIncarnationScope(sc, globalIncarnationRegistry) +} + +func LocalIncarnationScope(sc Scope, k string) Scope { + return newMultiIncarnationScope( + sc, + &incarnationRegistry{key: k, counters: make(map[counterKey]uint)}, + ) +} + +func (mis *multiIncarnationScope) namespace() string { return mis.scope.namespace() } +func (mis *multiIncarnationScope) tags() map[string]string { return mis.scope.tags() } +func (mis *multiIncarnationScope) rootScope() *rootScope { return mis.scope.rootScope() } + +func (mis *multiIncarnationScope) Scope(k string, vs map[string]string) Scope { + if _, ok := vs[mis.registry.key]; ok { + panic(fmt.Sprintf("scope can not include the incarnation key %q", mis.registry.key)) + } + + return &multiIncarnationScope{ + scope: mis.scope.Scope(k, vs), + currentKey: mis.currentKey.add(k, vs), + registry: mis.registry, + } +} + +func (mis *multiIncarnationScope) RootScope() Scope { + return &multiIncarnationScope{ + scope: mis.scope.RootScope(), + registry: mis.registry, + } +} + +type abstractVector[T any] interface { + WithLabels(...string) T +} + +type multiIncarnationVector[T any] struct { + cv abstractVector[T] + ls []string + + currentKey counterKey + registry *incarnationRegistry +} + +func (micv *multiIncarnationVector[T]) WithLabels(vs ...string) T { + if len(vs) != len(micv.ls) { + panic("wrong number of label values") + } + + ck := micv.currentKey + + for i, l := range micv.ls { + ck = ck.appendTag(l, vs[i]) + } + + return micv.cv.WithLabels( + append( + vs, + strconv.Itoa(int(micv.registry.next(ck))), + )..., + ) +} + +func (mis *multiIncarnationScope) Counter(k string) Counter { + return mis.CounterVector(k, nil).WithLabels() +} + +func (mis *multiIncarnationScope) CounterVector(k string, ls []string) CounterVector { + return &multiIncarnationVector[Counter]{ + cv: mis.scope.CounterVector(k, append(ls, mis.registry.key)), + ls: ls, + currentKey: mis.currentKey.add(k, nil), + registry: mis.registry, + } +} + +func (mis *multiIncarnationScope) Gauge(k string) Gauge { + return mis.GaugeVector(k, nil).WithLabels() +} + +func (mis *multiIncarnationScope) GaugeVector(k string, ls []string) GaugeVector { + return &multiIncarnationVector[Gauge]{ + cv: mis.scope.GaugeVector(k, append(ls, mis.registry.key)), + ls: ls, + currentKey: mis.currentKey.add(k, nil), + registry: mis.registry, + } +} + +func (mis *multiIncarnationScope) Histogram(k string, opts ...HistogramOption) Histogram { + return mis.HistogramVector(k, nil, opts...).WithLabels() +} + +func (mis *multiIncarnationScope) HistogramVector(k string, ls []string, opts ...HistogramOption) HistogramVector { + return &multiIncarnationVector[Histogram]{ + cv: mis.scope.HistogramVector(k, append(ls, mis.registry.key), opts...), + ls: ls, + currentKey: mis.currentKey.add(k, nil), + registry: mis.registry, + } +} diff --git a/multi_incarnation_scope_test.go b/multi_incarnation_scope_test.go new file mode 100644 index 0000000..8a57d60 --- /dev/null +++ b/multi_incarnation_scope_test.go @@ -0,0 +1,43 @@ +package stats + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestMultiIncarnationScope(t *testing.T) { + c := NewStaticCollector() + + sc := LocalIncarnationScope(RootScope(c), "incarnation") + + g1 := sc.Scope("foo", map[string]string{"bar": "buz"}).Gauge("bar") + + g1.Update(1) + + sc.GaugeVector("foo_bar", []string{"bar"}).WithLabels("buz").Update(2) + + g1.Update(3) + + sc.Scope("foo", map[string]string{"bar": "buz"}).Gauge("bar").Update(4) + + sc.Counter("foo_bar_1").Inc() + + assert.Equal( + t, + []Int64Snapshot{ + {Name: "foo_bar", Labels: map[string]string{"bar": "buz", "incarnation": "0"}, Value: 3}, + {Name: "foo_bar", Labels: map[string]string{"bar": "buz", "incarnation": "1"}, Value: 2}, + {Name: "foo_bar", Labels: map[string]string{"bar": "buz", "incarnation": "2"}, Value: 4}, + }, + c.Get().Gauges, + ) + + assert.Equal( + t, + []Int64Snapshot{ + {Name: "foo_bar_1", Labels: map[string]string{"incarnation": "0"}, Value: 1}, + }, + c.Get().Counters, + ) +}