mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-22 14:11:27 +00:00
fix(observers): separate packet metrics from presence updates
This commit is contained in:
1 parent
fd7c6b3eb7
commit
5fbba24a4a
31 files changed
+844
-93
No files matched your search
@@ -256,6 +256,7 @@ var activityIntervals = map[string]time.Duration{
|
||||
// @Param observerId path string true "Observer UUID"
|
||||
// @Param range query string false "Trailing window as a Go duration, max 720h (default 24h); max 48h when interval is under 1h"
|
||||
// @Param interval query string false "Bucket size: 5m, 15m, 1h, 6h or 24h (default 15m)"
|
||||
// @Param until query int false "Optional exclusive end in epoch milliseconds; aligned down to a complete bucket, at most 30 days old"
|
||||
// @Success 200 {object} api.ObserverActivity
|
||||
// @Failure 400 {object} handlers.APIError
|
||||
// @Failure 404 {object} handlers.APIError
|
||||
@@ -295,7 +296,17 @@ func getObserverActivity(reader api.Reader) http.HandlerFunc {
|
||||
respondError(w, http.StatusBadRequest, "range/interval exceeds 1000 buckets")
|
||||
return
|
||||
}
|
||||
activity, err := reader.GetObserverActivity(r.Context(), observerID, window, interval)
|
||||
until := time.Time{}
|
||||
if values, ok := r.URL.Query()["until"]; ok {
|
||||
n, err := strconv.ParseInt(values[0], 10, 64)
|
||||
now := time.Now()
|
||||
if len(values) != 1 || err != nil || n < 0 || n > now.UnixMilli() || n < now.Add(-720*time.Hour).UnixMilli() {
|
||||
respondError(w, http.StatusBadRequest, "until must be one past epoch-millisecond timestamp within 30 days")
|
||||
return
|
||||
}
|
||||
until = time.UnixMilli(n)
|
||||
}
|
||||
activity, err := reader.GetObserverActivity(r.Context(), observerID, window, interval, until)
|
||||
if err != nil {
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
respondError(w, http.StatusNotFound, "observer not found")
|
||||
|
||||
@@ -200,7 +200,7 @@ func TestGetObserverActivity_Defaults(t *testing.T) {
|
||||
var gotWindow, gotInterval time.Duration
|
||||
r := chi.NewRouter()
|
||||
r.Get("/observers/{observerId}/activity", getObserverActivity(stubReader{
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, window, interval time.Duration) (*api.ObserverActivity, error) {
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, window, interval time.Duration, _ time.Time) (*api.ObserverActivity, error) {
|
||||
gotWindow, gotInterval = window, interval
|
||||
return &api.ObserverActivity{Points: []api.ObserverActivityPoint{}}, nil
|
||||
},
|
||||
@@ -232,7 +232,7 @@ func TestGetObserverActivity_CustomRangeAndInterval(t *testing.T) {
|
||||
var gotWindow, gotInterval time.Duration
|
||||
r := chi.NewRouter()
|
||||
r.Get("/observers/{observerId}/activity", getObserverActivity(stubReader{
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, window, interval time.Duration) (*api.ObserverActivity, error) {
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, window, interval time.Duration, _ time.Time) (*api.ObserverActivity, error) {
|
||||
gotWindow, gotInterval = window, interval
|
||||
return &api.ObserverActivity{Points: []api.ObserverActivityPoint{}}, nil
|
||||
},
|
||||
@@ -264,7 +264,7 @@ func TestGetObserverActivity_SubHourRangeLimit(t *testing.T) {
|
||||
|
||||
r := chi.NewRouter()
|
||||
r.Get("/observers/{observerId}/activity", getObserverActivity(stubReader{
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, _, _ time.Duration) (*api.ObserverActivity, error) {
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, _, _ time.Duration, _ time.Time) (*api.ObserverActivity, error) {
|
||||
t.Fatal("reader should not be called")
|
||||
return nil, nil
|
||||
},
|
||||
@@ -286,7 +286,7 @@ func TestGetObserverActivity_SubHourRangeLimit(t *testing.T) {
|
||||
var gotWindow, gotInterval time.Duration
|
||||
ok := chi.NewRouter()
|
||||
ok.Get("/observers/{observerId}/activity", getObserverActivity(stubReader{
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, window, interval time.Duration) (*api.ObserverActivity, error) {
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, window, interval time.Duration, _ time.Time) (*api.ObserverActivity, error) {
|
||||
gotWindow, gotInterval = window, interval
|
||||
return &api.ObserverActivity{Points: []api.ObserverActivityPoint{}}, nil
|
||||
},
|
||||
@@ -309,6 +309,10 @@ func TestGetObserverActivity_BadRequests(t *testing.T) {
|
||||
query string
|
||||
}{
|
||||
{"invalid uuid", "not-a-uuid", ""},
|
||||
{"bad end", "00000000-0000-0000-0000-000000000001", "?until=banana"},
|
||||
{"negative end", "00000000-0000-0000-0000-000000000001", "?until=-1"},
|
||||
{"duplicate end", "00000000-0000-0000-0000-000000000001", "?until=1&until=2"},
|
||||
{"future end", "00000000-0000-0000-0000-000000000001", "?until=253402300799999"},
|
||||
{"unparseable range", "00000000-0000-0000-0000-000000000001", "?range=banana"},
|
||||
{"range too long", "00000000-0000-0000-0000-000000000001", "?range=721h"},
|
||||
{"zero range", "00000000-0000-0000-0000-000000000001", "?range=0"},
|
||||
@@ -319,7 +323,7 @@ func TestGetObserverActivity_BadRequests(t *testing.T) {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
r := chi.NewRouter()
|
||||
r.Get("/observers/{observerId}/activity", getObserverActivity(stubReader{
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, _, _ time.Duration) (*api.ObserverActivity, error) {
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, _, _ time.Duration, _ time.Time) (*api.ObserverActivity, error) {
|
||||
t.Fatal("reader should not be called")
|
||||
return nil, nil
|
||||
},
|
||||
@@ -338,7 +342,7 @@ func TestGetObserverActivity_NotFound(t *testing.T) {
|
||||
observerID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
r := chi.NewRouter()
|
||||
r.Get("/observers/{observerId}/activity", getObserverActivity(stubReader{
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, _, _ time.Duration) (*api.ObserverActivity, error) {
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, _, _ time.Duration, _ time.Time) (*api.ObserverActivity, error) {
|
||||
return nil, pgx.ErrNoRows
|
||||
},
|
||||
}))
|
||||
@@ -364,7 +368,7 @@ func TestGetObserverActivity_ReaderError(t *testing.T) {
|
||||
observerID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
r := chi.NewRouter()
|
||||
r.Get("/observers/{observerId}/activity", getObserverActivity(stubReader{
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, _, _ time.Duration) (*api.ObserverActivity, error) {
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, _, _ time.Duration, _ time.Time) (*api.ObserverActivity, error) {
|
||||
return nil, errors.New("boom")
|
||||
},
|
||||
}))
|
||||
@@ -381,7 +385,7 @@ func TestGetObserverActivity_NullsSerialise(t *testing.T) {
|
||||
observerID := uuid.MustParse("00000000-0000-0000-0000-000000000001")
|
||||
r := chi.NewRouter()
|
||||
r.Get("/observers/{observerId}/activity", getObserverActivity(stubReader{
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, _, _ time.Duration) (*api.ObserverActivity, error) {
|
||||
getObserverActivity: func(_ context.Context, _ uuid.UUID, _, _ time.Duration, _ time.Time) (*api.ObserverActivity, error) {
|
||||
return &api.ObserverActivity{
|
||||
PayloadTypes: []api.PayloadBreakdownItem{},
|
||||
Points: []api.ObserverActivityPoint{{T: 1757376000000, Observations: 3}},
|
||||
|
||||
@@ -32,7 +32,7 @@ type stubReader struct {
|
||||
getObserver func(ctx context.Context, observerID uuid.UUID) (*api.Observer, error)
|
||||
getObserverTelemetry func(ctx context.Context, observerID uuid.UUID, since, until time.Time, afterID int64) (*api.ObserverTelemetry, error)
|
||||
getObserverTelemetryBucketed func(ctx context.Context, observerID uuid.UUID, since, until time.Time, bucketHours int32) ([]api.ObserverTelemetryPoint, error)
|
||||
getObserverActivity func(ctx context.Context, observerID uuid.UUID, window, interval time.Duration) (*api.ObserverActivity, error)
|
||||
getObserverActivity func(ctx context.Context, observerID uuid.UUID, window, interval time.Duration, until time.Time) (*api.ObserverActivity, error)
|
||||
getObserverScopes func(ctx context.Context, observerID uuid.UUID) ([]string, error)
|
||||
listObserverAdverts func(ctx context.Context, observerID uuid.UUID, cursor int64, limit int32) (api.Page[api.AdvertObservation], error)
|
||||
listNodes func(ctx context.Context, nodeType int16, iatas []string, supportsMultibytePaths, supportsMultibyteTraces *bool, pubkey []byte, pubkeyPrefix, name, scope string, cursor int64, limit int32, includeNeighbors bool) (api.Page[api.NodeSummary], error)
|
||||
@@ -186,9 +186,9 @@ func (s stubReader) GetObserverTelemetryBucketed(ctx context.Context, observerID
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (s stubReader) GetObserverActivity(ctx context.Context, observerID uuid.UUID, window, interval time.Duration) (*api.ObserverActivity, error) {
|
||||
func (s stubReader) GetObserverActivity(ctx context.Context, observerID uuid.UUID, window, interval time.Duration, until time.Time) (*api.ObserverActivity, error) {
|
||||
if s.getObserverActivity != nil {
|
||||
return s.getObserverActivity(ctx, observerID, window, interval)
|
||||
return s.getObserverActivity(ctx, observerID, window, interval, until)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
@@ -44,8 +44,8 @@ type Observer struct {
|
||||
LastStatusAt *int64 `json:"lastStatusAt,omitempty"` // epoch ms
|
||||
FirstSeen int64 `json:"firstSeen"` // epoch ms
|
||||
LastSeen int64 `json:"lastSeen"` // epoch ms
|
||||
ObservationCount int64 `json:"observationCount"`
|
||||
Brokers []ObserverBroker `json:"brokers"` // broker names this observer has been seen on
|
||||
ObservationCount int64 `json:"observationCount"` // legacy cumulative presence counter; includes non-packet events
|
||||
Brokers []ObserverBroker `json:"brokers"` // broker names this observer has been seen on
|
||||
}
|
||||
|
||||
// ObserverTelemetryPoint is a single telemetry snapshot for an observer.
|
||||
@@ -89,7 +89,22 @@ type ObserverActivityPoint struct {
|
||||
}
|
||||
|
||||
// ObserverActivity is the per-observer heard-activity response.
|
||||
// ObserverActivitySummary describes stored packet records, never MQTT presence events.
|
||||
type ObserverActivitySummary struct {
|
||||
RecordedPackets int64 `json:"recordedPackets"`
|
||||
LastCompleteHour int64 `json:"lastCompleteHour"`
|
||||
LastCompleteHourStart int64 `json:"lastCompleteHourStart"`
|
||||
LastCompleteHourEnd int64 `json:"lastCompleteHourEnd"`
|
||||
LatestRecordedAt *int64 `json:"latestRecordedAt"`
|
||||
}
|
||||
|
||||
type ObserverActivity struct {
|
||||
WindowStart int64 `json:"windowStart"` // inclusive complete-bucket start, epoch ms
|
||||
WindowEnd int64 `json:"windowEnd"` // exclusive complete-bucket end, epoch ms
|
||||
GeneratedAt int64 `json:"generatedAt"` // response computation time, not proof of continuous coverage
|
||||
Source string `json:"source"` // raw or hourly; missing records do not prove an outage
|
||||
Summary *ObserverActivitySummary `json:"summary,omitempty"`
|
||||
|
||||
Range string `json:"range"`
|
||||
Interval string `json:"interval"`
|
||||
Radio *ObserverActivityRadio `json:"radio"`
|
||||
|
||||
@@ -100,7 +100,7 @@ type Reader interface {
|
||||
// GetObserverActivity returns bucketed heard-activity for an observer over the trailing window.
|
||||
// interval >= 1h is served from the hourly rollup. Returns pgx.ErrNoRows for an unknown observer.
|
||||
// Range and Interval on the result are left empty for the handler to fill.
|
||||
GetObserverActivity(ctx context.Context, observerID uuid.UUID, window, interval time.Duration) (*ObserverActivity, error)
|
||||
GetObserverActivity(ctx context.Context, observerID uuid.UUID, window, interval time.Duration, until time.Time) (*ObserverActivity, error)
|
||||
|
||||
// GetObserverScopes returns the names of all transport scopes an observer has
|
||||
// been seen forwarding packets for, ordered alphabetically.
|
||||
|
||||
Reference in new issue
Block a user