Skip to content

Commit 876dd0b

Browse files
Merge pull request erikdarlingdata#627 from erikdarlingdata/dev
Release v2.3.0
2 parents 8ed7a95 + 6722fb7 commit 876dd0b

142 files changed

Lines changed: 29456 additions & 1585 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coderabbit.yaml

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
2+
3+
language: "en-US"
4+
early_access: false
5+
enable_free_tier: true
6+
7+
reviews:
8+
profile: "chill"
9+
high_level_summary: true
10+
review_status: true
11+
commit_status: true
12+
collapse_walkthrough: true
13+
sequence_diagrams: false
14+
poem: false
15+
16+
path_filters:
17+
- "!**/*.Designer.cs"
18+
- "!**/bin/**"
19+
- "!**/obj/**"
20+
- "!**/publish/**"
21+
- "!**/*.user"
22+
- "!**/*.suo"
23+
24+
path_instructions:
25+
- path: "Dashboard/**/*.cs"
26+
instructions: >
27+
This is a WPF .NET 8 desktop app (Dashboard) that reads from SQL Server.
28+
Uses data binding, async/await patterns, and INotifyPropertyChanged.
29+
Watch for: null reference risks, disposal of SQL connections,
30+
thread safety with UI dispatch, and proper async patterns.
31+
- path: "Lite/**/*.cs"
32+
instructions: >
33+
This is a WPF .NET 8 desktop app (Lite) that collects SQL Server DMV data
34+
into a local DuckDB database. Uses ReaderWriterLockSlim for DB coordination.
35+
Watch for: connection disposal, thread safety, DuckDB access patterns,
36+
and proper async/await usage.
37+
- path: "**/*.sql"
38+
instructions: >
39+
T-SQL stored procedures and scripts for SQL Server.
40+
Watch for: SQL injection risks, missing error handling (TRY/CATCH),
41+
proper use of SET NOCOUNT ON, and parameter sniffing concerns.
42+
- path: "Installers/**"
43+
instructions: >
44+
WiX-based MSI installer projects. Be cautious about upgrade paths
45+
and file versioning. Schema upgrades go in upgrades/ folder, not install scripts.
46+
47+
auto_review:
48+
enabled: true
49+
drafts: false
50+
base_branches:
51+
- "dev"
52+
- "main"
53+
54+
tools:
55+
gitleaks:
56+
enabled: true
57+
github-checks:
58+
enabled: true
59+
60+
chat:
61+
auto_reply: true
62+
63+
knowledge_base:
64+
learnings:
65+
scope: "local"
66+
pull_requests:
67+
scope: "local"

