From 1a790ed9b4a27b4dd29c509f0814d222e20c1597 Mon Sep 17 00:00:00 2001 From: Gelu Mihes Date: Thu, 14 May 2026 15:04:30 +0300 Subject: [PATCH] Changes --- .../startup-helpers/EnvironmentDiagnostics.cs | 30 +++++++-- Helpers/startup-helpers/StartupExtensions.cs | 31 +++++++++ .../startup-helpers/startup-helpers.csproj | 2 + Jobs/cv-cleanup-job/Dockerfile | 2 + Jobs/cv-cleanup-job/Program.cs | 44 ++++++++++--- Jobs/cv-cleanup-job/appsettings.json | 65 ++++++++++++++++++- Jobs/cv-cleanup-job/cv-cleanup-job.csproj | 5 ++ docker-compose/.env.template | 2 +- docker-compose/docker-compose.production.yml | 20 ++++++ docker-compose/docker-compose.staging.yml | 21 ++++++ docker-compose/docker-compose.yml | 24 ++++++- 11 files changed, 229 insertions(+), 17 deletions(-) diff --git a/Helpers/startup-helpers/EnvironmentDiagnostics.cs b/Helpers/startup-helpers/EnvironmentDiagnostics.cs index acba34f..31be6b6 100644 --- a/Helpers/startup-helpers/EnvironmentDiagnostics.cs +++ b/Helpers/startup-helpers/EnvironmentDiagnostics.cs @@ -1,5 +1,6 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; namespace StartupHelpers; @@ -7,12 +8,33 @@ namespace StartupHelpers; public static class EnvironmentDiagnostics { public static void LogEnvironmentSettings(ILogger logger, IConfiguration configuration, IWebHostEnvironment environment) + { + LogEnvironmentSettingsCore(logger, configuration, environment.ApplicationName, environment.EnvironmentName, + environment.ContentRootPath, environment.WebRootPath); + } + + public static void LogEnvironmentSettings(ILogger logger, IConfiguration configuration, IHostEnvironment environment) + { + LogEnvironmentSettingsCore(logger, configuration, environment.ApplicationName, environment.EnvironmentName, + environment.ContentRootPath, webRootPath: null); + } + + private static void LogEnvironmentSettingsCore( + ILogger logger, + IConfiguration configuration, + string applicationName, + string environmentName, + string contentRootPath, + string? webRootPath) { logger.LogInformation("==================== ENVIRONMENT SETTINGS ===================="); - logger.LogInformation("Application Name: {ApplicationName}", environment.ApplicationName); - logger.LogInformation("Environment Name: {EnvironmentName}", environment.EnvironmentName); - logger.LogInformation("Content Root Path: {ContentRootPath}", environment.ContentRootPath); - logger.LogInformation("Web Root Path: {WebRootPath}", environment.WebRootPath); + logger.LogInformation("Application Name: {ApplicationName}", applicationName); + logger.LogInformation("Environment Name: {EnvironmentName}", environmentName); + logger.LogInformation("Content Root Path: {ContentRootPath}", contentRootPath); + if (!string.IsNullOrEmpty(webRootPath)) + { + logger.LogInformation("Web Root Path: {WebRootPath}", webRootPath); + } logger.LogInformation("-------------- Environment Variables --------------"); var envVars = Environment.GetEnvironmentVariables(); diff --git a/Helpers/startup-helpers/StartupExtensions.cs b/Helpers/startup-helpers/StartupExtensions.cs index 65b6c80..d72b74e 100644 --- a/Helpers/startup-helpers/StartupExtensions.cs +++ b/Helpers/startup-helpers/StartupExtensions.cs @@ -44,6 +44,22 @@ public static class StartupExtensions }); } + public static void ConfigureJsonSerilog(this HostApplicationBuilder builder, string serviceName, string appVersion) + { + builder.Services.AddSerilog((services, configuration) => + { + configuration + .ReadFrom.Configuration(builder.Configuration) + .ReadFrom.Services(services) + .Enrich.FromLogContext() + .Enrich.WithMachineName() + .Enrich.WithEnvironmentName() + .Enrich.WithProperty("Service", serviceName) + .Enrich.WithProperty("AppVersion", appVersion) + .WriteTo.Console(new Serilog.Formatting.Json.JsonFormatter()); + }); + } + public static void AddAzureKeyVaultIfConfigured(this WebApplicationBuilder builder) { var keyVaultUri = builder.Configuration["KeyVault:VaultUri"]; @@ -131,6 +147,21 @@ public static class StartupExtensions } } + public static void LogHostStartupDiagnostics(this IHost host, string serviceName) + { + var logger = host.Services.GetRequiredService().CreateLogger(serviceName); + logger.LogInformation("{Service} starting up...", serviceName); + var environment = host.Services.GetRequiredService(); + logger.LogInformation("Environment: {Environment}", environment.EnvironmentName); + + var configuration = host.Services.GetRequiredService(); + var logEnvironmentOnStartup = configuration.GetValue("LogEnvironmentOnStartup", defaultValue: true); + if (logEnvironmentOnStartup) + { + EnvironmentDiagnostics.LogEnvironmentSettings(logger, configuration, environment); + } + } + public static void UseDefaultSerilogRequestLogging(this WebApplication app, bool includeProxyHeaders = false) { app.UseSerilogRequestLogging(options => diff --git a/Helpers/startup-helpers/startup-helpers.csproj b/Helpers/startup-helpers/startup-helpers.csproj index 8a5bc9a..00b9f4a 100644 --- a/Helpers/startup-helpers/startup-helpers.csproj +++ b/Helpers/startup-helpers/startup-helpers.csproj @@ -17,6 +17,8 @@ + + diff --git a/Jobs/cv-cleanup-job/Dockerfile b/Jobs/cv-cleanup-job/Dockerfile index cee0119..1acd652 100644 --- a/Jobs/cv-cleanup-job/Dockerfile +++ b/Jobs/cv-cleanup-job/Dockerfile @@ -4,6 +4,7 @@ WORKDIR /src COPY Jobs/cv-cleanup-job/cv-cleanup-job.csproj Jobs/cv-cleanup-job/ COPY Jobs/job-scheduler/job-scheduler.csproj Jobs/job-scheduler/ +COPY Helpers/startup-helpers/startup-helpers.csproj Helpers/startup-helpers/ COPY Apis/api-models/api-models.csproj Apis/api-models/ COPY Apis/shared-models/shared-models.csproj Apis/shared-models/ @@ -11,6 +12,7 @@ RUN dotnet restore Jobs/cv-cleanup-job/cv-cleanup-job.csproj COPY Jobs/cv-cleanup-job/ Jobs/cv-cleanup-job/ COPY Jobs/job-scheduler/ Jobs/job-scheduler/ +COPY Helpers/startup-helpers/ Helpers/startup-helpers/ COPY Apis/api-models/ Apis/api-models/ COPY Apis/shared-models/ Apis/shared-models/ diff --git a/Jobs/cv-cleanup-job/Program.cs b/Jobs/cv-cleanup-job/Program.cs index 44b92a9..1a83d4b 100644 --- a/Jobs/cv-cleanup-job/Program.cs +++ b/Jobs/cv-cleanup-job/Program.cs @@ -1,21 +1,47 @@ +using System.Reflection; using CvCleanupJob.Tasks; using JobScheduler.Scheduling; using JobScheduler.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Models.Settings; +using Serilog; +using StartupHelpers; -var builder = Host.CreateApplicationBuilder(args); +const string ServiceName = "cv-cleanup-job"; -builder.Services.Configure(builder.Configuration.GetSection("FileStorage")); +StartupExtensions.LoadDotEnvFile(); +var appVersion = StartupExtensions.GetApplicationVersion(Assembly.GetExecutingAssembly()); -builder.Services.AddSingleton(); -builder.Services.AddSingleton>(sp => new IJobTask[] +try { - sp.GetRequiredService(), -}); + var builder = Host.CreateApplicationBuilder(args); -builder.Services.AddHostedService(); + builder.ConfigureJsonSerilog(ServiceName, appVersion); + Log.Information("Starting {Service} version {AppVersion}", ServiceName, appVersion); -var host = builder.Build(); -await host.RunAsync(); + builder.Services.Configure(builder.Configuration.GetSection("FileStorage")); + + builder.Services.AddSingleton(); + builder.Services.AddSingleton>(sp => new IJobTask[] + { + sp.GetRequiredService(), + }); + + builder.Services.AddHostedService(); + + var host = builder.Build(); + + host.LogHostStartupDiagnostics(ServiceName); + + Log.Information("{Service} startup complete. Background scheduler is running.", ServiceName); + await host.RunAsync(); +} +catch (Exception ex) +{ + Log.Fatal(ex, "{Service} terminated unexpectedly", ServiceName); +} +finally +{ + Log.CloseAndFlush(); +} diff --git a/Jobs/cv-cleanup-job/appsettings.json b/Jobs/cv-cleanup-job/appsettings.json index 61b78bd..1dc46c2 100644 --- a/Jobs/cv-cleanup-job/appsettings.json +++ b/Jobs/cv-cleanup-job/appsettings.json @@ -1,10 +1,73 @@ { + "Serilog": { + "Using": [ + "Serilog.Sinks.Console", + "Serilog.Sinks.File", + "Serilog.Sinks.Email" + ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Microsoft.AspNetCore": "Warning", + "Microsoft.Extensions.Hosting": "Information", + "Microsoft.Hosting.Lifetime": "Information", + "System.Net.Http.HttpClient": "Warning", + "CvCleanupJob": "Information", + "JobScheduler": "Information" + } + }, + "WriteTo": [ + { + "Name": "Console", + "Args": { + "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "File", + "Args": { + "path": "logs/cv-cleanup-job-.log", + "rollingInterval": "Day", + "retainedFileCountLimit": 30, + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}" + } + }, + { + "Name": "Email", + "Args": { + "restrictedToMinimumLevel": "Error", + "fromEmail": "", + "toEmail": "", + "mailServer": "", + "networkCredential": { + "userName": "", + "password": "" + }, + "port": 587, + "enableSsl": true, + "emailSubject": "[mihes.ro CV cleanup job] Error Alert", + "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}", + "batchPostingLimit": 10, + "period": "0.00:05:00" + } + } + ], + "Enrich": [ + "FromLogContext", + "WithMachineName", + "WithEnvironmentName" + ] + }, "Logging": { "LogLevel": { "Default": "Information", - "Microsoft.Hosting.Lifetime": "Information" + "Microsoft.Hosting.Lifetime": "Information", + "Microsoft.Extensions.Hosting": "Information", + "CvCleanupJob": "Information", + "JobScheduler": "Information" } }, + "LogEnvironmentOnStartup": true, "FileStorage": { "Path": "Files" }, diff --git a/Jobs/cv-cleanup-job/cv-cleanup-job.csproj b/Jobs/cv-cleanup-job/cv-cleanup-job.csproj index f5ed4a2..9e93dfc 100644 --- a/Jobs/cv-cleanup-job/cv-cleanup-job.csproj +++ b/Jobs/cv-cleanup-job/cv-cleanup-job.csproj @@ -12,8 +12,13 @@ + + + + + diff --git a/docker-compose/.env.template b/docker-compose/.env.template index 8bd6a6c..c960f2c 100644 --- a/docker-compose/.env.template +++ b/docker-compose/.env.template @@ -75,7 +75,7 @@ Jobs__CvStorageCleanupInterval=01:00:00 Jobs__CvStorageMaxTotalSizeMegabytes=40 # File Storage -FileStorage__Path=/opt/myai/files +FileStorage__Path=Files FileStorage__DefaultFileName= FileStorage__ToEmail= FileStorage__SubjectPrefix=[File Download] diff --git a/docker-compose/docker-compose.production.yml b/docker-compose/docker-compose.production.yml index 0b79c3a..66fa390 100644 --- a/docker-compose/docker-compose.production.yml +++ b/docker-compose/docker-compose.production.yml @@ -212,14 +212,34 @@ services: depends_on: - api environment: + # Worker + diagnostics (matches Jobs/cv-cleanup-job appsettings) - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Production} - APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.production} + - LogEnvironmentOnStartup=${LogEnvironmentOnStartup:-true} + + # FileStorage: matches cv-cleanup-job appsettings FileStorage section - FileStorage__Path=Files + + # Jobs: matches cv-cleanup-job appsettings Jobs:Tasks - Jobs__Tasks__0__Enabled=${Jobs__CvStorageCleanupEnabled:-true} - Jobs__Tasks__0__Interval=${Jobs__CvStorageCleanupInterval:-01:00:00} - Jobs__Tasks__0__Parameters__MaxTotalSizeMegabytes=${Jobs__CvStorageMaxTotalSizeMegabytes:-40} + + # Logging / Serilog (matches Jobs/cv-cleanup-job appsettings Serilog section; WriteTo index 2 = Email) - Logging__LogLevel__Default=${Logging__LogLevel__Default:-Information} - Logging__LogLevel__Microsoft=${Logging__LogLevel__Microsoft:-Warning} + - Logging__LogLevel__Microsoft__Hosting__Lifetime=${Logging__LogLevel__Microsoft__Hosting__Lifetime:-Information} + - Logging__LogLevel__CvCleanupJob=${Logging__LogLevel__CvCleanupJob:-Information} + - Logging__LogLevel__JobScheduler=${Logging__LogLevel__JobScheduler:-Information} + - Serilog__MinimumLevel__Override__CvCleanupJob=${Serilog__MinimumLevel__Override__CvCleanupJob:-Information} + - Serilog__MinimumLevel__Override__JobScheduler=${Serilog__MinimumLevel__Override__JobScheduler:-Information} + - Serilog__WriteTo__2__Args__fromEmail=${Serilog__WriteTo__2__Args__fromEmail:-} + - Serilog__WriteTo__2__Args__toEmail=${Serilog__WriteTo__2__Args__toEmail:-} + - Serilog__WriteTo__2__Args__mailServer=${Serilog__WriteTo__2__Args__mailServer:-} + - Serilog__WriteTo__2__Args__networkCredential__userName=${Serilog__WriteTo__2__Args__networkCredential__userName:-} + - Serilog__WriteTo__2__Args__networkCredential__password=${Serilog__WriteTo__2__Args__networkCredential__password:-} + - Serilog__WriteTo__2__Args__port=${Serilog__WriteTo__2__Args__port:-587} + - Serilog__WriteTo__2__Args__enableSsl=${Serilog__WriteTo__2__Args__enableSsl:-true} volumes: - /opt/myai/logs/cv-cleanup-job:/app/logs - /opt/myai/files:/app/Files diff --git a/docker-compose/docker-compose.staging.yml b/docker-compose/docker-compose.staging.yml index 2c40dc6..10901cc 100644 --- a/docker-compose/docker-compose.staging.yml +++ b/docker-compose/docker-compose.staging.yml @@ -212,15 +212,36 @@ services: depends_on: - api environment: + # Worker + diagnostics (matches Jobs/cv-cleanup-job appsettings) - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Staging} - APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.staging} + - LogEnvironmentOnStartup=${LogEnvironmentOnStartup:-true} + + # FileStorage: matches cv-cleanup-job appsettings FileStorage section - FileStorage__Path=Files + + # Jobs: matches cv-cleanup-job appsettings Jobs:Tasks - Jobs__Tasks__0__Enabled=${Jobs__CvStorageCleanupEnabled:-true} - Jobs__Tasks__0__Interval=${Jobs__CvStorageCleanupInterval:-01:00:00} - Jobs__Tasks__0__Parameters__MaxTotalSizeMegabytes=${Jobs__CvStorageMaxTotalSizeMegabytes:-40} + + # Logging / Serilog (matches Jobs/cv-cleanup-job appsettings Serilog section; WriteTo index 2 = Email) - Logging__LogLevel__Default=${Logging__LogLevel__Default:-Information} - Logging__LogLevel__Microsoft=${Logging__LogLevel__Microsoft:-Warning} + - Logging__LogLevel__Microsoft__Hosting__Lifetime=${Logging__LogLevel__Microsoft__Hosting__Lifetime:-Information} + - Logging__LogLevel__CvCleanupJob=${Logging__LogLevel__CvCleanupJob:-Information} + - Logging__LogLevel__JobScheduler=${Logging__LogLevel__JobScheduler:-Information} + - Serilog__MinimumLevel__Override__CvCleanupJob=${Serilog__MinimumLevel__Override__CvCleanupJob:-Information} + - Serilog__MinimumLevel__Override__JobScheduler=${Serilog__MinimumLevel__Override__JobScheduler:-Information} + - Serilog__WriteTo__2__Args__fromEmail=${Serilog__WriteTo__2__Args__fromEmail:-} + - Serilog__WriteTo__2__Args__toEmail=${Serilog__WriteTo__2__Args__toEmail:-} + - Serilog__WriteTo__2__Args__mailServer=${Serilog__WriteTo__2__Args__mailServer:-} + - Serilog__WriteTo__2__Args__networkCredential__userName=${Serilog__WriteTo__2__Args__networkCredential__userName:-} + - Serilog__WriteTo__2__Args__networkCredential__password=${Serilog__WriteTo__2__Args__networkCredential__password:-} + - Serilog__WriteTo__2__Args__port=${Serilog__WriteTo__2__Args__port:-587} + - Serilog__WriteTo__2__Args__enableSsl=${Serilog__WriteTo__2__Args__enableSsl:-true} volumes: + - /opt/myai/logs/cv-cleanup-job:/app/logs - /opt/myai/files:/app/Files networks: - myai-network diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 9884ea8..ad088ef 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -220,7 +220,7 @@ services: - Serilog__WriteTo__2__Args__enableSsl=${Serilog__WriteTo__2__Args__enableSsl:-true} volumes: - ../Apis/api/logs:/app/logs - - ${FileStorage__Path:-../Files}:/app/Files + - ../Apis/api/Files:/app/Files networks: - myai-network restart: unless-stopped @@ -237,16 +237,36 @@ services: env_file: - .env environment: + # Worker + diagnostics (matches Jobs/cv-cleanup-job appsettings) - ASPNETCORE_ENVIRONMENT=${ASPNETCORE_ENVIRONMENT:-Development} - APP_ENVIRONMENT_NAME=${APP_ENVIRONMENT_NAME:-myai.local} + - LogEnvironmentOnStartup=${LogEnvironmentOnStartup:-true} + + # FileStorage: matches cv-cleanup-job appsettings FileStorage section - FileStorage__Path=${FileStorage__Path:-Files} + + # Jobs: matches cv-cleanup-job appsettings Jobs:Tasks - Jobs__Tasks__0__Enabled=${Jobs__CvStorageCleanupEnabled:-true} - Jobs__Tasks__0__Interval=${Jobs__CvStorageCleanupInterval:-01:00:00} - Jobs__Tasks__0__Parameters__MaxTotalSizeMegabytes=${Jobs__CvStorageMaxTotalSizeMegabytes:-40} + + # Logging / Serilog (matches Jobs/cv-cleanup-job appsettings Serilog section; WriteTo index 2 = Email) - Logging__LogLevel__Default=${Logging__LogLevel__Default:-Information} - Logging__LogLevel__Microsoft=${Logging__LogLevel__Microsoft:-Warning} + - Logging__LogLevel__Microsoft__Hosting__Lifetime=${Logging__LogLevel__Microsoft__Hosting__Lifetime:-Information} + - Logging__LogLevel__CvCleanupJob=${Logging__LogLevel__CvCleanupJob:-Information} + - Logging__LogLevel__JobScheduler=${Logging__LogLevel__JobScheduler:-Information} + - Serilog__MinimumLevel__Override__CvCleanupJob=${Serilog__MinimumLevel__Override__CvCleanupJob:-Information} + - Serilog__MinimumLevel__Override__JobScheduler=${Serilog__MinimumLevel__Override__JobScheduler:-Information} + - Serilog__WriteTo__2__Args__fromEmail=${Serilog__WriteTo__2__Args__fromEmail:-} + - Serilog__WriteTo__2__Args__toEmail=${Serilog__WriteTo__2__Args__toEmail:-} + - Serilog__WriteTo__2__Args__mailServer=${Serilog__WriteTo__2__Args__mailServer:-} + - Serilog__WriteTo__2__Args__networkCredential__userName=${Serilog__WriteTo__2__Args__networkCredential__userName:-} + - Serilog__WriteTo__2__Args__networkCredential__password=${Serilog__WriteTo__2__Args__networkCredential__password:-} + - Serilog__WriteTo__2__Args__port=${Serilog__WriteTo__2__Args__port:-587} + - Serilog__WriteTo__2__Args__enableSsl=${Serilog__WriteTo__2__Args__enableSsl:-true} volumes: - - ../Apis/api/logs:/app/logs + - ../Jobs/cv-cleanup-job/logs:/app/logs - ../Apis/api/Files:/app/Files networks: - myai-network