This commit is contained in:
Jeroen Saey
2026-02-16 12:01:02 +01:00
parent 8d2157be1a
commit 739d395b14
2 changed files with 182 additions and 22 deletions

View File

@@ -13,8 +13,8 @@
<Copyright>EonaCat (Jeroen Saey)</Copyright> <Copyright>EonaCat (Jeroen Saey)</Copyright>
<PackageTags>EonaCat;Logger;EonaCatLogger;Log;Writer;Jeroen;Saey</PackageTags> <PackageTags>EonaCat;Logger;EonaCatLogger;Log;Writer;Jeroen;Saey</PackageTags>
<PackageIconUrl /> <PackageIconUrl />
<Version>1.7.6</Version> <Version>1.7.7</Version>
<FileVersion>1.7.6</FileVersion> <FileVersion>1.7.7</FileVersion>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile> <GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>

View File

@@ -6,6 +6,7 @@ using Microsoft.Extensions.Options;
using System; using System;
using System.Buffers; using System.Buffers;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.IO; using System.IO;
using System.IO.Compression; using System.IO.Compression;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
@@ -27,12 +28,16 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
private readonly Channel<LogMessage> _channel; private readonly Channel<LogMessage> _channel;
private readonly Thread _writerThread; 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 int _maxFileSize;
private readonly bool _encryptionEnabled; private readonly bool _encryptionEnabled;
private readonly Aes _aes; private readonly Aes _aes;
private readonly ICryptoTransform _encryptor; private readonly ICryptoTransform _encryptor;
public event Action<Exception>? OnError;
public bool IncludeCorrelationId { get; } public bool IncludeCorrelationId { get; }
public bool EnableCategoryRouting { get; } public bool EnableCategoryRouting { get; }
@@ -45,6 +50,8 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
[ThreadStatic] [ThreadStatic]
private static StringBuilder? _cachedStringBuilder; private static StringBuilder? _cachedStringBuilder;
public bool IsHealthy => _running && _stream != null;
public string LogFile => _filePath; public string LogFile => _filePath;
private volatile bool _running = true; private volatile bool _running = true;
private readonly int _maxRolloverFiles; private readonly int _maxRolloverFiles;
@@ -56,10 +63,23 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
{ {
var o = options.Value; var o = options.Value;
_filePath = Path.Combine(o.LogDirectory, string primaryDirectory = o.LogDirectory;
$"{o.FileNamePrefix}_{Environment.MachineName}_{DateTime.UtcNow:yyyyMMdd}.log"); 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; _maxFileSize = o.FileSizeLimit;
_maxRolloverFiles = o.MaxRolloverFiles; _maxRolloverFiles = o.MaxRolloverFiles;
@@ -77,8 +97,18 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
IncludeCorrelationId = o.IncludeCorrelationId; IncludeCorrelationId = o.IncludeCorrelationId;
EnableCategoryRouting = o.EnableCategoryRouting; EnableCategoryRouting = o.EnableCategoryRouting;
try
{
_stream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 1, FileOptions.WriteThrough | FileOptions.SequentialScan); _stream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 1, FileOptions.WriteThrough | FileOptions.SequentialScan);
_size = _stream.Length; _size = _stream.Length;
}
catch (Exception ex)
{
RaiseError(ex);
_running = false;
return;
}
_buffer = ArrayPool<byte>.Shared.Rent(BufferSize); _buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
_channel = Channel.CreateBounded<LogMessage>( _channel = Channel.CreateBounded<LogMessage>(
@@ -98,6 +128,34 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
_writerThread.Start(); _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<LogMessage> messages, CancellationToken token) internal override Task WriteMessagesAsync(IReadOnlyList<LogMessage> messages, CancellationToken token)
{ {
for (int i = 0; i < messages.Count; i++) for (int i = 0; i < messages.Count; i++)
@@ -116,6 +174,11 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
private void WriterLoop() private void WriterLoop()
{ {
if (_stream == null)
{
return;
}
var reader = _channel.Reader; var reader = _channel.Reader;
var spin = new SpinWait(); var spin = new SpinWait();
@@ -126,7 +189,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
WriteMessage(message); WriteMessage(message);
} }
FlushIfNeeded(); FlushIfNeededTimed();
spin.SpinOnce(); spin.SpinOnce();
} }
@@ -134,6 +197,33 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
FlushFinal(); 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)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void WriteMessage(LogMessage msg) private void WriteMessage(LogMessage msg)
{ {
@@ -162,7 +252,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
} }
// Trim trailing space // Trim trailing space
if (sb[sb.Length -1] == ' ') if (sb[sb.Length - 1] == ' ')
{ {
sb.Length--; sb.Length--;
} }
@@ -268,8 +358,18 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
} }
private void WriteDirect(byte[] data, int length) private void WriteDirect(byte[] data, int length)
{
try
{ {
_stream.Write(data, 0, length); _stream.Write(data, 0, length);
}
catch (Exception ex)
{
RaiseError(ex);
_running = false;
return;
}
_size += length; _size += length;
if (_maxFileSize > 0 && _size >= _maxFileSize) if (_maxFileSize > 0 && _size >= _maxFileSize)
@@ -294,7 +394,16 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
return; return;
} }
try
{
_stream.Write(_buffer, 0, _position); _stream.Write(_buffer, 0, _position);
}
catch (Exception ex)
{
RaiseError(ex);
_running = false;
}
_position = 0; _position = 0;
} }
@@ -307,6 +416,8 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
private readonly object _rollLock = new(); private readonly object _rollLock = new();
private void RollFile() private void RollFile()
{
try
{ {
lock (_rollLock) lock (_rollLock)
{ {
@@ -327,6 +438,12 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
_size = 0; _size = 0;
} }
} }
catch (Exception ex)
{
RaiseError(ex);
_running = false;
}
}
private void RotateCompressedFiles() private void RotateCompressedFiles()
{ {
int maxFiles = _maxRolloverFiles; int maxFiles = _maxRolloverFiles;
@@ -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) private void CompressToIndex(string sourceFile, int index)
{ {
@@ -395,6 +543,18 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
return Task.CompletedTask; return Task.CompletedTask;
} }
private void RaiseError(Exception ex)
{
try
{
OnError?.Invoke(ex);
}
catch
{
// Never allow logging failure to crash app
}
}
public new void Dispose() public new void Dispose()
{ {
OnShutdownFlushAsync().GetAwaiter().GetResult(); OnShutdownFlushAsync().GetAwaiter().GetResult();