This commit is contained in:
2026-02-05 21:42:43 +01:00
parent 9e44051677
commit cb7dd54d3f
13 changed files with 622 additions and 312 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.6.4</Version> <Version>1.6.5</Version>
<FileVersion>1.6.4</FileVersion> <FileVersion>1.6.5</FileVersion>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile> <GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
@@ -25,7 +25,7 @@
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<EVRevisionFormat>1.6.4+{chash:10}.{c:ymd}</EVRevisionFormat> <EVRevisionFormat>1.6.5+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVDefault>true</EVDefault> <EVDefault>true</EVDefault>
<EVInfo>true</EVInfo> <EVInfo>true</EVInfo>
<EVTagMatch>v[0-9]*</EVTagMatch> <EVTagMatch>v[0-9]*</EVTagMatch>

View File

@@ -43,26 +43,11 @@ public static class FileLoggerFactoryExtensions
fileLoggerOptions.FileNamePrefix = filenamePrefix; fileLoggerOptions.FileNamePrefix = filenamePrefix;
} }
builder.AddEonaCatFileLogger(options => builder = builder.AddEonaCatFileLogger(options =>
{ {
options.FileNamePrefix = fileLoggerOptions.FileNamePrefix; LoggerConfigurator.ApplyFileLoggerSettings(options, fileLoggerOptions);
options.FlushPeriod = fileLoggerOptions.FlushPeriod; });
options.RetainedFileCountLimit = fileLoggerOptions.RetainedFileCountLimit;
options.MaxWriteTries = fileLoggerOptions.MaxWriteTries;
options.FileSizeLimit = fileLoggerOptions.FileSizeLimit;
options.LogDirectory = fileLoggerOptions.LogDirectory;
options.BatchSize = fileLoggerOptions.BatchSize;
options.FileSizeLimit = fileLoggerOptions.FileSizeLimit;
options.IsEnabled = fileLoggerOptions.IsEnabled;
options.MaxRolloverFiles = fileLoggerOptions.MaxRolloverFiles;
options.UseLocalTime = fileLoggerOptions.UseLocalTime;
options.UseMask = fileLoggerOptions.UseMask;
options.Mask = fileLoggerOptions.Mask;
options.UseDefaultMasking = fileLoggerOptions.UseDefaultMasking;
options.MaskedKeywords = fileLoggerOptions.MaskedKeywords;
options.IncludeCorrelationId = fileLoggerOptions.IncludeCorrelationId;
}
);
return builder; return builder;
} }

View File

@@ -16,6 +16,8 @@ public class FileLoggerOptions : BatchingLoggerOptions
private int _fileSizeLimit = 200 * 1024 * 1024; private int _fileSizeLimit = 200 * 1024 * 1024;
private int _maxRolloverFiles = 10; private int _maxRolloverFiles = 10;
private int _retainedFileCountLimit = 50; private int _retainedFileCountLimit = 50;
public bool EnableCategoryRouting { get; set; }
public string Category { get; set; }
public static string DefaultPath => public static string DefaultPath =>
AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory;

View File

