From 6145f3a6cd1a0e4463532ee2ce77a7c6dcb50ad5 Mon Sep 17 00:00:00 2001 From: Jeroen Saey Date: Mon, 16 Feb 2026 14:04:13 +0100 Subject: [PATCH] Updated --- EonaCat.Logger/EonaCat.Logger.csproj | 4 +- .../EonaCatCoreLogger/FileLoggerProvider.cs | 585 +++++------------- 2 files changed, 172 insertions(+), 417 deletions(-) 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(); }