88 lines
3.3 KiB
C#
88 lines
3.3 KiB
C#
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);
|
|
}
|
|
}
|