mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-17 04:59:55 +00:00
feat(stats): add bounded path and hash-width analytics (#159)
This commit is contained in:
Vendored
+4
@@ -35,6 +35,10 @@ func (s *stubReader) GetSignalStats(context.Context, time.Time, time.Time, []str
|
||||
return nil, s.err
|
||||
}
|
||||
|
||||
func (s *stubReader) GetPathStats(context.Context, time.Time, time.Time, []string) (*api.PathStats, error) {
|
||||
return nil, s.err
|
||||
}
|
||||
|
||||
func (s *stubReader) ListIATAs(_ context.Context) ([]api.IATA, error) {
|
||||
s.calls++
|
||||
return s.iatas, s.err
|
||||
|
||||
Vendored
+25
@@ -0,0 +1,25 @@
|
||||
// Copyright 2026 Beacon Contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
"github.com/MeshCore-Beacon/beacon-server/internal/api"
|
||||
)
|
||||
|
||||
func (cr *CachedReader) GetPathStats(ctx context.Context, since, until time.Time, iatas []string) (*api.PathStats, error) {
|
||||
sorted := append([]string{}, iatas...)
|
||||
slices.Sort(sorted)
|
||||
sorted = slices.Compact(sorted)
|
||||
segment, _ := json.Marshal(sorted) // [] and [""] must remain distinct (global vs empty region).
|
||||
key := fmt.Sprintf("%s%s:%d:%d", keyPathStatsPrefix, segment, since.UnixMilli(), until.UnixMilli())
|
||||
return getOrSet(ctx, cr.c, key, cr.ttl.Stats, func() (*api.PathStats, error) {
|
||||
return cr.inner.GetPathStats(ctx, since, until, iatas)
|
||||
})
|
||||
}
|
||||
Vendored
+48
@@ -0,0 +1,48 @@
|
||||
// Copyright 2026 Beacon Contributors
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"context"
|
||||
"slices"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/MeshCore-Beacon/beacon-server/internal/api"
|
||||
)
|
||||
|
||||
type pathStatsReader struct {
|
||||
api.Reader
|
||||
calls int64
|
||||
}
|
||||
|
||||
func (r *pathStatsReader) GetPathStats(context.Context, time.Time, time.Time, []string) (*api.PathStats, error) {
|
||||
r.calls++
|
||||
return &api.PathStats{Receptions: r.calls}, nil
|
||||
}
|
||||
|
||||
func TestPathCacheSeparatesWindowAndRegion(t *testing.T) {
|
||||
c, mr := newTestClient(t)
|
||||
inner := &pathStatsReader{}
|
||||
r := NewCachedReader(inner, c, CacheTTLs{Stats: time.Minute})
|
||||
for _, tc := range []struct {
|
||||
iatas []string
|
||||
since, until, want int64
|
||||
}{
|
||||
{nil, 0, 10, 1}, {[]string{}, 0, 10, 1}, {[]string{""}, 0, 10, 2}, {[]string{"YVR"}, 0, 10, 3},
|
||||
{[]string{"YVR", "YYJ"}, 0, 10, 4}, {[]string{"YYJ", "YVR", "YYJ"}, 0, 10, 4},
|
||||
{nil, 1, 10, 5}, {nil, 0, 11, 6}, {nil, 0, 10, 1},
|
||||
} {
|
||||
before := slices.Clone(tc.iatas)
|
||||
got, err := r.GetPathStats(context.Background(), time.UnixMilli(tc.since), time.UnixMilli(tc.until), tc.iatas)
|
||||
if err != nil || got.Receptions != tc.want || !slices.Equal(before, tc.iatas) {
|
||||
t.Fatalf("%+v: %+v %v", tc, got, err)
|
||||
}
|
||||
}
|
||||
mr.FastForward(time.Minute)
|
||||
got, err := r.GetPathStats(context.Background(), time.UnixMilli(0), time.UnixMilli(10), nil)
|
||||
if err != nil || got.Receptions != 7 {
|
||||
t.Fatalf("expiry: %+v %v", got, err)
|
||||
}
|
||||
}
|
||||
Vendored
+1
@@ -30,6 +30,7 @@ const (
|
||||
keyStatsOverviewPrefix = "beacon:stats:overview:"
|
||||
keyStatsObservationsPrefix = "beacon:stats:observations:"
|
||||
keySignalStatsPrefix = "beacon:stats:signal:"
|
||||
keyPathStatsPrefix = "beacon:stats:paths:"
|
||||
keyStatsBreakdownPrefix = "beacon:stats:breakdown:"
|
||||
keyStatsTopNodesPrefix = "beacon:stats:top-nodes:"
|
||||
keyStatsTopObsPrefix = "beacon:stats:top-observers:"
|
||||
|
||||
Reference in New Issue
Block a user