From 600374a6f0fbfb14d5aa143ca7325594f229dd1a Mon Sep 17 00:00:00 2001 From: TekH Date: Sat, 10 Oct 2026 16:53:37 +0200 Subject: [PATCH] feat(api): integrate Serilog bootstrap and in-app log viewer --- .../DigitalData.TaskFlow.API/Program.cs | 176 +++++++++++++----- 1 file changed, 128 insertions(+), 48 deletions(-) diff --git a/src/presentation/DigitalData.TaskFlow.API/Program.cs b/src/presentation/DigitalData.TaskFlow.API/Program.cs index 16690df..4d9d1d5 100644 --- a/src/presentation/DigitalData.TaskFlow.API/Program.cs +++ b/src/presentation/DigitalData.TaskFlow.API/Program.cs @@ -2,65 +2,145 @@ using DigitalData.TaskFlow.Application; using DigitalData.TaskFlow.API.Middleware; using DigitalData.TaskFlow.API.Options; using DigitalData.TaskFlow.Infrastructure; -using Microsoft.OpenApi; +using Microsoft.OpenApi.Models; using Scalar.AspNetCore; +using Serilog; +using Serilog.Events; +using Serilog.Ui.Core.Extensions; +using Serilog.Ui.SqliteDataProvider.Extensions; +using Serilog.Ui.Web.Extensions; -var builder = WebApplication.CreateBuilder(args); -var config = builder.Configuration; +var tempConfig = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false, reloadOnChange: false) + .Build(); -// Add services to the container. -builder.Services.AddApplicationServices(config); -builder.Services.AddInfrastructureServices(config); +var logDirectoryRaw = tempConfig.GetValue("Application:LogDirectory") ?? "logs"; +var logDirectory = Path.IsPathRooted(logDirectoryRaw) + ? logDirectoryRaw + : Path.Combine(AppContext.BaseDirectory, logDirectoryRaw); +Directory.CreateDirectory(logDirectory); +var sqliteDbPath = Path.Combine(logDirectory, "logs.db"); -builder.Services.AddControllers(); -// Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle -builder.Services.AddEndpointsApiExplorer(); -builder.Services.AddSwaggerGen(options => +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.AspNetCore", LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.EntityFrameworkCore", LogEventLevel.Warning) + .MinimumLevel.Override("System", LogEventLevel.Warning) + .Enrich.FromLogContext() + .WriteTo.File( + path: Path.Combine(logDirectory, "taskflow-.log"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 30, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.SQLite(sqliteDbPath, storeTimestampInUtc: true) + .CreateBootstrapLogger(); + +try { - options.SwaggerDoc("v1", new OpenApiInfo + Log.Information("Starting TaskFlow API"); + + var builder = WebApplication.CreateBuilder(args); + var config = builder.Configuration; + + builder.Host.UseSerilog((context, services, loggerConfiguration) => { - Title = "DigitalData TaskFlow API", - Version = "v1", - Description = "Integration API for taskFLOW workflow and document operations.", - Contact = new OpenApiContact + loggerConfiguration + .ReadFrom.Configuration(context.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .WriteTo.File( + path: Path.Combine(logDirectory, "taskflow-.log"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: 30, + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.SQLite(sqliteDbPath, storeTimestampInUtc: true); + + if (context.HostingEnvironment.IsDevelopment()) { - Name = "Digital Data GmbH", - Url = new Uri("https://digitaldata.works/"), - Email = "info-flow@digitaldata.works" + loggerConfiguration.WriteTo.Console( + outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}"); } }); -}); -builder.Services - .AddOptions() - .Bind(config.GetSection(SwaggerOptions.SectionName)); + // Add services to the container. + builder.Services.AddApplicationServices(config); + builder.Services.AddInfrastructureServices(config); -var app = builder.Build(); - -var swaggerOptions = app.Services - .GetRequiredService>() - .Value; - -var enableSwagger = app.Environment.IsDevelopment() || swaggerOptions.EnabledInProduction; - -// Configure the HTTP request pipeline. -app.UseMiddleware(); - -if (enableSwagger) -{ - app.UseSwagger(); - app.UseSwaggerUI(); - app.MapScalarApiReference(options => + builder.Services.AddControllers(); + builder.Services.AddEndpointsApiExplorer(); + builder.Services.AddSwaggerGen(options => { - options.Title = "DigitalData TaskFlow API"; - options.Theme = ScalarTheme.BluePlanet; + options.SwaggerDoc("v1", new OpenApiInfo + { + Title = "DigitalData TaskFlow API", + Version = "v1", + Description = """ + Integration API for taskFLOW workflow and document operations. + + --- + + - [Serilog Log Viewer](/serilog-ui) + """, + Contact = new OpenApiContact + { + Name = "Digital Data GmbH", + Url = new Uri("https://digitaldata.works/"), + Email = "info-flow@digitaldata.works" + } + }); }); + + builder.Services + .AddOptions() + .Bind(config.GetSection(SwaggerOptions.SectionName)); + + builder.Services.AddSerilogUi(options => + { + options.UseSqliteServer(databaseOptions => + { + databaseOptions.WithConnectionString($"Data Source={sqliteDbPath}"); + databaseOptions.WithTable("Logs"); + }); + }); + + var app = builder.Build(); + + var swaggerOptions = app.Services + .GetRequiredService>() + .Value; + + var enableSwagger = app.Environment.IsDevelopment() || swaggerOptions.EnabledInProduction; + + app.UseMiddleware(); + app.UseSerilogRequestLogging(); + + if (enableSwagger) + { + app.UseSwagger(); + app.UseSwaggerUI(); + app.MapScalarApiReference(options => + { + options.Title = "DigitalData TaskFlow API"; + options.Theme = ScalarTheme.BluePlanet; + }); + } + + app.UseHttpsRedirection(); + app.UseSerilogUi(); + app.UseAuthorization(); + app.MapControllers(); + + Log.Information("TaskFlow API started successfully"); + app.Run(); +} +catch (Exception exception) +{ + Log.Fatal(exception, "TaskFlow API failed to start"); + throw; +} +finally +{ + Log.CloseAndFlush(); } - -app.UseHttpsRedirection(); - -app.UseAuthorization(); - -app.MapControllers(); - -app.Run();