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
60 changes: 59 additions & 1 deletion Lite/Controls/ServerTab.xaml
Original file line number Diff line number Diff line change
Expand Up @@ -1130,6 +1130,63 @@
</Grid>
</TabItem>

<!-- Daily Summary Tab -->
<TabItem Header="Daily Summary">
<Grid Margin="8">
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>

<!-- Date Selection -->
<StackPanel Grid.Row="0" Orientation="Horizontal" Margin="0,0,0,6">
<TextBlock Text="Summary Date:" VerticalAlignment="Center" Margin="0,0,6,0" FontWeight="Bold"/>
<Button x:Name="DailySummaryTodayButton" Content="Today" Click="DailySummaryToday_Click" Margin="0,0,6,0" Padding="8,4" MinWidth="60" Style="{StaticResource AccentButton}"/>
<DatePicker x:Name="DailySummaryDatePicker" Width="120" Margin="0,0,6,0" SelectedDateChanged="DailySummaryDate_Changed" CalendarOpened="DatePicker_CalendarOpened"/>
<Button Content="Refresh" Click="DailySummaryRefresh_Click" Margin="0,0,6,0" Padding="8,4" MinWidth="60" Style="{StaticResource SuccessButton}"/>
<TextBlock x:Name="DailySummaryIndicator" Text="Showing: Today (UTC)" VerticalAlignment="Center"
FontStyle="Italic" Foreground="{DynamicResource AccentBrush}" FontWeight="SemiBold"/>
</StackPanel>

<!-- Summary Grid -->
<DataGrid Grid.Row="1" x:Name="DailySummaryGrid"
AutoGenerateColumns="False" IsReadOnly="True"
RowStyle="{StaticResource GridRowStyle}"
HeadersVisibility="Column" GridLinesVisibility="Horizontal">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding SummaryDateFormatted}" Width="110">
<DataGridTextColumn.Header><TextBlock Text="Date" FontWeight="Bold"/></DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding OverallHealth}" Width="120">
<DataGridTextColumn.Header><TextBlock Text="Health" FontWeight="Bold"/></DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding TotalWaitFormatted}" ElementStyle="{StaticResource NumericCell}" Width="120">
<DataGridTextColumn.Header><TextBlock Text="Total Wait" FontWeight="Bold"/></DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding TopWaitType}" Width="180">
<DataGridTextColumn.Header><TextBlock Text="Top Wait Type" FontWeight="Bold"/></DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding UniqueQueries, StringFormat=N0}" ElementStyle="{StaticResource NumericCell}" Width="120">
<DataGridTextColumn.Header><TextBlock Text="Unique Queries" FontWeight="Bold"/></DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding DeadlockCount, StringFormat=N0}" ElementStyle="{StaticResource NumericCell}" Width="100">
<DataGridTextColumn.Header><TextBlock Text="Deadlocks" FontWeight="Bold"/></DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding BlockingEvents, StringFormat=N0}" ElementStyle="{StaticResource NumericCell}" Width="120">
<DataGridTextColumn.Header><TextBlock Text="Blocking Events" FontWeight="Bold"/></DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding HighCpuEvents, StringFormat=N0}" ElementStyle="{StaticResource NumericCell}" Width="110">
<DataGridTextColumn.Header><TextBlock Text="High CPU" FontWeight="Bold"/></DataGridTextColumn.Header>
</DataGridTextColumn>
<DataGridTextColumn Binding="{Binding CollectionErrors, StringFormat=N0}" ElementStyle="{StaticResource NumericCell}" Width="120">
<DataGridTextColumn.Header><TextBlock Text="Collect Errors" FontWeight="Bold"/></DataGridTextColumn.Header>
</DataGridTextColumn>
</DataGrid.Columns>
</DataGrid>
<TextBlock Grid.Row="1" x:Name="DailySummaryNoData" Style="{StaticResource NoDataMessage}"/>
</Grid>
</TabItem>

