This commit is contained in:
2026-02-12 21:57:53 +01:00
parent f240c3dcf8
commit 4d54574cef

View File

@@ -44,8 +44,9 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
private readonly ConcurrentDictionary<string, FileState> _files = new(); private readonly ConcurrentDictionary<string, FileState> _files = new();
private readonly ConcurrentDictionary<string, ConcurrentQueue<LogMessage>> _messageQueues = new(); private readonly ConcurrentDictionary<string, ConcurrentQueue<LogMessage>> _messageQueues = new();
private const int BufferSize = 1024 * 1024; private const int BufferSize = 4 * 1024 * 1024; // 4 MB
private static readonly Encoding Utf8 = new UTF8Encoding(false); private static readonly Encoding Utf8 = new UTF8Encoding(false);
private readonly SemaphoreSlim _queueSignal = new(0);
public bool IncludeCorrelationId { get; } public bool IncludeCorrelationId { get; }
public bool EnableCategoryRouting { get; } public bool EnableCategoryRouting { get; }
@@ -58,7 +59,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
public event EventHandler<string> OnRollOver; public event EventHandler<string> OnRollOver;
private readonly Timer _flushTimer; private readonly Timer _flushTimer;
private readonly TimeSpan _flushInterval = TimeSpan.FromSeconds(1); private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(100);
private readonly string _fallbackPath; private readonly string _fallbackPath;
private readonly Aes _aes; private readonly Aes _aes;
@@ -252,14 +253,16 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
private async Task FlushMessagesBatchAsync(FileState state, ConcurrentQueue<LogMessage> queue) private async Task FlushMessagesBatchAsync(FileState state, ConcurrentQueue<LogMessage> queue)
{ {
const int maxBatch = 128; const int maxBatch = 5000;
int batchCount = 0; int batchCount = 0;
await state.WriteLock.WaitAsync(); // Rent a buffer
try byte[] combined = ArrayPool<byte>.Shared.Rent(BufferSize);
{ int pos = 0;
while (queue.TryDequeue(out var msg) && batchCount < maxBatch) while (queue.TryDequeue(out var msg) && batchCount < maxBatch)
{ {
// Rotate file if date changed
var msgDate = msg.Timestamp.UtcDateTime.Date; var msgDate = msg.Timestamp.UtcDateTime.Date;
if (state.Date != msgDate) if (state.Date != msgDate)
{ {
@@ -267,43 +270,66 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
RotateByDate(state, msgDate, string.Empty); RotateByDate(state, msgDate, string.Empty);
} }
var text = BuildMessage(msg); var msgText = BuildMessage(msg);
int byteCount = Utf8.GetByteCount(text); int byteCount = Utf8.GetByteCount(msgText);
// Flush buffer if message won't fit // Flush buffer if message won't fit
if (state.BufferPosition + byteCount > BufferSize) if (pos + byteCount > combined.Length)
{
await FlushBufferAsync(state).ConfigureAwait(false);
pos = 0;
}
Utf8.GetBytes(msgText, 0, msgText.Length, combined, pos);
pos += byteCount;
batchCount++;
}
if (pos == 0)
{
ArrayPool<byte>.Shared.Return(combined);
return;
}
byte[] dataToWrite;
if (_isEncryptionEnabled)
{
// Encrypt creates a new array, old buffer cleared immediately
dataToWrite = Encrypt(combined.AsSpan(0, pos).ToArray());
Array.Clear(combined, 0, pos);
}
else
{
dataToWrite = combined;
}
// Flush buffer if needed
if (state.BufferPosition + dataToWrite.Length > BufferSize)
{ {
await FlushBufferAsync(state).ConfigureAwait(false); await FlushBufferAsync(state).ConfigureAwait(false);
} }
// Encode directly into buffer // Rollover if file exceeds max size
Utf8.GetBytes(text, 0, text.Length, state.Buffer, state.BufferPosition); if (_maxFileSize > 0 && state.Size + dataToWrite.Length > _maxFileSize)
state.BufferPosition += byteCount; {
state.Size += byteCount; await FlushBufferAsync(state).ConfigureAwait(false);
RollOverAndCompressOldest(state, string.Empty);
batchCount++;
} }
// Encrypt buffer in place if needed // Copy into buffer
if (_isEncryptionEnabled && state.BufferPosition > 0) Array.Copy(dataToWrite, 0, state.Buffer, state.BufferPosition, dataToWrite.Length);
state.BufferPosition += dataToWrite.Length;
state.Size += dataToWrite.Length;
if (!_isEncryptionEnabled)
{ {
var encrypted = Encrypt(state.Buffer.AsSpan(0, state.BufferPosition).ToArray()); // return pooled buffer if not encrypted
await state.Stream.WriteAsync(encrypted, 0, encrypted.Length).ConfigureAwait(false); ArrayPool<byte>.Shared.Return(dataToWrite);
await state.Stream.FlushAsync().ConfigureAwait(false);
Array.Clear(encrypted, 0, encrypted.Length);
state.BufferPosition = 0;
}
else if (state.BufferPosition > 0)
{
await state.Stream.WriteAsync(state.Buffer, 0, state.BufferPosition).ConfigureAwait(false);
await state.Stream.FlushAsync().ConfigureAwait(false);
state.BufferPosition = 0;
}
}
finally
{
state.WriteLock.Release();
} }
// Clear encrypted array immediately
Array.Clear(dataToWrite, 0, dataToWrite.Length);
} }
private async Task FlushBufferAsync(FileState state, CancellationToken token = default) private async Task FlushBufferAsync(FileState state, CancellationToken token = default)
@@ -344,14 +370,21 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
foreach (var f in files) foreach (var f in files)
{ {
_compressionQueue.Enqueue(f.FullName); EnqueueCompression(f.FullName);
} }
} }
private void EnqueueCompression(string file)
{
_compressionQueue.Enqueue(file);
_queueSignal.Release();
}
private async Task CompressionWorkerAsync() private async Task CompressionWorkerAsync()
{ {
while (!_disposed) while (!_disposed)
{ {
await _queueSignal.WaitAsync();
if (_compressionQueue.TryDequeue(out var filePath)) if (_compressionQueue.TryDequeue(out var filePath))
{ {
await _compressionSemaphore.WaitAsync(); await _compressionSemaphore.WaitAsync();
@@ -364,23 +397,22 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
_compressionSemaphore.Release(); _compressionSemaphore.Release();
} }
} }
else
{
// Sleep longer if no work to reduce CPU
await Task.Delay(2000);
}
} }
} }
private async Task CompressOldLogFileAsync(string filePath, int retryCount = 3) private async Task CompressOldLogFileAsync(string filePath, int retryCount = 3)
{ {
if (!File.Exists(filePath) || filePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) if (!File.Exists(filePath) || filePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
{
return; return;
}
var dir = Path.GetDirectoryName(filePath); var dir = Path.GetDirectoryName(filePath);
var name = Path.GetFileNameWithoutExtension(filePath); var name = Path.GetFileNameWithoutExtension(filePath); // "app_20260212_1"
var ext = Path.GetExtension(filePath); var ext = Path.GetExtension(filePath); // ".log"
// Determine the next _N.log.gz name
int suffix = 1; int suffix = 1;
string compressedFile; string compressedFile;
do do
@@ -395,16 +427,17 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
{ {
using var original = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); using var original = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
using var compressed = new FileStream(compressedFile, FileMode.CreateNew, FileAccess.Write); using var compressed = new FileStream(compressedFile, FileMode.CreateNew, FileAccess.Write);
using var gzip = new GZipStream(compressed, CompressionLevel.Fastest); // faster using var gzip = new GZipStream(compressed, CompressionLevel.Optimal);
await original.CopyToAsync(gzip).ConfigureAwait(false); await original.CopyToAsync(gzip).ConfigureAwait(false);
await gzip.FlushAsync().ConfigureAwait(false); await gzip.FlushAsync().ConfigureAwait(false);
File.Delete(filePath); File.Delete(filePath);
return; return; // success
} }
catch (IOException) catch (IOException)
{ {
// File busy? Wait a bit and retry
await Task.Delay(200); await Task.Delay(200);
} }
catch (Exception ex) catch (Exception ex)
@@ -678,7 +711,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
var oldestFile = Path.Combine(dir, $"{name}_{_maxRolloverFiles}{ext}"); var oldestFile = Path.Combine(dir, $"{name}_{_maxRolloverFiles}{ext}");
if (File.Exists(oldestFile)) if (File.Exists(oldestFile))
{ {
_compressionQueue.Enqueue(oldestFile); EnqueueCompression(oldestFile);
} }
} }