Skip to content
Closed
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
10 changes: 8 additions & 2 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,14 @@ releases/
packages/
*.nupkg

# SQLite databases
*.sqlite
# SQLite databases
*.sqlite

# Headless runtime data
Headless/data/
*.duckdb
*.duckdb.wal
*.parquet

# Lock files
*.lock
Expand Down
8 changes: 8 additions & 0 deletions Headless/Models/CollectorScheduleOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
namespace PerformanceMonitor.Headless.Models;

public sealed class CollectorScheduleOptions
{
public string Name { get; set; } = "";
public bool Enabled { get; set; } = true;
public int FrequencySeconds { get; set; } = 60;
}
29 changes: 29 additions & 0 deletions Headless/Models/MonitorOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
namespace PerformanceMonitor.Headless.Models;

public sealed class MonitorOptions
{
public string StoragePath { get; set; } = "data\\headless\\performance-monitor.duckdb";
public string ArchiveDirectory { get; set; } = "data\\headless\\parquet";
public int CollectionIntervalSeconds { get; set; } = 60;
public int MaxConcurrentServers { get; set; } = 8;
public int CommandTimeoutSeconds { get; set; } = 30;
public int ArchiveIntervalMinutes { get; set; } = 60;
public int HotDataDays { get; set; } = 7;
public List<CollectorScheduleOptions> Collectors { get; set; } = [];
public List<MonitoredServerOptions> Servers { get; set; } = [];

public IReadOnlyList<CollectorScheduleOptions> GetEffectiveCollectors()
{
if (Collectors.Count > 0)
{
return Collectors;
}

return
[
new() { Name = "server_properties", FrequencySeconds = 3600 },
new() { Name = "wait_stats", FrequencySeconds = 60 },
new() { Name = "cpu_utilization", FrequencySeconds = 60 }
];
}
}
28 changes: 28 additions & 0 deletions Headless/Models/MonitoredServerOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace PerformanceMonitor.Headless.Models;

public sealed class MonitoredServerOptions
{
public string Id { get; set; } = "";
public string DisplayName { get; set; } = "";
public string Purpose { get; set; } = "Unassigned";
public string? ConnectionString { get; set; }
public string? ConnectionStringEnvironmentVariable { get; set; }
public bool Enabled { get; set; } = true;

public string ServerNameForStorage => string.IsNullOrWhiteSpace(DisplayName) ? Id : DisplayName;
public string PurposeForDisplay => string.IsNullOrWhiteSpace(Purpose) ? "Unassigned" : Purpose.Trim();

public string ResolveConnectionString()
{
if (!string.IsNullOrWhiteSpace(ConnectionStringEnvironmentVariable))
{
var fromEnvironment = Environment.GetEnvironmentVariable(ConnectionStringEnvironmentVariable);
if (!string.IsNullOrWhiteSpace(fromEnvironment))
{
return Environment.ExpandEnvironmentVariables(fromEnvironment);
}
}

return Environment.ExpandEnvironmentVariables(ConnectionString ?? "");
}
}
82 changes: 82 additions & 0 deletions Headless/Models/TelemetryModels.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
namespace PerformanceMonitor.Headless.Models;

public sealed record ServerHealthDto(
string ServerId,
string DisplayName,
string Purpose,
bool IsEnabled,
DateTime? LastSeenTime,
string LastStatus,
string? LastError,
string? ProductVersion,
string? Edition,
int? SqlMajorVersion,
string HealthState,
string HealthReason,
int ActiveAlertCount,
int? LatestSqlCpuUtilization,
string? TopWaitType);

public sealed record CollectionLogDto(
DateTime CollectionTime,
string ServerId,
string ServerName,
string CollectorName,
string Status,
int RowsCollected,
int DurationMs,
string? ErrorMessage);

public sealed record ActiveAlertDto(
DateTime RaisedAt,
string ServerId,
string ServerName,
string Source,
string Severity,
string Message,
string TargetTab);

public sealed record TopWaitDto(
string WaitType,
long WaitTimeDeltaMs,
long SignalWaitTimeDeltaMs,
long WaitingTasksDelta);

public sealed record CpuSampleDto(
DateTime SampleTime,
int SqlServerCpuUtilization,
int OtherProcessCpuUtilization);

public sealed record EstateSummaryDto(
int ServerCount,
int GreenCount,
int YellowCount,
int RedCount,
int ErrorCount,
int DisabledCount,
DateTime GeneratedAt,
IReadOnlyList<ServerHealthDto> Servers,
IReadOnlyList<ActiveAlertDto> ActiveAlerts);

