fleet-manager: chrony tracking metrics relative to the NTP source

The agent only reported chrony as synced|unsynced — nothing about how well the
clock tracks. Parse `chronyc tracking` for the source-relative metrics.

- proto: HealthSnapshot.chrony_offset_seconds (16, signed system-time offset,
  fast=+), chrony_source (17, reference name/IP), chrony_stratum (18); regenerated.
- agent: replace chronyState() with chronyTracking()/parseChrony() — one exec,
  parses Leap status + Reference ID + Stratum + System time offset (+ test).
- manager: Health.{ChronyOffset,ChronySource,ChronyStratum}; snapshotToHealth maps
  them; influx writes chrony_offset_seconds/_stratum/_source and whitelists the
  offset + stratum as graphable fields.
- UI: chronySummary() + offsetMs() — the chrony row now shows
  "synced · 192.168.50.10 · s3 · +0.01ms" instead of just "synced".
This commit is contained in:
Audiobuzz
2026-07-10 06:47:27 +00:00
parent df7dad81b7
commit 1c68c37824
11 changed files with 222 additions and 45 deletions
+33
View File
@@ -68,6 +68,39 @@ func TestParseUptime(t *testing.T) {
}
}
func TestParseChrony(t *testing.T) {
out := `Reference ID : C0A8320A (192.168.50.10)
Stratum : 3
Ref time (UTC) : Fri Jul 10 06:00:00 2026
System time : 0.000012345 seconds slow of NTP time
Last offset : +0.000001234 seconds
Frequency : 12.345 ppm slow
Leap status : Normal
`
c := parseChrony(out)
if c.state != "synced" {
t.Errorf("state = %q, want synced", c.state)
}
if c.source != "192.168.50.10" {
t.Errorf("source = %q, want 192.168.50.10", c.source)
}
if c.stratum != 3 {
t.Errorf("stratum = %d, want 3", c.stratum)
}
// "slow of NTP time" -> negative offset (sign-sensitive check).
if c.offset >= 0 || math.Abs(c.offset+0.000012345) > 1e-9 {
t.Errorf("offset = %v, want -0.000012345", c.offset)
}
// unsynced when Leap status isn't Normal.
if c := parseChrony("Leap status : Not synchronised\n"); c.state != "unsynced" {
t.Errorf("state = %q, want unsynced", c.state)
}
// empty / unavailable.
if c := parseChrony(""); c.state != "unsynced" || c.source != "" {
t.Errorf("empty parse = %+v", c)
}
}
func TestParseHandle(t *testing.T) {
for _, tc := range []struct {
in string