using ECMJobRunner.Application; using ECMJobRunner.Infrastructure; using ECMJobRunner.WebCron; using ECMJobRunner.WebCron.HealthCheck; using ECMJobRunner.WebCron.Middleware; using ECMJobRunner.WebCron.ProfileWorker; using Hangfire; using Hangfire.SqlServer; using Microsoft.Data.SqlClient; using Microsoft.Extensions.Diagnostics.HealthChecks; using Serilog; using Serilog.Ui.Core.Extensions; using Serilog.Ui.SqliteDataProvider.Extensions; using Serilog.Ui.Web.Extensions; // Enable Serilog self-diagnostics Serilog.Debugging.SelfLog.Enable(msg => Console.WriteLine($"[SERILOG] {msg}")); // Build temporary configuration to read log directory var tempConfig = new ConfigurationBuilder() .SetBasePath(Directory.GetCurrentDirectory()) .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true) .Build(); // Get log directory from configuration var logDirectory = tempConfig.GetValue("Application:LogDirectory") ?? throw new InvalidOperationException("Application:LogDirectory not found in configuration."); var sqliteDbPath = Path.Combine(logDirectory, "logs.db"); Console.WriteLine($"[INFO] SQLite Log Database Path: {sqliteDbPath}"); // Configure Serilog with SQLite sink Log.Logger = new LoggerConfiguration() .MinimumLevel.Information() .MinimumLevel.Override("Microsoft.AspNetCore", Serilog.Events.LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.EntityFrameworkCore", Serilog.Events.LogEventLevel.Warning) .MinimumLevel.Override("Hangfire", Serilog.Events.LogEventLevel.Information) .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}") .WriteTo.SQLite(sqliteDbPath, storeTimestampInUtc: true) .Enrich.FromLogContext() .CreateLogger(); try { Log.Information("Starting ECMJobRunner.WebCron application"); var builder = WebApplication.CreateBuilder(args); // Use Serilog for logging builder.Host.UseSerilog(); // Configure Windows Service hosting if enabled if (builder.Configuration.GetValue("HostingOptions:UseWindowsService")) { builder.Host.UseWindowsService(); } // Add services to the container. builder.Services.AddControllers(); builder.Services.AddProfileWorker(builder.Configuration); // Register services var cnnStr = builder.Configuration.GetConnectionString("SDD-VMP04-SQL17") ?? throw new InvalidOperationException("Connection string 'SDD-VMP04-SQL17' not found."); builder.Services.AddJobRunnerInfrastructure(cnnStr); var recClientApiUrl = builder.Configuration.GetValue("ReC:ApiUrl") ?? throw new InvalidOperationException("ReC:ApiUrl not found."); builder.Services.AddJobRunnerServices(recClientApiUrl, builder.Configuration); // Get Hangfire storage configuration var useInMemory = builder.Configuration.GetValue("Hangfire:InMemory"); // Add Hangfire services with configurable storage builder.Services.AddHangfire(configuration => { configuration .SetDataCompatibilityLevel(CompatibilityLevel.Version_180) .UseSimpleAssemblyNameTypeSerializer() .UseRecommendedSerializerSettings(); // Configure storage based on appsettings if (useInMemory) { configuration.UseInMemoryStorage(); } else // Use SQL Server storage { configuration.UseSqlServerStorage(cnnStr, new SqlServerStorageOptions { CommandBatchMaxTimeout = TimeSpan.FromMinutes(5), SlidingInvisibilityTimeout = TimeSpan.FromMinutes(5), QueuePollInterval = TimeSpan.Zero, UseRecommendedIsolationLevel = true, DisableGlobalLocks = true, SqlClientFactory = SqlClientFactory.Instance }); } }); // Add Hangfire server builder.Services.AddHangfireServer(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); // Add health checks builder.Services.AddHealthChecks() .AddCheck("profile-worker", tags: ["ready", "worker"]); // Add Serilog.UI with SQLite provider - use same path from configuration var serilogUiLogDirectory = builder.Configuration.GetValue("Application:LogDirectory") ?? throw new InvalidOperationException("Application:LogDirectory not found in configuration."); var serilogUiDbPath = Path.Combine(serilogUiLogDirectory, "logs.db"); builder.Services.AddSerilogUi(logUIOpt => { logUIOpt.UseSqliteServer(dbOpt => { dbOpt.WithConnectionString($"Data Source={serilogUiDbPath}"); dbOpt.WithTable("Logs"); }); }); var app = builder.Build(); app.UseMiddleware(); // Configure the HTTP request pipeline. if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); // Enable static files for wwwroot app.UseStaticFiles(); app.UseAuthorization(); // Add Hangfire Dashboard with no authentication (for development) app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = [new AllowAllDashboardAuthorizationFilter()] }); // Add Health Check UI route (accessible outside Hangfire dashboard) app.MapGet("/health-ui", async (HealthCheckService healthCheckService, HttpContext context) => { var report = await healthCheckService.CheckHealthAsync(context.RequestAborted); var html = HealthCheckHtmlGenerator.Generate(report); context.Response.ContentType = "text/html"; await context.Response.WriteAsync(html); return Results.Empty; }); // Add Serilog.UI Dashboard app.UseSerilogUi(); app.MapControllers(); // Map health check endpoints app.MapHealthChecks("/health", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = _ => true, ResponseWriter = async (context, report) => { context.Response.ContentType = "application/json"; var result = System.Text.Json.JsonSerializer.Serialize(new { status = report.Status.ToString(), timestamp = DateTime.Now, checks = report.Entries.Select(e => new { name = e.Key, status = e.Value.Status.ToString(), description = e.Value.Description, duration = e.Value.Duration.TotalMilliseconds, exception = e.Value.Exception?.Message, data = e.Value.Data }) }); await context.Response.WriteAsync(result); } }); app.MapHealthChecks("/health/ready", new Microsoft.AspNetCore.Diagnostics.HealthChecks.HealthCheckOptions { Predicate = check => check.Tags.Contains("ready") }); // Redirect root path to Health UI app.MapGet("/", () => Results.Redirect("/health-ui")); app.Run(); } catch (Exception ex) { Log.Fatal(ex, "Application terminated unexpectedly"); } finally { Log.CloseAndFlush(); }