From 739d395b142d77c47269392a5217a67a27f54887 Mon Sep 17 00:00:00 2001 From: Jeroen Saey Date: Mon, 16 Feb 2026 12:01:02 +0100 Subject: [PATCH] Updated --- EonaCat.Logger/EonaCat.Logger.csproj | 4 +- .../EonaCatCoreLogger/FileLoggerProvider.cs | 200 ++++++++++++++++-- 2 files changed, 182 insertions(+), 22 deletions(-) diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj index cee49df..52effbe 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.6 - 1.7.6 + 1.7.7 + 1.7.7 README.md True LICENSE diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs index 1dd4d46..992d0ed 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs @@ -6,6 +6,7 @@ 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; @@ -27,12 +28,16 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable private readonly Channel _channel; private readonly Thread _writerThread; - private readonly string _filePath; + private static readonly TimeSpan FlushInterval = TimeSpan.FromMilliseconds(500); + private long _lastFlushTicks = Stopwatch.GetTimestamp(); + + private string _filePath; private readonly int _maxFileSize; private readonly bool _encryptionEnabled; private readonly Aes _aes; private readonly ICryptoTransform _encryptor; + public event Action? OnError; public bool IncludeCorrelationId { get; } public bool EnableCategoryRouting { get; } @@ -45,6 +50,8 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable [ThreadStatic] private static StringBuilder? _cachedStringBuilder; + public bool IsHealthy => _running && _stream != null; + public string LogFile => _filePath; private volatile bool _running = true; private readonly int _maxRolloverFiles; @@ -56,10 +63,23 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable { var o = options.Value; - _filePath = Path.Combine(o.LogDirectory, - $"{o.FileNamePrefix}_{Environment.MachineName}_{DateTime.UtcNow:yyyyMMdd}.log"); + string primaryDirectory = o.LogDirectory; + string fileName = $"{o.FileNamePrefix}_{Environment.MachineName}_{DateTime.UtcNow:yyyyMMdd}.log"; - Directory.CreateDirectory(o.LogDirectory); + _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; + return; + } + } _maxFileSize = o.FileSizeLimit; _maxRolloverFiles = o.MaxRolloverFiles; @@ -77,8 +97,18 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable IncludeCorrelationId = o.IncludeCorrelationId; EnableCategoryRouting = o.EnableCategoryRouting; - _stream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 1, FileOptions.WriteThrough | FileOptions.SequentialScan); - _size = _stream.Length; + 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( @@ -98,6 +128,34 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable _writerThread.Start(); } + private bool TryInitializePath(string directory, string fileName) + { + try + { + Directory.CreateDirectory(directory); + + string fullPath = Path.Combine(directory, fileName); + + if (!EnsureWritable(fullPath)) + { + return false; + } + + _filePath = fullPath; + + _stream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 1, FileOptions.WriteThrough | FileOptions.SequentialScan); + _size = _stream.Length; + + return true; + } + catch (Exception ex) + { + RaiseError(ex); + return false; + } + } + + internal override Task WriteMessagesAsync(IReadOnlyList messages, CancellationToken token) { for (int i = 0; i < messages.Count; i++) @@ -116,6 +174,11 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable private void WriterLoop() { + if (_stream == null) + { + return; + } + var reader = _channel.Reader; var spin = new SpinWait(); @@ -126,7 +189,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable WriteMessage(message); } - FlushIfNeeded(); + FlushIfNeededTimed(); spin.SpinOnce(); } @@ -134,6 +197,33 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable FlushFinal(); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void FlushIfNeededTimed() + { + if (_position == 0) + { + return; + } + + var nowTicks = DateTime.UtcNow.Ticks; + + // Size-based flush (existing behavior) + if (_position >= FlushThreshold) + { + FlushInternal(); + _lastFlushTicks = nowTicks; + return; + } + + // Time-based flush (new behavior) + if (nowTicks - _lastFlushTicks >= FlushInterval.Ticks) + { + FlushInternal(); + _lastFlushTicks = nowTicks; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] private void WriteMessage(LogMessage msg) { @@ -162,7 +252,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable } // Trim trailing space - if (sb[sb.Length -1] == ' ') + if (sb[sb.Length - 1] == ' ') { sb.Length--; } @@ -269,7 +359,17 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable private void WriteDirect(byte[] data, int length) { - _stream.Write(data, 0, length); + try + { + _stream.Write(data, 0, length); + } + catch (Exception ex) + { + RaiseError(ex); + _running = false; + return; + } + _size += length; if (_maxFileSize > 0 && _size >= _maxFileSize) @@ -294,7 +394,16 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable return; } - _stream.Write(_buffer, 0, _position); + try + { + _stream.Write(_buffer, 0, _position); + } + catch (Exception ex) + { + RaiseError(ex); + _running = false; + } + _position = 0; } @@ -308,23 +417,31 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable private void RollFile() { - lock (_rollLock) + try { - FlushInternal(); + lock (_rollLock) + { + FlushInternal(); - _stream?.Dispose(); - _stream = null; + _stream?.Dispose(); + _stream = null; - RotateCompressedFiles(); + RotateCompressedFiles(); - string tempArchive = _filePath + ".rolling"; + string tempArchive = _filePath + ".rolling"; - File.Move(_filePath, tempArchive); + File.Move(_filePath, tempArchive); - CompressToIndex(tempArchive, 1); + CompressToIndex(tempArchive, 1); - _stream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.SequentialScan); - _size = 0; + _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() @@ -362,6 +479,37 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable } } + 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 + } + + return true; + } + catch (Exception ex) + { + RaiseError(ex); + return false; + } + } + private void CompressToIndex(string sourceFile, int index) { @@ -395,6 +543,18 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable return Task.CompletedTask; } + private void RaiseError(Exception ex) + { + try + { + OnError?.Invoke(ex); + } + catch + { + // Never allow logging failure to crash app + } + } + public new void Dispose() { OnShutdownFlushAsync().GetAwaiter().GetResult();