Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- **Analysis findings: anomaly detections fold into the regular incident, and alerts dedup per incident (Stage B of the recs-engine remainder)** ([#1493]) — an `ANOMALY_*` detection and the regular finding for the same symptom (e.g. an `ANOMALY_WAIT_PROFILE` dominated by CX* waits and the CXPACKET "Parallelism waits" finding, or `ANOMALY_CPU_SPIKE` and `CPU_SQL_PERCENT`) stopped rendering as two separate cards and firing two separate e-mails for one event. A new shared `AnomalyIncidentReconciler` runs after incident clustering in all three analysis services (Darling, Lite, Dashboard) and, for each surviving anomaly story, rewrites its stamped incident id onto the regular finding of the same symptom family — matched wherever that family fact sits in the parent story (its root OR any mid/leaf member it consumed, so an `ANOMALY_CPU_SPIKE` still folds into an `SOS_SCHEDULER_YIELD`-rooted CPU story that merely *contains* `CPU_SQL_PERCENT` or the burst `CPU_SPIKE`, which is the common correlated case where dedup matters most), and only when that parent exists in the **same run and same database**; a no-parent anomaly stays solo, and a db-scoped `ANOMALY_OBJECT_*` never folds across databases. The family map mirrors the collector's own wait grouping exactly (`FactCollectorHelpers.WaitFamilyKey`: CX* → CXPACKET, general lock modes → LCK, with `LCK_M_S` / range / schema locks kept separate), and `ANOMALY_WAIT_PROFILE` resolves to the family of its dominant `contrib_<TYPE>` driver. This is fold-not-suppress — nothing is dropped, only the incident tag is reconciled, so the anomaly's vs-baseline context and any sub-threshold or no-regular-path signal are preserved. `AnalysisNotificationService` groups by `incident_id` (falling back to the story-path hash for legacy/absolution findings that have no id, so those never collapse together) and sends **one alert per incident per cycle** — led by the highest-severity member whose re-notify bucket is not cooling and naming the co-fired rest in that single message via the shared `CoFiredSummary` — but **escalates on CRITICAL**: a member at or above the shared `>= 1.5` CRITICAL severity cutoff (the same band the Dashboard/Lite/Darling recommendation readers use) is cooled on its OWN per-finding story-path-hash bucket instead of the incident bucket, so a NEW critical symptom joining an already-alerted incident sends a fresh e-mail naming that critical — never silently held under the incident cooldown — while below-critical co-fired symptoms stay deduped one-per-incident. Each cycle still collapses simultaneously co-firing symptoms into a single message (send-if-any-bucket-fresh, stamp-all, the same rule the live `IncidentCooldown` path uses); a lone critical e-mails once and respects its own cooldown, and two genuinely distinct incidents never collapse. Also fixes a latent cross-database clustering bug: `InferenceEngine.ClusterIntoIncidents` no longer unions two db-scoped findings from DIFFERENT databases into one incident (server-scoped findings still bridge freely, so same-database and server-wide incidents are unaffected).
- **Darling: `collect.collection_log` becomes a TimescaleDB hypertable — O(1) `drop_chunks` retention + compression, scale-readiness for the up-to-500-servers store** ([#1479]) — `collection_log` (the per-collector-run observability log) is the store's highest-volume plain table (~123k rows / 17 MB at 5-server scale) and was a heap with insert + a daily batched-DELETE purge, full-scanned every Overview refresh by the per-server `MAX(collection_time)` freshness read. It is now a TimescaleDB hypertable (schema **V23**): retention becomes an O(1) `drop_chunks` metadata drop instead of DELETE churn (`DarlingRetention`, with a batched-DELETE fallback so a plain-PostgreSQL store — or a table whose conversion failed — still honors the 60-day horizon), the freshness read hits only the newest chunk per server via the kept `(server_id, collection_time)` index, and old chunks compress (segment by `server_id`, compress-after 1 day) as the archival tier. Like the [#1478] memory tuning this is scale-readiness, **not** a fix for observed pressure (autovacuum keeps up at 5-server scale). The conversion is applied AUTHORITATIVELY at runtime by `TimescaleSupport.EnsureCollectionLogHypertableAsync` — the same proven-live, non-transactional, failure-isolated path every collector table uses — deliberately NOT solely by the V23 migration, because migrations run BEFORE `CREATE EXTENSION timescaledb` (so a fresh store's migration-time extension guard is false) and inside the startup-critical migration transaction (a `create_hypertable(migrate_data)` failure there would brick startup); the V23 migration carries a guarded, `EXCEPTION`-safe best-effort conversion as an upgrade fast-path, and yields to the runtime path otherwise. `collection_log` lives outside `CollectorCatalog.All`, so the managed-Postgres `max_background_workers` sizing now derives from `TimescaleSupport.HypertableCount` (catalog + 1) so its compression policy is not under-provisioned, and the viewer's connect-time schema-skew probe gains a plain-PostgreSQL-safe V23 sentinel (`collection_log` is a hypertable via the core `pg_trigger` `ts_insert_blocker` catalog — never the extension-only `timescaledb_information` view, which would throw on plain PG — OR the store has no `timescaledb`, where V23 is an object-invisible no-op) so a correctly-migrated store is never falsely gated. Plain PostgreSQL remains fully supported (the conversion no-ops; DELETE-based retention stands).
- **Darling viewer W2b: the Recommendations aggregate tab re-skinned to Lite's advise-only card design** ([#1262]) — the finding grid + read-only detail pane become a header (its OWN server selector, independent of the sidebar; a Refresh button; a status line showing the last analysis time) over a scrollable list of collapsible **incident** sections, each holding severity-banded cards (a severity badge, the affected `[database]`, the title, and the advice) — the same shaping as Lite's `RecommendationsTab`, grouped by `incident_id`, reads unchanged on the existing `analysis_findings` path. Every card offers **Ask AI** (copies the MCP investigation prompt ported verbatim from Lite, referencing `analyze_server` / `get_analysis_findings`); a card whose finding's persisted remediation carries a copy-paste statement also offers **Copy fix** (the suggested T-SQL, rebuilt from the stored `remediation_action_json` the way the Dashboard reader derives it on read-back, since the ephemeral drill-down `FactRemediation.GenerateForFinding` needs is not persisted). Advise-only: no Apply, and the finding **mute/unmute context menu + Muted column were removed** — mute moves to the Alert History surface per Lite; the `ViewerDataService` mute read/write API and its tests are retained for that side. There is no in-app "Generate now" (the service runs analysis on its own 30-minute cadence): the tab refreshes on activation, its Refresh button, and its own server-selector change only — never the 60-second timer — and the status line surfaces the last analysis time instead. Ungated tests pin the incident grouping, the card content (Copy-fix payload, Ask-AI prompt text), and the severity banding; a gated live-Postgres round-trip proves the persisted-action to Copy-fix path.
- **Lite: `date_diff` replaced with the PostgreSQL-shared spelling, bit-exactly (headless groundwork, phase 1a)** ([#1262]) — PostgreSQL has no `date_diff` function, and the naive `EXTRACT(EPOCH ...)` substitution is subtly different (elapsed time vs. DuckDB's boundary-crossing count — off by one around second/day boundaries). All 11 sites now use forms proven **value-identical** by adversarial probe: the ten per-collection rate denominators (`interval_seconds` via `LAG`) become `extract(epoch FROM (date_trunc('second', a) - date_trunc('second', b)))` (truncate-then-diff = exact boundary count; the one reader-exposed site keeps its BIGINT type via CAST), and the FinOps `days_of_data` becomes plain `DATE` subtraction (`CAST(max AS DATE) - CAST(min AS DATE)`), which both engines define as whole day-boundary counts. Probe also caught a portability trap avoided here: DuckDB's `date_trunc('day', ts)` returns a `DATE` where PostgreSQL returns a timestamp, so the day site deliberately avoids `date_trunc`
Expand Down Expand Up @@ -268,6 +269,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
[#1484]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1484
[#1488]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1488
[#1486]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1486
[#1493]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1493
[#1492]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1492
[#1491]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1491
[#1487]: https://github.com/erikdarlingdata/PerformanceMonitor/pull/1487
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,13 @@ public async Task<List<AnalysisFinding>> AnalyzeAsync(AnalysisContext context)
var incidents = _engine.ClusterIntoIncidents(stories, facts);
IncidentId.StampClusters(context.ServerName, incidents);

// 3.7. Fold each ANOMALY_* story into the REGULAR finding that describes the same symptom
// (same run, same database) by rewriting its stamped incident id onto that parent's — so
// the anomaly stops rendering as its own card / its own email. No-parent anomalies stay
// solo; db-scoped object anomalies never cross databases. Presentation-only: nothing is
// dropped, only the incident tag is reconciled.
AnomalyIncidentReconciler.Reconcile(stories);

// 4. Mute-filter the stories into the surviving findings (the Dashboard twin's D2/P2
// reorder) — WITHOUT inserting yet, so enrichment + action-build happen on the
// survivors first and the BUILT RemediationAction is persisted on each row. Muted/
Expand Down
Loading
Loading