Improve position validation logging and reduce spam

- Remove duplicate validation logging between mergeAircraftData and updateHistories
- Add rate limiting: errors logged max once per 10 seconds per aircraft
- Add rate limiting: warnings logged max once per 30 seconds per aircraft
- Only log first error/warning message to reduce log verbosity
- Clean up validation log entries when aircraft become stale
- Maintain memory efficiency with automatic cleanup

This significantly reduces log spam from aircraft with persistent
position validation issues (e.g., CPR decoding problems).

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Ole-Morten Duesund 2025-08-24 20:39:57 +02:00
commit 960f47682d

View file

@ -271,6 +271,7 @@ type Merger struct {
historyLimit int // Maximum history points to retain
staleTimeout time.Duration // Time before aircraft considered stale (15 seconds)
updateMetrics map[uint32]*updateMetric // ICAO24 -> update rate calculation data
validationLog map[uint32]time.Time // ICAO24 -> last validation log time (rate limiting)
}
// updateMetric tracks recent update times for calculating update rates.
@ -301,6 +302,7 @@ func NewMerger() (*Merger, error) {
historyLimit: 500,
staleTimeout: 15 * time.Second, // Aircraft timeout - reasonable for ADS-B tracking
updateMetrics: make(map[uint32]*updateMetric),
validationLog: make(map[uint32]time.Time),
}, nil
}
@ -453,10 +455,14 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
validation := m.validatePosition(new, state, timestamp)
if !validation.Valid {
// Log validation errors and skip position update
// Rate-limited logging: only log once every 10 seconds per aircraft
if lastLog, exists := m.validationLog[new.ICAO24]; !exists || timestamp.Sub(lastLog) > 10*time.Second {
icaoHex := fmt.Sprintf("%06X", new.ICAO24)
for _, err := range validation.Errors {
log.Printf("[POSITION_VALIDATION] ICAO %s: REJECTED position update - %s", icaoHex, err)
// Only log first error to reduce spam
if len(validation.Errors) > 0 {
log.Printf("[POSITION_VALIDATION] ICAO %s: REJECTED - %s", icaoHex, validation.Errors[0])
m.validationLog[new.ICAO24] = timestamp
}
}
} else {
// Position is valid, proceed with normal logic
@ -483,10 +489,15 @@ func (m *Merger) mergeAircraftData(state *AircraftState, new *modes.Aircraft, so
}
}
// Log warnings even if position is valid
for _, warning := range validation.Warnings {
// Rate-limited warning logging
if len(validation.Warnings) > 0 {
// Only log warnings once every 30 seconds per aircraft
warningKey := new.ICAO24 + 0x10000000 // Offset to differentiate from error logging
if lastLog, exists := m.validationLog[warningKey]; !exists || timestamp.Sub(lastLog) > 30*time.Second {
icaoHex := fmt.Sprintf("%06X", new.ICAO24)
log.Printf("[POSITION_VALIDATION] ICAO %s: WARNING - %s", icaoHex, warning)
log.Printf("[POSITION_VALIDATION] ICAO %s: WARNING - %s", icaoHex, validation.Warnings[0])
m.validationLog[warningKey] = timestamp
}
}
}
@ -611,19 +622,8 @@ func (m *Merger) updateHistories(state *AircraftState, aircraft *modes.Aircraft,
Longitude: aircraft.Longitude,
Source: sourceID,
})
} else {
// Log validation errors for debugging
icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24)
for _, err := range validation.Errors {
log.Printf("[POSITION_VALIDATION] ICAO %s: REJECTED - %s", icaoHex, err)
}
}
// Log warnings even for valid positions
for _, warning := range validation.Warnings {
icaoHex := fmt.Sprintf("%06X", aircraft.ICAO24)
log.Printf("[POSITION_VALIDATION] ICAO %s: WARNING - %s", icaoHex, warning)
}
// Note: Validation errors/warnings are already logged in mergeAircraftData
}
// Signal history
@ -847,6 +847,9 @@ func (m *Merger) CleanupStale() {
if now.Sub(state.LastUpdate) > m.staleTimeout {
delete(m.aircraft, icao)
delete(m.updateMetrics, icao)
// Clean up validation log entries
delete(m.validationLog, icao)
delete(m.validationLog, icao+0x10000000) // Warning key
}
}
}