Skip to content

Commit 22427df

Browse files
authored
refactor(diagnostics): emit system.ntp_synchronized + top-level pi_throttle (#102)
* refactor(diagnostics): emit system.ntp_synchronized + top-level pi_throttle Splits the misnamed pi_health wire block. NTP-sync is a universal host signal and moves into system.ntp_synchronized; the eight vcgencmd throttle bits become a flat top-level pi_throttle block, absent on non-Pi feeders. collect_pi_ntp_sync renames to collect_ntp_sync. * test(diagnostics): pin pi_throttle key-set and cover ntp_synchronized=false Stub vcgencmd alongside timedatectl in the no-probes test so a Pi-based dev box can't leak vcgencmd output through the stub gap. Add an exact-key-set assertion on pi_throttle (the server sanitizer rejects partial blocks). Add a 'no' NTP case so 'false' is pinned through the prune pass. Tone down the collect_ntp_sync comment to match runtime behaviour in containers / stripped images.
1 parent d22c448 commit 22427df

2 files changed

Lines changed: 82 additions & 50 deletions

File tree

scripts/airplanes-diagnostics.sh

Lines changed: 29 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -288,8 +288,8 @@ collect_network() {
288288
# bit 1 freq_capped_now bit 17 freq_capped_ever
289289
# bit 2 throttled_now bit 18 throttled_ever
290290
# bit 3 soft_temp_limit_now bit 19 soft_temp_limit_ever
291-
# Sets pi_health_* globals. Returns 0 on success, 1 if vcgencmd absent or
292-
# its output unparseable.
291+
# Sets the eight pi_*_now / pi_*_ever globals. Returns 0 on success, 1
292+
# if vcgencmd is absent or its output is unparseable.
293293
collect_pi_throttle() {
294294
command -v vcgencmd >/dev/null 2>&1 || return 1
295295
local raw value
@@ -308,8 +308,13 @@ collect_pi_throttle() {
308308
pi_soft_temp_limit_ever=$(( (n >> 19) & 1 ))
309309
}
310310

311-
# timedatectl show -p NTPSynchronized --value -> "yes" / "no" / ""
312-
collect_pi_ntp_sync() {
311+
# timedatectl show -p NTPSynchronized --value -> "yes" / "no" / "".
312+
# Universal host signal — any systemd host with a running timedated can
313+
# answer, not just Pis. Sits at `system.ntp_synchronized` on the wire,
314+
# alongside uptime and CPU. Containers, chroots, and stripped images may
315+
# ship `timedatectl` but fail at runtime; the helper returns 1 and the
316+
# field is omitted from the payload by the prune pass.
317+
collect_ntp_sync() {
313318
command -v timedatectl >/dev/null 2>&1 || return 1
314319
local raw
315320
raw="$(timeout 3s timedatectl show -p NTPSynchronized --value 2>/dev/null)" || return 1
@@ -460,15 +465,15 @@ build_service_json() {
460465
| with_entries(select(.value != null and .value != ""))'
461466
}
462467

463-
# Build the pi_health block as JSON, or print "null" if neither sub-probe
464-
# produced data. Sub-probes are independent — a missing/broken
465-
# `timedatectl` doesn't suppress vcgencmd throttle data and vice versa.
466-
build_pi_health_json() {
467-
local throttle_json='null' ntp_json='null'
468+
# Build the flat pi_throttle block as JSON, or print "null" if vcgencmd
469+
# is absent or its output failed to parse. Pi-only — absent on every
470+
# non-Pi feeder. NTP-sync lives in `system.ntp_synchronized` now (it is
471+
# universal-host, not Pi-specific) and is plumbed separately.
472+
build_pi_throttle_json() {
468473
local pi_undervoltage_now=0 pi_freq_capped_now=0 pi_throttled_now=0 pi_soft_temp_limit_now=0
469474
local pi_undervoltage_ever=0 pi_freq_capped_ever=0 pi_throttled_ever=0 pi_soft_temp_limit_ever=0
470475
if command -v vcgencmd >/dev/null 2>&1 && collect_pi_throttle; then
471-
throttle_json="$(jq -nc \
476+
jq -nc \
472477
--argjson uv_now "$pi_undervoltage_now" \
473478
--argjson fc_now "$pi_freq_capped_now" \
474479
--argjson th_now "$pi_throttled_now" \
@@ -484,22 +489,10 @@ build_pi_health_json() {
484489
undervoltage_ever: ($uv_ever == 1),
485490
freq_capped_ever: ($fc_ever == 1),
486491
throttled_ever: ($th_ever == 1),
487-
soft_temp_limit_ever: ($st_ever == 1)}')"
488-
fi
489-
if command -v timedatectl >/dev/null 2>&1; then
490-
local ntp
491-
if ntp="$(collect_pi_ntp_sync)"; then
492-
ntp_json="$ntp"
493-
fi
494-
fi
495-
if [[ "$throttle_json" == 'null' && "$ntp_json" == 'null' ]]; then
496-
printf 'null'
492+
soft_temp_limit_ever: ($st_ever == 1)}'
497493
return
498494
fi
499-
jq -nc \
500-
--argjson throttle "$throttle_json" \
501-
--argjson ntp "$ntp_json" \
502-
'{throttle: $throttle, ntp_synchronized: $ntp}'
495+
printf 'null'
503496
}
504497

505498
# nullable_num VALUE — echoes the value if non-empty, otherwise "null".
@@ -689,8 +682,13 @@ main() {
689682
svc_978="$(build_service_json airplanes-978 "$(root_path /run/airplanes-978/state)")"
690683
fi
691684

692-
local pi_health_json
693-
pi_health_json="$(build_pi_health_json)"
685+
local pi_throttle_json ntp_sync_json
686+
pi_throttle_json="$(build_pi_throttle_json)"
687+
# Universal host-clock signal — emits "true"/"false" or null on
688+
# any non-systemd host (no `timedatectl`). Wire path:
689+
# `system.ntp_synchronized`. Server stamps `system.clock_skew_seconds`
690+
# separately at ingest.
691+
ntp_sync_json="$(collect_ntp_sync 2>/dev/null || printf 'null')"
694692

695693
local feed_scripts_version os_pretty_name os_id os_version_id kernel architecture image_release
696694
feed_scripts_version="$(get_feed_scripts_version || true)"
@@ -730,7 +728,8 @@ main() {
730728
--argjson svc_readsb "$svc_readsb" \
731729
--argjson svc_dump978 "$svc_dump978" \
732730
--argjson svc_978 "$svc_978" \
733-
--argjson pi_health "$pi_health_json" \
731+
--argjson pi_throttle "$pi_throttle_json" \
732+
--argjson ntp_synchronized "$ntp_sync_json" \
734733
--arg feed_scripts "${feed_scripts_version:-}" \
735734
--arg os_pretty_name "${os_pretty_name:-}" \
736735
--arg os_id "${os_id:-}" \
@@ -758,7 +757,8 @@ main() {
758757
disk: {
759758
used_percent: $disk_used_pct,
760759
total_bytes: $disk_total_bytes
761-
}
760+
},
761+
ntp_synchronized: $ntp_synchronized
762762
},
763763
network: {
764764
connection_type: $net_connection_type,
@@ -774,7 +774,7 @@ main() {
774774
architecture: $architecture,
775775
image_release: $image_release
776776
},
777-
pi_health: $pi_health
777+
pi_throttle: $pi_throttle
778778
}
779779
| def _prune:
780780
if type == "object" then

test/test_airplanes_diagnostics.bats

Lines changed: 53 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -645,23 +645,35 @@ SH
645645
[ "$output" = '-58' ]
646646
}
647647

648-
@test "POST body omits pi_health when both vcgencmd and timedatectl are absent" {
649-
# Block both probes by stubbing them to fail. The default setup
650-
# doesn't ship vcgencmd or timedatectl stubs, but the dev host
651-
# likely has timedatectl on PATH (it's installed by systemd) which
652-
# would otherwise produce a pi_health.ntp_synchronized field.
648+
@test "POST body omits pi_throttle and system.ntp_synchronized when both vcgencmd and timedatectl are absent" {
649+
# Stub BOTH probes to fail. The dev / CI host usually ships
650+
# `timedatectl` (installed by systemd) and a Pi-based dev box would
651+
# also have `vcgencmd`; either of those leaking into this test would
652+
# produce a payload that contradicts the test's title.
653+
cat > "$STUB_DIR/vcgencmd" <<'SH'
654+
#!/usr/bin/env bash
655+
exit 1
656+
SH
657+
chmod +x "$STUB_DIR/vcgencmd"
653658
cat > "$STUB_DIR/timedatectl" <<'SH'
654659
#!/usr/bin/env bash
655660
exit 1
656661
SH
657662
chmod +x "$STUB_DIR/timedatectl"
658663
run_script
659664
[ "$status" -eq 0 ]
660-
run jq '.pi_health // empty' "$BODY_LOG"
661-
[ -z "$output" ]
665+
# pi_throttle is absent on non-Pi (no vcgencmd).
666+
run jq -e 'has("pi_throttle") | not' "$BODY_LOG"
667+
[ "$status" -eq 0 ]
668+
# NTP-sync absent in system block (broken timedatectl).
669+
run jq -e '.system | has("ntp_synchronized") | not' "$BODY_LOG"
670+
[ "$status" -eq 0 ]
671+
# Old container is gone — sentinel against accidental regressions.
672+
run jq -e 'has("pi_health") | not' "$BODY_LOG"
673+
[ "$status" -eq 0 ]
662674
}
663675

664-
@test "pi_health throttle survives a broken timedatectl (sub-probes independent)" {
676+
@test "pi_throttle survives a broken timedatectl (probes are independent)" {
665677
cat > "$STUB_DIR/vcgencmd" <<'SH'
666678
#!/usr/bin/env bash
667679
[[ "$1" == "get_throttled" ]] && printf 'throttled=0x0\n'
@@ -674,15 +686,16 @@ SH
674686
chmod +x "$STUB_DIR/timedatectl"
675687
run_script
676688
[ "$status" -eq 0 ]
677-
# throttle block is present, all bits false
678-
run jq -er '.pi_health.throttle.throttled_now' "$BODY_LOG"
689+
# pi_throttle bits present, all false
690+
run jq -er '.pi_throttle.throttled_now' "$BODY_LOG"
679691
[ "$output" = 'false' ]
680-
# ntp_synchronized is dropped (broken probe)
681-
run jq '.pi_health.ntp_synchronized // empty' "$BODY_LOG"
682-
[ -z "$output" ]
692+
# NTP-sync is absent (broken probe). With separate top-level fields
693+
# the independence is now structural, but the sentinel stays.
694+
run jq -e '.system | has("ntp_synchronized") | not' "$BODY_LOG"
695+
[ "$status" -eq 0 ]
683696
}
684697

685-
@test "POST body pi_health bit decode for vcgencmd=0x50005" {
698+
@test "POST body pi_throttle bit decode for vcgencmd=0x50005" {
686699
cat > "$STUB_DIR/vcgencmd" <<'SH'
687700
#!/usr/bin/env bash
688701
[[ "$1" == "get_throttled" ]] && printf 'throttled=0x50005\n'
@@ -697,20 +710,39 @@ SH
697710
[ "$status" -eq 0 ]
698711
# bits 0, 2, 16, 18 set: undervoltage_now, throttled_now,
699712
# undervoltage_ever, throttled_ever — all true.
700-
run jq -er '.pi_health.throttle.undervoltage_now' "$BODY_LOG"
713+
run jq -er '.pi_throttle.undervoltage_now' "$BODY_LOG"
701714
[ "$output" = 'true' ]
702-
run jq -er '.pi_health.throttle.throttled_now' "$BODY_LOG"
715+
run jq -er '.pi_throttle.throttled_now' "$BODY_LOG"
703716
[ "$output" = 'true' ]
704-
run jq -er '.pi_health.throttle.undervoltage_ever' "$BODY_LOG"
717+
run jq -er '.pi_throttle.undervoltage_ever' "$BODY_LOG"
705718
[ "$output" = 'true' ]
706-
run jq -er '.pi_health.throttle.throttled_ever' "$BODY_LOG"
719+
run jq -er '.pi_throttle.throttled_ever' "$BODY_LOG"
707720
[ "$output" = 'true' ]
708-
run jq -er '.pi_health.throttle.freq_capped_now' "$BODY_LOG"
721+
run jq -er '.pi_throttle.freq_capped_now' "$BODY_LOG"
709722
[ "$output" = 'false' ]
710-
run jq -er '.pi_health.throttle.soft_temp_limit_ever' "$BODY_LOG"
723+
run jq -er '.pi_throttle.soft_temp_limit_ever' "$BODY_LOG"
711724
[ "$output" = 'false' ]
712-
run jq -er '.pi_health.ntp_synchronized' "$BODY_LOG"
725+
run jq -er '.system.ntp_synchronized' "$BODY_LOG"
713726
[ "$output" = 'true' ]
727+
# Exact key-set: the server sanitizer drops the whole pi_throttle
728+
# block unless all 8 known keys are present. Pinning the wire shape
729+
# here catches an accidental drop of any one bit.
730+
run jq -er '.pi_throttle | keys | sort | join(",")' "$BODY_LOG"
731+
[ "$output" = 'freq_capped_ever,freq_capped_now,soft_temp_limit_ever,soft_temp_limit_now,throttled_ever,throttled_now,undervoltage_ever,undervoltage_now' ]
732+
}
733+
734+
@test "system.ntp_synchronized is false when timedatectl reports no" {
735+
# Pin that `false` survives the prune pass — `_prune` strips empty
736+
# strings and nulls but must keep boolean false.
737+
cat > "$STUB_DIR/timedatectl" <<'SH'
738+
#!/usr/bin/env bash
739+
[[ "$1" == "show" ]] && printf 'no\n'
740+
SH
741+
chmod +x "$STUB_DIR/timedatectl"
742+
run_script
743+
[ "$status" -eq 0 ]
744+
run jq -er '.system.ntp_synchronized' "$BODY_LOG"
745+
[ "$output" = 'false' ]
714746
}
715747

716748
# ---- response handling ----

0 commit comments

Comments
 (0)