Code
·
72 lines
·
3195 bytes
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72using 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>(TState state) where TState : notnull =>
scopeProvider.Push(state);
public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel)) return;
var scopeData = new Dictionary<string, string>();
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<KeyValuePair<string,object>> (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<string, string> into)
{
if (scope is IEnumerable<KeyValuePair<string, object?>> 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() ?? "";
}
}