<!-- Collection Health Tab -->
<TabItem Header="Collection Health">
<Grid Margin="8">
Expand All @@ -1138,7 +1195,8 @@
<DataGrid x:Name="CollectionHealthGrid"
AutoGenerateColumns="False" IsReadOnly="True"
RowStyle="{StaticResource GridRowStyle}"
HeadersVisibility="Column" GridLinesVisibility="Horizontal">
HeadersVisibility="Column" GridLinesVisibility="Horizontal"
MouseDoubleClick="CollectionHealthGrid_MouseDoubleClick">
<DataGrid.Columns>
<DataGridTextColumn Binding="{Binding CollectorName}" Width="180">
<DataGridTextColumn.Header><StackPanel Orientation="Horizontal"><Button Style="{DynamicResource ColumnFilterButtonStyle}" Tag="CollectorName" Click="FilterButton_Click" Margin="0,0,4,0"/><TextBlock Text="Collector" FontWeight="Bold" VerticalAlignment="Center"/></StackPanel></DataGridTextColumn.Header>
Expand Down
53 changes: 52 additions & 1 deletion Lite/Controls/ServerTab.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public partial class ServerTab : UserControl
private DataGridFilterManager<TraceFlagRow>? _traceFlagsFilterMgr;
private DataGridFilterManager<CollectorHealthRow>? _collectionHealthFilterMgr;
private DataGridFilterManager<CollectionLogRow>? _collectionLogFilterMgr;
private DateTime? _dailySummaryDate; // null = today

private static readonly HashSet<string> _defaultPerfmonCounters = new(StringComparer.OrdinalIgnoreCase)
{
Expand Down Expand Up @@ -488,14 +489,15 @@ private async System.Threading.Tasks.Task RefreshAllDataAsync()
var runningJobsTask = SafeQueryAsync(() => _dataService.GetRunningJobsAsync(_serverId));
var collectionHealthTask = SafeQueryAsync(() => _dataService.GetCollectionHealthAsync(_serverId));
var collectionLogTask = SafeQueryAsync(() => _dataService.GetRecentCollectionLogAsync(_serverId, hoursBack));
var dailySummaryTask = _dataService.GetDailySummaryAsync(_serverId, _dailySummaryDate);
/* Core data tasks */
await System.Threading.Tasks.Task.WhenAll(
snapshotsTask, cpuTask, memoryTask, memoryTrendTask,
queryStatsTask, procStatsTask, fileIoTask, fileIoTrendTask, tempDbTask, tempDbFileIoTask,
deadlockTask, blockedProcessTask, waitTypesTask, memoryClerkTypesTask, perfmonCountersTask,
queryStoreTask, memoryGrantTrendTask,
serverConfigTask, databaseConfigTask, databaseScopedConfigTask, traceFlagsTask,
runningJobsTask, collectionHealthTask, collectionLogTask);
runningJobsTask, collectionHealthTask, collectionLogTask, dailySummaryTask);

