From ac67308f21a94e0490e8b2589fae032dfe2c8dec Mon Sep 17 00:00:00 2001 From: "Enot (ded) Skelly" Date: Thu, 23 Jul 2026 08:43:26 -0700 Subject: [PATCH] fix: dont log empty status from observers closes #79 --- .../009_clean_zeroed_observer_telemetry.sql | 19 +++++++ internal/ingest/status.go | 57 ++++++++++--------- 2 files changed, 50 insertions(+), 26 deletions(-) create mode 100644 db/migrations/009_clean_zeroed_observer_telemetry.sql diff --git a/db/migrations/009_clean_zeroed_observer_telemetry.sql b/db/migrations/009_clean_zeroed_observer_telemetry.sql new file mode 100644 index 0000000..3289dc5 --- /dev/null +++ b/db/migrations/009_clean_zeroed_observer_telemetry.sql @@ -0,0 +1,19 @@ +-- Copyright 2026 Beacon Contributors +-- SPDX-License-Identifier: AGPL-3.0-or-later + +-- Cleans up observer_telemetry rows written from /status messages whose +-- "stats" object was missing or renamed. status.go's json.Unmarshal +-- silently tolerated the missing object, leaving the stats struct at Go +-- zero-values, and the row was inserted unconditionally from there -- +-- uptime_secs=0, battery_mv=0, noise_floor=0, tx/rx_air_secs=0. With the +-- hourly ON CONFLICT (observer_id, reported_at) DO NOTHING dedup, +-- whichever message landed first per hour won, so these zero rows +-- regularly became the hour's telemetry and corrupted the 24h/7d/30d +-- aggregates (AVG(noise_floor_db), AVG(battery_voltage_mv), MAX-MIN +-- airtime). +-- +-- Safe because a running observer never reports uptime_seconds == 0; +-- this is the same signal now used by status.go to skip the insert going +-- forward (see internal/ingest/status.go handleStatus). + +DELETE FROM observer_telemetry WHERE uptime_seconds = 0; diff --git a/internal/ingest/status.go b/internal/ingest/status.go index fc32628..8a9421c 100644 --- a/internal/ingest/status.go +++ b/internal/ingest/status.go @@ -96,7 +96,12 @@ func (w *Worker) handleStatus(ctx context.Context, pubkeyHex string, raw []byte) StatusMetadata: raw, LastStatusAt: time.Now(), } - params.UptimeSeconds = &envelope.Stats.UptimeSeconds + // A running observer never reports 0, so treat this the same as a missing + // stats object and leave the existing value alone (COALESCE in the SQL) + // rather than stomping it with a zero. + if envelope.Stats.UptimeSeconds != 0 { + params.UptimeSeconds = &envelope.Stats.UptimeSeconds + } if envelope.Stats.BatteryMV != 0 { batteryLevel := float32(envelope.Stats.BatteryMV) / 1000 params.BatteryLevel = &batteryLevel @@ -151,32 +156,32 @@ func (w *Worker) handleStatus(ctx context.Context, pubkeyHex string, raw []byte) return } // Store a telemetry snapshot at the configured resolution. - resolution := w.cfg.TelemetryResolution - if resolution == 0 { - resolution = time.Hour - } - reportedAt := time.Now().Truncate(resolution) - batteryMV := int32(envelope.Stats.BatteryMV) - txAirSecs := float32(envelope.Stats.TxAirSecs) - rxAirSecs := float32(envelope.Stats.RxAirSecs) - queueLen := int32(envelope.Stats.QueueLen) - debugFlags := int32(envelope.Stats.DebugFlags) - recvErrors := int32(envelope.Stats.RecvErrors) + // A running observer never reports uptime_secs == 0, so its absence (or a + // missing/renamed "stats" object entirely) means this payload has no usable + // stats — skip the insert rather than writing an all-zero row that would win + // the hourly dedup and pollute the telemetry aggregates. + if envelope.Stats.UptimeSeconds == 0 { + log.Printf("ingest[%s]: status from %s has no usable stats (uptime_secs missing or zero), skipping telemetry insert", w.cfg.BrokerName, pubkeyHex) + } else { + resolution := w.cfg.TelemetryResolution + if resolution == 0 { + resolution = time.Hour + } + reportedAt := time.Now().Truncate(resolution) + batteryMV := int32(envelope.Stats.BatteryMV) + txAirSecs := float32(envelope.Stats.TxAirSecs) + rxAirSecs := float32(envelope.Stats.RxAirSecs) + queueLen := int32(envelope.Stats.QueueLen) + debugFlags := int32(envelope.Stats.DebugFlags) + recvErrors := int32(envelope.Stats.RecvErrors) - if err := w.db.InsertObserverTelemetry( - ctx, - observerID, - reportedAt, - &batteryMV, - &txAirSecs, - &rxAirSecs, - envelope.Stats.NoiseFloor, - envelope.Stats.UptimeSeconds, - &queueLen, - &debugFlags, - &recvErrors, - ); err != nil { - log.Printf("ingest[%s]: db: insert telemetry failed for %s: %v", w.cfg.BrokerName, pubkeyHex, err) + if err := w.db.InsertObserverTelemetry( + ctx, observerID, reportedAt, &batteryMV, &txAirSecs, &rxAirSecs, + envelope.Stats.NoiseFloor, envelope.Stats.UptimeSeconds, + &queueLen, &debugFlags, &recvErrors, + ); err != nil { + log.Printf("ingest[%s]: db: insert telemetry failed for %s: %v", w.cfg.BrokerName, pubkeyHex, err) + } } iata, err := w.db.GetObserverLastIATA(ctx, observerID)