public sealed record ServerPropertiesSnapshot(
string MachineName,
string? InstanceName,
string ProductVersion,
string ProductLevel,
string Edition,
int EngineEdition,
int SqlMajorVersion,
int CpuCount,
long PhysicalMemoryMb,
DateTime SqlServerStartTime);

public sealed record WaitStatSnapshot(
string WaitType,
long WaitingTasksCount,
long WaitTimeMs,
long SignalWaitTimeMs);

public sealed record CpuSample(
DateTime SampleTime,
int SqlServerCpuUtilization,
int OtherProcessCpuUtilization);
33 changes: 33 additions & 0 deletions Headless/PerformanceMonitor.Headless.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<RootNamespace>PerformanceMonitor.Headless</RootNamespace>
<AssemblyName>PerformanceMonitor.Headless</AssemblyName>
<Product>SQL Server Performance Monitor Headless</Product>
<Version>2.10.0</Version>
<AssemblyVersion>2.10.0.0</AssemblyVersion>
<FileVersion>2.10.0.0</FileVersion>
<InformationalVersion>2.10.0-headless</InformationalVersion>
<Company>Darling Data, LLC</Company>
<Copyright>Copyright (c) 2026 Darling Data, LLC</Copyright>
<EnableNETAnalyzers>true</EnableNETAnalyzers>
<AnalysisLevel>latest-recommended</AnalysisLevel>
<NoWarn>CA1001;CA1305;CA1845;CA1848;CA1861;CA2007;CA2100</NoWarn>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="DuckDB.NET.Data" Version="1.5.2" />
<PackageReference Include="DuckDB.NET.Bindings.Full" Version="1.5.2" />
<PackageReference Include="Microsoft.Data.SqlClient" Version="7.0.1" />
<PackageReference Include="Microsoft.Data.SqlClient.Extensions.Azure" Version="1.0.0" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.7" />
</ItemGroup>

<ItemGroup>
<None Update="appsettings.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>
</ItemGroup>
</Project>
60 changes: 60 additions & 0 deletions Headless/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
using PerformanceMonitor.Headless.Models;
using PerformanceMonitor.Headless.Services;
using PerformanceMonitor.Headless.Storage;

var builder = WebApplication.CreateBuilder(args);

builder.Host.UseWindowsService();
builder.Services.Configure<MonitorOptions>(builder.Configuration.GetSection("Monitor"));
builder.Services.AddSingleton<HeadlessStore>();
builder.Services.AddHostedService<SqlEstateCollectorService>();

var app = builder.Build();

app.UseDefaultFiles();
app.UseStaticFiles();

app.MapGet("/api/health", () => Results.Ok(new { status = "ok", generated_at = DateTime.UtcNow }));

app.MapGet("/api/storage", (HeadlessStore store) => Results.Ok(new
{
duckdb = store.DatabasePath,
parquet = store.ArchiveDirectory
}));

app.MapGet("/api/summary", async (HeadlessStore store, CancellationToken cancellationToken)
=> Results.Ok(await store.GetEstateSummaryAsync(cancellationToken)));

app.MapGet("/api/servers", async (HeadlessStore store, CancellationToken cancellationToken)
=> Results.Ok(await store.GetServersAsync(cancellationToken)));

app.MapGet("/api/alerts", async (HeadlessStore store, CancellationToken cancellationToken)
=> Results.Ok(await store.GetActiveAlertsAsync(cancellationToken)));

app.MapGet("/api/collection-log", async (HeadlessStore store, int? limit, CancellationToken cancellationToken)
=> Results.Ok(await store.GetCollectionLogAsync(limit ?? 200, cancellationToken)));

app.MapGet("/api/servers/{serverId}/waits", async (
string serverId,
HeadlessStore store,
int? hours,
int? limit,
CancellationToken cancellationToken) =>
{
var rows = await store.GetTopWaitsAsync(serverId, hours ?? 1, limit ?? 20, cancellationToken);
return Results.Ok(rows);
});

app.MapGet("/api/servers/{serverId}/cpu", async (
string serverId,
HeadlessStore store,
int? hours,
CancellationToken cancellationToken) =>
{
var rows = await store.GetCpuSamplesAsync(serverId, hours ?? 1, cancellationToken);
return Results.Ok(rows);
});

app.MapFallbackToFile("index.html");

app.Run();
Loading
Loading