This commit is contained in:
henrygd
2026-04-22 17:42:11 -04:00
parent 6472af1ba4
commit 16e0f6c4a2
16 changed files with 421 additions and 417 deletions

View File

@@ -0,0 +1,58 @@
//go:build testing
package records_test
import (
"testing"
"github.com/henrygd/beszel/internal/records"
"github.com/henrygd/beszel/internal/tests"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestAverageProbeStats(t *testing.T) {
hub, err := tests.NewTestHub(t.TempDir())
require.NoError(t, err)
defer hub.Cleanup()
rm := records.NewRecordManager(hub)
user, err := tests.CreateUser(hub, "probe-avg@example.com", "testtesttest")
require.NoError(t, err)
system, err := tests.CreateRecord(hub, "systems", map[string]any{
"name": "probe-avg-system",
"host": "localhost",
"port": "45876",
"status": "up",
"users": []string{user.Id},
})
require.NoError(t, err)
recordA, err := tests.CreateRecord(hub, "network_probe_stats", map[string]any{
"system": system.Id,
"type": "1m",
"stats": `{"icmp:1.1.1.1":[10,80,8,14,1]}`,
})
require.NoError(t, err)
recordB, err := tests.CreateRecord(hub, "network_probe_stats", map[string]any{
"system": system.Id,
"type": "1m",
"stats": `{"icmp:1.1.1.1":[40,100,9,50,5]}`,
})
require.NoError(t, err)
result := rm.AverageProbeStats(hub.DB(), records.RecordIds{
{Id: recordA.Id},
{Id: recordB.Id},
})
stats, ok := result["icmp:1.1.1.1"]
require.True(t, ok)
require.Len(t, stats, 5)
assert.Equal(t, 25.0, stats[0])
assert.Equal(t, 90.0, stats[1])
assert.Equal(t, 8.0, stats[2])
assert.Equal(t, 50.0, stats[3])
assert.Equal(t, 3.0, stats[4])
}

View File

@@ -507,11 +507,11 @@ func AverageContainerStatsSlice(records [][]container.Stats) []container.Stats {
}
// AverageProbeStats averages probe stats across multiple records.
// For each probe key: avg of avgs, min of mins, max of maxes, avg of losses.
// For each probe key: avg of average fields, min of mins, and max of maxes.
func (rm *RecordManager) AverageProbeStats(db dbx.Builder, records RecordIds) map[string]probe.Result {
type probeValues struct {
sums probe.Result
count float64
sums probe.Result
counts []int
}
query := db.NewQuery("SELECT stats FROM network_probe_stats WHERE id = {:id}")
@@ -529,35 +529,52 @@ func (rm *RecordManager) AverageProbeStats(db dbx.Builder, records RecordIds) ma
for key, vals := range rawStats {
s, ok := sums[key]
if !ok {
s = &probeValues{sums: make(probe.Result, len(vals))}
s = &probeValues{sums: make(probe.Result, len(vals)), counts: make([]int, len(vals))}
sums[key] = s
}
if len(vals) > len(s.sums) {
expandedSums := make(probe.Result, len(vals))
copy(expandedSums, s.sums)
s.sums = expandedSums
expandedCounts := make([]int, len(vals))
copy(expandedCounts, s.counts)
s.counts = expandedCounts
}
for i := range vals {
switch i {
case 1: // min fields
if s.count == 0 || vals[i] < s.sums[i] {
case 2: // min fields
if s.counts[i] == 0 || vals[i] < s.sums[i] {
s.sums[i] = vals[i]
}
case 2: // max fields
if vals[i] > s.sums[i] {
case 3: // max fields
if s.counts[i] == 0 || vals[i] > s.sums[i] {
s.sums[i] = vals[i]
}
default: // average fields
s.sums[i] += vals[i]
}
s.counts[i]++
}
s.count++
}
}
// compute final averages
result := make(map[string]probe.Result, len(sums))
for key, s := range sums {
if s.count == 0 {
if len(s.counts) == 0 {
continue
}
s.sums[0] = twoDecimals(s.sums[0] / s.count) // avg latency
s.sums[3] = twoDecimals(s.sums[3] / s.count) // packet loss
for i := range s.sums {
switch i {
case 2, 3: // min and max fields should not be averaged
continue
default:
if s.counts[i] > 0 {
s.sums[i] = twoDecimals(s.sums[i] / float64(s.counts[i]))
}
}
}
result[key] = s.sums
}
return result