From d2eb3b259afdc8ccd001d24670e985b33beab944 Mon Sep 17 00:00:00 2001 From: henrygd Date: Wed, 29 Apr 2026 15:49:43 -0400 Subject: [PATCH] updates --- agent/probe.go | 49 +++++--------- agent/probe_test.go | 66 ++++++++++--------- internal/entities/probe/probe.go | 25 +++---- internal/hub/probes.go | 14 ++-- internal/hub/systems/system.go | 20 +++--- internal/hub/systems/system_probes.go | 2 +- internal/records/probe_averaging_test.go | 15 ++--- internal/records/records.go | 12 ++-- .../network-probes-table.tsx | 6 +- .../network-probes-table/probe-dialog.tsx | 2 +- .../routes/system/charts/probes-charts.tsx | 2 +- 11 files changed, 103 insertions(+), 110 deletions(-) diff --git a/agent/probe.go b/agent/probe.go index f4e40dfd..61312705 100644 --- a/agent/probe.go +++ b/agent/probe.go @@ -147,27 +147,27 @@ func (agg probeAggregate) hasData() bool { return agg.totalCount > 0 } -// result converts the aggregate into the probe result slice format. +// result converts the aggregate into the probe result format. func (agg probeAggregate) result() probe.Result { avg := agg.avgResponse() - minUs := 0.0 - if agg.successCount > 0 { - minUs = float64(agg.minUs) + result := probe.Result{ + AvgResponse: avg, + MinResponse: agg.minUs, + MaxResponse: agg.maxUs, + PacketLoss: agg.lossPercentage(), } - return probe.Result{ - avg, - minUs, - float64(agg.maxUs), - agg.lossPercentage(), + if agg.successCount == 0 { + result.MinResponse, result.MaxResponse = 0, 0 } + return result } // avgResponse returns the rounded average of successful samples. -func (agg probeAggregate) avgResponse() float64 { +func (agg probeAggregate) avgResponse() int64 { if agg.successCount == 0 { return 0 } - return float64(agg.sumUs / agg.successCount) + return agg.sumUs / agg.successCount } @@ -398,33 +398,20 @@ func (task *probeTask) resultLocked(duration time.Duration, now time.Time) (prob agg := task.aggregateLocked(duration, now) hourAgg := task.aggregateLocked(time.Hour, now) if !agg.hasData() { - return nil, false + return probe.Result{}, false } result := agg.result() - res := result[0] - res1h := hourAgg.avgResponse() - resMin := result[1] - resMin1h := float64(hourAgg.minUs) - resMax := result[2] - resMax1h := float64(hourAgg.maxUs) - loss := result[3] - loss1h := hourAgg.lossPercentage() + result.AvgResponse1h = hourAgg.avgResponse() + result.MinResponse1h = hourAgg.minUs + result.MaxResponse1h = hourAgg.maxUs + result.PacketLoss1h = hourAgg.lossPercentage() if hourAgg.successCount == 0 { - resMin1h, resMax1h = 0, 0 + result.MinResponse1h, result.MaxResponse1h = 0, 0 } - return probe.Result{ - res, - res1h, - resMin, - resMin1h, - resMax, - resMax1h, - loss, - loss1h, - }, true + return result, true } // aggregateSamplesSince aggregates raw samples newer than the cutoff. diff --git a/agent/probe_test.go b/agent/probe_test.go index 0aface32..4c182cc4 100644 --- a/agent/probe_test.go +++ b/agent/probe_test.go @@ -24,10 +24,11 @@ func TestProbeTaskAggregateLockedUsesRawSamplesForShortWindows(t *testing.T) { require.True(t, agg.hasData()) assert.Equal(t, int64(2), agg.totalCount) assert.Equal(t, int64(1), agg.successCount) - assert.Equal(t, 20.0, agg.result()[0]) - assert.Equal(t, 20.0, agg.result()[1]) - assert.Equal(t, 20.0, agg.result()[2]) - assert.Equal(t, 50.0, agg.result()[3]) + result := agg.result() + assert.Equal(t, int64(20), result.AvgResponse) + assert.Equal(t, int64(20), result.MinResponse) + assert.Equal(t, int64(20), result.MaxResponse) + assert.Equal(t, 50.0, result.PacketLoss) } func TestProbeTaskAggregateLockedUsesMinuteBucketsForLongWindows(t *testing.T) { @@ -44,10 +45,11 @@ func TestProbeTaskAggregateLockedUsesMinuteBucketsForLongWindows(t *testing.T) { require.True(t, agg.hasData()) assert.Equal(t, int64(4), agg.totalCount) assert.Equal(t, int64(3), agg.successCount) - assert.Equal(t, 30.0, agg.result()[0]) - assert.Equal(t, 20.0, agg.result()[1]) - assert.Equal(t, 40.0, agg.result()[2]) - assert.Equal(t, 25.0, agg.result()[3]) + result := agg.result() + assert.Equal(t, int64(30), result.AvgResponse) + assert.Equal(t, int64(20), result.MinResponse) + assert.Equal(t, int64(40), result.MaxResponse) + assert.Equal(t, 25.0, result.PacketLoss) } func TestProbeTaskAddSampleLockedTrimsRawSamplesButKeepsBucketHistory(t *testing.T) { @@ -64,10 +66,11 @@ func TestProbeTaskAddSampleLockedTrimsRawSamplesButKeepsBucketHistory(t *testing require.True(t, agg.hasData()) assert.Equal(t, int64(2), agg.totalCount) assert.Equal(t, int64(2), agg.successCount) - assert.Equal(t, 15.0, agg.result()[0]) - assert.Equal(t, 10.0, agg.result()[1]) - assert.Equal(t, 20.0, agg.result()[2]) - assert.Equal(t, 0.0, agg.result()[3]) + result := agg.result() + assert.Equal(t, int64(15), result.AvgResponse) + assert.Equal(t, int64(10), result.MinResponse) + assert.Equal(t, int64(20), result.MaxResponse) + assert.Equal(t, 0.0, result.PacketLoss) } func TestProbeManagerGetResultsIncludesHourResponseRange(t *testing.T) { @@ -84,13 +87,14 @@ func TestProbeManagerGetResultsIncludesHourResponseRange(t *testing.T) { results := pm.GetResults(uint16(time.Minute / time.Millisecond)) result, ok := results["probe-1"] require.True(t, ok) - require.Len(t, result, 6) - assert.Equal(t, 30.0, result[0]) - assert.Equal(t, 25.0, result[1]) - assert.Equal(t, 10.0, result[2]) - assert.Equal(t, 40.0, result[3]) - assert.Equal(t, 50.0, result[4]) - assert.Equal(t, 20.0, result[5]) + assert.Equal(t, int64(30), result.AvgResponse) + assert.Equal(t, int64(25), result.AvgResponse1h) + assert.Equal(t, int64(30), result.MinResponse) + assert.Equal(t, int64(10), result.MinResponse1h) + assert.Equal(t, int64(30), result.MaxResponse) + assert.Equal(t, int64(40), result.MaxResponse1h) + assert.Equal(t, 50.0, result.PacketLoss) + assert.Equal(t, 20.0, result.PacketLoss1h) } func TestProbeManagerGetResultsIncludesLossOnlyHourData(t *testing.T) { @@ -104,13 +108,14 @@ func TestProbeManagerGetResultsIncludesLossOnlyHourData(t *testing.T) { results := pm.GetResults(uint16(time.Minute / time.Millisecond)) result, ok := results["probe-1"] require.True(t, ok) - require.Len(t, result, 6) - assert.Equal(t, 0.0, result[0]) - assert.Equal(t, 0.0, result[1]) - assert.Equal(t, 0.0, result[2]) - assert.Equal(t, 0.0, result[3]) - assert.Equal(t, 100.0, result[4]) - assert.Equal(t, 100.0, result[5]) + assert.Equal(t, int64(0), result.AvgResponse) + assert.Equal(t, int64(0), result.AvgResponse1h) + assert.Equal(t, int64(0), result.MinResponse) + assert.Equal(t, int64(0), result.MinResponse1h) + assert.Equal(t, int64(0), result.MaxResponse) + assert.Equal(t, int64(0), result.MaxResponse1h) + assert.Equal(t, 100.0, result.PacketLoss) + assert.Equal(t, 100.0, result.PacketLoss1h) } func TestProbeConfigResultKeyUsesSyncedID(t *testing.T) { @@ -207,10 +212,9 @@ func TestProbeManagerApplySyncUpsertRunsImmediatelyAndReturnsResult(t *testing.T defer pm.Stop() require.NoError(t, err) - require.Len(t, resp.Result, 6) - assert.GreaterOrEqual(t, resp.Result[0], 0.0) - assert.Equal(t, 0.0, resp.Result[4]) - assert.Equal(t, 0.0, resp.Result[5]) + assert.GreaterOrEqual(t, resp.Result.AvgResponse, int64(0)) + assert.Equal(t, 0.0, resp.Result.PacketLoss) + assert.Equal(t, 0.0, resp.Result.PacketLoss1h) task := pm.probes["probe-1"] require.NotNil(t, task) @@ -252,7 +256,7 @@ func TestProbeManagerUpsertProbeKeepsHistoryWhenOnlyIntervalChanges(t *testing.T require.True(t, agg.hasData()) assert.Equal(t, int64(2), agg.totalCount) assert.Equal(t, int64(2), agg.successCount) - assert.Equal(t, 18.0, agg.avgResponse()) + assert.Equal(t, int64(18), agg.avgResponse()) select { case <-existingTask.cancel: diff --git a/internal/entities/probe/probe.go b/internal/entities/probe/probe.go index 1b2f5501..2392b57e 100644 --- a/internal/entities/probe/probe.go +++ b/internal/entities/probe/probe.go @@ -51,14 +51,15 @@ type SyncResponse struct { // 6: packet loss percentage (0-100) // // 7: 1h packet loss percentage (0-100) -type Result []float64 - -// Get returns the value at the specified index or 0 if the index is out of range. -func (r Result) Get(index int) float64 { - if index < len(r) { - return r[index] - } - return 0 +type Result struct { + AvgResponse int64 `cbor:"0,keyasint,omitempty"` + AvgResponse1h int64 `cbor:"1,keyasint,omitempty"` + MinResponse int64 `cbor:"2,keyasint,omitempty"` + MinResponse1h int64 `cbor:"3,keyasint,omitempty"` + MaxResponse int64 `cbor:"4,keyasint,omitempty"` + MaxResponse1h int64 `cbor:"5,keyasint,omitempty"` + PacketLoss float64 `cbor:"6,keyasint,omitempty"` + PacketLoss1h float64 `cbor:"7,keyasint,omitempty"` } // Stats holds only 1m values for a single target, which are used for charts. @@ -74,9 +75,9 @@ type Stats []float64 func (s Stats) FromResult(result Result) Stats { return Stats{ - result.Get(0), // avg response - result.Get(2), // min response - result.Get(4), // max response - result.Get(6), // packet loss + float64(result.AvgResponse), + float64(result.MinResponse), + float64(result.MaxResponse), + result.PacketLoss, } } diff --git a/internal/hub/probes.go b/internal/hub/probes.go index 85a96107..483a1550 100644 --- a/internal/hub/probes.go +++ b/internal/hub/probes.go @@ -96,14 +96,12 @@ func probeConfigFromRecord(record *core.Record) *probe.Config { // setProbeResultFields stores the latest probe result values on the record. func setProbeResultFields(record *core.Record, result probe.Result) { - now := time.Now().UTC() - nowString := now.Format(types.DefaultDateLayout) - record.Set("res", result.Get(0)) - record.Set("resAvg1h", result.Get(1)) - record.Set("resMin1h", result.Get(3)) - record.Set("resMax1h", result.Get(5)) - // record.Set("loss", result.Get(4)) - record.Set("loss1h", result.Get(7)) + nowString := time.Now().UTC().Format(types.DefaultDateLayout) + record.Set("res", result.AvgResponse) + record.Set("resAvg1h", result.AvgResponse1h) + record.Set("resMin1h", result.MinResponse1h) + record.Set("resMax1h", result.MaxResponse1h) + record.Set("loss1h", result.PacketLoss1h) record.Set("updated", nowString) } diff --git a/internal/hub/systems/system.go b/internal/hub/systems/system.go index 5c0494b3..ddbd9bff 100644 --- a/internal/hub/systems/system.go +++ b/internal/hub/systems/system.go @@ -320,7 +320,7 @@ func updateNetworkProbesRecords(app core.App, probeResults map[string]probe.Resu return nil } var err error - probeCollectionName := "network_probes" + const probeCollectionName = "network_probes" // If realtime updates are active, we save via PocketBase records to trigger realtime events. // Otherwise we can do a more efficient direct update via SQL @@ -345,14 +345,14 @@ func updateNetworkProbesRecords(app core.App, probeResults map[string]probe.Resu } // update network_probes records - for id, values := range probeResults { + for id, result := range probeResults { probeData := map[string]any{ "id": id, - "res": values.Get(0), - "resAvg1h": values.Get(1), - "resMin1h": values.Get(3), - "resMax1h": values.Get(5), - "loss1h": values.Get(7), + "res": result.AvgResponse, + "resAvg1h": result.AvgResponse1h, + "resMin1h": result.MinResponse1h, + "resMax1h": result.MaxResponse1h, + "loss1h": result.PacketLoss1h, "updated": nowString, } switch realtimeActive { @@ -372,12 +372,12 @@ func updateNetworkProbesRecords(app core.App, probeResults map[string]probe.Resu } // handle stats collection as well - statsCollectionName := "network_probe_stats" + const statsCollectionName = "network_probe_stats" // we don't need the hour values for the stats collection stats := make(map[string]probe.Stats, len(probeResults)) - for key, values := range probeResults { - stats[key] = probe.Stats{}.FromResult(values) + for key, result := range probeResults { + stats[key] = probe.Stats{}.FromResult(result) } statsRecordData := map[string]any{ diff --git a/internal/hub/systems/system_probes.go b/internal/hub/systems/system_probes.go index 83575ec4..1cfcd5de 100644 --- a/internal/hub/systems/system_probes.go +++ b/internal/hub/systems/system_probes.go @@ -24,7 +24,7 @@ func (sys *System) UpsertNetworkProbe(config probe.Config, runNow bool) (*probe. if err != nil { return nil, err } - if len(resp.Result) == 0 { + if resp.Result == (probe.Result{}) { return nil, nil } result := resp.Result diff --git a/internal/records/probe_averaging_test.go b/internal/records/probe_averaging_test.go index b711b239..d103df17 100644 --- a/internal/records/probe_averaging_test.go +++ b/internal/records/probe_averaging_test.go @@ -32,13 +32,13 @@ func TestAverageProbeStats(t *testing.T) { 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]}`, + "stats": `{"icmp:1.1.1.1":[10,5,20,1.5]}`, }) 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]}`, + "stats": `{"icmp:1.1.1.1":[22.5,10,60,0]}`, }) require.NoError(t, err) @@ -49,10 +49,9 @@ func TestAverageProbeStats(t *testing.T) { 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]) + require.Len(t, stats, 4) + assert.InDelta(t, 16.25, stats[0], 0.001) // avg of avg + assert.InDelta(t, 5, stats[1], 0.001) // min of mins + assert.InDelta(t, 60, stats[2], 0.001) // max of maxes + assert.InDelta(t, 0.75, stats[3], 0.001) // avg of packet loss } diff --git a/internal/records/records.go b/internal/records/records.go index 9f6d3c9f..8707a80a 100644 --- a/internal/records/records.go +++ b/internal/records/records.go @@ -532,9 +532,9 @@ func AverageContainerStatsSlice(records [][]container.Stats) []container.Stats { // AverageProbeStats averages probe stats across multiple records. // 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 { +func (rm *RecordManager) AverageProbeStats(db dbx.Builder, records RecordIds) map[string]probe.Stats { type probeValues struct { - sums probe.Result + sums probe.Stats counts []int } @@ -546,18 +546,18 @@ func (rm *RecordManager) AverageProbeStats(db dbx.Builder, records RecordIds) ma for _, rec := range records { row.Stats = row.Stats[:0] query.Bind(dbx.Params{"id": rec.Id}).One(&row) - var rawStats map[string]probe.Result + var rawStats map[string]probe.Stats if err := json.Unmarshal(row.Stats, &rawStats); err != nil { continue } for key, vals := range rawStats { s, ok := sums[key] if !ok { - s = &probeValues{sums: make(probe.Result, len(vals)), counts: make([]int, len(vals))} + s = &probeValues{sums: make(probe.Stats, len(vals)), counts: make([]int, len(vals))} sums[key] = s } if len(vals) > len(s.sums) { - expandedSums := make(probe.Result, len(vals)) + expandedSums := make(probe.Stats, len(vals)) copy(expandedSums, s.sums) s.sums = expandedSums @@ -584,7 +584,7 @@ func (rm *RecordManager) AverageProbeStats(db dbx.Builder, records RecordIds) ma } // compute final averages - result := make(map[string]probe.Result, len(sums)) + result := make(map[string]probe.Stats, len(sums)) for key, s := range sums { if len(s.counts) == 0 { continue diff --git a/internal/site/src/components/network-probes-table/network-probes-table.tsx b/internal/site/src/components/network-probes-table/network-probes-table.tsx index f09b9529..629997b5 100644 --- a/internal/site/src/components/network-probes-table/network-probes-table.tsx +++ b/internal/site/src/components/network-probes-table/network-probes-table.tsx @@ -37,7 +37,7 @@ import { $allSystemsById, $chartTime, $direction } from "@/lib/stores" import { cn, isVisuallyLonger, useBrowserStorage } from "@/lib/utils" import type { NetworkProbeRecord } from "@/types" import { AddProbeDialog, EditProbeDialog } from "./probe-dialog" -import { XIcon } from "lucide-react" +import { ArrowLeftRightIcon, EthernetPortIcon, GlobeIcon, ServerIcon, XIcon } from "lucide-react" import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "@/components/ui/sheet" import ChartTimeSelect from "@/components/charts/chart-time-select" import { LossChart, AvgMinMaxResponseChart } from "@/components/routes/system/charts/probes-charts" @@ -501,16 +501,20 @@ function NetworkProbeSheetContent({ {probeLabel} + {system?.name ?? ""} + {probe.protocol.toUpperCase()} + {probe.target} {probe.port > 0 && ( <> + {probe.port} )} diff --git a/internal/site/src/components/network-probes-table/probe-dialog.tsx b/internal/site/src/components/network-probes-table/probe-dialog.tsx index c8df42ce..0ed2c2d6 100644 --- a/internal/site/src/components/network-probes-table/probe-dialog.tsx +++ b/internal/site/src/components/network-probes-table/probe-dialog.tsx @@ -40,7 +40,7 @@ type NormalizedProbeValues = Omit & { type BulkProbeLineSource = Pick -const defaultInterval = 20 +const defaultInterval = 30 const ProbeProtocolSchema = v.picklist(["icmp", "tcp", "http"]) diff --git a/internal/site/src/components/routes/system/charts/probes-charts.tsx b/internal/site/src/components/routes/system/charts/probes-charts.tsx index 32a490f5..7848abde 100644 --- a/internal/site/src/components/routes/system/charts/probes-charts.tsx +++ b/internal/site/src/components/routes/system/charts/probes-charts.tsx @@ -81,7 +81,7 @@ function ProbeChart({ return probeStats.filter((record) => visibleKeys.some((id) => record.stats?.[id] != null)) }, [probeStats, visibleKeys]) - const legend = dataPoints.length < 10 && dataPoints.length > 1 + const legend = dataPoints.length < 10 && showFilter return (