diff --git a/CHANGELOG.md b/CHANGELOG.md index f10f250d..ea936445 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,57 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [2.1.0] - 2026-03-04 + +### Important + +- **Schema upgrade**: The `config.collection_schedule` table gains two new columns (`collect_query`, `collect_plan`) for optional query text and execution plan collection. Both default to enabled to preserve existing behavior. Upgrade scripts run automatically via the CLI/GUI installer and use idempotent checks. + +### Added + +- **Light theme and "Cool Breeze" theme** — full light mode support for both Dashboard and Lite with live preview in settings ([#347]) +- **Standalone Plan Viewer** — open, paste (Ctrl+V), or drag & drop `.sqlplan` files independent of any server connection, with tabbed multi-plan support ([#359]) +- **Time display mode toggle** — show timestamps in Server Time, Local Time, or UTC with timezone labels across all grids and tooltips ([#17]) +- **30 PlanAnalyzer rules** — expanded from 12 to 30 rules covering implicit conversions, GetRangeThroughConvert, lazy spools, OR expansion, exchange spills, RID lookups, and more ([#327], [#349], [#356], [#379]) +- **Wait stats banner** in plan viewer showing top waits for the query ([#373]) +- **UDF runtime details** — CPU and elapsed time shown in Runtime Summary pane when UDFs are present ([#382]) +- **Sortable statement grid** and canvas panning in plan viewer ([#331]) +- **Comma-separated column filters** — enter multiple values separated by commas in text filters ([#348]) +- **Optional query text and plan collection** — per-collector flags in `config.collection_schedule` to disable query text or plan capture ([#337]) +- **`--preserve-jobs` installer flag** — keep existing SQL Agent job schedules during upgrade ([#326]) +- **Copy Query Text** context menu on Dashboard statements grid ([#367]) +- **Server list sorting** by display name in both Dashboard and Lite ([#30]) +- **Warning status icon** in server health indicators ([#355]) +- Reserved threads and 10 missing ShowPlan XML attributes in plan viewer ([#378]) +- Nightly build workflow for CI ([#332]) + +### Changed + +- PlanAnalyzer warning messages rewritten to be actionable with expert-guided per-rule advice ([#370], [#371]) +- PlanAnalyzer rule tuning: time-based spill analysis (Rule 7), lowered parallel skew thresholds (Rule 8), memory grant floor raised to 1GB/4GB (Rule 9), skip PROBE-only bitmap predicates (Rule 11) ([#341], [#342], [#343], [#358]) +- First-run collector lookback reduced from 3-7 days to 1 hour for faster initial data ([#335]) +- Plan canvas aligns top-left and resets scroll on statement switch ([#366]) +- Plan viewer polish: index suggestions, property panel improvements, muted brush audit ([#365]) +- Add Server dialog visual parity between Dashboard and Lite with theme-driven PasswordBox styling ([#289]) + +### Fixed + +- **OverflowException** on wait stats page with large decimal values — SQL Server `decimal(38,24)` exceeding .NET precision ([#395]) +- **SQL dumps** on mirroring passive servers with RESTORING databases ([#384]) +- **UI hang** when adding first server to Dashboard ([#387]) +- **UTC/local timezone mismatch** in blocked process XML processor ([#383]) +- **AG secondary filter** skipping all inaccessible databases in cross-database collectors ([#325]) +- DuckDB column aliases in long-running queries ([#391]) +- sp_server_diagnostics and WAITFOR excluded from long-running query alerts ([#362]) +- UDF timing units corrected: microseconds to milliseconds ([#338]) +- DuckDB migration ordering after archive-and-reset ([#314]) +- Int16 cast error in long-running query alerts ([#313]) +- Missing dark mode on 19 SystemEventsContent charts ([#321]) +- Missing tooltips on charts after theme changes ([#319]) +- Operator time per-thread calculation synced across all plan viewers ([#392]) +- Theme StaticResource/DynamicResource binding fix for runtime theme switching +- Memory grant MB display, missing index quality scoring, wildcard LIKE detection ([#393]) + ## [2.0.0] - 2026-02-25 ### Important @@ -180,6 +231,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Delta normalization for per-second rate calculations - Dark theme UI +[2.1.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v2.0.0...v2.1.0 [2.0.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v1.3.0...v2.0.0 [1.3.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v1.2.0...v1.3.0 [1.2.0]: https://github.com/erikdarlingdata/PerformanceMonitor/compare/v1.1.0...v1.2.0 @@ -274,3 +326,45 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 [#281]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/281 [#284]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/284 [#287]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/287 +[#313]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/313 +[#314]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/314 +[#17]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/17 +[#30]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/30 +[#319]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/319 +[#321]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/321 +[#325]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/325 +[#326]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/326 +[#327]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/327 +[#331]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/331 +[#332]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/332 +[#335]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/335 +[#337]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/337 +[#338]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/338 +[#341]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/341 +[#342]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/342 +[#343]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/343 +[#347]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/347 +[#348]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/348 +[#349]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/349 +[#355]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/355 +[#356]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/356 +[#358]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/358 +[#359]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/359 +[#362]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/362 +[#365]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/365 +[#366]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/366 +[#367]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/367 +[#370]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/370 +[#371]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/371 +[#373]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/373 +[#378]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/378 +[#379]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/379 +[#382]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/382 +[#383]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/383 +[#384]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/384 +[#387]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/387 +[#391]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/391 +[#392]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/392 +[#393]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/393 +[#289]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/289 +[#395]: https://github.com/erikdarlingdata/PerformanceMonitor/issues/395 diff --git a/Dashboard/Dashboard.csproj b/Dashboard/Dashboard.csproj index 76222ed5..0f215418 100644 --- a/Dashboard/Dashboard.csproj +++ b/Dashboard/Dashboard.csproj @@ -7,6 +7,9 @@ PerformanceMonitorDashboard SQL Server Performance Monitor Dashboard 2.1.0 + 2.1.0.0 + 2.1.0.0 + 2.1.0 Darling Data, LLC Copyright © 2026 Darling Data, LLC EDD.ico diff --git a/Installer/Program.cs b/Installer/Program.cs index f00279ba..aa91403e 100644 --- a/Installer/Program.cs +++ b/Installer/Program.cs @@ -775,6 +775,14 @@ This validates the installation and creates dynamically-generated tables { await connection.OpenAsync().ConfigureAwait(false); + /*Capture timestamp before running so we only check errors from this run. + Use SYSDATETIME() (local) because collection_time is stored in server local time.*/ + DateTime validationStart; + using (var command = new SqlCommand("SELECT SYSDATETIME();", connection)) + { + validationStart = (DateTime)(await command.ExecuteScalarAsync().ConfigureAwait(false))!; + } + /*Run master collector once with @force_run_all to collect everything immediately*/ Console.Write("Executing master collector... "); using (var command = new SqlCommand("EXECUTE PerformanceMonitor.collect.scheduled_master_collector @force_run_all = 1, @debug = 0;", connection)) @@ -785,37 +793,46 @@ This validates the installation and creates dynamically-generated tables Console.WriteLine("✓ Success"); /* - Verify data was collected + Verify data was collected — only from this validation run, not historical errors */ Console.WriteLine(); Console.Write("Verifying data collection... "); - /* First check total count in collection_log */ - int totalLogEntries = 0; - using (var command = new SqlCommand(@" - SELECT COUNT(*) FROM PerformanceMonitor.config.collection_log;", connection)) - { - totalLogEntries = (int)(await command.ExecuteScalarAsync().ConfigureAwait(false) ?? 0); - } - - /* Check successful collections (all time - we just installed) */ + /* Check successful collections from this run */ int collectedCount = 0; using (var command = new SqlCommand(@" SELECT COUNT(DISTINCT collector_name) FROM PerformanceMonitor.config.collection_log - WHERE collection_status = 'SUCCESS';", connection)) + WHERE collection_status = 'SUCCESS' + AND collection_time >= @validation_start;", connection)) { + command.Parameters.AddWithValue("@validation_start", validationStart); collectedCount = (int)(await command.ExecuteScalarAsync().ConfigureAwait(false) ?? 0); } + /* Total log entries from this run */ + int totalLogEntries = 0; + using (var command = new SqlCommand(@" + SELECT COUNT(*) + FROM PerformanceMonitor.config.collection_log + WHERE collection_time >= @validation_start;", connection)) + { + command.Parameters.AddWithValue("@validation_start", validationStart); + totalLogEntries = (int)(await command.ExecuteScalarAsync().ConfigureAwait(false) ?? 0); + } + Console.WriteLine($"✓ {collectedCount} collectors ran successfully (total log entries: {totalLogEntries})"); - /* Show failed collectors if any */ + /* Show failed collectors from this run */ int errorCount = 0; using (var command = new SqlCommand(@" - SELECT COUNT(*) FROM PerformanceMonitor.config.collection_log WHERE collection_status = 'ERROR';", connection)) + SELECT COUNT(*) + FROM PerformanceMonitor.config.collection_log + WHERE collection_status = 'ERROR' + AND collection_time >= @validation_start;", connection)) { + command.Parameters.AddWithValue("@validation_start", validationStart); errorCount = (int)(await command.ExecuteScalarAsync().ConfigureAwait(false) ?? 0); } @@ -829,8 +846,10 @@ FROM PerformanceMonitor.config.collection_log error_message FROM PerformanceMonitor.config.collection_log WHERE collection_status = 'ERROR' + AND collection_time >= @validation_start ORDER BY collection_time DESC;", connection)) { + command.Parameters.AddWithValue("@validation_start", validationStart); using (var reader = await command.ExecuteReaderAsync().ConfigureAwait(false)) { while (await reader.ReadAsync().ConfigureAwait(false)) @@ -857,8 +876,10 @@ SELECT TOP 5 error_message FROM PerformanceMonitor.config.collection_log WHERE collection_status = 'SUCCESS' + AND collection_time >= @validation_start ORDER BY collection_time DESC;", connection)) { + command.Parameters.AddWithValue("@validation_start", validationStart); using (var reader = await command.ExecuteReaderAsync().ConfigureAwait(false)) { while (await reader.ReadAsync().ConfigureAwait(false)) diff --git a/InstallerGui/InstallerGui.csproj b/InstallerGui/InstallerGui.csproj index 392d4a36..5a1fbd4f 100644 --- a/InstallerGui/InstallerGui.csproj +++ b/InstallerGui/InstallerGui.csproj @@ -9,6 +9,9 @@ PerformanceMonitorInstallerGui SQL Server Performance Monitor Installer 2.1.0 + 2.1.0.0 + 2.1.0.0 + 2.1.0 Darling Data, LLC Copyright © 2026 Darling Data, LLC EDD.ico diff --git a/InstallerGui/Services/InstallationService.cs b/InstallerGui/Services/InstallationService.cs index dd6705b6..30e7b668 100644 --- a/InstallerGui/Services/InstallationService.cs +++ b/InstallerGui/Services/InstallationService.cs @@ -659,6 +659,14 @@ public async Task InstallDependenciesAsync( using var connection = new SqlConnection(connectionString); await connection.OpenAsync(cancellationToken).ConfigureAwait(false); + /*Capture timestamp before running so we only check errors from this run. + Use SYSDATETIME() (local) because collection_time is stored in server local time.*/ + DateTime validationStart; + using (var command = new SqlCommand("SELECT SYSDATETIME();", connection)) + { + validationStart = (DateTime)(await command.ExecuteScalarAsync(cancellationToken).ConfigureAwait(false))!; + } + /*Run master collector with @force_run_all*/ progress?.Report(new InstallationProgress { @@ -680,7 +688,7 @@ public async Task InstallDependenciesAsync( Status = "Success" }); - /*Check results*/ + /*Check results — only from this validation run, not historical errors*/ int successCount = 0; int errorCount = 0; @@ -688,8 +696,10 @@ public async Task InstallDependenciesAsync( SELECT success_count = COUNT_BIG(DISTINCT CASE WHEN collection_status = 'SUCCESS' THEN collector_name END), error_count = SUM(CASE WHEN collection_status = 'ERROR' THEN 1 ELSE 0 END) - FROM PerformanceMonitor.config.collection_log;", connection)) + FROM PerformanceMonitor.config.collection_log + WHERE collection_time >= @validation_start;", connection)) { + command.Parameters.AddWithValue("@validation_start", validationStart); using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); if (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) { @@ -711,7 +721,9 @@ public async Task InstallDependenciesAsync( SELECT collector_name, error_message FROM PerformanceMonitor.config.collection_log WHERE collection_status = 'ERROR' + AND collection_time >= @validation_start ORDER BY collection_time DESC;", connection); + command.Parameters.AddWithValue("@validation_start", validationStart); using var reader = await command.ExecuteReaderAsync(cancellationToken).ConfigureAwait(false); while (await reader.ReadAsync(cancellationToken).ConfigureAwait(false)) diff --git a/Lite/PerformanceMonitorLite.csproj b/Lite/PerformanceMonitorLite.csproj index 6604905b..88ee7eb2 100644 --- a/Lite/PerformanceMonitorLite.csproj +++ b/Lite/PerformanceMonitorLite.csproj @@ -8,6 +8,9 @@ PerformanceMonitorLite SQL Server Performance Monitor Lite 2.1.0 + 2.1.0.0 + 2.1.0.0 + 2.1.0 Darling Data, LLC Copyright © 2026 Darling Data, LLC Lightweight SQL Server performance monitoring - no installation required on target servers diff --git a/README.md b/README.md index b0e4b57b..6b383ad9 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ | **Requires** | sysadmin + SQL Agent running | `VIEW SERVER STATE` (that's it) | | **Get started** | Run the installer, open the dashboard | Download, run, add a server, done | -Both editions include real-time alerts (system tray + email), charts and graphs, dark theme, CSV export, and a built-in MCP server for AI-powered analysis with tools like Claude. +Both editions include real-time alerts (system tray + email), charts and graphs, dark and light themes, CSV export, and a built-in MCP server for AI-powered analysis with tools like Claude. --- @@ -38,11 +38,13 @@ Both editions include real-time alerts (system tray + email), charts and graphs, ## What You Get -🔍 **30+ specialized T-SQL collectors** running on configurable schedules — wait stats, query performance, blocking chains, deadlock graphs, memory grants, file I/O, tempdb, perfmon counters, and more +🔍 **32 specialized T-SQL collectors** running on configurable schedules — wait stats, query performance, blocking chains, deadlock graphs, memory grants, file I/O, tempdb, perfmon counters, and more. Query text and execution plan collection can be disabled per-collector for sensitive environments. 🚨 **Real-time alerts** for blocking, deadlocks, and high CPU — system tray notifications plus styled HTML emails with full XML attachments for offline analysis -📊 **NOC-style dashboard** with green/yellow/red health cards, auto-refresh, configurable time ranges, and dark theme +📊 **NOC-style dashboard** with green/yellow/red health cards, auto-refresh, configurable time ranges, and dark/light themes + +📋 **Graphical plan viewer** with native ShowPlan rendering, 30-rule PlanAnalyzer, operator-level cost breakdown, and a standalone mode for opening `.sqlplan` files without a server connection 🤖 **Built-in MCP server** with 27-31 read-only tools for AI analysis — ask Claude Code or Cursor "what are the top wait types on my server?" and get answers from your actual monitoring data @@ -155,8 +157,10 @@ The installer automatically tests the connection, executes SQL scripts, download | `SERVER` | SQL Server instance name (positional, required) | | `USERNAME PASSWORD` | SQL Authentication credentials (positional, optional) | | `--reinstall` | Drop existing database and perform clean install | +| `--preserve-jobs` | Keep existing SQL Agent job schedules during upgrade | | `--encrypt=optional\|mandatory\|strict` | Connection encryption level (default: mandatory) | | `--trust-cert` | Trust server certificate without validation (default: require valid cert) | +| `--help` | Show usage information and exit | **Environment variable:** Set `PM_SQL_PASSWORD` to avoid passing the password on the command line. @@ -190,7 +194,8 @@ ORDER BY collection_time DESC; ### What Gets Installed - **PerformanceMonitor database** with collection tables and reporting views -- **30 collector stored procedures** for gathering metrics (including SQL Agent job monitoring) +- **32 collector stored procedures** for gathering metrics (including SQL Agent job monitoring) +- **Configurable collection** — query text and execution plan capture can be disabled per-collector via `config.collection_schedule` (`collect_query`, `collect_plan` columns) for sensitive or high-volume environments - **Delta framework** for calculating per-second rates from cumulative DMVs - **Community dependencies:** sp_WhoIsActive, sp_HealthParser, sp_HumanEventsBlockViewer, sp_BlitzLock - **SQL Agent jobs:** Collection (every 1 minute), Data Retention (daily at 2:00 AM), and Hung Job Monitor (collection job watchdog, every 5 minutes) @@ -259,13 +264,16 @@ The Full Edition supports Azure SQL Managed Instance and AWS RDS for SQL Server | AWS RDS for SQL Server | Supported | Supported | | Azure SQL Database | Not supported | Supported | | Multi-server from one seat | Per-server install | Built-in | -| Collectors | 30 | 20 | +| Collectors | 32 | 20 | | Agent job monitoring | Duration vs historical avg/p95 | Duration vs historical avg/p95 | | Data storage | SQL Server (on target) | DuckDB + Parquet (local) | -| Execution plans | Collected and stored | Download on demand | +| Execution plans | Collected and stored (can be disabled per-collector) | Download on demand | +| Graphical plan viewer | Built-in with 30-rule PlanAnalyzer | Built-in with 30-rule PlanAnalyzer | +| Standalone plan viewer | Open/paste/drag `.sqlplan` files | Open/paste/drag `.sqlplan` files | | Community tools (sp_WhoIsActive, sp_BlitzLock) | Installed automatically | Not needed | | Alerts (tray + email) | Blocking, deadlocks, CPU | Blocking, deadlocks, CPU | | Dashboard | Separate app | Built-in | +| Themes | Dark and light | Dark and light | | Portability | Server-bound | Single executable | | MCP server (LLM integration) | Built into Dashboard (27 tools) | Built-in (31 tools) | @@ -301,7 +309,7 @@ Plus a NOC-style landing page with server health cards (green/yellow/red severit | **Perfmon** | Selectable SQL Server performance counters over time | | **Configuration** | Server configuration, database configuration, scoped configuration, trace flags | -Both editions feature auto-refresh, configurable time ranges, right-click CSV export, system tray integration, and dark theme. +Both editions feature auto-refresh, configurable time ranges, right-click CSV export, system tray integration, dark and light themes, and timezone display options (server time, local time, or UTC). --- @@ -463,7 +471,7 @@ Common issues: Monitor/ │ │ Full Edition (server-installed collectors + separate dashboard) -├── install/ # 55 SQL installation scripts +├── install/ # 54 SQL installation scripts ├── upgrades/ # Version-specific upgrade scripts ├── Installer/ # CLI installer for Full Edition database (C#) ├── InstallerGui/ # GUI installer for Full Edition database (WPF) diff --git a/upgrades/2.0.0-to-2.1.0/02_default_trace_events_new_columns.sql b/upgrades/2.0.0-to-2.1.0/02_default_trace_events_new_columns.sql new file mode 100644 index 00000000..1e28811d --- /dev/null +++ b/upgrades/2.0.0-to-2.1.0/02_default_trace_events_new_columns.sql @@ -0,0 +1,40 @@ +/* +Copyright 2026 Darling Data, LLC +https://www.erikdarling.com/ + +Add duration_us and end_time columns to collect.default_trace_events +These columns were added in the 2.1.0 install scripts for the default trace collector +*/ + +USE PerformanceMonitor; +GO + +IF NOT EXISTS +( + SELECT + 1/0 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = N'collect' + AND TABLE_NAME = N'default_trace_events' + AND COLUMN_NAME = N'duration_us' +) +BEGIN + ALTER TABLE collect.default_trace_events + ADD duration_us bigint NULL; +END; +GO + +IF NOT EXISTS +( + SELECT + 1/0 + FROM INFORMATION_SCHEMA.COLUMNS + WHERE TABLE_SCHEMA = N'collect' + AND TABLE_NAME = N'default_trace_events' + AND COLUMN_NAME = N'end_time' +) +BEGIN + ALTER TABLE collect.default_trace_events + ADD end_time datetime2(7) NULL; +END; +GO diff --git a/upgrades/2.0.0-to-2.1.0/upgrade.txt b/upgrades/2.0.0-to-2.1.0/upgrade.txt index cffb7769..0d1781e5 100644 --- a/upgrades/2.0.0-to-2.1.0/upgrade.txt +++ b/upgrades/2.0.0-to-2.1.0/upgrade.txt @@ -1 +1,2 @@ 01_collection_schedule_optional_text.sql +02_default_trace_events_new_columns.sql