/* Trend chart tasks - run separately so failures don't kill the whole refresh */
var lockWaitTrendTask = SafeQueryAsync(() => _dataService.GetLockWaitTrendAsync(_serverId, hoursBack, fromDate, toDate));
Expand Down Expand Up @@ -538,6 +540,11 @@ await System.Threading.Tasks.Task.WhenAll(
_runningJobsFilterMgr!.UpdateData(runningJobsTask.Result);
_collectionHealthFilterMgr!.UpdateData(collectionHealthTask.Result);
_collectionLogFilterMgr!.UpdateData(collectionLogTask.Result);
var dailySummary = await dailySummaryTask;
DailySummaryGrid.ItemsSource = dailySummary != null
? new List<DailySummaryRow> { dailySummary } : null;
DailySummaryNoData.Visibility = dailySummary == null
? System.Windows.Visibility.Visible : System.Windows.Visibility.Collapsed;
UpdateCollectorDurationChart(collectionLogTask.Result);

/* Update memory summary */
Expand Down Expand Up @@ -2063,6 +2070,50 @@ private void QueryStoreGrid_MouseDoubleClick(object sender, System.Windows.Input
}


private void CollectionHealthGrid_MouseDoubleClick(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
if (CollectionHealthGrid.SelectedItem is not CollectorHealthRow item) return;

var window = new Windows.CollectionLogWindow(_dataService, _serverId, item.CollectorName);
window.Owner = Window.GetWindow(this);
window.ShowDialog();
}

private void DailySummaryToday_Click(object sender, RoutedEventArgs e)
{
_dailySummaryDate = null;
DailySummaryDatePicker.SelectedDate = null;
DailySummaryTodayButton.FontWeight = FontWeights.Bold;
DailySummaryIndicator.Text = "Showing: Today (UTC)";
DailySummaryRefresh_Click(sender, e);
}

private void DailySummaryDate_Changed(object sender, SelectionChangedEventArgs e)
{
if (DailySummaryDatePicker.SelectedDate.HasValue)
{
_dailySummaryDate = DailySummaryDatePicker.SelectedDate.Value.Date;
DailySummaryTodayButton.FontWeight = FontWeights.Normal;
DailySummaryIndicator.Text = $"Showing: {_dailySummaryDate.Value:MMM d, yyyy}";
}
}

private async void DailySummaryRefresh_Click(object sender, RoutedEventArgs e)
{
try
{
var result = await _dataService.GetDailySummaryAsync(_serverId, _dailySummaryDate);
DailySummaryGrid.ItemsSource = result != null
? new List<DailySummaryRow> { result } : null;
DailySummaryNoData.Visibility = result == null
? Visibility.Visible : Visibility.Collapsed;
}
catch (Exception ex)
{
AppLogger.Error("DailySummary", $"Error refreshing: {ex.Message}");
}
}

private async void DownloadQueryStatsPlan_Click(object sender, RoutedEventArgs e)
{
if (sender is not Button btn || btn.DataContext is not QueryStatsRow row) return;
Expand Down
49 changes: 49 additions & 0 deletions Lite/Services/LocalDataService.CollectionHealth.cs
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,55 @@ ORDER BY collection_time DESC

return items;
}

/// <summary>
/// Gets collection log entries for a specific collector on a server.
/// </summary>
public async Task<List<CollectionLogRow>> GetCollectionLogByCollectorAsync(int serverId, string collectorName, int hoursBack = 168)
{
using var connection = await OpenConnectionAsync();
using var command = connection.CreateCommand();
command.CommandText = @"
SELECT
collector_name,
collection_time,
duration_ms,
sql_duration_ms,
duckdb_duration_ms,
rows_collected,
status,
error_message,
server_name
FROM v_collection_log
WHERE server_id = $1
AND collector_name = $2
AND collection_time >= $3
ORDER BY collection_time DESC";

command.Parameters.Add(new DuckDBParameter { Value = serverId });
command.Parameters.Add(new DuckDBParameter { Value = collectorName });
command.Parameters.Add(new DuckDBParameter { Value = DateTime.UtcNow.AddHours(-hoursBack) });

var items = new List<CollectionLogRow>();
using var reader = await command.ExecuteReaderAsync();
while (await reader.ReadAsync())
{
items.Add(new CollectionLogRow
{
CollectorName = reader.GetString(0),
CollectionTime = reader.GetDateTime(1),
DurationMs = reader.IsDBNull(2) ? null : (int?)Convert.ToInt32(reader.GetValue(2)),
SqlDurationMs = reader.IsDBNull(3) ? null : (int?)Convert.ToInt32(reader.GetValue(3)),
DuckDbDurationMs = reader.IsDBNull(4) ? null : (int?)Convert.ToInt32(reader.GetValue(4)),
RowsCollected = reader.IsDBNull(5) ? null : (int?)Convert.ToInt32(reader.GetValue(5)),
Status = reader.GetString(6),
ErrorMessage = reader.IsDBNull(7) ? null : reader.GetString(7),
ServerName = reader.IsDBNull(8) ? null : reader.GetString(8)
});
}

return items;
}
}

public class CollectionLogRow
Expand Down
127 changes: 127 additions & 0 deletions Lite/Services/LocalDataService.DailySummary.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
/*
* Copyright (c) 2026 Erik Darling, Darling Data LLC
*
* This file is part of the SQL Server Performance Monitor Lite.
*
* Licensed under the MIT License. See LICENSE file in the project root for full license information.
*/

using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using DuckDB.NET.Data;

namespace PerformanceMonitorLite.Services;

