mirror of
https://github.com/henrygd/beszel.git
synced 2026-09-22 01:17:48 +02:00
feat: add network monitors (ICMP/TCP/HTTP/DNS) (#2266)
Co-authored-by: xiaomiku01 <xiaomiku01@outlook.com> Co-authored-by: henrygd <hank@henrygd.me>
This commit is contained in:
226
internal/records/monitor_averaging_test.go
Normal file
226
internal/records/monitor_averaging_test.go
Normal file
@@ -0,0 +1,226 @@
|
||||
//go:build testing
|
||||
|
||||
package records_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
monitorEntity "github.com/henrygd/beszel/internal/entities/monitor"
|
||||
"github.com/henrygd/beszel/internal/records"
|
||||
"github.com/henrygd/beszel/internal/tests"
|
||||
|
||||
"github.com/pocketbase/dbx"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAverageMonitorStats(t *testing.T) {
|
||||
hub, err := tests.NewTestHub(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
defer hub.Cleanup()
|
||||
|
||||
collection, err := hub.FindCachedCollectionByNameOrId("network_monitor_stats")
|
||||
require.NoError(t, err)
|
||||
assert.Nil(t, collection.Fields.GetByName("res_avg"))
|
||||
assert.Nil(t, collection.Fields.GetByName("loss"))
|
||||
|
||||
rm := records.NewRecordManager(hub)
|
||||
user, err := tests.CreateUser(hub, "monitor-avg@example.com", "testtesttest")
|
||||
require.NoError(t, err)
|
||||
sys, err := tests.CreateRecord(hub, "systems", map[string]any{
|
||||
"name": "monitor-avg-system",
|
||||
"host": "localhost",
|
||||
"port": "45876",
|
||||
"status": "up",
|
||||
"users": []string{user.Id},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
monitor, err := tests.CreateRecord(hub, "network_monitors", map[string]any{
|
||||
"system": sys.Id,
|
||||
"name": "cloudflare",
|
||||
"target": "1.1.1.1",
|
||||
"protocol": "icmp",
|
||||
"interval": 30,
|
||||
"enabled": true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
created := time.Now().UnixMilli()
|
||||
// Unequal probe counts must weight both latency and loss.
|
||||
recordA, err := tests.CreateRecord(hub, "network_monitor_stats", map[string]any{
|
||||
"system": sys.Id,
|
||||
"monitor": monitor.Id,
|
||||
"type": "1m",
|
||||
"created": created,
|
||||
"res_min": 5,
|
||||
"res_max": 20,
|
||||
"total_count": 6, "success_count": 6, "res_sum": 60,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
recordB, err := tests.CreateRecord(hub, "network_monitor_stats", map[string]any{
|
||||
"system": sys.Id,
|
||||
"monitor": monitor.Id,
|
||||
"type": "1m",
|
||||
"created": created,
|
||||
"res_min": 10,
|
||||
"res_max": 60,
|
||||
"total_count": 1, "success_count": 1, "res_sum": 22,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
result, count, err := rm.AverageMonitorStats(hub.DB(), monitor.Id, "1m", created-1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
assert.Equal(t, monitorEntity.Stats{ResAvg: 11.71, ResMin: 5, ResMax: 60, TotalCount: 7, SuccessCount: 7, ResponseSum: 82}, result)
|
||||
|
||||
for _, tc := range []struct {
|
||||
name, monitor, recordType string
|
||||
after int64
|
||||
}{
|
||||
{"other monitor", "missing", "1m", created - 1},
|
||||
{"other type", monitor.Id, "10m", created - 1},
|
||||
{"exclusive cutoff", monitor.Id, "1m", created},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
stats, count, err := rm.AverageMonitorStats(hub.DB(), tc.monitor, tc.recordType, tc.after)
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, count)
|
||||
assert.Equal(t, monitorEntity.Stats{}, stats)
|
||||
})
|
||||
}
|
||||
|
||||
// A failure-only bucket counts toward loss but must not lower latency.
|
||||
recordB.Set("res_min", 0)
|
||||
recordB.Set("res_max", 0)
|
||||
recordB.Set("success_count", 0)
|
||||
recordB.Set("res_sum", 0)
|
||||
require.NoError(t, hub.Save(recordB))
|
||||
result, count, err = rm.AverageMonitorStats(hub.DB(), monitor.Id, "1m", created-1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
assert.Equal(t, monitorEntity.Stats{ResAvg: 10, ResMin: 5, ResMax: 20, Loss: 14.29, TotalCount: 7, SuccessCount: 6, ResponseSum: 60}, result)
|
||||
// Sparse monitor records must propagate through every rollup level.
|
||||
rm.CreateLongerRecords()
|
||||
for _, recordType := range []string{"10m", "20m", "120m", "480m"} {
|
||||
rollups, err := hub.FindAllRecords("network_monitor_stats", dbx.HashExp{"monitor": monitor.Id, "type": recordType})
|
||||
require.NoError(t, err)
|
||||
require.Len(t, rollups, 1, recordType)
|
||||
assert.Equal(t, 5.0, rollups[0].GetFloat("res_min"))
|
||||
assert.Equal(t, 20.0, rollups[0].GetFloat("res_max"))
|
||||
assert.Equal(t, 7, rollups[0].GetInt("total_count"))
|
||||
assert.Equal(t, 6, rollups[0].GetInt("success_count"))
|
||||
assert.Equal(t, 60, rollups[0].GetInt("res_sum"))
|
||||
// A sibling with a different number of probes must retain its actual
|
||||
// weight when the next tier combines their underlying counts.
|
||||
_, err = tests.CreateRecord(hub, "network_monitor_stats", map[string]any{
|
||||
"system": sys.Id, "monitor": monitor.Id, "type": recordType, "created": created,
|
||||
"res_min": 100, "res_max": 100,
|
||||
"total_count": 3, "success_count": 1, "res_sum": 100,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
merged, count, err := rm.AverageMonitorStats(hub.DB(), monitor.Id, recordType, created-1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, 2, count)
|
||||
assert.Equal(t, monitorEntity.Stats{
|
||||
ResAvg: 22.86, ResMin: 5, ResMax: 100, Loss: 30,
|
||||
TotalCount: 10, SuccessCount: 7, ResponseSum: 160,
|
||||
}, merged)
|
||||
}
|
||||
// All failures produce zero latency, while a genuine zero-microsecond
|
||||
// success remains a valid minimum (it must not be filtered out as a sentinel).
|
||||
recordA.Set("success_count", 0)
|
||||
recordA.Set("res_sum", 0)
|
||||
require.NoError(t, hub.Save(recordA))
|
||||
result, _, err = rm.AverageMonitorStats(hub.DB(), monitor.Id, "1m", created-1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, monitorEntity.Stats{TotalCount: 7, Loss: 100}, result)
|
||||
recordA.Set("success_count", 1)
|
||||
recordA.Set("res_min", 0)
|
||||
recordA.Set("res_max", 0)
|
||||
require.NoError(t, hub.Save(recordA))
|
||||
result, _, err = rm.AverageMonitorStats(hub.DB(), monitor.Id, "1m", created-1)
|
||||
require.NoError(t, err)
|
||||
assert.Equal(t, monitorEntity.Stats{TotalCount: 7, SuccessCount: 1, Loss: 85.71}, result)
|
||||
}
|
||||
|
||||
func TestSparseMonitorRollups(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
interval int
|
||||
samples int
|
||||
disabled bool
|
||||
}{
|
||||
{"five minute interval", 300, 2, false},
|
||||
{"ten minute interval", 600, 1, false},
|
||||
{"fifteen minute interval", 900, 1, false},
|
||||
{"empty window", 900, 0, false},
|
||||
{"disabled with pending history", 300, 2, true},
|
||||
{"disabled empty window", 900, 0, true},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
hub, err := tests.NewTestHub(t.TempDir())
|
||||
require.NoError(t, err)
|
||||
defer hub.Cleanup()
|
||||
user, err := tests.CreateUser(hub, "sparse-monitor@example.com", "testtesttest")
|
||||
require.NoError(t, err)
|
||||
sys, err := tests.CreateRecord(hub, "systems", map[string]any{
|
||||
"name": "sparse-monitor-system", "host": "localhost", "port": "45876", "status": "up",
|
||||
"users": []string{user.Id},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
monitor, err := tests.CreateRecord(hub, "network_monitors", map[string]any{
|
||||
"system": sys.Id, "target": "1.1.1.1", "protocol": "icmp",
|
||||
"interval": tc.interval, "enabled": true,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
now := time.Now()
|
||||
for i := range tc.samples {
|
||||
_, err := tests.CreateRecord(hub, "network_monitor_stats", map[string]any{
|
||||
"system": sys.Id, "monitor": monitor.Id, "type": "1m",
|
||||
"created": now.Add(-time.Minute - time.Duration(i*tc.interval)*time.Second).UnixMilli(),
|
||||
"res_min": 8, "res_max": 20,
|
||||
"total_count": 4, "success_count": 3, "res_sum": 36,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
// Other collections must still reject fewer than nine minute records.
|
||||
for _, collection := range []string{"system_stats", "container_stats"} {
|
||||
stats := `{"cpu":10}`
|
||||
if collection == "container_stats" {
|
||||
stats = `[{"name":"test","cpu":10}]`
|
||||
}
|
||||
for range 8 {
|
||||
_, err := tests.CreateRecord(hub, collection, map[string]any{
|
||||
"system": sys.Id, "type": "1m", "stats": stats,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
if tc.disabled {
|
||||
monitor.Set("enabled", false)
|
||||
require.NoError(t, hub.Save(monitor))
|
||||
}
|
||||
records.NewRecordManager(hub).CreateLongerRecords()
|
||||
for _, recordType := range []string{"10m", "20m", "120m", "480m"} {
|
||||
rollups, err := hub.FindAllRecords("network_monitor_stats", dbx.HashExp{"monitor": monitor.Id, "type": recordType})
|
||||
require.NoError(t, err)
|
||||
if tc.samples == 0 {
|
||||
assert.Empty(t, rollups, recordType)
|
||||
} else {
|
||||
require.Len(t, rollups, 1, recordType)
|
||||
assert.Equal(t, 8.0, rollups[0].GetFloat("res_min"))
|
||||
assert.Equal(t, 20.0, rollups[0].GetFloat("res_max"))
|
||||
assert.Equal(t, 4*tc.samples, rollups[0].GetInt("total_count"))
|
||||
assert.Equal(t, 3*tc.samples, rollups[0].GetInt("success_count"))
|
||||
assert.Equal(t, 36*tc.samples, rollups[0].GetInt("res_sum"))
|
||||
}
|
||||
for _, collection := range []string{"system_stats", "container_stats"} {
|
||||
count, err := hub.CountRecords(collection, dbx.HashExp{"system": sys.Id, "type": recordType})
|
||||
require.NoError(t, err)
|
||||
assert.Zero(t, count, "%s %s", collection, recordType)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user