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>
<PackageReference Include="Microsoft.AspNetCore.Components.WebAssembly.Server" 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>
<PropertyGroup>
+97 -62
View File
@@ -4,82 +4,117 @@ using JdeScoping.DataSync.Options;
using JdeScoping.ExcelIO.Options;
using JdeScoping.Database;
using JdeScoping.Host.Startup;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Serilog;
using Serilog.Events;
var builder = WebApplication.CreateBuilder(args);
// Configure Serilog with console and daily rolling file
var logsPath = Path.Combine(AppContext.BaseDirectory, "logs");
Directory.CreateDirectory(logsPath);
// Windows Service support (no-op on non-Windows)
builder.Host.UseWindowsService();
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();
// Run database migrations (skip in Testing environment)
// Note: IDatabaseMigrator interface enables mocking for integration tests
if (!builder.Environment.IsEnvironment("Testing"))
try
{
// 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");
var builder = WebApplication.CreateBuilder(args);
IDatabaseMigrator migrator = new DatabaseMigrator(builder.Configuration);
var migrationResult = migrator.Migrate();
// Windows Service support (no-op on non-Windows)
builder.Host.UseWindowsService();
if (!migrationResult.Successful)
// Replace default logging with Serilog
builder.Services.AddLogging(logging =>
{
startupLogger.LogError(migrationResult.Error, "Database migration failed: {ErrorMessage}", migrationResult.Error?.Message);
return 1;
}
}
logging.ClearProviders();
logging.AddSerilog(dispose: true);
});
// ASP.NET Core services
builder.Services.AddRazorPages();
// Module registration (in dependency order)
builder.Services
.AddDataAccess(builder.Configuration) // 1. Database access + search processing
.AddInfrastructure(builder.Configuration) // 2. Infrastructure (JDE/CMS/Auth)
.AddDataSyncServices(builder.Configuration) // 3. Data sync background service
.AddExcelIO(builder.Configuration) // 4. Result export
.AddWebApi(builder.Configuration); // 5. Web API (controllers, auth, SignalR)
var app = builder.Build();
// Configuration validation (skip in Testing environment)
if (!app.Environment.IsEnvironment("Testing"))
{
var configLogger = app.Services.GetRequiredService<ILoggerFactory>()
.CreateLogger("JdeScoping.Host.ConfigurationValidation");
if (!ConfigurationValidationRunner.ValidateConfiguration(app.Services, configLogger))
// Run database migrations (skip in Testing environment)
// Note: IDatabaseMigrator interface enables mocking for integration tests
if (!builder.Environment.IsEnvironment("Testing"))
{
configLogger.LogCritical("Application startup aborted due to configuration validation failures");
return 1;
IDatabaseMigrator migrator = new DatabaseMigrator(builder.Configuration);
var migrationResult = migrator.Migrate();
if (!migrationResult.Successful)
{
Log.Error(migrationResult.Error, "Database migration failed: {ErrorMessage}", migrationResult.Error?.Message);
return 1;
}
}
// ASP.NET Core services
builder.Services.AddRazorPages();
// Module registration (in dependency order)
builder.Services
.AddDataAccess(builder.Configuration) // 1. Database access + search processing
.AddInfrastructure(builder.Configuration) // 2. Infrastructure (JDE/CMS/Auth)
.AddDataSyncServices(builder.Configuration) // 3. Data sync background service
.AddExcelIO(builder.Configuration) // 4. Result export
.AddWebApi(builder.Configuration); // 5. Web API (controllers, auth, SignalR)
var app = builder.Build();
// Configuration validation (skip in Testing environment)
if (!app.Environment.IsEnvironment("Testing"))
{
var configLogger = app.Services.GetRequiredService<ILoggerFactory>()
.CreateLogger("JdeScoping.Host.ConfigurationValidation");
if (!ConfigurationValidationRunner.ValidateConfiguration(app.Services, configLogger))
{
Log.Fatal("Application startup aborted due to configuration validation failures");
return 1;
}
}
// Startup validation - verify critical services are registered
ValidateServices(app.Services);
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
{
app.UseWebAssemblyDebugging();
}
app.UseStaticFiles();
app.UseBlazorFrameworkFiles();
app.UseRouting();
// Configure Web API middleware (authentication, authorization, controllers, SignalR hub)
app.UseWebApi();
app.MapRazorPages();
app.MapFallbackToFile("index.html");
// Register shutdown handler for clean Serilog disposal
app.Lifetime.ApplicationStopping.Register(Log.CloseAndFlush);
app.Run();
return 0;
}
// Startup validation - verify critical services are registered
ValidateServices(app.Services);
// Configure the HTTP request pipeline
if (app.Environment.IsDevelopment())
catch (Exception ex)
{
app.UseWebAssemblyDebugging();
Log.Fatal(ex, "Application terminated unexpectedly");
return 1;
}
finally
{
Log.CloseAndFlush();
}
app.UseStaticFiles();
app.UseBlazorFrameworkFiles();
app.UseRouting();
// Configure Web API middleware (authentication, authorization, controllers, SignalR hub)
app.UseWebApi();
app.MapRazorPages();
app.MapFallbackToFile("index.html");
app.Run();
return 0;
// Validates that critical services are properly registered
static void ValidateServices(IServiceProvider services)