mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 20:22:53 +00:00
245177e531
golangci-lint had accumulated 12 findings since 5abb2d8f4. All fixed with
real dedup (no nolint suppressions) and the repo's existing errcheck idioms:
- dupl internal/monitoring: docker/host identity-conflict trackers were
structural clones; extracted a shared identityFlapTracker core with a
domain-neutral identityConflict result. Per-domain files now hold only
the window const and the model translation. Tracker-behavior tests
consolidated into identity_flap_tracker_test.go; Monitor-level
translation and Apply*Report integration tests remain per domain.
- dupl internal/api/router.go: VM/container workload chart loops shared a
16-line live-fallback block; extracted guestChartSeriesWithLiveFallback
over a guestLiveMetricsView interface both views satisfy.
- dupl internal/storagehealth/risk.go: SMART attribute copying extracted
into applySMARTAttributes shared by both assessors (same
*models.SMARTAttributes type on both inputs).
- errcheck pkg/audit/sqlite_logger.go: three defer tx.Rollback() sites
now use the repo-wide defer func() { _ = tx.Rollback() }() idiom.
- errcheck telemetry/notifications tests: send() errors now fail the
test; queue.Stop() uses the package's _ = idiom.
Full test suites pass for all six touched packages.
Contract-Neutral: lint-hygiene restoration: dupl dedup (identical logic extracted to shared helpers) and errcheck idiom fixes; no public-contract or behavioral delta
142 lines
4.2 KiB
Go
142 lines
4.2 KiB
Go
package monitoring
|
|
|
|
import (
|
|
"sort"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// identityFlapTracker is the shared revisit-detection core behind the
|
|
// Docker-host and host-agent identity conflict trackers. It watches the
|
|
// stream of (hostname, secondary) identity pairs folded into a single
|
|
// resource identity and detects when two distinct machines are behind it.
|
|
//
|
|
// The signal is a *revisit*: a value switches away from the previous one and
|
|
// back to one already seen inside the window. A genuine rename transitions
|
|
// exactly once and never revisits the old value, so it does not trip the
|
|
// detector; clones that share an identity alternate every report cycle and
|
|
// trip it immediately. The window also controls how long a detected conflict
|
|
// stays visible after the flapping stops.
|
|
//
|
|
// What the secondary field means (machine ID, report IP) is the adapter's
|
|
// business; this core only tracks values.
|
|
type identityFlapTracker struct {
|
|
window time.Duration
|
|
|
|
hostnames map[string]time.Time
|
|
secondaries map[string]time.Time
|
|
|
|
lastHostname string
|
|
lastSecondary string
|
|
|
|
conflictSince time.Time
|
|
conflictLastSeen time.Time
|
|
}
|
|
|
|
// identityConflict is the tracker's domain-neutral verdict; adapters
|
|
// translate it into their platform's conflict model.
|
|
type identityConflict struct {
|
|
hostnames []string
|
|
// secondaries is nil unless the secondary values themselves diverge; when
|
|
// clones share one machine ID / report IP the hostname list already
|
|
// carries the story.
|
|
secondaries []string
|
|
firstSeen time.Time
|
|
lastSeen time.Time
|
|
}
|
|
|
|
func newIdentityFlapTracker(window time.Duration) *identityFlapTracker {
|
|
return &identityFlapTracker{
|
|
window: window,
|
|
hostnames: make(map[string]time.Time),
|
|
secondaries: make(map[string]time.Time),
|
|
}
|
|
}
|
|
|
|
// observe records one report's identity fields and returns the active
|
|
// conflict, or nil when the identity looks healthy.
|
|
func (t *identityFlapTracker) observe(hostname, secondary string, now time.Time) *identityConflict {
|
|
hostname = strings.TrimSpace(hostname)
|
|
secondary = strings.TrimSpace(secondary)
|
|
|
|
pruneOlderThan(t.hostnames, now.Add(-t.window))
|
|
pruneOlderThan(t.secondaries, now.Add(-t.window))
|
|
|
|
revisit := false
|
|
if hostname != "" {
|
|
if _, seen := t.hostnames[hostname]; seen && t.lastHostname != "" && t.lastHostname != hostname {
|
|
revisit = true
|
|
}
|
|
t.hostnames[hostname] = now
|
|
t.lastHostname = hostname
|
|
}
|
|
if secondary != "" {
|
|
if _, seen := t.secondaries[secondary]; seen && t.lastSecondary != "" && t.lastSecondary != secondary {
|
|
revisit = true
|
|
}
|
|
t.secondaries[secondary] = now
|
|
t.lastSecondary = secondary
|
|
}
|
|
|
|
if revisit {
|
|
if t.conflictSince.IsZero() || now.Sub(t.conflictLastSeen) > t.window {
|
|
t.conflictSince = now
|
|
}
|
|
t.conflictLastSeen = now
|
|
}
|
|
|
|
if t.conflictLastSeen.IsZero() || now.Sub(t.conflictLastSeen) > t.window {
|
|
t.conflictSince = time.Time{}
|
|
t.conflictLastSeen = time.Time{}
|
|
return nil
|
|
}
|
|
|
|
conflict := &identityConflict{
|
|
hostnames: sortedKeys(t.hostnames),
|
|
firstSeen: t.conflictSince,
|
|
lastSeen: t.conflictLastSeen,
|
|
}
|
|
if len(t.secondaries) > 1 {
|
|
conflict.secondaries = sortedKeys(t.secondaries)
|
|
}
|
|
return conflict
|
|
}
|
|
|
|
// observeIdentityFlap feeds one report's identity fields into the flap
|
|
// tracker for the resolved identifier, lazily creating the tracker map and
|
|
// entry, and returns the active conflict, if any. Callers must not hold m.mu.
|
|
func (m *Monitor) observeIdentityFlap(trackers *map[string]*identityFlapTracker, window time.Duration, identifier, hostname, secondary string, now time.Time) *identityConflict {
|
|
if strings.TrimSpace(identifier) == "" {
|
|
return nil
|
|
}
|
|
|
|
m.mu.Lock()
|
|
defer m.mu.Unlock()
|
|
if *trackers == nil {
|
|
*trackers = make(map[string]*identityFlapTracker)
|
|
}
|
|
tracker, ok := (*trackers)[identifier]
|
|
if !ok {
|
|
tracker = newIdentityFlapTracker(window)
|
|
(*trackers)[identifier] = tracker
|
|
}
|
|
return tracker.observe(hostname, secondary, now)
|
|
}
|
|
|
|
func pruneOlderThan(entries map[string]time.Time, cutoff time.Time) {
|
|
for key, seenAt := range entries {
|
|
if seenAt.Before(cutoff) {
|
|
delete(entries, key)
|
|
}
|
|
}
|
|
}
|
|
|
|
func sortedKeys(entries map[string]time.Time) []string {
|
|
keys := make([]string, 0, len(entries))
|
|
for key := range entries {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
return keys
|
|
}
|