@@ -0,0 +1,87 @@
|
||||
using System.Collections.Concurrent;
|
||||
using Microsoft.Extensions.Options;
|
||||
using ROLAC.API.Entities.Logging;
|
||||
using MsLogLevel = Microsoft.Extensions.Logging.LogLevel;
|
||||
|
||||
namespace ROLAC.API.Services.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// A singleton <see cref="ILoggerProvider"/> that turns framework/app log events into
|
||||
/// <see cref="SystemLog"/> rows enqueued onto <see cref="SystemLogQueue"/>. It depends only on
|
||||
/// singletons (queue, options, IHttpContextAccessor) and NEVER touches a DbContext — that is
|
||||
/// what makes the enqueue-only design safe from the singleton logging pipeline.
|
||||
/// </summary>
|
||||
[ProviderAlias("Database")]
|
||||
public sealed class DbLoggerProvider : ILoggerProvider
|
||||
{
|
||||
private readonly SystemLogQueue _queue;
|
||||
private readonly DatabaseLoggerOptions _options;
|
||||
private readonly IHttpContextAccessor _http;
|
||||
private readonly ConcurrentDictionary<string, DbLogger> _loggers = new();
|
||||
|
||||
public DbLoggerProvider(
|
||||
SystemLogQueue queue, IOptions<DatabaseLoggerOptions> options, IHttpContextAccessor http)
|
||||
{
|
||||
_queue = queue;
|
||||
_options = options.Value;
|
||||
_http = http;
|
||||
}
|
||||
|
||||
public ILogger CreateLogger(string categoryName) =>
|
||||
_loggers.GetOrAdd(categoryName, name => new DbLogger(name, _queue, _options, _http));
|
||||
|
||||
public void Dispose() => _loggers.Clear();
|
||||
}
|
||||
|
||||
/// <summary>The per-category logger. Drops events below the floor or in excluded categories.</summary>
|
||||
internal sealed class DbLogger : ILogger
|
||||
{
|
||||
private readonly string _category;
|
||||
private readonly SystemLogQueue _queue;
|
||||
private readonly DatabaseLoggerOptions _options;
|
||||
private readonly IHttpContextAccessor _http;
|
||||
private readonly bool _excluded;
|
||||
|
||||
public DbLogger(
|
||||
string category, SystemLogQueue queue, DatabaseLoggerOptions options, IHttpContextAccessor http)
|
||||
{
|
||||
_category = category;
|
||||
_queue = queue;
|
||||
_options = options;
|
||||
_http = http;
|
||||
_excluded = options.ExcludedCategories.Any(prefix =>
|
||||
category.StartsWith(prefix, StringComparison.Ordinal));
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
public bool IsEnabled(MsLogLevel logLevel) =>
|
||||
!_excluded && logLevel != MsLogLevel.None && logLevel >= _options.MinimumLevel;
|
||||
|
||||
public void Log<TState>(
|
||||
MsLogLevel logLevel, EventId eventId, TState state, Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
return;
|
||||
|
||||
var context = _http.HttpContext;
|
||||
|
||||
var log = new SystemLog
|
||||
{
|
||||
Timestamp = DateTimeOffset.UtcNow,
|
||||
Level = LogLevelMap.FromMs(logLevel),
|
||||
Category = _category,
|
||||
EventId = eventId.Id == 0 ? null : eventId.Id,
|
||||
Message = formatter(state, exception),
|
||||
Exception = exception?.ToString(),
|
||||
RequestPath = context?.Request.Path.Value,
|
||||
HttpMethod = context?.Request.Method,
|
||||
UserId = context?.User.FindFirst("sub")?.Value,
|
||||
IpAddress = context?.Connection.RemoteIpAddress?.ToString(),
|
||||
CorrelationId = context?.TraceIdentifier,
|
||||
};
|
||||
|
||||
_queue.TryEnqueue(log);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user