public partial class LocalDataService
{
/// <summary>
/// Gets daily summary for a specific date (or today if null).
/// </summary>
public async Task<DailySummaryRow?> GetDailySummaryAsync(int serverId, DateTime? summaryDate = null)
{
using var connection = await OpenConnectionAsync();
using var command = connection.CreateCommand();

var targetDate = summaryDate?.Date ?? DateTime.UtcNow.Date;
var dayStart = targetDate;
var dayEnd = targetDate.AddDays(1);

command.CommandText = @"
SELECT
COALESCE(
(SELECT SUM(delta_wait_time_ms) / 1000.0
FROM v_wait_stats
WHERE server_id = $1
AND collection_time >= $2 AND collection_time < $3
AND delta_wait_time_ms > 0), 0
) AS total_wait_sec,
(SELECT wait_type
FROM v_wait_stats
WHERE server_id = $1
AND collection_time >= $2 AND collection_time < $3
AND delta_wait_time_ms > 0
ORDER BY delta_wait_time_ms DESC
LIMIT 1
) AS top_wait_type,
COALESCE(
(SELECT COUNT(DISTINCT query_hash)
FROM v_query_stats
WHERE server_id = $1
AND collection_time >= $2 AND collection_time < $3), 0
) AS unique_queries,
COALESCE(
(SELECT COUNT(*)
FROM v_deadlocks
WHERE server_id = $1
AND collection_time >= $2 AND collection_time < $3), 0
) AS deadlock_count,
COALESCE(
(SELECT COUNT(*)
FROM v_blocked_process_reports
WHERE server_id = $1
AND collection_time >= $2 AND collection_time < $3), 0
) AS blocking_events,
COALESCE(
(SELECT COUNT(*)
FROM v_cpu_utilization_stats
WHERE server_id = $1
AND sqlserver_cpu_utilization >= 80
AND collection_time >= $2 AND collection_time < $3), 0
) AS high_cpu_events,
COALESCE(
(SELECT COUNT(*)
FROM v_collection_log
WHERE server_id = $1
AND status = 'ERROR'
AND collection_time >= $2 AND collection_time < $3), 0
) AS collection_errors";

command.Parameters.Add(new DuckDBParameter { Value = serverId });
command.Parameters.Add(new DuckDBParameter { Value = dayStart });
command.Parameters.Add(new DuckDBParameter { Value = dayEnd });

using var reader = await command.ExecuteReaderAsync();
if (!await reader.ReadAsync()) return null;

var deadlocks = reader.IsDBNull(3) ? 0L : Convert.ToInt64(reader.GetValue(3));
var blocking = reader.IsDBNull(4) ? 0L : Convert.ToInt64(reader.GetValue(4));
var highCpu = reader.IsDBNull(5) ? 0L : Convert.ToInt64(reader.GetValue(5));

var health = "NORMAL";
if (deadlocks > 0) health = "DEADLOCKS";
else if (highCpu > 5) health = "CPU_CRITICAL";
else if (blocking > 10) health = "BLOCKING";

return new DailySummaryRow
{
SummaryDate = targetDate,
TotalWaitTimeSec = reader.IsDBNull(0) ? 0m : Convert.ToDecimal(reader.GetValue(0)),
TopWaitType = reader.IsDBNull(1) ? "" : reader.GetString(1),
UniqueQueries = reader.IsDBNull(2) ? 0L : Convert.ToInt64(reader.GetValue(2)),
DeadlockCount = deadlocks,
BlockingEvents = blocking,
HighCpuEvents = highCpu,
CollectionErrors = reader.IsDBNull(6) ? 0L : Convert.ToInt64(reader.GetValue(6)),
OverallHealth = health
};
}
}

public class DailySummaryRow
{
public DateTime SummaryDate { get; set; }
public decimal TotalWaitTimeSec { get; set; }
public string TopWaitType { get; set; } = "";
public long UniqueQueries { get; set; }
public long DeadlockCount { get; set; }
public long BlockingEvents { get; set; }
public long HighCpuEvents { get; set; }
public long CollectionErrors { get; set; }
public string OverallHealth { get; set; } = "";

public string SummaryDateFormatted => SummaryDate.ToString("yyyy-MM-dd");
public string TotalWaitFormatted => TotalWaitTimeSec < 1000
? $"{TotalWaitTimeSec:N1} s"
: $"{TotalWaitTimeSec / 60:N1} min";
}
Loading
Loading