using Microsoft.AspNetCore.Rewrite; using Microsoft.EntityFrameworkCore; 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; // 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); // Use Serilog for logging builder.Host.UseSerilog(); var config = builder.Configuration; Directory .GetFiles(builder.Environment.ContentRootPath, "appsettings.*.json", SearchOption.TopDirectoryOnly) .Where(file => Path.GetFileName(file) != $"appsettings.Development.json") .Where(file => Path.GetFileName(file) != $"appsettings.migration.json") .ToList() .ForEach(file => config.AddJsonFile(file, true, true)); // Add services to the container. builder.Services.AddRecServices(options => { options.LuckyPennySoftwareLicenseKey = builder.Configuration["LuckyPennySoftwareLicenseKey"]; options.ConfigureRecActions(config.GetSection("RecAction")); options.ConfigureSqlException(config.GetSection("SqlException")); }); builder.Services.AddRecInfrastructure(options => { options.ConfigureDbContext((provider, opt) => { var cnnStr = builder.Configuration.GetConnectionString("Default") ?? throw new InvalidOperationException("Connection string is not found."); var logger = provider.GetRequiredService>(); var enableSensitiveDataLogging = config.GetValue("EfCore:EnableSensitiveDataLogging", true); var enableDetailedErrors = config.GetValue("EfCore:EnableDetailedErrors", false); opt.UseSqlServer(cnnStr) .LogTo(log => logger.LogInformation("{log}", log), LogLevel.Trace) .EnableSensitiveDataLogging(enableSensitiveDataLogging) .EnableDetailedErrors(enableDetailedErrors); }); }); builder.Services.AddControllers(options => { options.Filters.Add(); }); // Learn more about configuring Swagger/OpenAPI at https://aka.ms/aspnetcore/swashbuckle builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(c => { var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); 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(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment() || config.GetValue("UseSwagger")) { app.UseSwagger(); app.UseSwaggerUI(); var rewriteOptions = new RewriteOptions().AddRedirect("^$", "swagger"); app.UseRewriter(rewriteOptions); } app.UseHttpsRedirection(); app.UseAuthorization(); // Serilog Web UI is always active (both Development and Production) app.UseSerilogUi(); app.MapControllers(); app.Run(); } catch (Exception ex) { Log.Fatal(ex, "Stopped program because of exception"); throw; } finally { Log.CloseAndFlush(); } public partial class Program;