Skip to content

Commit 22e6d7e

Browse files
committed
Record real ICMP RTT as ping latency, not wall-clock
The ping check spaces probes 200ms apart, so with count > 1 the wall-clock duration of Check() is dominated by the inter-probe interval, not the round-trip time. Add an optional LatencyReporter interface consulted by runOnce; the ping check reports AvgRtt from the run, falling back to wall-clock on failure. Other check types are unaffected. Fixes #1
1 parent f2a428e commit 22e6d7e

4 files changed

Lines changed: 61 additions & 1 deletion

File tree

internal/check/check.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,14 @@ type Checker interface {
2424
Check(ctx context.Context) (ok bool, message string)
2525
}
2626

27+
// LatencyReporter is optionally implemented by checkers that measure a truer
28+
// latency than the wall-clock duration of Check (e.g. ping's ICMP RTT, which
29+
// excludes the inter-probe interval). A non-positive value means no
30+
// measurement is available and the wall-clock duration is used instead.
31+
type LatencyReporter interface {
32+
Latency() time.Duration
33+
}
34+
2735
// New builds a Checker from a validated monitor config.
2836
func New(m config.Monitor) (Checker, error) {
2937
var (
@@ -87,11 +95,17 @@ func runOnce(ctx context.Context, m config.Monitor, c Checker) Result {
8795
defer cancel()
8896
start := time.Now()
8997
ok, msg := c.Check(cctx)
98+
latency := time.Since(start)
99+
if lr, isLR := c.(LatencyReporter); isLR {
100+
if l := lr.Latency(); l > 0 {
101+
latency = l
102+
}
103+
}
90104
return Result{
91105
Monitor: m.Name,
92106
Time: start,
93107
OK: ok,
94-
Latency: time.Since(start),
108+
Latency: latency,
95109
Message: msg,
96110
}
97111
}

internal/check/check_test.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package check
2+
3+
import (
4+
"context"
5+
"testing"
6+
"time"
7+
8+
"gjallar/internal/config"
9+
)
10+
11+
type fakeReporter struct {
12+
rtt time.Duration
13+
}
14+
15+
func (f *fakeReporter) Check(ctx context.Context) (bool, string) { return true, "" }
16+
func (f *fakeReporter) Latency() time.Duration { return f.rtt }
17+
18+
func TestRunOnceLatencyReporter(t *testing.T) {
19+
m := config.Monitor{Name: "t", Timeout: config.Duration(time.Second)}
20+
21+
r := runOnce(context.Background(), m, &fakeReporter{rtt: 300 * time.Microsecond})
22+
if r.Latency != 300*time.Microsecond {
23+
t.Errorf("Latency = %v, want reported 300µs", r.Latency)
24+
}
25+
26+
// A non-positive report falls back to the wall-clock duration.
27+
r = runOnce(context.Background(), m, &fakeReporter{rtt: 0})
28+
if r.Latency <= 0 {
29+
t.Errorf("Latency = %v, want wall-clock fallback > 0", r.Latency)
30+
}
31+
}

internal/check/ping.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ type pingCheck struct {
1515
count int
1616
privileged bool
1717
timeout time.Duration
18+
lastRTT time.Duration
1819
}
1920

2021
func newPingCheck(m config.Monitor) (*pingCheck, error) {
@@ -27,6 +28,7 @@ func newPingCheck(m config.Monitor) (*pingCheck, error) {
2728
}
2829

2930
func (c *pingCheck) Check(ctx context.Context) (bool, string) {
31+
c.lastRTT = 0
3032
pinger, err := probing.NewPinger(c.host)
3133
if err != nil {
3234
return false, err.Error()
@@ -43,12 +45,18 @@ func (c *pingCheck) Check(ctx context.Context) (bool, string) {
4345
if stats.PacketsRecv == 0 {
4446
return false, fmt.Sprintf("no reply from %s (%d packets sent)", c.host, stats.PacketsSent)
4547
}
48+
c.lastRTT = stats.AvgRtt
4649
if stats.PacketLoss > 0 {
4750
return true, fmt.Sprintf("%.0f%% loss, avg %s", stats.PacketLoss, stats.AvgRtt.Round(time.Millisecond))
4851
}
4952
return true, ""
5053
}
5154

55+
// Latency reports the measured ICMP round-trip time instead of the wall-clock
56+
// duration of Check, which includes the 200ms inter-probe interval. Safe
57+
// without locking: each monitor's Checker runs sequentially in one goroutine.
58+
func (c *pingCheck) Latency() time.Duration { return c.lastRTT }
59+
5260
// SelfTestPing pings 127.0.0.1 once so a missing capability or sysctl fails
5361
// at startup with an actionable message instead of every check failing forever.
5462
func SelfTestPing(privileged bool) error {

internal/check/ping_test.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ func TestPingLocalhost(t *testing.T) {
2222
if ok, msg := c.Check(context.Background()); !ok {
2323
t.Errorf("ping 127.0.0.1 failed: %s", msg)
2424
}
25+
// Latency must be the ICMP RTT, not the ~200ms inter-probe interval.
26+
if rtt := c.Latency(); rtt <= 0 || rtt >= 200*time.Millisecond {
27+
t.Errorf("Latency() = %v, want a real localhost RTT", rtt)
28+
}
2529
}
2630

2731
func TestPingUnreachable(t *testing.T) {
@@ -39,4 +43,7 @@ func TestPingUnreachable(t *testing.T) {
3943
if ok, msg := c.Check(context.Background()); ok {
4044
t.Errorf("expected failure, got ok (%s)", msg)
4145
}
46+
if rtt := c.Latency(); rtt != 0 {
47+
t.Errorf("Latency() = %v after failed check, want 0", rtt)
48+
}
4249
}

0 commit comments

Comments
 (0)