This commit is contained in:
henrygd
2026-04-29 15:49:43 -04:00
parent b89314889d
commit d2eb3b259a
11 changed files with 103 additions and 110 deletions

View File

@@ -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,
}
}

View File

@@ -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)
}

View File

@@ -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{

View File

@@ -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

View File

@@ -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
}

View File

@@ -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

View File

@@ -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({
<SheetHeader className="mb-0 border-b p-0 pb-4">
<SheetTitle>{probeLabel}</SheetTitle>
<SheetDescription className="flex flex-wrap items-center gap-x-2 gap-y-1">
<ServerIcon className="size-3.5 text-muted-foreground" />
<Link className="hover:underline" href={getPagePath($router, "system", { id: system?.id ?? "" })}>
{system?.name ?? ""}
</Link>
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
<ArrowLeftRightIcon className="size-3.5 text-muted-foreground" />
{probe.protocol.toUpperCase()}
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
<GlobeIcon className="size-3.5 text-muted-foreground" />
{probe.target}
{probe.port > 0 && (
<>
<Separator orientation="vertical" className="h-2.5 bg-muted-foreground opacity-70" />
<EthernetPortIcon className="size-3.5 text-muted-foreground" />
<span>{probe.port}</span>
</>
)}

View File

@@ -40,7 +40,7 @@ type NormalizedProbeValues = Omit<ProbeValues, "system" | "interval"> & {
type BulkProbeLineSource = Pick<NetworkProbeRecord, "target" | "protocol" | "port" | "interval" | "name">
const defaultInterval = 20
const defaultInterval = 30
const ProbeProtocolSchema = v.picklist(["icmp", "tcp", "http"])

View File

@@ -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 (
<ChartCard