CHANGELOG.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,87 @@ All notable changes to this project will be documented in this file.
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [2.3.0] - 2026-03-18
9+
10+
### Important
11+
12+
- **Schema upgrade**: Six columns widened across three tables (`query_stats`, `cpu_scheduler_stats`, `waiting_tasks`, `database_size_stats`) to match DMV documentation types. These are in-place ALTER COLUMN operations — fast on any table size, no data migration. Upgrade scripts run automatically via the CLI/GUI installer.
13+
- **SQL Server version check**: Both installers now reject SQL Server 2014 and earlier before running any scripts, with a clear error message. Azure MI (EngineEdition 8) is always accepted. ([#543])
14+
- **Installer adversarial tests**: 35 automated tests covering upgrade failures, data survival, idempotency, version detection fallback, file filtering, restricted permissions, and more. These run as part of pre-release validation. ([#543])
15+
16+
### Added
17+
18+
- **ErikAI analysis engine** — rule-based inference engine for Lite that scores server health across wait stats, CPU, memory, I/O, blocking, tempdb, and query performance. Surfaces actionable findings with severity, detail, and recommended actions. Includes anomaly detection (baseline comparison for acute deviations), bad actor detection (per-query scoring for consistently terrible queries), and CPU spike detection for bursty workloads. ([#589], [#593])
19+
- **ErikAI Dashboard port** — full analysis engine ported to Dashboard with SQL Server backend ([#590])
20+
- **FinOps cost optimization recommendations** — Phase 1-4 checks: enterprise feature audit, CPU/memory right-sizing, compression savings estimator, unused index cost quantification, dormant database detection, dev/test workload detection, VM right-sizing, storage tier optimization, reserved capacity candidates ([#564])
21+
- **FinOps High Impact Queries** — 80/20 analysis showing which queries consume the most resources across all dimensions ([#564])
22+
- **FinOps dollar-denominated cost attribution** — per-server monthly cost setting with proportional database-level breakdown ([#564])
23+
- **On-demand plan fetch** for bad actor and analysis findings — click to retrieve execution plans for flagged queries ([#604])
24+
- **Plan analysis integration** — findings include execution plan analysis when plans are available ([#594])
25+
- **Server unreachable email alerts** — Dashboard sends email (not just tray notification) when a monitored server goes offline or comes back online ([#529])
26+
- **Column filters on all FinOps DataGrids** — filter funnel icons on every column header across all 7 FinOps grids in Lite and Dashboard ([#562])
27+
- **Column filters on Dashboard** IdleDatabases, TempDB, and Index Analysis grids
28+
- **Lite data import** — "Import Data" button brings in monitoring history from a previous Lite install via parquet files, preserving trend data across version upgrades ([#566])
29+
- **Per-server Utility Database setting** — Lite can call community stored procedures (sp_IndexCleanup) from a database other than master ([#555])
30+
- **SQL Server version check** in both CLI and GUI installers — rejects 2014 and earlier with a clear message ([#543])
31+
- **Execution plan analysis MCP tools** for both Dashboard and Lite
32+
- **Full MCP tool coverage** — Dashboard expanded from 28 to 57 tools, Lite from 32 to 51 tools ([#576], [#577])
33+
- **Self-sufficient analyze_server drill-down** — MCP tool returns complete analysis, not breadcrumb trail ([#578])
34+
- **NuGet package dependency licenses** in THIRD_PARTY_NOTICES.md
35+
36+
### Changed
37+
38+
- **Azure SQL DB FinOps** — all collectors (database sizes, query stats, file I/O) now connect to each database individually instead of only querying master. Server Inventory uses dynamic SQL to avoid `sys.master_files` dependency. ([#557])
39+
- **Index Analysis scroll fix** — both summary and detail grids now use proportional heights instead of Auto, so they scroll independently with large result sets ([#554])
40+
- **Dashboard Add Server dialog** — increased MaxHeight from 700 to 850px so buttons are visible when SQL auth fields are shown
41+
- **GUI installer** — Uninstall button now correctly enables after a successful install
42+
- **GUI installer** — fixed encryption mapping and history logging ([#612])
43+
- **Dashboard visible sub-tab only refresh** on auto-refresh ticks ([#528])
44+
- Analysis engine decouples data maturity check from analysis window
45+
46+
### Fixed
47+
48+
- **Installer dropping database on every upgrade**`00_uninstall.sql` excluded from install file list, installer aborts on upgrade failure, version detection fallback returns "1.0.0" instead of null ([#538], [#539])
49+
- **SQL dumps on mirroring passive servers** from FinOps collectors ([#535])
50+
- **RetrievedFromCache** always showing False ([#536])
51+
- **Arithmetic overflow** in query_stats collector for dop/thread columns ([#547])
52+
- **Lite perfmon chart bugs** and Dashboard ScottPlot crash handling ([#544], [#545])
53+
- **PLE=0 scoring bug** — was scored as harmless, now correctly flagged ([#543])
54+
- **PercentRank >1.0** bug in HealthCalculator
55+
- **6 verified Lite bugs** from code review ([#611])
56+
- **Enterprise feature audit text** — partitioning is not Enterprise-only
57+
- **FinOps collector scheduling**, server switch, and utilization bugs
58+
- **Dashboard drill-down** Unicode arrow in story path split
59+
- **Empty DataGrid scrollbar artifacts** — hide grids when empty across all FinOps tabs
60+
- **Query preview** — truncated in row, full text in tooltip
61+
62+
[#529]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/529
63+
[#535]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/535
64+
[#536]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/536
65+
[#538]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/538
66+
[#539]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/539
67+
[#543]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/543
68+
[#544]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/544
69+
[#545]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/545
70+
[#547]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/547
71+
[#554]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/554
72+
[#555]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/555
73+
[#557]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/557
74+
[#562]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/562
75+
[#564]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/564
76+
[#566]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/566
77+
[#576]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/576
78+
[#577]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/577
79+
[#578]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/578
80+
[#528]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/528
81+
[#589]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/589
82+
[#590]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/590
83+
[#593]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/593
84+
[#594]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/594
85+
[#604]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/604
86+
[#611]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/611
87+
[#612]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/612
88+
889
## [2.2.0] - 2026-03-11
990

1091
**Contributors:** [@HannahVernon](https://github.com/HannahVernon), [@ClaudioESSilva](https://github.com/ClaudioESSilva), [@dphugo](https://github.com/dphugo), [@Orestes](https://github.com/Orestes) — thank you!

Dashboard/AddServerDialog.xaml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
33
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
44
Title="Add SQL Server"
5-
SizeToContent="Height" Width="450" MaxHeight="700"
5+
SizeToContent="Height" Width="450" MaxHeight="850"
66
WindowStartupLocation="CenterOwner"
77
ResizeMode="NoResize"
88
Background="{DynamicResource BackgroundBrush}"
@@ -100,6 +100,14 @@
100100
<CheckBox x:Name="IsFavoriteCheckBox" Content="Mark as favorite (appears at top of list)"
101101
Foreground="{DynamicResource ForegroundBrush}" Margin="0,0,0,6"/>
102102

103+
<!-- Monthly Cost -->
104+
<StackPanel Orientation="Horizontal" Margin="0,0,0,6">
105+
<TextBlock Text="Monthly Cost ($):" Width="110" VerticalAlignment="Center"
106+
Foreground="{DynamicResource ForegroundBrush}"/>
107+
<TextBox x:Name="MonthlyCostTextBox" Width="100" TextAlignment="Right" Text="0"
108+
ToolTip="What this server costs per month (license, compute, storage combined). Used for FinOps cost attribution. Leave 0 to hide cost columns."/>
109+
</StackPanel>
110+
103111
<!-- Description -->
104112
<TextBlock Text="Description (optional):" Margin="0,4,0,4"
105113
Foreground="{DynamicResource ForegroundBrush}"/>

Dashboard/AddServerDialog.xaml.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ public AddServerDialog(ServerConnection existingServer)
4242
ServerNameTextBox.Text = existingServer.ServerName;
4343
DescriptionTextBox.Text = existingServer.Description;
4444
IsFavoriteCheckBox.IsChecked = existingServer.IsFavorite;
45+
MonthlyCostTextBox.Text = existingServer.MonthlyCostUsd.ToString(System.Globalization.CultureInfo.InvariantCulture);
4546

4647
// Load encryption settings
4748
EncryptModeComboBox.SelectedIndex = existingServer.EncryptMode switch
@@ -328,9 +329,15 @@ private async void Save_Click(object sender, RoutedEventArgs e)
328329
ServerConnection.IsFavorite = IsFavoriteCheckBox.IsChecked == true;
329330
ServerConnection.EncryptMode = GetSelectedEncryptMode();
330331
ServerConnection.TrustServerCertificate = TrustServerCertificateCheckBox.IsChecked == true;
332+
if (decimal.TryParse(MonthlyCostTextBox.Text, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var editCost) && editCost >= 0)
333+
ServerConnection.MonthlyCostUsd = editCost;
331334
}
332335
else
333336
{
337+
decimal monthlyCost = 0m;
338+
if (decimal.TryParse(MonthlyCostTextBox.Text, System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var newCost) && newCost >= 0)
339+
monthlyCost = newCost;
340+
334341
ServerConnection = new ServerConnection
335342
{
336343
DisplayName = displayName,
@@ -341,7 +348,8 @@ private async void Save_Click(object sender, RoutedEventArgs e)
341348
CreatedDate = DateTime.Now,
342349
LastConnected = DateTime.Now,
343350
EncryptMode = GetSelectedEncryptMode(),
344-
TrustServerCertificate = TrustServerCertificateCheckBox.IsChecked == true
351+
TrustServerCertificate = TrustServerCertificateCheckBox.IsChecked == true,
352+
MonthlyCostUsd = monthlyCost
345353
};
346354
}
347355

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
namespace PerformanceMonitorDashboard.Analysis;
5+
6+
/// <summary>
7+
/// A scored observation from collected data.
8+
/// </summary>
9+
public class Fact
10+
{
11+
public string Source { get; set; } = string.Empty;
12+
public string Key { get; set; } = string.Empty;
13+
public double Value { get; set; }
14+
public double BaseSeverity { get; set; }
15+
public double Severity { get; set; }
16+
public int ServerId { get; set; }
17+
public string? DatabaseName { get; set; }
18+
19+
/// <summary>
20+
/// Raw metric values for analysis and audit trail.
21+
/// Keys are metric-specific (e.g., "wait_time_ms", "waiting_tasks_count").
22+
/// </summary>
23+
public Dictionary<string, double> Metadata { get; set; } = [];
24+
25+
/// <summary>
26+
/// Amplifiers that were evaluated for this fact.
27+
/// </summary>
28+
public List<AmplifierResult> AmplifierResults { get; set; } = [];
29+
}
30+
31+
/// <summary>
32+
/// Result of evaluating a single amplifier against the fact set.
33+
/// </summary>
34+
public class AmplifierResult
35+
{
36+
public string Description { get; set; } = string.Empty;
37+
public bool Matched { get; set; }
38+
public double Boost { get; set; }
39+
}
40+
41+
/// <summary>
42+
/// A conditional edge in the relationship graph.
43+
/// </summary>
44+
public class Edge
45+
{
46+
public string Source { get; set; } = string.Empty;
47+
public string Destination { get; set; } = string.Empty;
48+
public string Category { get; set; } = string.Empty;
49+
public string PredicateDescription { get; set; } = string.Empty;
50+
51+
/// <summary>
52+
/// Evaluates whether this edge should be followed given the current fact set.
53+
/// </summary>
54+
public Func<IReadOnlyDictionary<string, Fact>, bool> Predicate { get; set; } = _ => false;
55+
}
56+
57+
/// <summary>
58+
/// A complete analysis story — the path from root symptom to leaf recommendation.
59+
/// </summary>
60+
public class AnalysisStory
61+
{
62+
public string RootFactKey { get; set; } = string.Empty;
63+
public double RootFactValue { get; set; }
64+
public double Severity { get; set; }
65+
public double Confidence { get; set; }
66+
public string Category { get; set; } = string.Empty;
67+
public List<string> Path { get; set; } = [];
68+
public string StoryPath { get; set; } = string.Empty;
69+
public string StoryPathHash { get; set; } = string.Empty;
70+
public string StoryText { get; set; } = string.Empty;
71+
public string? LeafFactKey { get; set; }
72+
public double? LeafFactValue { get; set; }
73+
public int FactCount { get; set; }
74+
public bool IsAbsolution { get; set; }
75+
}
76+
77+
/// <summary>
78+
/// A persisted finding from a previous analysis run.
79+
/// Maps to the analysis_findings DuckDB table.
80+
/// </summary>
81+
public class AnalysisFinding
82+
{
83+
public long FindingId { get; set; }
84+
public DateTime AnalysisTime { get; set; }
85+
public int ServerId { get; set; }
86+
public string ServerName { get; set; } = string.Empty;
87+
public string? DatabaseName { get; set; }
88+
public DateTime? TimeRangeStart { get; set; }
89+
public DateTime? TimeRangeEnd { get; set; }
90+
public double Severity { get; set; }
91+
public double Confidence { get; set; }
92+
public string Category { get; set; } = string.Empty;
93+
public string StoryPath { get; set; } = string.Empty;
94+
public string StoryPathHash { get; set; } = string.Empty;
95+
public string StoryText { get; set; } = string.Empty;
96+
public string RootFactKey { get; set; } = string.Empty;
97+
public double? RootFactValue { get; set; }
98+
public string? LeafFactKey { get; set; }
99+
public double? LeafFactValue { get; set; }
100+
public int FactCount { get; set; }
101+
102+
/// <summary>
103+
/// Drill-down data collected after graph traversal. Ephemeral — not persisted to DuckDB.
104+
/// Contains supporting detail keyed by category (e.g., "top_deadlocks", "queries_at_spike").
105+
/// </summary>
106+
public Dictionary<string, object>? DrillDown { get; set; }
107+
}
108+
109+
/// <summary>
110+
/// A muted finding pattern. Maps to the analysis_muted DuckDB table.
111+
/// </summary>
112+
public class AnalysisMuted
113+
{
114+
public long MuteId { get; set; }
115+
public int? ServerId { get; set; }
116+
public string? DatabaseName { get; set; }
117+
public string StoryPathHash { get; set; } = string.Empty;
118+
public string StoryPath { get; set; } = string.Empty;
119+
public DateTime MutedDate { get; set; }
120+
public string? Reason { get; set; }
121+
}
122+
123+
/// <summary>
124+
/// A user-configured exclusion filter. Maps to the analysis_exclusions DuckDB table.
125+
/// </summary>
126+
public class AnalysisExclusion
127+
{
128+
public long ExclusionId { get; set; }
129+
public string ExclusionType { get; set; } = string.Empty;
130+
public string ExclusionValue { get; set; } = string.Empty;
131+
public int? ServerId { get; set; }
132+
public string? DatabaseName { get; set; }
133+
public bool IsEnabled { get; set; } = true;
134+
public DateTime CreatedDate { get; set; }
135+
public string? Description { get; set; }
136+
}
137+
138+
/// <summary>
139+
/// A severity threshold value. Maps to the analysis_thresholds DuckDB table.
140+
/// </summary>
141+
public class AnalysisThreshold
142+
{
143+
public long ThresholdId { get; set; }
144+
public string Category { get; set; } = string.Empty;
145+
public string FactKey { get; set; } = string.Empty;
146+
public string ThresholdType { get; set; } = string.Empty;
147+
public double ThresholdValue { get; set; }
148+
public int? ServerId { get; set; }
149+
public string? DatabaseName { get; set; }
150+
public bool IsEnabled { get; set; } = true;
151+
public DateTime ModifiedDate { get; set; }
152+
}

0 commit comments

Comments
 (0)