103 lines
3.7 KiB
C#
103 lines
3.7 KiB
C#
using Microsoft.EntityFrameworkCore;
|
|
using ROLAC.API.Data.Logging;
|
|
using ROLAC.API.Entities.Logging;
|
|
|
|
namespace ROLAC.API.Services.Logging;
|
|
|
|
/// <summary>
|
|
/// The single consumer that drains <see cref="SystemLogQueue"/> and batch-inserts rows through
|
|
/// the dedicated <see cref="LogDbContext"/> (a fresh DI scope per batch). Persistence failures
|
|
/// are swallowed to <c>Console.Error</c> only — they must never propagate back into the logging
|
|
/// pipeline or crash the host.
|
|
/// </summary>
|
|
public sealed class LogWriterBackgroundService : BackgroundService
|
|
{
|
|
private const int MaxBatchSize = 200;
|
|
private static readonly TimeSpan FlushInterval = TimeSpan.FromSeconds(1);
|
|
|
|
private readonly SystemLogQueue _queue;
|
|
private readonly IServiceScopeFactory _scopeFactory;
|
|
|
|
public LogWriterBackgroundService(SystemLogQueue queue, IServiceScopeFactory scopeFactory)
|
|
{
|
|
_queue = queue;
|
|
_scopeFactory = scopeFactory;
|
|
}
|
|
|
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
|
{
|
|
var systemBatch = new List<SystemLog>(MaxBatchSize);
|
|
var auditBatch = new List<AuditLog>(MaxBatchSize);
|
|
|
|
try
|
|
{
|
|
await foreach (var envelope in _queue.ReadAllAsync(stoppingToken))
|
|
{
|
|
if (envelope.System is not null) systemBatch.Add(envelope.System);
|
|
if (envelope.Audit is not null) auditBatch.Add(envelope.Audit);
|
|
|
|
// Coalesce a short burst into one round-trip; flush on size or a brief idle.
|
|
if (systemBatch.Count + auditBatch.Count >= MaxBatchSize)
|
|
{
|
|
await FlushAsync(systemBatch, auditBatch, stoppingToken);
|
|
continue;
|
|
}
|
|
|
|
if (!await WaitForMoreAsync(FlushInterval, stoppingToken))
|
|
await FlushAsync(systemBatch, auditBatch, stoppingToken);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Shutting down — drain whatever is buffered.
|
|
}
|
|
|
|
await FlushAsync(systemBatch, auditBatch, CancellationToken.None);
|
|
}
|
|
|
|
/// <summary>Brief debounce so bursts coalesce; returns false once the window elapses.</summary>
|
|
private static async Task<bool> WaitForMoreAsync(TimeSpan window, CancellationToken token)
|
|
{
|
|
try
|
|
{
|
|
await Task.Delay(window, token);
|
|
return false;
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private async Task FlushAsync(
|
|
List<SystemLog> systemBatch, List<AuditLog> auditBatch, CancellationToken token)
|
|
{
|
|
if (systemBatch.Count == 0 && auditBatch.Count == 0)
|
|
return;
|
|
|
|
try
|
|
{
|
|
using var scope = _scopeFactory.CreateScope();
|
|
var db = scope.ServiceProvider.GetRequiredService<LogDbContext>();
|
|
|
|
if (systemBatch.Count > 0) db.SystemLogs.AddRange(systemBatch);
|
|
if (auditBatch.Count > 0) db.AuditLogs.AddRange(auditBatch);
|
|
|
|
await db.SaveChangesAsync(token);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Last resort: never throw out of the log writer. Include the inner exception —
|
|
// the EF wrapper message alone ("An error occurred while saving...") hides the cause.
|
|
var detail = ex.InnerException is null ? ex.Message : $"{ex.Message} -> {ex.InnerException.Message}";
|
|
await Console.Error.WriteLineAsync(
|
|
$"[LogWriter] Failed to persist {systemBatch.Count} system + {auditBatch.Count} audit rows: {detail}");
|
|
}
|
|
finally
|
|
{
|
|
systemBatch.Clear();
|
|
auditBatch.Clear();
|
|
}
|
|
}
|
|
}
|