using MailSharp.MailClient.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; namespace MailSharp.MailClient.Services.Logging; // Custom ILogger sink that persists every log entry to LiteDB (see LogStore) so logs survive an // app restart and are visible/filterable on the Maintenance page - unlike the console provider, // which is lost the moment the process isn't attached to an interactive terminal (e.g. running as // a Windows Service). Registered alongside the console provider, not instead of it; both receive // the same events and both respect the standard Logging:LogLevel configuration, since this provider // does no filtering of its own beyond what ILoggerFactory already applies. public class LiteDbLoggerProvider(ILogStore logStore) : ILoggerProvider { // LoggerExternalScopeProvider is the framework's own IExternalScopeProvider implementation // (AsyncLocal-backed) - owning one here rather than sharing the host's means every // logger.BeginScope() call made anywhere in the app is captured for this sink specifically, // without depending on ISupportExternalScopeProvider wiring from the logging builder. private readonly IExternalScopeProvider _scopeProvider = new LoggerExternalScopeProvider(); public ILogger CreateLogger(string categoryName) => new LiteDbLogger(categoryName, logStore, _scopeProvider); public void Dispose() { } } public class LiteDbLogger(string categoryName, ILogStore logStore, IExternalScopeProvider scopeProvider) : ILogger { public IDisposable? BeginScope(TState state) where TState : notnull => scopeProvider.Push(state); public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { if (!IsEnabled(logLevel)) return; var scopeData = new Dictionary(); scopeProvider.ForEachScope((scope, dict) => Flatten(scope, dict), scopeData); int? accountId = scopeData.TryGetValue("accountId", out var accountIdText) && int.TryParse(accountIdText, out var id) ? id : null; logStore.Add(new LogEntry { Timestamp = DateTime.UtcNow, Level = logLevel, Category = categoryName, Message = formatter(state, exception), Exception = exception?.ToString(), EventId = eventId.Id == 0 ? null : eventId.Id, AccountId = accountId, ScopeData = scopeData }); } // BeginScope() is most commonly called with an IEnumerable> (an // anonymous object or Dictionary, as passed throughout ImapService/SmtpService/the API // controllers) - flatten that shape; anything else just contributes its ToString() as a single // unnamed entry so scopes built some other way still show up rather than being silently dropped. private static void Flatten(object? scope, Dictionary into) { if (scope is IEnumerable> pairs) { foreach (var kvp in pairs) { if (kvp.Value != null) into[kvp.Key] = kvp.Value.ToString() ?? ""; } return; } if (scope != null) into[$"scope{into.Count}"] = scope.ToString() ?? ""; } }