@@ -1,4 +1,9 @@
using System; using EonaCat.Logger.EonaCatCoreLogger;
using EonaCat.Logger.EonaCatCoreLogger.Internal;
using EonaCat.Logger.Managers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@@ -6,12 +11,6 @@ using System.Linq;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using EonaCat.Logger.EonaCatCoreLogger.Internal;
using EonaCat.Logger.Managers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace EonaCat.Logger.EonaCatCoreLogger;
[ProviderAlias("EonaCatFileLogger")] [ProviderAlias("EonaCatFileLogger")]
public sealed class FileLoggerProvider : BatchingLoggerProvider public sealed class FileLoggerProvider : BatchingLoggerProvider
@@ -22,212 +21,371 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
private readonly int _maxRetainedFiles; private readonly int _maxRetainedFiles;
private readonly int _maxRolloverFiles; private readonly int _maxRolloverFiles;
private string _logFile; private readonly LoggerScopedContext _context = new LoggerScopedContext();
private long _currentFileSize; private readonly Dictionary<string, FileState> _files = new Dictionary<string, FileState>();
private Timer _flushTimer; private static readonly ConcurrentDictionary<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
private readonly ConcurrentDictionary<string, StringBuilder> _buffers = new();
private readonly ConcurrentDictionary<string, long> _fileSizes = new();
private readonly SemaphoreSlim _writeLock = new(1, 1);
private readonly SemaphoreSlim _rolloverLock = new(1, 1);
private readonly LoggerScopedContext _context = new(); // High-performance buffer pool
private static readonly ConcurrentBag<byte[]> _bufferPool = new ConcurrentBag<byte[]>();
private const int BufferSize = 256 * 1024; // 256KB buffers
private const int MaxPoolSize = 100;
public string LogFile => _logFile ?? string.Empty; // Pre-allocated StringBuilder pool for low memory
private static readonly ConcurrentBag<StringBuilder> _stringBuilderPool = new ConcurrentBag<StringBuilder>();
private const int MaxStringBuilderPoolSize = 50;
public bool IncludeCorrelationId { get; }
public bool EnableCategoryRouting { get; }
public event EventHandler<ErrorMessage> OnError; public event EventHandler<ErrorMessage> OnError;
public event EventHandler<string> OnRollOver; public event EventHandler<string> OnRollOver;
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options) public string LogFile
{
get
{
FileState state;
return _files.TryGetValue(string.Empty, out state) ? state.FilePath : string.Empty;
}
}
private sealed class FileState
{
public string FilePath;
public long Size;
public DateTime Date;
public readonly SemaphoreSlim WriteLock = new SemaphoreSlim(1, 1);
// Buffered writing for high throughput
public byte[] Buffer;
public int BufferPosition;
public FileStream Stream;
}
public FileLoggerProvider(IOptions<FileLoggerOptions> options)
: base(options)
{ {
var o = options.Value; var o = options.Value;
if (o == null)
{
throw new ArgumentNullException("options");
}
_path = o.LogDirectory; _path = o.LogDirectory;
_fileNamePrefix = o.FileNamePrefix; _fileNamePrefix = o.FileNamePrefix;
_maxFileSize = o.FileSizeLimit; _maxFileSize = o.FileSizeLimit;
_maxRetainedFiles = o.RetainedFileCountLimit; _maxRetainedFiles = o.RetainedFileCountLimit;
_maxRolloverFiles = o.MaxRolloverFiles; _maxRolloverFiles = o.MaxRolloverFiles;
IncludeCorrelationId = o.IncludeCorrelationId; IncludeCorrelationId = o.IncludeCorrelationId;
EnableCategoryRouting = o.EnableCategoryRouting;
Directory.CreateDirectory(_path);
InitializeCurrentFile();
_flushTimer = new Timer(_ => FlushAllAsync().GetAwaiter().GetResult(), null, 5000, 5000);
}
private async Task FlushAllAsync()
{
foreach (var kv in _buffers)
{
await FlushAsync(kv.Key, kv.Value, CancellationToken.None);
}
}
public bool IncludeCorrelationId { get; }
internal override async Task WriteMessagesAsync(
IReadOnlyList<LogMessage> messages,
CancellationToken token)
{
if (messages.Count == 0)
{
return;
}
Directory.CreateDirectory(_path); Directory.CreateDirectory(_path);
// Group messages by date // Initialize
foreach (var group in messages.GroupBy(m => (m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day))) var defaultState = CreateFileState(DateTime.UtcNow.Date, options.Value.Category);
{ _files[string.Empty] = defaultState;
var file = GetFullName(group.Key);
InitializeFile(file);
var stringBuilder = _buffers.GetOrAdd(file, _ => new StringBuilder(4096));
lock (stringBuilder)
{
foreach (var message in group)
{
AppendMessage(stringBuilder, message);
}
}
await FlushAsync(file, stringBuilder, token).ConfigureAwait(false);
DeleteOldLogFiles();
}
} }
private void AppendMessage(StringBuilder sb, LogMessage msg) internal override async Task WriteMessagesAsync(IReadOnlyList<LogMessage> messages, CancellationToken token)
{ {
// Ensure correlation id exists (once per async context) // Group messages by category for batch processing
if (IncludeCorrelationId) if (EnableCategoryRouting)
{ {
var cid = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); var grouped = messages.GroupBy(m => SanitizeCategory(m.Category));
_context.Set("CorrelationId", cid); foreach (var group in grouped)
}
sb.Append(msg.Message);
var ctx = _context.GetAll();
if (ctx.Count > 0)
{
sb.Append(" [");
bool first = true;
foreach (var kv in ctx)
{ {
if (!first) var categoryKey = group.Key;
FileState state;
if (!_files.TryGetValue(categoryKey, out state))
{ {
sb.Append(' '); var date = DateTime.UtcNow.Date;
state = CreateFileState(date, categoryKey);
_files[categoryKey] = state;
} }
sb.Append(kv.Key).Append('=').Append(kv.Value); await WriteBatchAsync(state, group, categoryKey, token);
first = false;
} }
sb.Append(']'); }
else
{
var categoryKey = string.Empty;
FileState state;
if (!_files.TryGetValue(categoryKey, out state))
{
var date = DateTime.UtcNow.Date;
state = CreateFileState(date, categoryKey);
_files[categoryKey] = state;
}
await WriteBatchAsync(state, messages, categoryKey, token);
} }
sb.AppendLine(); DeleteOldLogFiles();
} }
public void InitializeCurrentFile() private async Task WriteBatchAsync(FileState state, IEnumerable<LogMessage> messages, string categoryKey, CancellationToken token)
{ {
if (!string.IsNullOrEmpty(_logFile)) await state.WriteLock.WaitAsync(token);
{
return;
}
_logFile = GetFullName((DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day));
_currentFileSize = File.Exists(_logFile) ? new FileInfo(_logFile).Length : 0;
}
private async Task FlushAsync(string file, StringBuilder stringBuilder, CancellationToken token)
{
if (stringBuilder.Length == 0)
{
return;
}
await _writeLock.WaitAsync(token).ConfigureAwait(false);
try try
{ {
var text = stringBuilder.ToString(); foreach (var msg in messages)
byte[] bytesToWrite = Encoding.UTF8.GetBytes(text);
using (var fileStream = new FileStream(
file,
FileMode.Append,
FileAccess.Write,
FileShare.Read,
64 * 1024,
useAsync: true))
{ {
await fileStream.WriteAsync(bytesToWrite, 0, bytesToWrite.Length, token); if (token.IsCancellationRequested)
break;
var date = msg.Timestamp.UtcDateTime.Date;
// Rotate file by date
if (state.Date != date)
{
await FlushBufferAsync(state);
RotateByDate(state, date, categoryKey);
}
WriteMessageToBuffer(state, msg);
// Flush buffer if nearly full or file size limit reached
if (state.BufferPosition >= BufferSize - 1024 || state.Size >= _maxFileSize)
{
await FlushBufferAsync(state);
if (state.Size >= _maxFileSize)
{
RollOver(state, categoryKey);
}
}
} }
// Update current file size correctly // Final flush for this batch
_fileSizes.AddOrUpdate(file, bytesToWrite.Length, (_, old) => old + bytesToWrite.Length); await FlushBufferAsync(state);
_currentFileSize = _fileSizes[file]; }
catch (Exception ex)
stringBuilder.Clear(); {
if (OnError != null)
if (_fileSizes[file] >= _maxFileSize)
{ {
await RollOverAsync(file).ConfigureAwait(false); OnError.Invoke(this, new ErrorMessage { Exception = ex, Message = ex.Message });
} }
} }
finally finally
{ {
_writeLock.Release(); state.WriteLock.Release();
} }
} }
private async Task RollOverAsync(string file) private FileState CreateFileState(DateTime date, string category)
{
var path = GetFullName(date, category);
var buffer = GetBuffer();
var stream = new FileStream(
path,
FileMode.Append,
FileAccess.Write,
FileShare.ReadWrite,
BufferSize,
FileOptions.Asynchronous | FileOptions.SequentialScan);
var state = new FileState
{
Stream = stream,
FilePath = path,
Size = GetFileSize(path),
Date = date,
Buffer = buffer,
BufferPosition = 0
};
return state;
}
private static byte[] GetBuffer()
{
byte[] buffer;
if (!_bufferPool.TryTake(out buffer))
{
buffer = new byte[BufferSize];
}
return buffer;
}
private static void ReturnBuffer(byte[] buffer)
{
if (_bufferPool.Count < MaxPoolSize)
{
Array.Clear(buffer, 0, buffer.Length);
_bufferPool.Add(buffer);
}
}
private static StringBuilder GetStringBuilder()
{
StringBuilder sb;
if (!_stringBuilderPool.TryTake(out sb))
{
sb = new StringBuilder(512);
}
else
{
sb.Clear();
}
return sb;
}
private static void ReturnStringBuilder(StringBuilder sb)
{
if (_stringBuilderPool.Count < MaxStringBuilderPoolSize && sb.Capacity <= 2048)
{
sb.Clear();
_stringBuilderPool.Add(sb);
}
}
private static long GetFileSize(string path)
{ {
await _rolloverLock.WaitAsync().ConfigureAwait(false);
try try
{ {
var dir = Path.GetDirectoryName(file); return File.Exists(path) ? new FileInfo(path).Length : 0;
var name = Path.GetFileNameWithoutExtension(file); }
var ext = Path.GetExtension(file); catch
{
return 0;
}
}
for (int i = _maxRolloverFiles - 1; i >= 1; i--) private void RotateByDate(FileState state, DateTime newDate, string category)
{
if (state.Stream != null)
{
state.Stream.Dispose();
}
state.Date = newDate;
state.FilePath = GetFullName(newDate, category);
state.Size = GetFileSize(state.FilePath);
state.BufferPosition = 0;
state.Stream = new FileStream(
state.FilePath,
FileMode.Append,
FileAccess.Write,
FileShare.ReadWrite,
BufferSize,
FileOptions.Asynchronous | FileOptions.SequentialScan);
}
private void RollOver(FileState state, string category)
{
if (state.Stream != null)
{
state.Stream.Dispose();
state.Stream = null;
}
var dir = Path.GetDirectoryName(state.FilePath);
var name = Path.GetFileNameWithoutExtension(state.FilePath);
var ext = Path.GetExtension(state.FilePath);
for (int i = _maxRolloverFiles - 1; i >= 1; i--)
{
var src = Path.Combine(dir, string.Format("{0}.{1}{2}", name, i, ext));
var dst = Path.Combine(dir, string.Format("{0}.{1}{2}", name, i + 1, ext));
if (File.Exists(dst))
{ {
var source = Path.Combine(dir, $"{name}.{i}{ext}"); File.Delete(dst);
var destination = Path.Combine(dir, $"{name}.{i + 1}{ext}"); }
if (File.Exists(destination)) if (File.Exists(src))
{ {
File.Delete(destination); File.Move(src, dst);
} }
}
if (File.Exists(source)) var first = Path.Combine(dir, string.Format("{0}.1{1}", name, ext));
if (File.Exists(state.FilePath))
{
File.Move(state.FilePath, first);
}
if (OnRollOver != null)
{
OnRollOver.Invoke(this, state.FilePath);
}
state.Size = 0;
state.BufferPosition = 0;
state.Stream = new FileStream(
state.FilePath,
FileMode.Append,
FileAccess.Write,
FileShare.ReadWrite,
BufferSize,
FileOptions.Asynchronous | FileOptions.SequentialScan);
}
private void WriteMessageToBuffer(FileState state, LogMessage msg)
{
var sb = GetStringBuilder();
try
{
sb.Append(msg.Message);
if (IncludeCorrelationId)
{
var ctx = _context.GetAll();
if (ctx.Count > 0)
{ {
File.Move(source, destination); sb.Append(" [");
bool first = true;
foreach (var kv in ctx)
{
if (!first)
{
sb.Append(' ');
}
sb.Append(kv.Key).Append('=').Append(kv.Value);
first = false;
}
sb.Append(']');
} }
} }
var firstRoll = Path.Combine(dir, $"{name}.1{ext}"); sb.AppendLine();
if (File.Exists(file))
var text = sb.ToString();
var byteCount = Encoding.UTF8.GetByteCount(text);
// If message won't fit in buffer, flush first
if (state.BufferPosition + byteCount > BufferSize)
{ {
File.Move(file, firstRoll); FlushBufferAsync(state).Wait();
} }
_fileSizes[file] = 0; // Write to buffer
var written = Encoding.UTF8.GetBytes(text, 0, text.Length, state.Buffer, state.BufferPosition);
state.BufferPosition += written;
state.Size += written;
} }
finally finally
{ {
_rolloverLock.Release(); ReturnStringBuilder(sb);
OnRollOver?.Invoke(this, file);
} }
} }
private void InitializeFile(string file) private async Task FlushBufferAsync(FileState state)
{ {
_fileSizes.TryAdd(file, File.Exists(file) ? new FileInfo(file).Length : 0); if (state.BufferPosition == 0 || state.Stream == null)
_buffers.TryAdd(file, new StringBuilder(4096)); return;
}
private string GetFullName((int Year, int Month, int Day) g) => await state.Stream.WriteAsync(state.Buffer, 0, state.BufferPosition);
string.IsNullOrWhiteSpace(_fileNamePrefix) await state.Stream.FlushAsync();
? Path.Combine(_path, $"{Environment.MachineName}_{g.Year:0000}{g.Month:00}{g.Day:00}.log") state.BufferPosition = 0;
: Path.Combine(_path, $"{_fileNamePrefix}_{Environment.MachineName}_{g.Year:0000}{g.Month:00}{g.Day:00}.log"); }
private void DeleteOldLogFiles() private void DeleteOldLogFiles()
{ {
@@ -237,19 +395,17 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
} }
var files = new DirectoryInfo(_path) var files = new DirectoryInfo(_path)
.GetFiles($"{_fileNamePrefix}*") .GetFiles(string.Format("{0}*", _fileNamePrefix))
.OrderByDescending(f => .OrderByDescending(f =>
{ {
// Parse date from filename instead of CreationTimeUtc
var name = Path.GetFileNameWithoutExtension(f.Name); var name = Path.GetFileNameWithoutExtension(f.Name);
var parts = name.Split('_'); var parts = name.Split('_');
var datePart = parts.Length > 1 ? parts[1] : parts[0]; var datePart = parts.LastOrDefault();
if (DateTime.TryParseExact(datePart, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out var dt)) DateTime dt;
{ return DateTime.TryParseExact(datePart, "yyyyMMdd", null,
return dt; System.Globalization.DateTimeStyles.None, out dt)
} ? dt
: DateTime.MinValue;
return DateTime.MinValue;
}) })
.Skip(_maxRetainedFiles); .Skip(_maxRetainedFiles);
@@ -258,4 +414,61 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
try { f.Delete(); } catch { } try { f.Delete(); } catch { }
} }
} }
private string GetFullName(DateTime date, string category)
{
var datePart = date.ToString("yyyyMMdd");
var machine = Environment.MachineName;
if (!EnableCategoryRouting || string.IsNullOrWhiteSpace(category))
{
return string.IsNullOrWhiteSpace(_fileNamePrefix)
? Path.Combine(_path, string.Format("{0}_{1}.log", machine, datePart))
: Path.Combine(_path, string.Format("{0}_{1}_{2}.log", _fileNamePrefix, machine, datePart));
}
var safeCategory = SanitizeCategory(category);
return string.IsNullOrWhiteSpace(_fileNamePrefix)
? Path.Combine(_path, string.Format("{0}_{1}_{2}.log", machine, safeCategory, datePart))
: Path.Combine(_path, string.Format("{0}_{1}_{2}_{3}.log", _fileNamePrefix, machine, safeCategory, datePart));
}
private static string SanitizeCategory(string category)
{
foreach (var c in Path.GetInvalidFileNameChars())
{
category = category.Replace(c, '_');
}
return category.Replace('.', '_');
}
protected override void OnShutdownFlush()
{
foreach (var kvp in _files)
{
var state = kvp.Value;
if (state != null)
{
try
{
FlushBufferAsync(state).Wait();
if (state.Stream != null)
{
state.Stream.Dispose();
}
if (state.Buffer != null)
{
ReturnBuffer(state.Buffer);
}
}
catch
{
// Ignore errors during shutdown
}
}
}
_files.Clear();
}
} }

