diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj
index cc5db48..c9548ad 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.8
- 1.7.8
+ 1.7.9
+ 1.7.9
README.md
True
LICENSE
diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs
index 2dce145..b93f5f9 100644
--- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs
+++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs
@@ -6,10 +6,8 @@ using Microsoft.Extensions.Options;
using System;
using System.Buffers;
using System.Collections.Generic;
-using System.Diagnostics;
using System.IO;
using System.IO.Compression;
-using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading;
@@ -22,58 +20,53 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
private const int BufferSize = 64 * 1024;
private const int ChannelCapacity = 8192;
private const int FlushThreshold = 48 * 1024;
-
private static readonly UTF8Encoding Utf8 = new(false);
private readonly Channel _channel;
- private readonly Thread _writerThread;
-
- private static readonly TimeSpan FlushInterval = TimeSpan.FromMilliseconds(500);
- private long _lastFlushTicks = Stopwatch.GetTimestamp();
+ private readonly Task _writerTask;
+ private readonly SemaphoreSlim _flushSemaphore = new(1, 1);
private string _filePath;
private readonly int _maxFileSize;
+ private readonly int _maxRolloverFiles;
private readonly bool _encryptionEnabled;
- private readonly Aes _aes;
- private readonly ICryptoTransform _encryptor;
+ private FileStream _fileStream;
+ private CryptoStream? _cryptoStream;
+ private byte[] _buffer;
+ private int _position;
+ private long _size;
+
+ private readonly Aes? _aes;
public event Action? OnError;
public bool IncludeCorrelationId { get; }
public bool EnableCategoryRouting { get; }
- private FileStream _stream;
- private byte[] _buffer;
- private int _position;
- private long _size;
-
[ThreadStatic]
private static StringBuilder? _cachedStringBuilder;
- public bool IsHealthy => _running && _stream != null;
-
- public string LogFile => _filePath;
private volatile bool _running = true;
- private readonly int _maxRolloverFiles;
-
public ELogType MinimumLogLevel { get; set; }
private readonly LoggerScopedContext _context = new();
+ private static readonly TimeSpan FlushInterval = TimeSpan.FromMilliseconds(500);
+ private long _lastFlushTicks = DateTime.UtcNow.Ticks;
+
public FileLoggerProvider(IOptions options) : base(options)
{
- var o = options.Value;
+ AppDomain.CurrentDomain.ProcessExit += (s, e) => Dispose();
+ AppDomain.CurrentDomain.UnhandledException += (s, e) => Dispose();
+ var o = options.Value;
string primaryDirectory = o.LogDirectory;
string fileName = $"{o.FileNamePrefix}_{Environment.MachineName}_{DateTime.UtcNow:yyyyMMdd}.log";
-
_filePath = Path.Combine(primaryDirectory, fileName);
if (!TryInitializePath(primaryDirectory, fileName))
{
- // Fallback to temp path with required format: EonaCat_date.log
string tempDirectory = Path.GetTempPath();
string fallbackFileName = $"EonaCat_{DateTime.UtcNow:yyyyMMdd}.log";
-
if (!TryInitializePath(tempDirectory, fallbackFileName))
{
_running = false;
@@ -83,7 +76,6 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
_maxFileSize = o.FileSizeLimit;
_maxRolloverFiles = o.MaxRolloverFiles;
-
_encryptionEnabled = o.EncryptionKey != null && o.EncryptionIV != null;
if (_encryptionEnabled)
@@ -91,41 +83,21 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
_aes = Aes.Create();
_aes.Key = o.EncryptionKey;
_aes.IV = o.EncryptionIV;
- _encryptor = _aes.CreateEncryptor();
}
IncludeCorrelationId = o.IncludeCorrelationId;
EnableCategoryRouting = o.EnableCategoryRouting;
- try
- {
- _stream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 1, FileOptions.WriteThrough | FileOptions.SequentialScan);
- _size = _stream.Length;
- }
- catch (Exception ex)
- {
- RaiseError(ex);
- _running = false;
- return;
- }
-
_buffer = ArrayPool.Shared.Rent(BufferSize);
- _channel = Channel.CreateBounded(
- new BoundedChannelOptions(ChannelCapacity)
- {
- SingleReader = true,
- SingleWriter = false,
- FullMode = BoundedChannelFullMode.Wait
- });
-
- _writerThread = new Thread(WriterLoop)
+ _channel = Channel.CreateBounded(new BoundedChannelOptions(ChannelCapacity)
{
- IsBackground = true,
- Priority = ThreadPriority.AboveNormal
- };
+ SingleReader = true,
+ SingleWriter = false,
+ FullMode = BoundedChannelFullMode.Wait
+ });
- _writerThread.Start();
+ _writerTask = Task.Run(WriterLoopAsync);
}
private bool TryInitializePath(string directory, string fileName)
@@ -133,19 +105,21 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
try
{
Directory.CreateDirectory(directory);
-
string fullPath = Path.Combine(directory, fileName);
-
if (!EnsureWritable(fullPath))
{
return false;
}
_filePath = fullPath;
+ _fileStream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.SequentialScan | FileOptions.WriteThrough);
- _stream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 1, FileOptions.WriteThrough | FileOptions.SequentialScan);
- _size = _stream.Length;
+ if (_encryptionEnabled && _aes != null)
+ {
+ _cryptoStream = new CryptoStream(_fileStream, _aes.CreateEncryptor(), CryptoStreamMode.Write);
+ }
+ _size = _fileStream.Length;
return true;
}
catch (Exception ex)
@@ -155,15 +129,13 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
}
}
-
internal override Task WriteMessagesAsync(IReadOnlyList messages, CancellationToken token)
{
- for (int i = 0; i < messages.Count; i++)
+ foreach (var msg in messages)
{
- var message = messages[i];
- if (message.Level >= MinimumLogLevel)
+ if (msg.Level >= MinimumLogLevel)
{
- while (!_channel.Writer.TryWrite(message))
+ while (!_channel.Writer.TryWrite(msg))
{
Thread.SpinWait(1);
}
@@ -172,172 +144,169 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
return Task.CompletedTask;
}
- private void WriterLoop()
+ private async Task WriterLoopAsync()
{
- if (_stream == null)
- {
- return;
- }
-
var reader = _channel.Reader;
- var spin = new SpinWait();
+ List batch = new();
while (_running || reader.Count > 0)
{
- while (reader.TryRead(out var message))
+ batch.Clear();
+ while (reader.TryRead(out var msg))
{
- WriteMessage(message);
+ batch.Add(msg);
}
- FlushIfNeededTimed();
+ if (batch.Count > 0)
+ {
+ await WriteBatchAsync(batch);
+ }
- spin.SpinOnce();
+ await Task.Delay(50); // idle wait
}
- FlushFinal();
+ await FlushFinalAsync();
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void FlushIfNeededTimed()
+ private async Task WriteBatchAsync(IReadOnlyList batch)
+ {
+ var sb = AcquireStringBuilder();
+ foreach (var msg in batch)
+ {
+ if (IncludeCorrelationId)
+ {
+ var ctx = _context.GetAll();
+ if (ctx.Count > 0)
+ {
+ sb.Append(" [");
+ foreach (var kv in ctx)
+ {
+ sb.Append(kv.Key).Append('=').Append(kv.Value).Append(' ');
+ }
+
+ sb.Length--; // trim
+ sb.Append(']');
+ }
+ }
+ sb.Append(' ').Append(msg.Message).AppendLine();
+ }
+
+ int maxBytes = Utf8.GetMaxByteCount(sb.Length);
+ byte[] rented = ArrayPool.Shared.Rent(maxBytes);
+ int byteCount = Utf8.GetBytes(sb.ToString(), 0, sb.Length, rented, 0);
+ WriteToBuffer(rented, byteCount);
+ ArrayPool.Shared.Return(rented, true);
+ ReleaseStringBuilder(sb);
+
+ await FlushIfNeededAsync();
+ }
+
+ private async Task FlushIfNeededAsync()
+ {
+ long now = DateTime.UtcNow.Ticks;
+ if (_position >= FlushThreshold || now - _lastFlushTicks >= FlushInterval.Ticks)
+ {
+ await _flushSemaphore.WaitAsync();
+ try
+ {
+ await FlushInternalAsync();
+ _lastFlushTicks = now;
+ }
+ finally { _flushSemaphore.Release(); }
+ }
+ }
+
+ private void WriteToBuffer(byte[] data, int length)
+ {
+ if (_position + length > _buffer.Length)
+ {
+ FlushInternalAsync().GetAwaiter().GetResult();
+ }
+
+ Buffer.BlockCopy(data, 0, _buffer, _position, length);
+ _position += length;
+ _size += length;
+
+ if (_maxFileSize > 0 && _size >= _maxFileSize)
+ {
+ Task.Run(RollFileAsync);
+ }
+ }
+
+ private async Task FlushInternalAsync()
{
if (_position == 0)
{
return;
}
- var nowTicks = DateTime.UtcNow.Ticks;
-
- // Size-based flush (existing behavior)
- if (_position >= FlushThreshold)
+ try
{
- FlushInternal();
- _lastFlushTicks = nowTicks;
- return;
+ if (_cryptoStream != null)
+ {
+ await _cryptoStream.WriteAsync(_buffer, 0, _position);
+ await _cryptoStream.FlushAsync();
+ }
+ else
+ {
+ await _fileStream.WriteAsync(_buffer, 0, _position);
+ await _fileStream.FlushAsync();
+ }
}
+ catch (Exception ex) { RaiseError(ex); }
- // Time-based flush (new behavior)
- if (nowTicks - _lastFlushTicks >= FlushInterval.Ticks)
- {
- FlushInternal();
- _lastFlushTicks = nowTicks;
- }
+ _position = 0;
}
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void WriteMessage(LogMessage msg)
+ private async Task FlushFinalAsync()
{
- var sb = AcquireStringBuilder();
+ await FlushInternalAsync();
- if (IncludeCorrelationId)
- {
- var ctx = _context.GetAll();
- var tags = msg.Tags;
+ _cryptoStream?.FlushFinalBlock();
- if (ctx.Count > 0 || (tags?.Length ?? 0) > 0)
- {
- sb.Append(" [");
-
- foreach (var kv in ctx)
- {
- sb.Append(kv.Key).Append('=').Append(kv.Value).Append(' ');
- }
-
- if (tags != null)
- {
- for (int i = 0; i < tags.Length; i++)
- {
- sb.Append("tag=").Append(tags[i]).Append(' ');
- }
- }
-
- // Trim trailing space
- if (sb[sb.Length - 1] == ' ')
- {
- sb.Length--;
- }
-
- sb.Append(']');
- }
-
- // Ensure correlation id exists
- var correlationId = _context.Get("CorrelationId");
- if (correlationId == null)
- {
- correlationId = Guid.NewGuid().ToString();
- _context.Set("CorrelationId", correlationId);
- }
- }
-
- sb.Append(' ')
- .Append(msg.Message)
- .AppendLine();
-
- int charCount = sb.Length;
- int maxBytes = Utf8.GetMaxByteCount(charCount);
- byte[] rented = ArrayPool.Shared.Rent(maxBytes);
-
- int byteCount = Utf8.GetBytes(sb.ToString(), 0, charCount, rented, 0);
-
- byte[] data = rented;
- int length = byteCount;
-
- if (_encryptionEnabled)
- {
- data = _encryptor.TransformFinalBlock(rented, 0, byteCount);
- length = data.Length;
- ArrayPool.Shared.Return(rented, true);
- rented = null;
- }
-
- WriteToBuffer(data, length);
-
- if (_maxFileSize > 0 && _size >= _maxFileSize)
- {
- RollFile();
- }
-
- if (rented != null)
- {
- ArrayPool.Shared.Return(rented, true);
- }
-
- ReleaseStringBuilder(sb);
+ await _fileStream.FlushAsync();
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
- private void WriteToBuffer(byte[] data, int length)
+
+ private async Task RollFileAsync()
{
- if (length > BufferSize)
+ try
{
- if (_position > 0)
+ await _flushSemaphore.WaitAsync();
+ FlushInternalAsync().GetAwaiter().GetResult();
+
+ _cryptoStream?.Dispose();
+ _fileStream.Dispose();
+
+ string tempArchive = _filePath + ".rolling";
+ File.Move(_filePath, tempArchive);
+
+ // Compression in background
+ await Task.Run(() =>
{
- FlushInternal();
+ string dest = _filePath.Replace(".log", "_1.log.gz");
+ using var input = File.OpenRead(tempArchive);
+ using var output = File.Create(dest);
+ using var gzip = new GZipStream(output, CompressionLevel.Fastest);
+ input.CopyTo(gzip);
+ File.Delete(tempArchive);
+ });
+
+ _fileStream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.SequentialScan);
+ if (_encryptionEnabled && _aes != null)
+ {
+ _cryptoStream = new CryptoStream(_fileStream, _aes.CreateEncryptor(), CryptoStreamMode.Write);
}
- WriteDirect(data, length);
- return;
+ _size = 0;
}
-
- if (_position + length > BufferSize)
- {
- FlushInternal();
- }
-
- Buffer.BlockCopy(data, 0, _buffer, _position, length);
- _position += length;
- _size += length;
+ finally { _flushSemaphore.Release(); }
}
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static StringBuilder AcquireStringBuilder()
{
var sb = _cachedStringBuilder;
- if (sb == null)
- {
- sb = new StringBuilder(256);
- _cachedStringBuilder = sb;
- }
+ if (sb == null) { sb = new StringBuilder(256); _cachedStringBuilder = sb; }
else
{
sb.Clear();
@@ -346,255 +315,41 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
return sb;
}
- private const int MaxBuilderCapacity = 8 * 1024;
-
- [MethodImpl(MethodImplOptions.AggressiveInlining)]
private static void ReleaseStringBuilder(StringBuilder sb)
{
- if (sb.Capacity > MaxBuilderCapacity)
+ if (sb.Capacity > 8 * 1024)
{
_cachedStringBuilder = new StringBuilder(256);
}
}
- private void WriteDirect(byte[] data, int length)
- {
- try
- {
- if (_stream == null || !File.Exists(_filePath))
- {
- RecreateStream();
- }
-
- _stream.Write(data, 0, length);
- }
- catch (IOException ioEx) when (!File.Exists(_filePath))
- {
- // File was removed while writing, recreate and retry once
- RecreateStream();
- _stream.Write(data, 0, length);
- }
- catch (Exception ex)
- {
- RaiseError(ex);
- _running = false;
- return;
- }
-
- _size += length;
-
- if (_maxFileSize > 0 && _size >= _maxFileSize)
- {
- RollFile();
- }
- }
-
- private void RecreateStream()
- {
- try
- {
- Directory.CreateDirectory(Path.GetDirectoryName(_filePath)!);
-
- _stream?.Dispose();
- _stream = new FileStream(
- _filePath,
- FileMode.Append,
- FileAccess.Write,
- FileShare.ReadWrite | FileShare.Delete,
- 4096,
- FileOptions.WriteThrough | FileOptions.SequentialScan);
-
- _size = _stream.Length;
- }
- catch (Exception ex)
- {
- RaiseError(ex);
- _running = false;
- }
- }
-
-
- private void FlushInternal()
- {
- if (_position == 0)
- {
- return;
- }
-
- try
- {
- if (_stream == null || !File.Exists(_filePath))
- {
- RecreateStream();
- }
-
- _stream.Write(_buffer, 0, _position);
- }
- catch (IOException ioEx) when (!File.Exists(_filePath))
- {
- RecreateStream();
- _stream.Write(_buffer, 0, _position);
- }
- catch (Exception ex)
- {
- RaiseError(ex);
- _running = false;
- }
-
- _position = 0;
- }
-
- private void FlushFinal()
- {
- FlushInternal();
- _stream.Flush(true);
- }
-
- private readonly object _rollLock = new();
-
- private void RollFile()
- {
- try
- {
- lock (_rollLock)
- {
- FlushInternal();
-
- _stream?.Dispose();
- _stream = null;
-
- RotateCompressedFiles();
-
- string tempArchive = _filePath + ".rolling";
-
- File.Move(_filePath, tempArchive);
-
- CompressToIndex(tempArchive, 1);
-
- _stream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.SequentialScan);
- _size = 0;
- }
- }
- catch (Exception ex)
- {
- RaiseError(ex);
- _running = false;
- }
- }
- private void RotateCompressedFiles()
- {
- int maxFiles = _maxRolloverFiles;
-
- string directory = Path.GetDirectoryName(_filePath)!;
- string nameWithoutExt = Path.GetFileNameWithoutExtension(_filePath);
- string extension = Path.GetExtension(_filePath); // .log
-
- for (int i = maxFiles; i >= 1; i--)
- {
- string current = Path.Combine(directory, $"{nameWithoutExt}_{i}{extension}.gz");
-
- if (!File.Exists(current))
- {
- continue;
- }
-
- if (i == maxFiles)
- {
- File.Delete(current);
- }
- else
- {
- string next = Path.Combine(directory, $"{nameWithoutExt}_{i + 1}{extension}.gz");
-
- if (File.Exists(next))
- {
- File.Delete(next);
- }
-
- File.Move(current, next);
- }
- }
- }
-
private bool EnsureWritable(string path)
{
try
{
- string? directory = Path.GetDirectoryName(path);
- if (string.IsNullOrWhiteSpace(directory))
- {
- return false;
- }
-
- Directory.CreateDirectory(directory);
-
- // Test write access
- using (var fs = new FileStream(
- path,
- FileMode.Append,
- FileAccess.Write,
- FileShare.ReadWrite | FileShare.Delete))
- {
- // Just opening is enough to validate permission
- }
-
+ Directory.CreateDirectory(Path.GetDirectoryName(path)!);
+ using var fs = new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete);
return true;
}
- catch (Exception ex)
- {
- RaiseError(ex);
- return false;
- }
- }
-
-
- private void CompressToIndex(string sourceFile, int index)
- {
- string directory = Path.GetDirectoryName(_filePath)!;
- string nameWithoutExtension = Path.GetFileNameWithoutExtension(_filePath);
- string extension = Path.GetExtension(_filePath); // .log
-
- // destination uses the same "_index" before .log + add .gz
- string destination = Path.Combine(directory, $"{nameWithoutExtension}_{index}{extension}.gz");
-
- using (var input = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read))
- using (var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None))
- using (var gzip = new GZipStream(output, CompressionLevel.Fastest))
- {
- input.CopyTo(gzip);
- }
-
- File.Delete(sourceFile);
- }
-
-
- protected override Task OnShutdownFlushAsync()
- {
- _running = false;
- _channel.Writer.Complete();
- _writerThread.Join();
-
- ArrayPool.Shared.Return(_buffer, true);
- _stream.Dispose();
- _aes?.Dispose();
- return Task.CompletedTask;
+ catch (Exception ex) { RaiseError(ex); return false; }
}
private void RaiseError(Exception ex)
{
- try
- {
- OnError?.Invoke(ex);
- }
- catch
- {
- // Never allow logging failure to crash app
- }
+ try { OnError?.Invoke(ex); } catch { }
}
- public new void Dispose()
+ protected override async Task OnShutdownFlushAsync()
{
- OnShutdownFlushAsync().GetAwaiter().GetResult();
- base.Dispose();
+ _running = false;
+ _channel.Writer.Complete();
+ await _writerTask;
+
+ ArrayPool.Shared.Return(_buffer, true);
+ _cryptoStream?.Dispose();
+ _fileStream.Dispose();
+ _aes?.Dispose();
}
+
+ public new void Dispose() => OnShutdownFlushAsync().GetAwaiter().GetResult();
}