From b67e926c5f76c68ab0c6c278d85913e3f6ce5ed9 Mon Sep 17 00:00:00 2001 From: claude Date: Mon, 1 Jun 2026 22:25:26 +0300 Subject: [PATCH] Fix Serilog email sink: configure in code, not JSON config Serilog.Settings.Configuration cannot deserialize NetworkCredential or MailKit's SecureSocketOptions from JSON, causing an InvalidOperationException in the binder and preventing containers from starting. Fix: remove Email from the WriteTo JSON array entirely and wire it in code inside ConfigureJsonSerilog using a dedicated SerilogEmail:* config section. The sink is skipped when From/To/Host are absent, so local dev is unaffected. Also renames the docker-compose env vars from the verbose Serilog__WriteTo__2__Args__* prefix to the clean SerilogEmail__* prefix. Co-Authored-By: Claude Sonnet 4.6 --- .../api/Controllers/FileDownloadController.cs | 4 +- Apis/api/appsettings.json | 20 +---- Apis/cv-matcher-api/appsettings.json | 20 +---- Apis/email-api/appsettings.json | 20 +---- Apis/rag-api/appsettings.json | 20 +---- Helpers/startup-helpers/StartupExtensions.cs | 36 +++++++++ Jobs/cv-cleanup-job/appsettings.json | 20 +---- Jobs/cv-search-job/appsettings.json | 20 +---- docker-compose/docker-compose.yml | 78 +++++++++---------- 9 files changed, 80 insertions(+), 158 deletions(-) diff --git a/Apis/api/Controllers/FileDownloadController.cs b/Apis/api/Controllers/FileDownloadController.cs index 5585648..1792d48 100644 --- a/Apis/api/Controllers/FileDownloadController.cs +++ b/Apis/api/Controllers/FileDownloadController.cs @@ -69,6 +69,8 @@ namespace Api.Controllers { try { + var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); + // Captcha required on the initial (full) download only — range requests are resume continuations. var isRangeRequest = !string.IsNullOrEmpty(Request.Headers[HeaderNames.Range].ToString()); if (!isRangeRequest) @@ -79,7 +81,6 @@ namespace Api.Controllers return BadRequest(new ErrorResponse { Error = "Captcha token is required.", Code = "captcha_token_missing" }); } - var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); var verdict = await _captcha.VerifyAsync(captchaToken, userIp, "file_download", CancellationToken.None); if (!verdict.Success) { @@ -125,7 +126,6 @@ namespace Api.Controllers if (!_contentTypeProvider.TryGetContentType(filePath, out var contentType)) contentType = "application/octet-stream"; - var userIp = HttpContext.Connection.RemoteIpAddress?.ToString(); _ = Task.Run(async () => { try diff --git a/Apis/api/appsettings.json b/Apis/api/appsettings.json index ab7d3f8..3d629cc 100644 --- a/Apis/api/appsettings.json +++ b/Apis/api/appsettings.json @@ -2,8 +2,7 @@ "Serilog": { "Using": [ "Serilog.Sinks.Console", - "Serilog.Sinks.File", - "Serilog.Sinks.Email" + "Serilog.Sinks.File" ], "MinimumLevel": { "Default": "Information", @@ -30,23 +29,6 @@ "retainedFileCountLimit": 30, "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}" } - }, - { - "Name": "Email", - "Args": { - "restrictedToMinimumLevel": "Error", - "from": "", - "to": "", - "host": "", - "credentials": { - "userName": "", - "password": "" - }, - "port": 587, - "connectionSecurity": "StartTls", - "subject": "[myAi API] Error Alert", - "body": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}" - } } ], "Enrich": [ diff --git a/Apis/cv-matcher-api/appsettings.json b/Apis/cv-matcher-api/appsettings.json index 19453e7..0d52579 100644 --- a/Apis/cv-matcher-api/appsettings.json +++ b/Apis/cv-matcher-api/appsettings.json @@ -2,8 +2,7 @@ "Serilog": { "Using": [ "Serilog.Sinks.Console", - "Serilog.Sinks.File", - "Serilog.Sinks.Email" + "Serilog.Sinks.File" ], "MinimumLevel": { "Default": "Information", @@ -30,23 +29,6 @@ "retainedFileCountLimit": 30, "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}" } - }, - { - "Name": "Email", - "Args": { - "restrictedToMinimumLevel": "Error", - "from": "", - "to": "", - "host": "", - "credentials": { - "userName": "", - "password": "" - }, - "port": 587, - "connectionSecurity": "StartTls", - "subject": "[myAi] CV Matcher API Error Alert", - "body": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}" - } } ], "Enrich": [ diff --git a/Apis/email-api/appsettings.json b/Apis/email-api/appsettings.json index ea46c3e..60d29b4 100644 --- a/Apis/email-api/appsettings.json +++ b/Apis/email-api/appsettings.json @@ -2,8 +2,7 @@ "Serilog": { "Using": [ "Serilog.Sinks.Console", - "Serilog.Sinks.File", - "Serilog.Sinks.Email" + "Serilog.Sinks.File" ], "MinimumLevel": { "Default": "Information", @@ -30,23 +29,6 @@ "retainedFileCountLimit": 30, "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}" } - }, - { - "Name": "Email", - "Args": { - "restrictedToMinimumLevel": "Error", - "from": "", - "to": "", - "host": "", - "credentials": { - "userName": "", - "password": "" - }, - "port": 587, - "connectionSecurity": "StartTls", - "subject": "[myAi] Email API Error Alert", - "body": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}" - } } ], "Enrich": [ diff --git a/Apis/rag-api/appsettings.json b/Apis/rag-api/appsettings.json index b75604b..820fb32 100644 --- a/Apis/rag-api/appsettings.json +++ b/Apis/rag-api/appsettings.json @@ -2,8 +2,7 @@ "Serilog": { "Using": [ "Serilog.Sinks.Console", - "Serilog.Sinks.File", - "Serilog.Sinks.Email" + "Serilog.Sinks.File" ], "MinimumLevel": { "Default": "Information", @@ -30,23 +29,6 @@ "retainedFileCountLimit": 30, "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}" } - }, - { - "Name": "Email", - "Args": { - "restrictedToMinimumLevel": "Error", - "from": "", - "to": "", - "host": "", - "credentials": { - "userName": "", - "password": "" - }, - "port": 587, - "connectionSecurity": "StartTls", - "subject": "[myAi] RAG API Error Alert", - "body": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}" - } } ], "Enrich": [ diff --git a/Helpers/startup-helpers/StartupExtensions.cs b/Helpers/startup-helpers/StartupExtensions.cs index e5fbc86..c806e1b 100644 --- a/Helpers/startup-helpers/StartupExtensions.cs +++ b/Helpers/startup-helpers/StartupExtensions.cs @@ -1,5 +1,7 @@ +using System.Net; using System.Reflection; using Azure.Identity; +using MailKit.Security; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Diagnostics; using Microsoft.AspNetCore.Http; @@ -9,6 +11,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Serilog; +using Serilog.Events; using Swashbuckle.AspNetCore.SwaggerGen; using Swashbuckle.AspNetCore.Annotations; @@ -41,6 +44,8 @@ public static class StartupExtensions .Enrich.WithProperty("Service", serviceName) .Enrich.WithProperty("AppVersion", appVersion) .WriteTo.Console(new Serilog.Formatting.Json.JsonFormatter()); + + AddEmailSinkIfConfigured(configuration, context.Configuration, serviceName); }); } @@ -57,9 +62,40 @@ public static class StartupExtensions .Enrich.WithProperty("Service", serviceName) .Enrich.WithProperty("AppVersion", appVersion) .WriteTo.Console(new Serilog.Formatting.Json.JsonFormatter()); + + AddEmailSinkIfConfigured(configuration, builder.Configuration, serviceName); }); } + private static void AddEmailSinkIfConfigured(LoggerConfiguration loggerConfig, IConfiguration appConfig, string serviceName) + { + var from = appConfig["SerilogEmail:From"]; + var to = appConfig["SerilogEmail:To"]; + var host = appConfig["SerilogEmail:Host"]; + + if (string.IsNullOrWhiteSpace(from) || string.IsNullOrWhiteSpace(to) || string.IsNullOrWhiteSpace(host)) + return; + + var port = appConfig.GetValue("SerilogEmail:Port", 587); + var userName = appConfig["SerilogEmail:UserName"]; + var password = appConfig["SerilogEmail:Password"]; + + NetworkCredential? credentials = null; + if (!string.IsNullOrWhiteSpace(userName)) + credentials = new NetworkCredential(userName, password); + + loggerConfig.WriteTo.Email( + from: from, + to: to, + host: host, + port: port, + connectionSecurity: SecureSocketOptions.StartTls, + credentials: credentials, + subject: $"[myAi {serviceName}] Error Alert", + body: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}", + restrictedToMinimumLevel: LogEventLevel.Error); + } + public static void AddAzureKeyVaultIfConfigured(this WebApplicationBuilder builder) { var keyVaultUri = builder.Configuration["KeyVault:VaultUri"]; diff --git a/Jobs/cv-cleanup-job/appsettings.json b/Jobs/cv-cleanup-job/appsettings.json index d1155cd..a033a90 100644 --- a/Jobs/cv-cleanup-job/appsettings.json +++ b/Jobs/cv-cleanup-job/appsettings.json @@ -2,8 +2,7 @@ "Serilog": { "Using": [ "Serilog.Sinks.Console", - "Serilog.Sinks.File", - "Serilog.Sinks.Email" + "Serilog.Sinks.File" ], "MinimumLevel": { "Default": "Information", @@ -31,23 +30,6 @@ "retainedFileCountLimit": 30, "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}" } - }, - { - "Name": "Email", - "Args": { - "restrictedToMinimumLevel": "Error", - "from": "", - "to": "", - "host": "", - "credentials": { - "userName": "", - "password": "" - }, - "port": 587, - "connectionSecurity": "StartTls", - "subject": "[myAi] CV Cleanup Job Error Alert", - "body": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}" - } } ], "Enrich": [ diff --git a/Jobs/cv-search-job/appsettings.json b/Jobs/cv-search-job/appsettings.json index f60bec7..c8217a8 100644 --- a/Jobs/cv-search-job/appsettings.json +++ b/Jobs/cv-search-job/appsettings.json @@ -13,8 +13,7 @@ "Serilog": { "Using": [ "Serilog.Sinks.Console", - "Serilog.Sinks.File", - "Serilog.Sinks.Email" + "Serilog.Sinks.File" ], "MinimumLevel": { "Default": "Information", @@ -42,23 +41,6 @@ "retainedFileCountLimit": 30, "outputTemplate": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}: {Message:lj}{NewLine}{Exception}" } - }, - { - "Name": "Email", - "Args": { - "restrictedToMinimumLevel": "Error", - "from": "", - "to": "", - "host": "", - "credentials": { - "userName": "", - "password": "" - }, - "port": 587, - "connectionSecurity": "StartTls", - "subject": "[myAi] CV Search Job Error Alert", - "body": "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}" - } } ], "Enrich": [ diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml index 2dcd1d1..b0e40f9 100644 --- a/docker-compose/docker-compose.yml +++ b/docker-compose/docker-compose.yml @@ -35,13 +35,12 @@ services: - Ai__Ollama__EmbeddingModel=${Ai__Ollama__EmbeddingModel:-nomic-embed-text} - Ai__Ollama__TimeoutSeconds=${Ai__Ollama__TimeoutSeconds:-180} - - Serilog__WriteTo__2__Args__from=${Serilog__WriteTo__2__Args__from:-} - - Serilog__WriteTo__2__Args__to=${Serilog__WriteTo__2__Args__to:-} - - Serilog__WriteTo__2__Args__host=${Serilog__WriteTo__2__Args__host:-} - - Serilog__WriteTo__2__Args__credentials__userName=${Serilog__WriteTo__2__Args__credentials__userName:-} - - Serilog__WriteTo__2__Args__credentials__password=${Serilog__WriteTo__2__Args__credentials__password:-} - - Serilog__WriteTo__2__Args__port=${Serilog__WriteTo__2__Args__port:-587} - - Serilog__WriteTo__2__Args__connectionSecurity=${Serilog__WriteTo__2__Args__connectionSecurity:-StartTls} + - SerilogEmail__From=${SerilogEmail__From:-} + - SerilogEmail__To=${SerilogEmail__To:-} + - SerilogEmail__Host=${SerilogEmail__Host:-} + - SerilogEmail__Port=${SerilogEmail__Port:-587} + - SerilogEmail__UserName=${SerilogEmail__UserName:-} + - SerilogEmail__Password=${SerilogEmail__Password:-} volumes: - ${LOGS_PATH:-/opt/myai/logs}/rag-api:/app/logs networks: @@ -85,13 +84,12 @@ services: - Matcher__DeepScoreTopN=${Matcher__DeepScoreTopN:-5} - Matcher__MaxJobTextChars=${Matcher__MaxJobTextChars:-60000} - - Serilog__WriteTo__2__Args__from=${Serilog__WriteTo__2__Args__from:-} - - Serilog__WriteTo__2__Args__to=${Serilog__WriteTo__2__Args__to:-} - - Serilog__WriteTo__2__Args__host=${Serilog__WriteTo__2__Args__host:-} - - Serilog__WriteTo__2__Args__credentials__userName=${Serilog__WriteTo__2__Args__credentials__userName:-} - - Serilog__WriteTo__2__Args__credentials__password=${Serilog__WriteTo__2__Args__credentials__password:-} - - Serilog__WriteTo__2__Args__port=${Serilog__WriteTo__2__Args__port:-587} - - Serilog__WriteTo__2__Args__connectionSecurity=${Serilog__WriteTo__2__Args__connectionSecurity:-StartTls} + - SerilogEmail__From=${SerilogEmail__From:-} + - SerilogEmail__To=${SerilogEmail__To:-} + - SerilogEmail__Host=${SerilogEmail__Host:-} + - SerilogEmail__Port=${SerilogEmail__Port:-587} + - SerilogEmail__UserName=${SerilogEmail__UserName:-} + - SerilogEmail__Password=${SerilogEmail__Password:-} volumes: - ${LOGS_PATH:-/opt/myai/logs}/cv-matcher-api:/app/logs networks: @@ -126,13 +124,12 @@ services: - FileStorage__Path=${FileStorage__Path:-Files} - - Serilog__WriteTo__2__Args__from=${Serilog__WriteTo__2__Args__from:-} - - Serilog__WriteTo__2__Args__to=${Serilog__WriteTo__2__Args__to:-} - - Serilog__WriteTo__2__Args__host=${Serilog__WriteTo__2__Args__host:-} - - Serilog__WriteTo__2__Args__credentials__userName=${Serilog__WriteTo__2__Args__credentials__userName:-} - - Serilog__WriteTo__2__Args__credentials__password=${Serilog__WriteTo__2__Args__credentials__password:-} - - Serilog__WriteTo__2__Args__port=${Serilog__WriteTo__2__Args__port:-587} - - Serilog__WriteTo__2__Args__connectionSecurity=${Serilog__WriteTo__2__Args__connectionSecurity:-StartTls} + - SerilogEmail__From=${SerilogEmail__From:-} + - SerilogEmail__To=${SerilogEmail__To:-} + - SerilogEmail__Host=${SerilogEmail__Host:-} + - SerilogEmail__Port=${SerilogEmail__Port:-587} + - SerilogEmail__UserName=${SerilogEmail__UserName:-} + - SerilogEmail__Password=${SerilogEmail__Password:-} volumes: - ${LOGS_PATH:-/opt/myai/logs}/email-api:/app/logs - ${FILES_PATH:-/opt/myai/files}:/app/Files @@ -201,13 +198,12 @@ services: - Cors__AllowedOrigins__0=${Cors__AllowedOrigins__0:-} - Cors__AllowedOrigins__1=${Cors__AllowedOrigins__1:-} - - Serilog__WriteTo__2__Args__from=${Serilog__WriteTo__2__Args__from:-} - - Serilog__WriteTo__2__Args__to=${Serilog__WriteTo__2__Args__to:-} - - Serilog__WriteTo__2__Args__host=${Serilog__WriteTo__2__Args__host:-} - - Serilog__WriteTo__2__Args__credentials__userName=${Serilog__WriteTo__2__Args__credentials__userName:-} - - Serilog__WriteTo__2__Args__credentials__password=${Serilog__WriteTo__2__Args__credentials__password:-} - - Serilog__WriteTo__2__Args__port=${Serilog__WriteTo__2__Args__port:-587} - - Serilog__WriteTo__2__Args__connectionSecurity=${Serilog__WriteTo__2__Args__connectionSecurity:-StartTls} + - SerilogEmail__From=${SerilogEmail__From:-} + - SerilogEmail__To=${SerilogEmail__To:-} + - SerilogEmail__Host=${SerilogEmail__Host:-} + - SerilogEmail__Port=${SerilogEmail__Port:-587} + - SerilogEmail__UserName=${SerilogEmail__UserName:-} + - SerilogEmail__Password=${SerilogEmail__Password:-} volumes: - ${LOGS_PATH:-/opt/myai/logs}/api:/app/logs - ${FILES_PATH:-/opt/myai/files}:/app/Files @@ -232,13 +228,12 @@ services: - Jobs__Tasks__0__Interval=${Jobs__CvStorageCleanupInterval:-01:00:00} - Jobs__Tasks__0__Parameters__MaxTotalSizeMegabytes=${Jobs__CvStorageMaxTotalSizeMegabytes:-40} - - Serilog__WriteTo__2__Args__from=${Serilog__WriteTo__2__Args__from:-} - - Serilog__WriteTo__2__Args__to=${Serilog__WriteTo__2__Args__to:-} - - Serilog__WriteTo__2__Args__host=${Serilog__WriteTo__2__Args__host:-} - - Serilog__WriteTo__2__Args__credentials__userName=${Serilog__WriteTo__2__Args__credentials__userName:-} - - Serilog__WriteTo__2__Args__credentials__password=${Serilog__WriteTo__2__Args__credentials__password:-} - - Serilog__WriteTo__2__Args__port=${Serilog__WriteTo__2__Args__port:-587} - - Serilog__WriteTo__2__Args__connectionSecurity=${Serilog__WriteTo__2__Args__connectionSecurity:-StartTls} + - SerilogEmail__From=${SerilogEmail__From:-} + - SerilogEmail__To=${SerilogEmail__To:-} + - SerilogEmail__Host=${SerilogEmail__Host:-} + - SerilogEmail__Port=${SerilogEmail__Port:-587} + - SerilogEmail__UserName=${SerilogEmail__UserName:-} + - SerilogEmail__Password=${SerilogEmail__Password:-} volumes: - ${LOGS_PATH:-/opt/myai/logs}/cv-cleanup-job:/app/logs - ${FILES_PATH:-/opt/myai/files}:/app/Files @@ -283,13 +278,12 @@ services: - Jobs__Tasks__0__Enabled=${Jobs__CvSearchEnabled:-true} - Jobs__Tasks__0__Interval=${Jobs__CvSearchInterval:-00:00:30} - - Serilog__WriteTo__2__Args__from=${Serilog__WriteTo__2__Args__from:-} - - Serilog__WriteTo__2__Args__to=${Serilog__WriteTo__2__Args__to:-} - - Serilog__WriteTo__2__Args__host=${Serilog__WriteTo__2__Args__host:-} - - Serilog__WriteTo__2__Args__credentials__userName=${Serilog__WriteTo__2__Args__credentials__userName:-} - - Serilog__WriteTo__2__Args__credentials__password=${Serilog__WriteTo__2__Args__credentials__password:-} - - Serilog__WriteTo__2__Args__port=${Serilog__WriteTo__2__Args__port:-587} - - Serilog__WriteTo__2__Args__connectionSecurity=${Serilog__WriteTo__2__Args__connectionSecurity:-StartTls} + - SerilogEmail__From=${SerilogEmail__From:-} + - SerilogEmail__To=${SerilogEmail__To:-} + - SerilogEmail__Host=${SerilogEmail__Host:-} + - SerilogEmail__Port=${SerilogEmail__Port:-587} + - SerilogEmail__UserName=${SerilogEmail__UserName:-} + - SerilogEmail__Password=${SerilogEmail__Password:-} volumes: - ${LOGS_PATH:-/opt/myai/logs}/cv-search-job:/app/logs - ${FILES_PATH:-/opt/myai/files}:/app/Files