View File

@@ -63,7 +63,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal
var writtenMessage = _provider.AddMessage(timestamp, formatted, _category); var writtenMessage = _provider.AddMessage(timestamp, formatted, _category);
_settings.RaiseOnLog(new EonaCatLogMessage _settings?.RaiseOnLog(new EonaCatLogMessage
{ {
DateTime = timestamp.DateTime, DateTime = timestamp.DateTime,
Message = writtenMessage, Message = writtenMessage,
@@ -79,7 +79,17 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal
} }
catch (Exception ex) catch (Exception ex)
{ {
Console.WriteLine($"Logging error: {ex.Message}"); _settings?.RaiseOnLogError(new EonaCatLogMessage
{
DateTime = Now.DateTime,
Message = $"Failed to log message: {ex.Message}",
LogType = ELogType.ERROR,
Category = _category,
Exception = ex,
Origin = string.IsNullOrWhiteSpace(_settings.LogOrigin)
? "BatchingLogger"
: _settings.LogOrigin
});
} }
} }

View File

@@ -1,76 +1,67 @@
using EonaCat.Logger.Managers; using EonaCat.Logger.EonaCatCoreLogger;
using EonaCat.Logger.EonaCatCoreLogger.Internal;
using EonaCat.Logger.Extensions;
using EonaCat.Logger.Managers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace EonaCat.Logger.EonaCatCoreLogger.Internal;
public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
{ {
private readonly int _batchSize; private readonly ConcurrentQueue<LogMessage> _queue = new();
private readonly BlockingCollection<LogMessage> _queue; private readonly Thread _worker;
private readonly CancellationTokenSource _cts = new(); private readonly CancellationTokenSource _cts = new();
private readonly Task _worker;
private readonly int _batchSize;
private readonly long _maxQueueBytes = 512L * 1024 * 1024; // 512MB safety
private long _currentQueueBytes;
private long _dropped;
private bool _disposed; private bool _disposed;
private LoggerSettings _loggerSettings; private LoggerSettings _loggerSettings;
private readonly AutoResetEvent _signal = new(false);
public event EventHandler<long>? OnLogDropped;
public event EventHandler<Exception>? OnError;
protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options) protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options)
{ {
var currentOptions = options.Value ?? throw new ArgumentNullException(nameof(options)); var o = options.Value ?? throw new ArgumentNullException(nameof(options));
_batchSize = o.BatchSize > 0 ? o.BatchSize : 100;
if (currentOptions.FlushPeriod <= TimeSpan.Zero) if (o is FileLoggerOptions file)
{
throw new ArgumentOutOfRangeException(nameof(currentOptions.FlushPeriod));
}
_batchSize = currentOptions.BatchSize > 0 ? currentOptions.BatchSize : 100;
_queue = new BlockingCollection<LogMessage>(Math.Max(1, _batchSize * 2));
if (currentOptions is FileLoggerOptions file)
{ {
UseLocalTime = file.UseLocalTime; UseLocalTime = file.UseLocalTime;
UseMask = file.UseMask; UseMask = file.UseMask;
LoggerSettings = file.LoggerSettings; LoggerSettings = file.LoggerSettings;
} }
_worker = Task.Factory.StartNew( _worker = new Thread(ProcessLoop)
ProcessLoop, {
_cts.Token, IsBackground = true,
TaskCreationOptions.LongRunning, Name = "EonaCat-LoggerWriter"
TaskScheduler.Default); };
_worker.Start();
} }
protected bool UseLocalTime { get; } protected bool UseLocalTime { get; }
public bool UseMask { get; } protected bool UseMask { get; }
protected DateTimeOffset NowOffset => protected DateTimeOffset NowOffset =>
UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow; UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
protected LoggerSettings LoggerSettings protected LoggerSettings LoggerSettings
{ {
get get => _loggerSettings ??= new LoggerSettings { UseLocalTime = UseLocalTime, UseMask = UseMask };
{
if (_loggerSettings == null)
{
_loggerSettings = new LoggerSettings
{
UseLocalTime = UseLocalTime,
UseMask = UseMask
};
}
return _loggerSettings;
}
set => _loggerSettings = value; set => _loggerSettings = value;
} }
public ILogger CreateLogger(string categoryName) public ILogger CreateLogger(string categoryName) => new BatchingLogger(this, categoryName, LoggerSettings);
=> new BatchingLogger(this, categoryName, LoggerSettings);
internal abstract Task WriteMessagesAsync( internal abstract Task WriteMessagesAsync(
IReadOnlyList<LogMessage> messages, IReadOnlyList<LogMessage> messages,
@@ -79,16 +70,30 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
internal string AddMessage(DateTimeOffset timestamp, string message, string category) internal string AddMessage(DateTimeOffset timestamp, string message, string category)
{ {
var log = CreateLogMessage(message, timestamp, category); var log = CreateLogMessage(message, timestamp, category);
_queue.Add(log); var size = log.EstimatedSize;
var newSize = Interlocked.Add(ref _currentQueueBytes, size);
if (newSize > _maxQueueBytes)
{
Interlocked.Add(ref _currentQueueBytes, -size);
var dropped = Interlocked.Increment(ref _dropped);
OnLogDropped?.Invoke(this, dropped);
return log.Message;
}
_queue.Enqueue(log);
_signal.Set();
return log.Message; return log.Message;
} }
private LogMessage CreateLogMessage(string message, DateTimeOffset ts, string category) private LogMessage CreateLogMessage(string message, DateTimeOffset ts, string category)
{ {
if (LoggerSettings.UseMask) if (LoggerSettings.UseMask)
{ {
SensitiveDataMasker sensitiveDataMasker = new SensitiveDataMasker(LoggerSettings); var masker = new SensitiveDataMasker(LoggerSettings);
message = sensitiveDataMasker.MaskSensitiveInformation(message); message = masker.MaskSensitiveInformation(message);
} }
return new LogMessage return new LogMessage
@@ -99,53 +104,58 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
}; };
} }
private async Task ProcessLoop() private void ProcessLoop()
{ {
var batch = new List<LogMessage>(_batchSize); var batch = new List<LogMessage>(_batchSize);
var flushInterval = LoggerSettings.FileLoggerOptions.FlushPeriod;
var timeoutMs = (int)Math.Min(flushInterval.TotalMilliseconds, int.MaxValue);
try while (!_cts.IsCancellationRequested)
{
while (!_cts.Token.IsCancellationRequested)
{
if (_queue.TryTake(out var item, timeoutMs, _cts.Token))
{
batch.Add(item);
}
if (batch.Count >= _batchSize ||
(batch.Count > 0 && !_queue.TryTake(out _, 0)))
{
await FlushBatchAsync(batch);
}
}
}
catch (OperationCanceledException)
{
if (batch.Count > 0)
{
await FlushBatchAsync(batch);
}
}
}
private async Task FlushBatchAsync(List<LogMessage> batch)
{
try
{
await WriteMessagesAsync(batch, _cts.Token).ConfigureAwait(false);
}
catch (Exception ex)
{
Console.WriteLine($"An error occurred while processing log batches. {ex.Message}");
}
finally
{ {
batch.Clear(); batch.Clear();
// Dequeue up to _batchSize messages
while (batch.Count < _batchSize && _queue.TryDequeue(out var msg))
{
batch.Add(msg);
Interlocked.Add(ref _currentQueueBytes, -msg.EstimatedSize);
}
if (batch.Count == 0)
{
// Wait until a new log arrives or cancellation is requested
WaitHandle.WaitAny(new WaitHandle[] { _signal, _cts.Token.WaitHandle });
continue;
}
try
{
WriteMessagesAsync(batch, _cts.Token).GetAwaiter().GetResult();
}
catch (Exception ex)
{
OnError?.Invoke(this, ex);
}
}
// Process any remaining messages before exit
batch.Clear();
while (_queue.TryDequeue(out var msg))
{
batch.Add(msg);
Interlocked.Add(ref _currentQueueBytes, -msg.EstimatedSize);
if (batch.Count >= _batchSize)
{
WriteMessagesAsync(batch, CancellationToken.None).GetAwaiter().GetResult();
batch.Clear();
}
}
if (batch.Count > 0)
{
WriteMessagesAsync(batch, CancellationToken.None).GetAwaiter().GetResult();
} }
} }
public void Dispose() public void Dispose()
{ {
if (_disposed) if (_disposed)
@@ -156,10 +166,15 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
_disposed = true; _disposed = true;
_cts.Cancel(); _cts.Cancel();
_queue.CompleteAdding(); _worker.Join();
OnShutdownFlush();
try { _worker.Wait(); } catch { }
_cts.Dispose(); _cts.Dispose();
_queue.Dispose(); }
protected virtual void OnShutdownFlush()
{
// default: Do nothing
} }
} }

