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();