Repository navigation
collection health: distinguish STOPPED (gone dark) from FAILING (still erroring) - #2694
Conversation
…l running and erroring) A collector whose AppliesTo gate flips off for a target (agent_status and running_jobs on an RDS SQL Server instance, where SQL Agent job data is not reachable) simply stops being invoked - no new success, no new error, nothing. Its last historical success sits inside the health window and ages past the FAILING cutoff exactly like a genuinely broken collector would, so get_collection_health and the fleet overview report it as FAILING - alarming noise that masks any GENUINE failure showing up the same way, and confirmed today as the actual field symptom behind "every server shows 2 collectors failing" on the use2 fleet. Adds a new CollectorHealthClassifier.Stopped band, keyed on hoursSinceLastRun (the newest run of ANY status, already selected as last_run_time in every health query) rather than hoursSinceLastSuccess: a collector that keeps being invoked and keeps failing has a small hoursSinceLastRun even while its success clock runs out, and correctly stays FAILING; a collector that has attempted nothing at all for that long reads STOPPED instead. hoursSinceLastRun can never exceed hoursSinceLastSuccess, so this recategorizes a strict subset of the old FAILING cases rather than suppressing anything. STOPPED does not count toward a server's failing-collector total, matching NO_PERMISSIONS. Wired through all four HealthStatus call sites (Lite, Darling service, Darling viewer, Darling fleet rollup) plus the fleet-level SQL, which did not previously select last_run_time at all. HoursSinceLastRun falls back to HoursSinceLastSuccess when unset, so a run can never read more dormant than a known success already implies. Separately investigated and confirmed NOT a bug: the existing PERMISSIONS/ NO_PERMISSIONS design (CollectorRuntimePrecondition) already deliberately buckets "a denied grant, a missing source object, and a platform limitation" under one status with the distinguishing detail quoted from the server's own error message - and the fleet rollup already excludes NO_PERMISSIONS from its failing count. No change needed there; forcing a split would go against a already-considered, well-documented design decision. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReviewWent through the classifier change, all four Correctness — looks solid. The Parity — no drift found. All 4 production call sites ( Two stale docs (not part of the diff, so I couldn't leave inline comments — GitHub only accepts review comments on lines within the PR's diff):
Neither is a functional issue, just doc completeness given this PR is explicitly about the shared classifier's contract. No security, SQL injection, or performance concerns — this is pure in-memory banding logic over already-fetched aggregate rows, no new query shapes beyond the one added |
…gresTests
CollectionHealth_SevenDayAggregate_BandsEachCollector_AgainstDevPostgres asserted
FAILING for a collector with exactly one old SUCCESS and nothing since - the real
SQL round-trip correctly reads STOPPED there (last_run_time equals that same old
success, so both clocks agree nothing has happened in 30h). Renamed the case and
added a sibling ("still_failing_collector": an old success PLUS a recent error)
proving FAILING still fires when there is genuinely recent activity of any kind.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
ReviewSolid, well-contained change. Traced the new
One very minor nit, not blocking: |
…stopped-band # Conflicts: # CHANGELOG.md
|
Reviewed the diff (excluding CHANGELOG.md content, which is fine on its own — see line-ending note below). Correctness — looks solid. The core fix is a clean, minimal addition: Traced Failing-count exclusion confirmed correct without any code change, as the PR claims: every consumer ( Fleet SQL column ordinal double-checked — UI layer: confirmed Tests: the decision-table and fixture reclassifications read as intended — e.g. the One nit: this PR flips No missing-index-DMV suggestions to make here — no T-SQL changed in this PR. |
Fix CHANGELOG.md: restore CRLF after an accidental LF renormalize (#2694 fallout)
…ikdarlingdata#2694) My merge-conflict resolution in erikdarlingdata#2694 (merging origin/dev's erikdarlingdata#2693 wraparound fix into my collection-health branch) committed without the .git/info/attributes CHANGELOG.md -text override in place, so git silently renormalized the WHOLE file to LF on staging. Content is byte-identical to what merged (verified with a CR-stripped diff); only line endings are restored to the CRLF this file has always used. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ze (erikdarlingdata#2695 fallout) PR erikdarlingdata#2695's own merge-conflict resolution (commit 9a33208, merging origin/dev to resolve a CHANGELOG collision with erikdarlingdata#2694) renormalized the whole file to LF again, the same trap erikdarlingdata#2696 had just fixed for erikdarlingdata#2694 earlier today. Confirmed by bisecting: 0b2b8b7 (the erikdarlingdata#2696 fix) was 3057/3057 CRLF; 9a33208 was 0/3059 CRLF. Rebuilt from the raw stored blob (confirmed 0 pre-existing CR bytes, so the LF->CRLF conversion is lossless) with the .git/info/attributes -text override in place so the commit isn't re-normalized on the way in. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…engine does, and three presence-flat alerts grade their severity from the bars the health bands already measured (#3653 alert riders) (#3660) PG poison wait: the clear arm requires an observed window. Since #2694 a quiet poison event writes no pg_wait_stats row, so the rows cannot witness their own absence; the read now carries the pg_wait_stats collector's own collection_log SUCCESS count over the same window (the #3537 xmin witness) as PostgresPoisonWaitWindow, and the host clears only when Observed. An unobserved window holds every active flag and logs once per server at Debug. A8e engine half: Deadlocks Detected grades Warning/Critical on the #3368 rate tiers (plumbed through IAlertEngineSettings.DeadlockRateThresholds so Darling's V120 knob governs; Lite returns the shipped pair), High CPU grades Critical at the CPU health band's 95% bar, tempdb Space fires an explicit Warning with no Critical tier (no measured bar to cite). Every graded fire sets the tier on the context and the outcome so both SKUs persist it.
What
get_collection_healthand the fleet overview band a collectorFAILINGpurely from hours-since-last-success. A collector whoseAppliesTogate flips off for a target (agent_status/running_jobson an RDS SQL Server instance, confirmed today) simply stops being invoked — no new success, no new error, nothing at all. Its old success ages past the FAILING cutoff and it reads exactly like a genuinely broken collector, which is real noise: every server on the use2 fleet shows "2 collectors failing" from this alone, and it's exactly the kind of noise that will mask a real problem during UAT this week.Fix
New
CollectorHealthClassifier.Stoppedband, keyed on hours since the newest run of ANY status (last_run_time, already selected in every health query) rather than hours since last success:hoursSinceLastRun(the failure itself is a run) → still correctlyFAILING.STOPPED.hoursSinceLastRuncan never exceedhoursSinceLastSuccess(every success is a run), so this strictly recategorizes a subset of what used to readFAILING— it never suppresses a real failure.STOPPEDdoes not count toward a server's failing-collector total, matching howNO_PERMISSIONSalready behaves (confirmed:ReadFailingCollectorCountsAsyncfilters on the literal"FAILING"string).Wired through all four
HealthStatuscall sites (Lite, Darling service, Darling viewer, Darling fleet rollup) plus the fleet-level SQL, which did not previously selectlast_run_timeat all — only the per-server queries did.HoursSinceLastRunfalls back toHoursSinceLastSuccesswhen unset, so a row can never read more dormant than a known success already implies (matters for hand-built test fixtures; real SQL always sets it onceTotalRuns > 0).What did NOT need a fix (investigated and confirmed correct)
The existing
PERMISSIONS/NO_PERMISSIONSdesign (CollectorRuntimePrecondition) already deliberately buckets "a denied grant, a missing source object, and a platform limitation the collector can't do anything about" under one status, with the distinguishing detail carried in the quoted server error text rather than a second status word — a documented, considered design decision. The fleet rollup already excludesNO_PERMISSIONSfrom its failing count. No change made there.Tests
Decision-table cases added to both
CollectorHealthClassifierTests.cscopies provingSTOPPEDvsFAILINGgiven identicalhoursSinceLastSuccessbut differenthoursSinceLastRun; three existing cases reclassified fromFAILING→STOPPEDwith explanatory comments (100%-success collectors whose only clock is last-success are, by definition, the STOPPED shape once no run of any kind exists since). Object-based fact tests inViewerDailyHealthTests.csupdated the same way.🤖 Generated with Claude Code