View File

@@ -9,4 +9,5 @@ public struct LogMessage
public DateTimeOffset Timestamp { get; set; } public DateTimeOffset Timestamp { get; set; }
public string Message { get; set; } public string Message { get; set; }
public string Category { get; set; } public string Category { get; set; }
public int EstimatedSize { get; set; }
} }

View File

@@ -54,7 +54,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
Exception exception, Func<TState, Exception, string> formatter) Exception exception, Func<TState, Exception, string> formatter)
{ {
if (!IsEnabled(logLevel) || formatter == null) if (!IsEnabled(logLevel) || formatter == null)
{
return; return;
}
try try
{ {
@@ -77,14 +79,20 @@ namespace EonaCat.Logger.EonaCatCoreLogger
{ {
sb.Append(" | Context: "); sb.Append(" | Context: ");
foreach (var kvp in contextData) foreach (var kvp in contextData)
{
sb.Append(kvp.Key).Append("=").Append(kvp.Value).Append("; "); sb.Append(kvp.Key).Append("=").Append(kvp.Value).Append("; ");
}
} }
if (exception != null) if (exception != null)
{
sb.Append(" | Exception: ").Append(exception); sb.Append(" | Exception: ").Append(exception);
}
if (!_logChannel.Writer.TryWrite(sb.ToString())) if (!_logChannel.Writer.TryWrite(sb.ToString()))
{
OnLogDropped?.Invoke(this, sb.ToString()); // notify if log is dropped OnLogDropped?.Invoke(this, sb.ToString()); // notify if log is dropped
}
} }
catch (Exception ex) catch (Exception ex)
{ {
@@ -136,7 +144,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
// Flush any remaining logs // Flush any remaining logs
if (batch.Count > 0) if (batch.Count > 0)
{
await SafeWriteBatch(writer, batch, client, token); await SafeWriteBatch(writer, batch, client, token);
}
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {

View File

@@ -0,0 +1,38 @@
using EonaCat.Logger.EonaCatCoreLogger;
namespace EonaCat.Logger
{
public static class LoggerConfigurator
{
/// <summary>
/// Copies file logger settings from the specified source options to the target options instance.
/// </summary>
/// <remarks>Only properties defined in FileLoggerOptions are copied. The method does not perform
/// a deep clone; reference-type properties are assigned directly. If either parameter is null, the method
/// returns without making changes.</remarks>
/// <param name="target">The FileLoggerOptions instance to which settings will be applied. If null, no changes are made.</param>
/// <param name="source">The FileLoggerOptions instance from which settings are copied. If null, no changes are made.</param>
public static void ApplyFileLoggerSettings(FileLoggerOptions target, FileLoggerOptions source)
{
if (target == null || source == null)
{
return;
}
target.FileNamePrefix = source.FileNamePrefix;
target.FlushPeriod = source.FlushPeriod;
target.RetainedFileCountLimit = source.RetainedFileCountLimit;
target.MaxWriteTries = source.MaxWriteTries;
target.FileSizeLimit = source.FileSizeLimit;
target.IsEnabled = source.IsEnabled;
target.MaxRolloverFiles = source.MaxRolloverFiles;
target.UseLocalTime = source.UseLocalTime;
target.UseMask = source.UseMask;
target.Mask = source.Mask;
target.UseDefaultMasking = source.UseDefaultMasking;
target.MaskedKeywords = source.MaskedKeywords;
target.IncludeCorrelationId = source.IncludeCorrelationId;
target.EnableCategoryRouting = source.EnableCategoryRouting;
}
}
}

View File

@@ -1,6 +1,4 @@
using EonaCat.Json; using EonaCat.Json;
using EonaCat.Logger.EonaCatCoreLogger;
using EonaCat.Logger.EonaCatCoreLogger.Internal;
using EonaCat.Logger.Extensions; using EonaCat.Logger.Extensions;
using EonaCat.Logger.Servers.GrayLog; using EonaCat.Logger.Servers.GrayLog;
using EonaCat.Logger.Servers.Splunk.Models; using EonaCat.Logger.Servers.Splunk.Models;
@@ -8,15 +6,11 @@ using Microsoft.Extensions.Logging;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Net; using System.Net;
using System.Runtime.InteropServices;
using System.Text; using System.Text;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using static EonaCat.Logger.Managers.LogHelper;
// This file is part of the EonaCat project(s) which is released under the Apache License. // This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details. // See the LICENSE file or go to https://EonaCat.com/License for full license details.

View File

@@ -31,15 +31,19 @@ namespace EonaCat.Logger.Managers
private bool _isDisposing; private bool _isDisposing;
private string _category; private string _category;
public LogManager(LoggerSettings settings) public LogManager(LoggerSettings settings, string category = null)
{ {
Settings = settings ?? CreateDefaultSettings(); Settings = settings ?? CreateDefaultSettings();
SetupLogManager(); _category = string.IsNullOrWhiteSpace(category) ? null : category;
} _category = settings.FileLoggerOptions.Category;
public LogManager(LoggerSettings settings, string category = null) : this(settings) if (string.IsNullOrEmpty(_category))
{ {
_category = string.IsNullOrWhiteSpace(category) ? "General" : category; _category = category;
settings.FileLoggerOptions.Category = _category;
}
SetupLogManager();
SetupFileLogger(settings); SetupFileLogger(settings);
// Subscribe to static events // Subscribe to static events
@@ -61,14 +65,8 @@ namespace EonaCat.Logger.Managers
{ {
if (LoggerProvider is FileLoggerProvider fileLoggerProvider) if (LoggerProvider is FileLoggerProvider fileLoggerProvider)
{ {
// Ensure log file is initialized
if (string.IsNullOrEmpty(fileLoggerProvider.LogFile))
{
fileLoggerProvider.InitializeCurrentFile();
}
return fileLoggerProvider.LogFile; return fileLoggerProvider.LogFile;
} }
return string.Empty; return string.Empty;
} }
} }
@@ -128,7 +126,9 @@ namespace EonaCat.Logger.Managers
public async Task StartNewLogAsync() public async Task StartNewLogAsync()
{ {
if (_isDisposing || _tokenSource.IsCancellationRequested) if (_isDisposing || _tokenSource.IsCancellationRequested)
{
return; return;
}
await _startLock.WaitAsync().ConfigureAwait(false); await _startLock.WaitAsync().ConfigureAwait(false);
try try
@@ -140,13 +140,8 @@ namespace EonaCat.Logger.Managers
if (!IsRunning) if (!IsRunning)
{ {
CreateLogger(); CreateLogger(_category);
Directory.CreateDirectory(Settings.FileLoggerOptions.LogDirectory); Directory.CreateDirectory(Settings.FileLoggerOptions.LogDirectory);
if (LoggerProvider is FileLoggerProvider fileProvider)
{
fileProvider.InitializeCurrentFile();
}
_logDate = CurrentDateTime; _logDate = CurrentDateTime;
IsRunning = true; IsRunning = true;
} }
@@ -175,7 +170,7 @@ namespace EonaCat.Logger.Managers
LogHelper.SendToFile(Logger, Settings, ELogType.INFO, stopMessage); LogHelper.SendToFile(Logger, Settings, ELogType.INFO, stopMessage);
} }
private void CreateLogger() private void CreateLogger(string categoryName = null)
{ {
if (Logger != null) if (Logger != null)
{ {
@@ -204,31 +199,15 @@ namespace EonaCat.Logger.Managers
serviceCollection.AddLogging(builder => serviceCollection.AddLogging(builder =>
builder.SetMinimumLevel(Settings.TypesToLog.Max().ToLogLevel()) builder.SetMinimumLevel(Settings.TypesToLog.Max().ToLogLevel())
.AddEonaCatFileLogger(config => .AddEonaCatFileLogger(options =>
{ {
var options = Settings.FileLoggerOptions; LoggerConfigurator.ApplyFileLoggerSettings(options, Settings.FileLoggerOptions);
config.LoggerSettings = Settings; }));
config.MaxWriteTries = options.MaxWriteTries;
config.RetainedFileCountLimit = options.RetainedFileCountLimit;
config.FlushPeriod = options.FlushPeriod;
config.IsEnabled = options.IsEnabled;
config.BatchSize = options.BatchSize;
config.FileSizeLimit = options.FileSizeLimit;
config.LogDirectory = options.LogDirectory;
config.FileNamePrefix = options.FileNamePrefix;
config.MaxRolloverFiles = options.MaxRolloverFiles;
config.UseLocalTime = Settings.UseLocalTime;
config.UseMask = Settings.UseMask;
config.Mask = options.Mask;
config.UseDefaultMasking = Settings.UseDefaultMasking;
config.MaskedKeywords = options.MaskedKeywords;
config.Settings = Settings;
}));
var serviceProvider = serviceCollection.BuildServiceProvider(); var serviceProvider = serviceCollection.BuildServiceProvider();
LoggerProvider = serviceProvider.GetService<ILoggerProvider>(); LoggerProvider = serviceProvider.GetService<ILoggerProvider>();
LoggerFactory = serviceProvider.GetService<ILoggerFactory>(); LoggerFactory = serviceProvider.GetService<ILoggerFactory>();
Logger = LoggerFactory.CreateLogger(Settings.Id); Logger = LoggerFactory.CreateLogger(_category ?? Settings.Id);
LogHelper.SendToFile(Logger, Settings, ELogType.INFO, LogHelper.GetStartupMessage()); LogHelper.SendToFile(Logger, Settings, ELogType.INFO, LogHelper.GetStartupMessage());
} }

View File

@@ -229,6 +229,7 @@ public class LoggerSettings
public bool UseDefaultMasking { get; set; } = true; public bool UseDefaultMasking { get; set; } = true;
public event LogDelegate OnLog; public event LogDelegate OnLog;
public event LogDelegate OnError;
private static FileLoggerOptions CreateDefaultFileLoggerOptions() private static FileLoggerOptions CreateDefaultFileLoggerOptions()
{ {
@@ -360,4 +361,14 @@ public class LoggerSettings
{ {
OnLogEvent(eonaCatLogMessage); OnLogEvent(eonaCatLogMessage);
} }
internal void RaiseOnLogError(EonaCatLogMessage eonaCatLogMessage)
{
OnLogErrorEvent(eonaCatLogMessage);
}
private void OnLogErrorEvent(EonaCatLogMessage eonaCatLogMessage)
{
OnError?.Invoke(eonaCatLogMessage);
}
} }

View File

@@ -16,6 +16,32 @@
{ {
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
_ = Task.Run(async () =>
{
var loggerSettings = new LoggerSettings();
loggerSettings.Id = "SPEEDTEST";
loggerSettings.UseLocalTime = true;
loggerSettings.FileLoggerOptions.UseLocalTime = true;
loggerSettings.EnableConsole = false;
loggerSettings.FileLoggerOptions.Category = "SpeedTests";
loggerSettings.FileLoggerOptions.EnableCategoryRouting = true;
loggerSettings.AllowAllLogTypes();
var logger = new LogManager(loggerSettings);
var i = 0;
while (true)
{
i++;
await logger.WriteAsync($"test to file {i} INFO").ConfigureAwait(false);
await logger.WriteAsync($"test to file {i} CRITICAL", ELogType.CRITICAL).ConfigureAwait(false);
await logger.WriteAsync($"test to file {i} DEBUG", ELogType.DEBUG).ConfigureAwait(false);
await logger.WriteAsync($"test to file {i} ERROR", ELogType.ERROR).ConfigureAwait(false);
await logger.WriteAsync($"test to file {i} TRACE", ELogType.TRACE).ConfigureAwait(false);
await Task.Delay(1).ConfigureAwait(false);
}
});
Console.ReadKey();
var _config = new MemoryGuardConfiguration var _config = new MemoryGuardConfiguration
{ {
MonitoringInterval = TimeSpan.FromSeconds(5), MonitoringInterval = TimeSpan.FromSeconds(5),
@@ -35,6 +61,30 @@
//MemoryGuard.Start(_config); //MemoryGuard.Start(_config);
_ = Task.Run(async () =>
{
var loggerSettings = new LoggerSettings();
loggerSettings.Id = "SPEEDTEST";
loggerSettings.UseLocalTime = true;
loggerSettings.FileLoggerOptions.UseLocalTime = true;
loggerSettings.FileLoggerOptions.Category = "SpeedTests";
loggerSettings.FileLoggerOptions.EnableCategoryRouting = true;
loggerSettings.AllowAllLogTypes();
var logger = new LogManager(loggerSettings);
var i = 0;
while (true)
{
i++;
await logger.WriteAsync($"test to file {i} INFO").ConfigureAwait(false);
await logger.WriteAsync($"test to file {i} CRITICAL", ELogType.CRITICAL).ConfigureAwait(false);
await logger.WriteAsync($"test to file {i} DEBUG", ELogType.DEBUG).ConfigureAwait(false);
await logger.WriteAsync($"test to file {i} ERROR", ELogType.ERROR).ConfigureAwait(false);
await logger.WriteAsync($"test to file {i} TRACE", ELogType.TRACE).ConfigureAwait(false);
await Task.Delay(1).ConfigureAwait(false);
}
});
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
int onLogCounter = 0; int onLogCounter = 0;
var defaultColor = Console.ForegroundColor; var defaultColor = Console.ForegroundColor;
@@ -215,6 +265,8 @@
loggerSettings.FileLoggerOptions.UseLocalTime = true; loggerSettings.FileLoggerOptions.UseLocalTime = true;
loggerSettings.UseLocalTime = true; loggerSettings.UseLocalTime = true;
loggerSettings.Id = "TEST"; loggerSettings.Id = "TEST";
loggerSettings.FileLoggerOptions.Category = "ExceptionTests";
loggerSettings.FileLoggerOptions.EnableCategoryRouting = true;
loggerSettings.TypesToLog.Add(ELogType.INFO); loggerSettings.TypesToLog.Add(ELogType.INFO);
var logger = new LogManager(loggerSettings); var logger = new LogManager(loggerSettings);
@@ -234,7 +286,7 @@
} }
} }
MemoryLeakTester.Start(logger); //MemoryLeakTester.Start(logger);
_ = Task.Run(RunMemoryReportTask).ConfigureAwait(false); _ = Task.Run(RunMemoryReportTask).ConfigureAwait(false);
_ = Task.Run(RunMaskTest).ConfigureAwait(false); _ = Task.Run(RunMaskTest).ConfigureAwait(false);
_ = Task.Run(RunWebLoggerTestsAsync).ConfigureAwait(false); _ = Task.Run(RunWebLoggerTestsAsync).ConfigureAwait(false);