From 003f4c60f6e0d209b3d955391286d930d941f578 Mon Sep 17 00:00:00 2001 From: TekH Date: Mon, 3 Aug 2026 17:11:13 +0200 Subject: [PATCH] refactor: migrate logging infrastructure from NLog to Serilog - Replace NLog packages with Serilog.AspNetCore and related dependencies - Add Serilog.UI with SQLite provider for log visualization - Implement environment-based logging configuration: * Development: Console output with simplified template * Production: File-based logging with separate files per level - Add SQLite sink for persistent log storage and web UI access - Configure rolling file policies with configurable retention - Update appsettings.Logging.json to use Serilog configuration format - Enable Serilog self-diagnostics for troubleshooting --- src/ReC.API/Program.cs | 112 +++++++++++++++++++++++---- src/ReC.API/ReC.API.csproj | 9 ++- src/ReC.API/appsettings.Logging.json | 53 +------------ 3 files changed, 109 insertions(+), 65 deletions(-) diff --git a/src/ReC.API/Program.cs b/src/ReC.API/Program.cs index 3da3948..7e7092b 100644 --- a/src/ReC.API/Program.cs +++ b/src/ReC.API/Program.cs @@ -1,27 +1,96 @@ using Microsoft.AspNetCore.Rewrite; using Microsoft.EntityFrameworkCore; -using NLog; -using NLog.Web; using ReC.API.Middleware; using ReC.Application; using ReC.Infrastructure; +using Serilog; +using Serilog.Ui.Core.Extensions; +using Serilog.Ui.SqliteDataProvider.Extensions; +using Serilog.Ui.Web.Extensions; using System.Reflection; using LogLevel = Microsoft.Extensions.Logging.LogLevel; -var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); -logger.Info("Logging initialized!"); +// Enable Serilog self-diagnostics +Serilog.Debugging.SelfLog.Enable(msg => Console.WriteLine($"[SERILOG] {msg}")); + +// Build temporary configuration to read log directory from appsettings.Logging.json +var tempConfig = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile("appsettings.json", optional: false) + .AddJsonFile("appsettings.Logging.json", optional: false) + .AddEnvironmentVariables() + .Build(); + +var isDevelopment = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") == "Development"; + +var logDirectory = tempConfig.GetValue("Serilog:LogDirectory") + ?? Path.Combine(AppContext.BaseDirectory, "logs"); +var retainedFileCountLimit = tempConfig.GetValue("Serilog:RetainedFileCountLimit", 30); + +Directory.CreateDirectory(logDirectory); + +var sqliteDbPath = Path.Combine(logDirectory, "logs.db"); +var logFilePathTemplate = Path.Combine(logDirectory, ".Rec.API-.log"); + +Console.WriteLine($"[INFO] Log Directory: {logDirectory}"); + +// Configure Serilog based on environment: +// Development : Console + SQLite (+ Web UI) +// Production : File (per-level) + SQLite (+ Web UI) +var loggerConfig = new LoggerConfiguration() + .MinimumLevel.Information() + .MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning) + .MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Warning) + .Enrich.FromLogContext(); + +if (isDevelopment) +{ + loggerConfig.WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"); +} +else +{ + loggerConfig + .WriteTo.File( + Path.Combine(logDirectory, ".Rec.API-Info.log"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: retainedFileCountLimit, + restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Information, + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.File( + Path.Combine(logDirectory, ".Rec.API-Warning.log"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: retainedFileCountLimit, + restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Warning, + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.File( + Path.Combine(logDirectory, ".Rec.API-Error.log"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: retainedFileCountLimit, + restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Error, + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") + .WriteTo.File( + Path.Combine(logDirectory, ".Rec.API-Critical.log"), + rollingInterval: RollingInterval.Day, + retainedFileCountLimit: retainedFileCountLimit, + restrictedToMinimumLevel: Serilog.Events.LogEventLevel.Fatal, + outputTemplate: "[{Timestamp:yyyy-MM-dd HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"); +} + +// SQLite sink is always active (used by Serilog Web UI) +loggerConfig.WriteTo.SQLite(sqliteDbPath, storeTimestampInUtc: true); + +Log.Logger = loggerConfig.CreateLogger(); + +Log.Information("Logging initialized!"); try { + Log.Information("Starting ReC.API application"); + var builder = WebApplication.CreateBuilder(args); - builder.Logging.SetMinimumLevel(LogLevel.Trace); - - if (!builder.Environment.IsDevelopment()) - { - builder.Logging.ClearProviders(); - builder.Host.UseNLog(); - } + // Use Serilog for logging + builder.Host.UseSerilog(); var config = builder.Configuration; @@ -71,6 +140,16 @@ try c.IncludeXmlComments(xmlPath); }); + // Serilog Web UI — SQLite provider, same path used by the SQLite sink above + builder.Services.AddSerilogUi(logUIOpt => + { + logUIOpt.UseSqliteServer(dbOpt => + { + dbOpt.WithConnectionString($"Data Source={sqliteDbPath}"); + dbOpt.WithTable("Logs"); + }); + }); + var app = builder.Build(); app.UseMiddleware(); @@ -89,14 +168,21 @@ try app.UseAuthorization(); + // Serilog Web UI is always active (both Development and Production) + app.UseSerilogUi(); + app.MapControllers(); app.Run(); } -catch(Exception ex) +catch (Exception ex) { - logger.Error(ex, "Stopped program because of exception"); + Log.Fatal(ex, "Stopped program because of exception"); throw; } +finally +{ + Log.CloseAndFlush(); +} public partial class Program; \ No newline at end of file diff --git a/src/ReC.API/ReC.API.csproj b/src/ReC.API/ReC.API.csproj index e05fc7e..472ae86 100644 --- a/src/ReC.API/ReC.API.csproj +++ b/src/ReC.API/ReC.API.csproj @@ -23,8 +23,13 @@ - - + + + + + + + diff --git a/src/ReC.API/appsettings.Logging.json b/src/ReC.API/appsettings.Logging.json index dc99d03..8a832a6 100644 --- a/src/ReC.API/appsettings.Logging.json +++ b/src/ReC.API/appsettings.Logging.json @@ -5,55 +5,8 @@ "Microsoft.AspNetCore": "Warning" } }, - "NLog": { - "throwConfigExceptions": true, - "variables": { - "logDirectory": "E:\\LogFiles\\Digital Data\\Rec.API", - "logFileNamePrefix": "${shortdate}.Rec.API" - }, - "targets": { - "infoLogs": { - "type": "File", - "fileName": "${logDirectory}\\${logFileNamePrefix}-Info.log", - "maxArchiveDays": 30 - }, - "warningLogs": { - "type": "File", - "fileName": "${logDirectory}\\${logFileNamePrefix}-Warning.log", - "maxArchiveDays": 30 - }, - "errorLogs": { - "type": "File", - "fileName": "${logDirectory}\\${logFileNamePrefix}-Error.log", - "maxArchiveDays": 30 - }, - "criticalLogs": { - "type": "File", - "fileName": "${logDirectory}\\${logFileNamePrefix}-Critical.log", - "maxArchiveDays": 30 - } - }, - "rules": [ - { - "logger": "*", - "level": "Info", - "writeTo": "infoLogs" - }, - { - "logger": "*", - "level": "Warn", - "writeTo": "warningLogs" - }, - { - "logger": "*", - "level": "Error", - "writeTo": "errorLogs" - }, - { - "logger": "*", - "level": "Fatal", - "writeTo": "criticalLogs" - } - ] + "Serilog": { + "LogDirectory": "E:\\LogFiles\\Digital Data\\Rec.API", + "RetainedFileCountLimit": 30 } } \ No newline at end of file