feat(host): add Serilog logging with console and file sinks

Configure structured logging following logging_style.md guidelines with
daily rolling log files (30-day retention) and proper shutdown handling.
This commit is contained in:
Joseph Doherty
2026-01-28 16:38:52 -05:00
parent f67b0e806e
commit 5dd17cbab8
2 changed files with 101 additions and 62 deletions
@@ -14,6 +14,10 @@
<ItemGroup> <ItemGroup>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" /> <PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" Version="10.0.1" />
<PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.1" /> <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="10.0.1" />
<PackageReference Include="Serilog" Version="4.*" />
<PackageReference Include="Serilog.Extensions.Logging" Version="8.*" />
<PackageReference Include="Serilog.Sinks.Console" Version="6.*" />
<PackageReference Include="Serilog.Sinks.File" Version="6.*" />
</ItemGroup> </ItemGroup>
<PropertyGroup> <PropertyGroup>
+44 -9
View File
@@ -4,30 +4,52 @@ using JdeScoping.DataSync.Options;
using JdeScoping.ExcelIO.Options; using JdeScoping.ExcelIO.Options;
using JdeScoping.Database; using JdeScoping.Database;
using JdeScoping.Host.Startup; using JdeScoping.Host.Startup;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Serilog;
using Serilog.Events;
// Configure Serilog with console and daily rolling file
var logsPath = Path.Combine(AppContext.BaseDirectory, "logs");
Directory.CreateDirectory(logsPath);
Log.Logger = new LoggerConfiguration()
.MinimumLevel.Information()
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
.MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning)
.Enrich.FromLogContext()
.WriteTo.Console(
outputTemplate: "{Timestamp:HH:mm:ss} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.WriteTo.File(
path: Path.Combine(logsPath, "scoping-.log"),
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 30,
outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}")
.CreateLogger();
try
{
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
// Windows Service support (no-op on non-Windows) // Windows Service support (no-op on non-Windows)
builder.Host.UseWindowsService(); builder.Host.UseWindowsService();
// Replace default logging with Serilog
builder.Services.AddLogging(logging =>
{
logging.ClearProviders();
logging.AddSerilog(dispose: true);
});
// Run database migrations (skip in Testing environment) // Run database migrations (skip in Testing environment)
// Note: IDatabaseMigrator interface enables mocking for integration tests // Note: IDatabaseMigrator interface enables mocking for integration tests
if (!builder.Environment.IsEnvironment("Testing")) if (!builder.Environment.IsEnvironment("Testing"))
{ {
// Create early logger for startup diagnostics
using var loggerFactory = LoggerFactory.Create(b => b
.AddConfiguration(builder.Configuration.GetSection("Logging"))
.AddConsole());
var startupLogger = loggerFactory.CreateLogger("JdeScoping.Host.Startup");
IDatabaseMigrator migrator = new DatabaseMigrator(builder.Configuration); IDatabaseMigrator migrator = new DatabaseMigrator(builder.Configuration);
var migrationResult = migrator.Migrate(); var migrationResult = migrator.Migrate();
if (!migrationResult.Successful) if (!migrationResult.Successful)
{ {
startupLogger.LogError(migrationResult.Error, "Database migration failed: {ErrorMessage}", migrationResult.Error?.Message); Log.Error(migrationResult.Error, "Database migration failed: {ErrorMessage}", migrationResult.Error?.Message);
return 1; return 1;
} }
} }
@@ -53,7 +75,7 @@ if (!app.Environment.IsEnvironment("Testing"))
if (!ConfigurationValidationRunner.ValidateConfiguration(app.Services, configLogger)) if (!ConfigurationValidationRunner.ValidateConfiguration(app.Services, configLogger))
{ {
configLogger.LogCritical("Application startup aborted due to configuration validation failures"); Log.Fatal("Application startup aborted due to configuration validation failures");
return 1; return 1;
} }
} }
@@ -77,9 +99,22 @@ app.UseWebApi();
app.MapRazorPages(); app.MapRazorPages();
app.MapFallbackToFile("index.html"); app.MapFallbackToFile("index.html");
// Register shutdown handler for clean Serilog disposal
app.Lifetime.ApplicationStopping.Register(Log.CloseAndFlush);
app.Run(); app.Run();
return 0; return 0;
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
return 1;
}
finally
{
Log.CloseAndFlush();
}
// Validates that critical services are properly registered // Validates that critical services are properly registered
static void ValidateServices(IServiceProvider services) static void ValidateServices(IServiceProvider services)