mirror of
https://github.com/MeshCore-Beacon/beacon-server.git
synced 2026-10-16 11:39:53 +00:00
Stats endpoints read hourly rollup tables filled once per closed hour by one advisory-locked task, instead of materialized views that re-scanned every observation on each refresh. IATA-set tables give exact distinct counts for any IATA set or region. Cleanup holds raw rows for unrolled hours (up to 24h). Adds /stats/series; ingest keeps packet observation counts, observer last IATA, channel message counts and trace tag summaries. 001_baseline.sql is rewritten, so pre-release 2.0 databases must be recreated. See docs/historical-stats.md.
171 lines
8.5 KiB
Go
171 lines
8.5 KiB
Go
// Copyright 2026 Beacon Contributors
|
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
|
|
package api
|
|
|
|
import "github.com/google/uuid"
|
|
|
|
// ObserverComparison counts distinct flood packet hashes in [Since, Until).
|
|
// The three groups partition TotalPackets, the union heard by either observer.
|
|
// Counts describe retained reported receptions, not radio packet loss.
|
|
type ObserverComparison struct {
|
|
ObserverA uuid.UUID `json:"observerA"`
|
|
ObserverB uuid.UUID `json:"observerB"`
|
|
Since int64 `json:"since"` // inclusive, epoch milliseconds
|
|
Until int64 `json:"until"` // exclusive, epoch milliseconds
|
|
TotalPackets int64 `json:"totalPackets"`
|
|
OnlyA int64 `json:"onlyA"`
|
|
OnlyB int64 `json:"onlyB"`
|
|
Both int64 `json:"both"`
|
|
}
|
|
|
|
// RadioPreset represents a unique radio configuration observed in a given IATA,
|
|
// aggregated from both observer status messages and node adverts.
|
|
type RadioPreset struct {
|
|
Preset string `json:"preset"` // "freqMhz,bwKhz,sf" e.g. "910.525,62.5,7"
|
|
IATA string `json:"iata"`
|
|
SourceType string `json:"sourceType"` // "observer" or "node"
|
|
Count int64 `json:"count"` // number of observers or nodes on this preset in this IATA
|
|
}
|
|
|
|
// StatsOverview is the top-level network summary for the overview endpoint.
|
|
// Covers the last 24 hours that can have been rolled; hours still missing contribute nothing.
|
|
type StatsOverview struct {
|
|
TotalPackets int64 `json:"totalPackets"` // distinct per hour (see StatsSeriesValues)
|
|
TotalObservations int64 `json:"totalObservations"`
|
|
ActiveObservers int64 `json:"activeObservers"`
|
|
ActiveIATAs int64 `json:"activeIatas"`
|
|
WindowHours int `json:"windowHours"` // always 24 for now
|
|
Since int64 `json:"since"` // epoch ms, start of the window
|
|
Until int64 `json:"until"` // epoch ms, exclusive end of the window
|
|
}
|
|
|
|
// StatsSeries is hourly network activity read from the analytics rollups. Only complete
|
|
// hours carry values; the summary combines them (see docs/historical-stats.md).
|
|
type StatsSeries struct {
|
|
Since int64 `json:"since"` // effective start, epoch ms on a UTC hour
|
|
Until int64 `json:"until"` // effective exclusive end, epoch ms on a UTC hour
|
|
Revision int64 `json:"revision"` // changes whenever rolled hours, their status or coverage change
|
|
EarliestComplete *int64 `json:"earliestComplete"` // first complete hour held, epoch ms; null if none
|
|
CompleteHours int `json:"completeHours"` // hours contributing to summary
|
|
Hours []StatsSeriesHour `json:"hours"`
|
|
Summary StatsSeriesValues `json:"summary"`
|
|
}
|
|
|
|
// StatsSeriesHour is one UTC hour of a StatsSeries.
|
|
type StatsSeriesHour struct {
|
|
Hour int64 `json:"hour"` // epoch ms, start of the hour
|
|
Status string `json:"status" enums:"complete,partial,missing"` // partial: raw rows deleted before it was rolled
|
|
Values *StatsSeriesValues `json:"values"` // null unless complete
|
|
}
|
|
|
|
// StatsSeriesValues are the card metrics for an hour or a whole series. Packet counts are
|
|
// distinct per hour, so a packet heard across an hour boundary counts in both hours.
|
|
type StatsSeriesValues struct {
|
|
Observations int64 `json:"observations"`
|
|
UniquePackets int64 `json:"uniquePackets"`
|
|
ActiveObservers int64 `json:"activeObservers"` // distinct across the hour or window
|
|
ActiveIATAs int64 `json:"activeIatas"` // distinct across the hour or window
|
|
ScopedPackets int64 `json:"scopedPackets"`
|
|
ActiveScopes int64 `json:"activeScopes"` // distinct across the hour or window
|
|
MaxPathEntries int32 `json:"maxPathEntries"`
|
|
SNRSum float64 `json:"snrSum"` // average = snrSum / snrSamples
|
|
SNRSamples int64 `json:"snrSamples"`
|
|
RSSISum float64 `json:"rssiSum"` // average = rssiSum / rssiSamples
|
|
RSSISamples int64 `json:"rssiSamples"`
|
|
}
|
|
|
|
// ObservationPoint is a single time-bucketed observation count for charting.
|
|
// Distinct counts don't sum across IATAs, so use /stats/series for those.
|
|
type ObservationPoint struct {
|
|
Hour int64 `json:"hour"` // epoch ms, start of the 1-hour bucket
|
|
IATA string `json:"iata"`
|
|
ObservationCount int64 `json:"observationCount"`
|
|
}
|
|
|
|
// PayloadBreakdownItem is a single payload type with its observation count.
|
|
type PayloadBreakdownItem struct {
|
|
PayloadType int16 `json:"payloadType"`
|
|
PayloadTypeName string `json:"payloadTypeName"`
|
|
Count int64 `json:"count"`
|
|
}
|
|
|
|
// ScopeStats represents aggregate statistics for a single transport scope.
|
|
type ScopeStats struct {
|
|
Name string `json:"name"` // normalized scope name e.g. "#bc"
|
|
PacketCount int64 `json:"packetCount"` // distinct packets matched to this scope
|
|
ObserverCount int64 `json:"observerCount"` // distinct observers that forwarded packets in this scope
|
|
NodeCount int64 `json:"nodeCount"` // distinct nodes with this as their default scope
|
|
}
|
|
|
|
// TopNode is a node ranked by how often its adverts were heard in the window.
|
|
type TopNode struct {
|
|
NodeID *uuid.UUID `json:"nodeId"` // null once the node row has been deleted
|
|
PublicKey string `json:"publicKey"` // hex
|
|
NodeName *string `json:"nodeName,omitempty"`
|
|
NodeType int16 `json:"nodeType"`
|
|
NodeTypeName string `json:"nodeTypeName"`
|
|
IATA string `json:"iata"`
|
|
ObservationCount int64 `json:"observationCount"`
|
|
LastHeard int64 `json:"lastHeard"` // epoch ms
|
|
}
|
|
|
|
// TopObserver is an observer ranked by observation count.
|
|
type TopObserver struct {
|
|
ObserverID uuid.UUID `json:"observerId"`
|
|
DisplayName *string `json:"displayName,omitempty"`
|
|
ObserverType *string `json:"observerType,omitempty"`
|
|
IATA string `json:"iata"`
|
|
ObservationCount int64 `json:"observationCount"`
|
|
}
|
|
|
|
// TopAdvertiser is a node ranked by distinct ADVERT packet count within the requested
|
|
// window. Count is per-advert, not per-hearing -- see GetStatsTopAdvertisers.
|
|
type TopAdvertiser struct {
|
|
NodeID *uuid.UUID `json:"nodeId"` // null once the node row has been deleted
|
|
PublicKey string `json:"publicKey"` // hex
|
|
NodeName *string `json:"nodeName,omitempty"`
|
|
NodeType int16 `json:"nodeType"`
|
|
NodeTypeName string `json:"nodeTypeName"`
|
|
IATA string `json:"iata"`
|
|
AdvertCount int64 `json:"advertCount"`
|
|
// FloodAdvertCount/DirectAdvertCount split AdvertCount by how the advert was routed:
|
|
// flood = route type 0 (transport_flood) or 1 (flood), broadcast with no known path;
|
|
// direct = route type 2 (direct) or 3 (transport_direct), routed along a known path.
|
|
// FloodAdvertCount + DirectAdvertCount == AdvertCount.
|
|
FloodAdvertCount int64 `json:"floodAdvertCount"`
|
|
DirectAdvertCount int64 `json:"directAdvertCount"`
|
|
LastHeard int64 `json:"lastHeard"` // epoch ms
|
|
}
|
|
|
|
// TopTalker is a companion name ranked by decrypted channel message count within the
|
|
// requested window. Grouped by sender name as decrypted from the message itself, not by
|
|
// node identity -- see GetStatsTopTalkers.
|
|
type TopTalker struct {
|
|
SenderName string `json:"senderName"`
|
|
MessageCount int64 `json:"messageCount"`
|
|
LastSent int64 `json:"lastSent"` // epoch ms
|
|
}
|
|
|
|
// NodeTypeCount shows the count of nodes of a given type with the type name
|
|
type NodeTypeCount struct {
|
|
NodeType int16 `json:"nodeType"`
|
|
NodeTypeName string `json:"nodeTypeName"`
|
|
Count int64 `json:"count"`
|
|
}
|
|
|
|
// ClockDriftEntry is a repeater or room server whose most recent advert-derived clock drift
|
|
// exceeds the configured threshold (nodes.clock_drift_threshold, default 5m) -- see
|
|
// GetStatsClockDrift. Ordered worst-drift-first. ClockDriftSeconds/ClockCheckedAt mirror the
|
|
// same-named fields on Node; unlike Node this list only ever contains out-of-sync nodes, so
|
|
// there's no ClockOutOfSync bool here -- being in the list already means true.
|
|
type ClockDriftEntry struct {
|
|
NodeID uuid.UUID `json:"nodeId"`
|
|
NodeName *string `json:"nodeName,omitempty"`
|
|
NodeType int16 `json:"nodeType"`
|
|
NodeTypeName string `json:"nodeTypeName"`
|
|
ClockDriftSeconds int `json:"clockDriftSeconds"` // signed; +ve = device ahead of server
|
|
ClockCheckedAt int64 `json:"clockCheckedAt"` // epoch ms
|
|
IATAs []NodeIATA `json:"iatas,omitempty"` // IATAs this node has been heard in
|
|
}
|