diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj
index 5a608bd..95f005c 100644
--- a/EonaCat.Logger/EonaCat.Logger.csproj
+++ b/EonaCat.Logger/EonaCat.Logger.csproj
@@ -13,8 +13,8 @@
EonaCat (Jeroen Saey)
EonaCat;Logger;EonaCatLogger;Log;Writer;Jeroen;Saey
- 1.7.2
- 1.7.2
+ 1.7.3
+ 1.7.3
README.md
True
LICENSE
@@ -25,7 +25,7 @@
- 1.7.2+{chash:10}.{c:ymd}
+ 1.7.3+{chash:10}.{c:ymd}
true
true
v[0-9]*
diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs
index 116cc52..9bed913 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs
@@ -31,26 +31,43 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
private const int MaxQueueSize = 50_000;
private const int MaxCategories = 256;
+ private const int MaxStringBuilderPoolSize = 32; // Limit pool size
+ private const int MaxCompressionQueueSize = 100; // Limit compression queue
public bool IsEncryptionEnabled => _encryptionKey != null && _encryptionIV != null;
- private bool _disposed;
+ private int _disposed;
private int _isFlushing;
public static TimeSpan FaultCooldown = TimeSpan.FromSeconds(60);
- private readonly ConcurrentQueue _compressionQueue = new();
- private readonly SemaphoreSlim _compressionSemaphore = new(1, 1);
- private readonly Task _compressionWorker;
+ private readonly ConcurrentQueue _compressionQueue = new ConcurrentQueue();
+ private readonly SemaphoreSlim _compressionSemaphore = new SemaphoreSlim(1, 1);
+ private Task _compressionWorker;
+ private readonly CancellationTokenSource _compressionCts = new CancellationTokenSource();
- private readonly LoggerScopedContext _context = new();
- private readonly ConcurrentDictionary _files = new();
+ private readonly LoggerScopedContext _context = new LoggerScopedContext();
+ private readonly ConcurrentDictionary _files = new ConcurrentDictionary();
private sealed class MessageQueue
{
- public readonly ConcurrentQueue Queue = new();
+ public readonly ConcurrentQueue Queue = new ConcurrentQueue();
public int Count;
+ public bool TryEnqueue(LogMessage msg, int maxSize)
+ {
+ // Check before incrementing
+ int currentCount = Volatile.Read(ref Count);
+ if (currentCount >= maxSize)
+ {
+ return false;
+ }
+
+ Queue.Enqueue(msg);
+ Interlocked.Increment(ref Count);
+ return true;
+ }
+
public bool TryDequeue(out LogMessage msg)
{
if (Queue.TryDequeue(out msg))
@@ -59,17 +76,23 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
return true;
}
- msg = default;
+ msg = default(LogMessage);
return false;
}
+
+ public void Clear()
+ {
+ while (Queue.TryDequeue(out _))
+ {
+ Interlocked.Decrement(ref Count);
+ }
+ }
}
-
- private readonly ConcurrentDictionary _messageQueues = new();
+ private readonly ConcurrentDictionary _messageQueues = new ConcurrentDictionary();
private const int BufferSize = 256 * 1024; // 256 KB
private static readonly Encoding Utf8 = new UTF8Encoding(false);
- private readonly SemaphoreSlim _queueSignal = new(0);
public bool IncludeCorrelationId { get; }
public bool EnableCategoryRouting { get; }
@@ -81,10 +104,14 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
public event EventHandler OnError;
public event EventHandler OnRollOver;
- private readonly Timer _flushTimer;
+ private Timer _flushTimer;
private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(100);
private readonly string _fallbackPath;
- private readonly Aes _aes;
+ private Aes _aes;
+
+ // Bounded StringBuilder pool with size limit
+ private readonly ConcurrentBag _stringBuilderPool = new ConcurrentBag();
+ private int _stringBuilderPoolCount;
private sealed class FileState : IDisposable
{
@@ -92,17 +119,23 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
public long Size;
public DateTime Date;
- public byte[] Buffer = ArrayPool.Shared.Rent(BufferSize);
+ public byte[] Buffer;
public int BufferPosition;
public FileStream Stream;
- public SemaphoreSlim WriteLock = new(1, 1);
+ public SemaphoreSlim WriteLock = new SemaphoreSlim(1, 1);
public bool IsFaulted;
public DateTime LastFailureUtc;
+ private int _disposed;
public void Dispose()
{
+ if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
+ {
+ return; // Already disposed
+ }
+
try
{
if (Buffer != null)
@@ -111,8 +144,22 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
Buffer = null;
}
- Stream?.Dispose();
- WriteLock?.Dispose();
+ if (Stream != null)
+ {
+ try
+ {
+ Stream.Flush();
+ }
+ catch { }
+ Stream.Dispose();
+ Stream = null;
+ }
+
+ if (WriteLock != null)
+ {
+ WriteLock.Dispose();
+ WriteLock = null;
+ }
}
catch { }
}
@@ -149,7 +196,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
_flushTimer = new Timer(FlushTimerCallback, null, _flushInterval, _flushInterval);
- _compressionWorker = Task.Run(CompressionWorkerAsync);
+ _compressionWorker = Task.Run(() => CompressionWorkerAsync(_compressionCts.Token));
}
private string EnsureWritableDirectory(string path)
@@ -175,23 +222,48 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
private void FlushTimerCallback(object state)
{
- if (_disposed)
+ if (_disposed == 1)
{
return;
}
- if (Interlocked.Exchange(ref _isFlushing, 1) == 1)
+ // Non-blocking check - skip if already flushing
+ if (Interlocked.CompareExchange(ref _isFlushing, 1, 0) != 0)
{
return;
}
- _ = PeriodicFlushAsync().ContinueWith(_ => Interlocked.Exchange(ref _isFlushing, 0));
+ // Fire-and-forget with proper error handling
+ _ = Task.Run(async () =>
+ {
+ try
+ {
+ await PeriodicFlushAsync().ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ OnError?.Invoke(this, new ErrorMessage
+ {
+ Exception = ex,
+ Message = "Error during periodic flush"
+ });
+ }
+ finally
+ {
+ Interlocked.Exchange(ref _isFlushing, 0);
+ }
+ });
}
internal override Task WriteMessagesAsync(
- IReadOnlyList messages,
- CancellationToken token)
+ IReadOnlyList messages,
+ CancellationToken token)
{
+ if (_disposed == 1)
+ {
+ return Task.CompletedTask;
+ }
+
foreach (var msg in messages)
{
if (msg.Level < MinimumLogLevel)
@@ -203,33 +275,45 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
? SanitizeCategory(msg.Category)
: string.Empty;
- var mq = _messageQueues.GetOrAdd(key, _ => new MessageQueue());
-
- if (Interlocked.Increment(ref mq.Count) > MaxQueueSize)
+ // Limit total categories to prevent unbounded growth
+ if (_messageQueues.Count >= MaxCategories && !_messageQueues.ContainsKey(key))
{
- if (mq.Queue.TryDequeue(out _))
- {
- Interlocked.Decrement(ref mq.Count);
- }
+ // Use default queue if we've hit category limit
+ key = string.Empty;
}
- mq.Queue.Enqueue(msg);
+ var mq = _messageQueues.GetOrAdd(key, _ => new MessageQueue());
+
+ // Use atomic enqueue with size check
+ if (!mq.TryEnqueue(msg, MaxQueueSize))
+ {
+ // Queue full - drop oldest message
+ if (mq.TryDequeue(out _))
+ {
+ mq.TryEnqueue(msg, MaxQueueSize);
+ }
+ }
}
return Task.CompletedTask;
}
-
-
private async Task PeriodicFlushAsync()
{
- if (_disposed)
+ if (_disposed == 1)
{
return;
}
+ var keysToRemove = new List();
+
foreach (var kv in _messageQueues)
{
+ if (_disposed == 1)
+ {
+ break;
+ }
+
var key = kv.Key;
var mq = kv.Value;
@@ -241,11 +325,8 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
if (!TryRecover(state))
{
- while (mq.Queue.TryDequeue(out _))
- {
- Interlocked.Decrement(ref mq.Count);
- }
-
+ // Drain queue on unrecoverable failure
+ mq.Clear();
continue;
}
@@ -254,7 +335,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
continue;
}
- // Non-blocking lock attempt
+ // Non-blocking lock attempt with immediate bailout
if (!state.WriteLock.Wait(0))
{
continue;
@@ -262,40 +343,50 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
try
{
- await FlushMessagesBatchAsync(state, mq)
- .ConfigureAwait(false);
+ await FlushMessagesBatchAsync(state, mq).ConfigureAwait(false);
if (state.BufferPosition > 0)
{
- await FlushBufferAsync(state)
- .ConfigureAwait(false);
+ await FlushBufferAsync(state).ConfigureAwait(false);
}
+
+ // Mark for cleanup if empty
+ if (mq.Count == 0 && !string.IsNullOrEmpty(key))
+ {
+ keysToRemove.Add(key);
+ }
+ }
+ catch (Exception ex)
+ {
+ OnError?.Invoke(this, new ErrorMessage
+ {
+ Exception = ex,
+ Message = $"Error flushing messages for category: {key}"
+ });
}
finally
{
state.WriteLock.Release();
}
+ }
- // Cleanup empty categories
- if (mq.Count == 0)
+ // Cleanup empty categories outside the iteration
+ foreach (var key in keysToRemove)
+ {
+ if (_messageQueues.TryRemove(key, out var removed))
{
- if (_messageQueues.TryRemove(key, out var removed))
- {
- while (removed.Queue.TryDequeue(out _)) { }
- }
+ removed.Clear();
+ }
- if (_files.TryRemove(key, out var removedState))
- {
- removedState.Dispose();
- }
+ if (_files.TryRemove(key, out var removedState))
+ {
+ removedState.Dispose();
}
}
QueueOldFilesForCompression();
}
-
-
private async Task FlushMessagesBatchAsync(FileState state, MessageQueue queue)
{
const int MaxBatch = 5000;
@@ -315,6 +406,11 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
// Rotate if date changed
if (state.Date != msgDate)
{
+ if (pos > 0)
+ {
+ await WriteBufferedData(state, combined, pos).ConfigureAwait(false);
+ pos = 0;
+ }
await FlushBufferAsync(state).ConfigureAwait(false);
RotateByDate(state, msgDate, string.Empty);
}
@@ -325,53 +421,29 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
// Grow buffer if needed
if (pos + requiredLength > combined.Length)
{
- int newSize = Math.Max(combined.Length * 2, pos + requiredLength);
- byte[] newBuffer = ArrayPool.Shared.Rent(newSize);
- Array.Copy(combined, 0, newBuffer, 0, pos);
- ArrayPool.Shared.Return(combined, clearArray: true);
- combined = newBuffer;
+ // Write what we have first
+ if (pos > 0)
+ {
+ await WriteBufferedData(state, combined, pos).ConfigureAwait(false);
+ pos = 0;
+ }
+
+ // If single message is huge, rent bigger buffer
+ if (requiredLength > combined.Length)
+ {
+ ArrayPool.Shared.Return(combined, clearArray: true);
+ combined = ArrayPool.Shared.Rent(requiredLength + 1024);
+ }
}
pos += Utf8.GetBytes(messageString, 0, messageString.Length, combined, pos);
batchCount++;
}
- if (pos == 0)
+ if (pos > 0)
{
- return; // nothing to write
+ await WriteBufferedData(state, combined, pos).ConfigureAwait(false);
}
-
- byte[] dataToWrite;
- if (_isEncryptionEnabled)
- {
- // Encrypt and clear combined buffer immediately
- dataToWrite = Encrypt(combined.AsSpan(0, pos).ToArray());
- }
- else
- {
- dataToWrite = combined;
- }
-
- // Flush buffer if needed
- if (state.BufferPosition + pos > BufferSize)
- {
- await FlushBufferAsync(state).ConfigureAwait(false);
- }
-
- // Rollover
- if (_maxFileSize > 0 && state.Size + pos > _maxFileSize)
- {
- await FlushBufferAsync(state).ConfigureAwait(false);
- RollOverAndCompressOldest(state, string.Empty);
- }
-
- // Copy to file buffer
- Array.Copy(dataToWrite, 0, state.Buffer, state.BufferPosition, pos);
- state.BufferPosition += pos;
- state.Size += pos;
-
- // Clear sensitive data
- Array.Clear(dataToWrite, 0, dataToWrite.Length);
}
finally
{
@@ -379,8 +451,57 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
}
}
+ private async Task WriteBufferedData(FileState state, byte[] data, int length)
+ {
+ byte[] dataToWrite = data;
+ int dataLength = length;
+ byte[] encryptedData = null;
- private async Task FlushBufferAsync(FileState state, CancellationToken token = default)
+ try
+ {
+ if (_isEncryptionEnabled)
+ {
+ // Encrypt - this creates a new array
+ encryptedData = Encrypt(data, length);
+ dataToWrite = encryptedData;
+ dataLength = encryptedData.Length;
+ }
+
+ // Flush buffer if needed
+ if (state.BufferPosition + dataLength > BufferSize)
+ {
+ await FlushBufferAsync(state).ConfigureAwait(false);
+ }
+
+ // Rollover check
+ if (_maxFileSize > 0 && state.Size + dataLength > _maxFileSize)
+ {
+ await FlushBufferAsync(state).ConfigureAwait(false);
+ RollOverAndCompressOldest(state, string.Empty);
+ }
+
+ // Ensure buffer exists
+ if (state.Buffer == null)
+ {
+ state.Buffer = ArrayPool.Shared.Rent(BufferSize);
+ }
+
+ // Copy to file buffer
+ Array.Copy(dataToWrite, 0, state.Buffer, state.BufferPosition, dataLength);
+ state.BufferPosition += dataLength;
+ state.Size += dataLength;
+ }
+ finally
+ {
+ // Clear and return encrypted data
+ if (encryptedData != null)
+ {
+ Array.Clear(encryptedData, 0, encryptedData.Length);
+ }
+ }
+ }
+
+ private async Task FlushBufferAsync(FileState state, CancellationToken token = default(CancellationToken))
{
if (state.IsFaulted || state.BufferPosition == 0 || state.Stream == null)
{
@@ -399,66 +520,93 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
finally
{
// Clear buffer to prevent sensitive data leak
- Array.Clear(state.Buffer, 0, state.BufferPosition);
+ if (state.Buffer != null)
+ {
+ Array.Clear(state.Buffer, 0, state.BufferPosition);
+ }
state.BufferPosition = 0;
}
}
private void QueueOldFilesForCompression()
{
- if (_maxRetainedFiles <= 0)
+ if (_maxRetainedFiles <= 0 || _disposed == 1)
{
return;
}
- var files = new DirectoryInfo(_path)
- .GetFiles($"{_fileNamePrefix}*")
- .OrderByDescending(f => f.LastWriteTimeUtc)
- .Skip(_maxRetainedFiles);
-
- foreach (var f in files)
+ try
{
- EnqueueCompression(f.FullName);
+ var dirInfo = new DirectoryInfo(_path);
+ if (!dirInfo.Exists)
+ {
+ return;
+ }
+
+ var files = dirInfo.GetFiles($"{_fileNamePrefix}*")
+ .Where(f => !f.Name.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
+ .OrderByDescending(f => f.LastWriteTimeUtc)
+ .Skip(_maxRetainedFiles)
+ .ToList();
+
+ foreach (var f in files)
+ {
+ EnqueueCompression(f.FullName);
+ }
+ }
+ catch
+ {
+ // Ignore errors in cleanup
}
}
private void EnqueueCompression(string file)
{
+ // Limit compression queue size to prevent unbounded growth
+ if (_compressionQueue.Count >= MaxCompressionQueueSize)
+ {
+ return;
+ }
+
_compressionQueue.Enqueue(file);
- _queueSignal.Release();
}
- private async Task CompressionWorkerAsync()
+ private async Task CompressionWorkerAsync(CancellationToken cancellationToken)
{
try
{
- while (!_disposed)
+ while (!cancellationToken.IsCancellationRequested)
{
try
{
- // Use a timeout to avoid hanging if disposed
- if (!await _queueSignal.WaitAsync(TimeSpan.FromSeconds(1)))
- {
- if (_disposed)
- {
- break;
- }
-
- continue;
- }
+ // Wait for work with cancellation
+ await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false);
}
- catch
+ catch (OperationCanceledException)
{
- // Handle cancellation or disposal race condition
break;
}
- if (_compressionQueue.TryDequeue(out var filePath))
+ // Process all queued files
+ while (_compressionQueue.TryDequeue(out var filePath))
{
- await _compressionSemaphore.WaitAsync();
+ if (cancellationToken.IsCancellationRequested)
+ {
+ break;
+ }
+
+ await _compressionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
- await CompressOldLogFileAsync(filePath);
+ await CompressOldLogFileAsync(filePath).ConfigureAwait(false);
+ }
+ catch (Exception ex)
+ {
+ OnError?.Invoke(this, new ErrorMessage
+ {
+ Exception = ex,
+ Message = $"Compression worker error: {filePath}"
+ });
}
finally
{
@@ -473,7 +621,6 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
}
}
-
private async Task CompressOldLogFileAsync(string filePath, int retryCount = 3)
{
if (!File.Exists(filePath) || filePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase))
@@ -482,8 +629,8 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
}
var dir = Path.GetDirectoryName(filePath);
- var name = Path.GetFileNameWithoutExtension(filePath); // "app_20260212_1"
- var ext = Path.GetExtension(filePath); // ".log"
+ var name = Path.GetFileNameWithoutExtension(filePath);
+ var ext = Path.GetExtension(filePath);
// Determine the next _N.log.gz name
int suffix = 1;
@@ -492,26 +639,37 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
{
compressedFile = Path.Combine(dir, $"{name}_{suffix}{ext}.gz");
suffix++;
- } while (File.Exists(compressedFile));
+ } while (File.Exists(compressedFile) && suffix < 1000); // Prevent infinite loop
for (int attempt = 0; attempt < retryCount; attempt++)
{
+ FileStream original = null;
+ FileStream compressed = null;
+ GZipStream gzip = null;
+
try
{
- using var original = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
- using var compressed = new FileStream(compressedFile, FileMode.CreateNew, FileAccess.Write);
- using var gzip = new GZipStream(compressed, CompressionLevel.Optimal);
+ original = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read);
+ compressed = new FileStream(compressedFile, FileMode.CreateNew, FileAccess.Write);
+ gzip = new GZipStream(compressed, CompressionLevel.Optimal);
await original.CopyToAsync(gzip).ConfigureAwait(false);
await gzip.FlushAsync().ConfigureAwait(false);
+ gzip.Dispose();
+ gzip = null;
+ compressed.Dispose();
+ compressed = null;
+ original.Dispose();
+ original = null;
+
File.Delete(filePath);
return; // success
}
catch (IOException)
{
// File busy? Wait a bit and retry
- await Task.Delay(200);
+ await Task.Delay(200).ConfigureAwait(false);
}
catch (Exception ex)
{
@@ -522,25 +680,28 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
});
return;
}
+ finally
+ {
+ if (gzip != null) gzip.Dispose();
+ if (compressed != null) compressed.Dispose();
+ if (original != null) original.Dispose();
+ }
}
}
- private byte[] Encrypt(byte[] plainBytes)
+ private byte[] Encrypt(byte[] plainBytes, int length)
{
- if (plainBytes == null || plainBytes.Length == 0)
+ if (plainBytes == null || length == 0)
{
- return plainBytes;
+ return Array.Empty();
}
- using var encryptor = _aes.CreateEncryptor();
- var encrypted = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
-
- // Clear plaintext
- Array.Clear(plainBytes, 0, plainBytes.Length);
- return encrypted;
+ using (var encryptor = _aes.CreateEncryptor())
+ {
+ return encryptor.TransformFinalBlock(plainBytes, 0, length);
+ }
}
-
public byte[] Decrypt(byte[] encryptedData)
{
if (!IsEncryptionEnabled || encryptedData == null || encryptedData.Length == 0)
@@ -548,18 +709,40 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
return encryptedData;
}
- using var decryptor = _aes.CreateDecryptor();
- using var ms = new MemoryStream(encryptedData);
- using var cryptoStream = new System.Security.Cryptography.CryptoStream(ms, decryptor, System.Security.Cryptography.CryptoStreamMode.Read);
- using var resultStream = new MemoryStream();
- cryptoStream.CopyTo(resultStream);
+ using (var decryptor = _aes.CreateDecryptor())
+ using (var ms = new MemoryStream(encryptedData))
+ using (var cryptoStream = new CryptoStream(ms, decryptor, CryptoStreamMode.Read))
+ using (var resultStream = new MemoryStream())
+ {
+ cryptoStream.CopyTo(resultStream);
+ return resultStream.ToArray();
+ }
+ }
- var result = resultStream.ToArray();
+ private StringBuilder RentStringBuilder()
+ {
+ if (_stringBuilderPool.TryTake(out var sb))
+ {
+ sb.Clear();
+ return sb;
+ }
+ return new StringBuilder(512);
+ }
- // Clear sensitive memory
- Array.Clear(encryptedData, 0, encryptedData.Length);
-
- return result;
+ private void ReturnStringBuilder(StringBuilder sb)
+ {
+ // Limit pool size and builder capacity to prevent unbounded growth
+ if (sb.Capacity < 8192 && _stringBuilderPoolCount < MaxStringBuilderPoolSize)
+ {
+ if (Interlocked.Increment(ref _stringBuilderPoolCount) <= MaxStringBuilderPoolSize)
+ {
+ _stringBuilderPool.Add(sb);
+ }
+ else
+ {
+ Interlocked.Decrement(ref _stringBuilderPoolCount);
+ }
+ }
}
private string BuildMessage(LogMessage msg)
@@ -570,34 +753,41 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
}
var ctx = _context.GetAll();
- if (ctx.Count == 0)
+ if (ctx.Count == 0 && (msg.Tags == null || !msg.Tags.Any()))
{
return msg.Message + Environment.NewLine;
}
- var sb = new StringBuilder(msg.Message.Length + 64);
- sb.Append(msg.Message).Append(" [");
-
- foreach (var (key, value) in ctx.Select(kv => (kv.Key, kv.Value)))
+ var sb = RentStringBuilder();
+ try
{
- sb.Append(key).Append('=').Append(value).Append(' ');
- }
+ sb.Append(msg.Message).Append(" [");
- if (msg.Tags != null)
- {
- foreach (var tag in msg.Tags)
+ foreach (var kv in ctx)
{
- sb.Append("tag=").Append(tag).Append(' ');
+ sb.Append(kv.Key).Append('=').Append(kv.Value).Append(' ');
}
- }
- if (sb[sb.Length - 1] == ' ')
+ if (msg.Tags != null)
+ {
+ foreach (var tag in msg.Tags)
+ {
+ sb.Append("tag=").Append(tag).Append(' ');
+ }
+ }
+
+ if (sb.Length > 0 && sb[sb.Length - 1] == ' ')
+ {
+ sb.Length--; // remove trailing space
+ }
+
+ sb.Append(']').AppendLine();
+ return sb.ToString();
+ }
+ finally
{
- sb.Length--; // remove trailing space
+ ReturnStringBuilder(sb);
}
-
- sb.Append(']').AppendLine();
- return sb.ToString();
}
private void HandleWriteFailure(FileState state, Exception ex)
@@ -605,8 +795,15 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
state.IsFaulted = true;
state.LastFailureUtc = DateTime.UtcNow;
- state.Stream?.Dispose();
- state.Stream = null;
+ if (state.Stream != null)
+ {
+ try
+ {
+ state.Stream.Dispose();
+ }
+ catch { }
+ state.Stream = null;
+ }
string originalDir = Path.GetDirectoryName(state.FilePath);
string fallbackDir = Directory.Exists(_fallbackPath) ? _fallbackPath : EnsureWritableDirectory(originalDir);
@@ -667,7 +864,6 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
catch
{
// Attempt fallback path if recovery fails
-
try
{
string fallbackFile = Path.Combine(_fallbackPath, Path.GetFileName(state.FilePath));
@@ -694,20 +890,18 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
private FileState CreateFileState(DateTime date, string category)
{
- // Get the intended log file path
var intendedPath = GetFullName(date, category);
-
- // Ensure directory is writable (falls back automatically if needed)
var writableDir = EnsureWritableDirectory(Path.GetDirectoryName(intendedPath));
var path = Path.Combine(writableDir, Path.GetFileName(intendedPath));
try
{
- return new FileState
+ var state = new FileState
{
FilePath = path,
Date = date,
Size = GetFileSize(path),
+ Buffer = ArrayPool.Shared.Rent(BufferSize),
Stream = new FileStream(
path,
FileMode.Append,
@@ -715,6 +909,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
FileShare.ReadWrite | FileShare.Delete
)
};
+ return state;
}
catch (Exception ex)
{
@@ -728,57 +923,116 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
{
FilePath = path,
Date = date,
- IsFaulted = true
+ IsFaulted = true,
+ Buffer = ArrayPool.Shared.Rent(BufferSize)
};
}
}
private void RotateByDate(FileState state, DateTime newDate, string category)
{
- state.Stream?.Dispose();
+ if (state.Stream != null)
+ {
+ try
+ {
+ state.Stream.Flush();
+ }
+ catch { }
+ state.Stream.Dispose();
+ state.Stream = null;
+ }
+
state.Date = newDate;
state.FilePath = GetFullName(newDate, category);
state.Size = GetFileSize(state.FilePath);
- state.Stream = new FileStream(state.FilePath, FileMode.Append,
- FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
+
+ try
+ {
+ state.Stream = new FileStream(state.FilePath, FileMode.Append,
+ FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
+ }
+ catch (Exception ex)
+ {
+ state.IsFaulted = true;
+ OnError?.Invoke(this, new ErrorMessage
+ {
+ Exception = ex,
+ Message = $"Failed to rotate log file: {state.FilePath}"
+ });
+ }
}
private void RollOverAndCompressOldest(FileState state, string category)
{
- state.Stream?.Dispose();
+ if (state.Stream != null)
+ {
+ try
+ {
+ state.Stream.Flush();
+ }
+ catch { }
+ state.Stream.Dispose();
+ state.Stream = null;
+ }
var dir = Path.GetDirectoryName(state.FilePath);
var name = Path.GetFileNameWithoutExtension(state.FilePath);
- var ext = Path.GetExtension(state.FilePath); // ".log"
+ var ext = Path.GetExtension(state.FilePath);
// Shift existing rolled files up
for (int i = _maxRolloverFiles - 1; i >= 1; i--)
{
var src = Path.Combine(dir, $"{name}_{i}{ext}");
var dst = Path.Combine(dir, $"{name}_{i + 1}{ext}");
- if (File.Exists(dst))
+ try
{
- File.Delete(dst);
- }
+ if (File.Exists(dst))
+ {
+ File.Delete(dst);
+ }
- if (File.Exists(src))
+ if (File.Exists(src))
+ {
+ File.Move(src, dst);
+ }
+ }
+ catch
{
- File.Move(src, dst);
+ // Continue on error
}
}
// Move current file to _1
var rolledFile = Path.Combine(dir, $"{name}_1{ext}");
- if (File.Exists(state.FilePath))
+ try
{
- File.Move(state.FilePath, rolledFile);
- }
+ if (File.Exists(state.FilePath))
+ {
+ File.Move(state.FilePath, rolledFile);
+ }
- OnRollOver?.Invoke(this, rolledFile);
+ OnRollOver?.Invoke(this, rolledFile);
+ }
+ catch
+ {
+ // Continue on error
+ }
// Create new active log file
state.Size = 0;
- state.Stream = new FileStream(state.FilePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
+ try
+ {
+ state.Stream = new FileStream(state.FilePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
+ }
+ catch (Exception ex)
+ {
+ state.IsFaulted = true;
+ OnError?.Invoke(this, new ErrorMessage
+ {
+ Exception = ex,
+ Message = $"Failed to create new log file after rollover: {state.FilePath}"
+ });
+ }
// Compress the oldest file safely
var oldestFile = Path.Combine(dir, $"{name}_{_maxRolloverFiles}{ext}");
@@ -807,66 +1061,117 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
private static string SanitizeCategory(string category)
{
- foreach (var c in Path.GetInvalidFileNameChars())
+ if (string.IsNullOrEmpty(category))
{
- category = category.Replace(c, '_');
+ return category;
}
- return category.Replace('.', '_');
+ var chars = category.ToCharArray();
+ var invalidChars = Path.GetInvalidFileNameChars();
+
+ for (int i = 0; i < chars.Length; i++)
+ {
+ if (Array.IndexOf(invalidChars, chars[i]) >= 0 || chars[i] == '.')
+ {
+ chars[i] = '_';
+ }
+ }
+
+ return new string(chars);
}
protected override async Task OnShutdownFlushAsync()
{
- _disposed = true;
- _flushTimer?.Dispose();
-
- using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
-
- try
+ if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
{
- await PeriodicFlushAsync().ConfigureAwait(false);
- }
- catch
- {
- // Do nothing
+ return; // Already disposed
}
- foreach (var state in _files.Values)
+ // Stop timer
+ if (_flushTimer != null)
+ {
+ _flushTimer.Dispose();
+ _flushTimer = null;
+ }
+
+ // Stop compression worker
+ _compressionCts.Cancel();
+
+ using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)))
{
try
{
- await FlushBufferAsync(state, cts.Token).ConfigureAwait(false);
+ await PeriodicFlushAsync().ConfigureAwait(false);
}
catch
{
- // Do nothing
+ // Ignore errors during shutdown
}
- state.Dispose();
- }
-
- _files.Clear();
- _messageQueues.Clear();
-
- // Wait for compression worker to finish remaining tasks with timeout
- try
- {
- if (_compressionWorker != null && !_compressionWorker.IsCompleted)
+ foreach (var state in _files.Values)
{
- await Task.WhenAny(_compressionWorker, Task.Delay(TimeSpan.FromSeconds(5)));
+ try
+ {
+ await FlushBufferAsync(state, cts.Token).ConfigureAwait(false);
+ }
+ catch
+ {
+ // Ignore errors during shutdown
+ }
+
+ state.Dispose();
+ }
+
+ _files.Clear();
+
+ // Clear all message queues
+ foreach (var mq in _messageQueues.Values)
+ {
+ mq.Clear();
+ }
+ _messageQueues.Clear();
+
+ // Wait for compression worker to finish with timeout
+ try
+ {
+ if (_compressionWorker != null && !_compressionWorker.IsCompleted)
+ {
+ await Task.WhenAny(_compressionWorker, Task.Delay(TimeSpan.FromSeconds(2))).ConfigureAwait(false);
+ }
+ }
+ catch
+ {
+ // Ignore errors
+ }
+
+ // Dispose resources
+ if (_compressionSemaphore != null)
+ {
+ _compressionSemaphore.Dispose();
+ }
+
+ if (_compressionCts != null)
+ {
+ _compressionCts.Dispose();
+ }
+
+ if (_aes != null)
+ {
+ _aes.Dispose();
+ _aes = null;
+ }
+
+ // Clear StringBuilder pool
+ while (_stringBuilderPool.TryTake(out _))
+ {
+ Interlocked.Decrement(ref _stringBuilderPoolCount);
}
}
- catch
- {
- // Do nothing
- }
-
- _queueSignal.Release();
-
- // Signal compression worker to stop and wait for it to finish
- _compressionSemaphore?.Dispose();
- _queueSignal?.Dispose();
-
- _aes?.Dispose();
}
-}
+
+ public new void Dispose()
+ {
+ OnShutdownFlushAsync().GetAwaiter().GetResult();
+ base.Dispose();
+ }
+}
\ No newline at end of file