This commit is contained in:
2026-02-02 21:42:49 +01:00
parent 269dde3860
commit d9454280b9
7 changed files with 187 additions and 121 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.1</Version> <Version>1.6.2</Version>
<FileVersion>1.6.1</FileVersion> <FileVersion>1.6.2</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.1+{chash:10}.{c:ymd}</EVRevisionFormat> <EVRevisionFormat>1.6.2+{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

@@ -13,9 +13,6 @@ using Microsoft.Extensions.Options;
namespace EonaCat.Logger.EonaCatCoreLogger; namespace EonaCat.Logger.EonaCatCoreLogger;
// 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.
[ProviderAlias("EonaCatFileLogger")] [ProviderAlias("EonaCatFileLogger")]
public sealed class FileLoggerProvider : BatchingLoggerProvider public sealed class FileLoggerProvider : BatchingLoggerProvider
{ {
@@ -25,19 +22,20 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
private readonly int _maxRetainedFiles; private readonly int _maxRetainedFiles;
private readonly int _maxRolloverFiles; private readonly int _maxRolloverFiles;
private readonly ConcurrentDictionary<string, StringBuilder> _buffer = new();
private readonly SemaphoreSlim _writeLock = new(1, 1);
private readonly SemaphoreSlim _rolloverLock = new(1, 1);
private readonly LoggerScopedContext _context = new();
private string _logFile; private string _logFile;
private long _currentFileSize; private long _currentFileSize;
private int _isWriting;
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();
public string LogFile => _logFile ?? string.Empty;
public event EventHandler<ErrorMessage> OnError; public event EventHandler<ErrorMessage> OnError;
public string LogFile => _logFile;
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options) public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
{ {
var o = options.Value; var o = options.Value;
@@ -47,68 +45,56 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
_maxRetainedFiles = o.RetainedFileCountLimit; _maxRetainedFiles = o.RetainedFileCountLimit;
_maxRolloverFiles = o.MaxRolloverFiles; _maxRolloverFiles = o.MaxRolloverFiles;
IncludeCorrelationId = o.IncludeCorrelationId; IncludeCorrelationId = o.IncludeCorrelationId;
Directory.CreateDirectory(_path);
InitializeCurrentFile();
} }
public bool IncludeCorrelationId { get; } public bool IncludeCorrelationId { get; }
protected override async Task WriteMessagesAsync( internal override async Task WriteMessagesAsync(
IReadOnlyList<LogMessage> messages, IReadOnlyList<LogMessage> messages,
CancellationToken token) CancellationToken token)
{ {
if (Interlocked.Exchange(ref _isWriting, 1) == 1) if (messages.Count == 0)
{ {
return; return;
} }
try Directory.CreateDirectory(_path);
// Group messages by date
foreach (var group in messages.GroupBy(m => (m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day)))
{ {
Directory.CreateDirectory(_path); var file = GetFullName(group.Key);
InitializeFile(file);
foreach (var group in messages.GroupBy(GetGrouping)) var sb = _buffers.GetOrAdd(file, _ => new StringBuilder(4096));
lock (sb)
{ {
var file = GetFullName(group.Key); foreach (var message in group)
InitializeFile(file);
var stringBuilder = _buffer.GetOrAdd(file, _ => new StringBuilder(4096));
lock (stringBuilder)
{ {
foreach (var message in group) AppendMessage(sb, message);
{
AppendMessage(stringBuilder, message);
}
} }
await FlushAsync(file, stringBuilder, token).ConfigureAwait(false);
DeleteOldLogFiles();
} }
}
catch (Exception ex) await FlushAsync(file, sb, token).ConfigureAwait(false);
{ DeleteOldLogFiles();
OnError?.Invoke(this, new ErrorMessage
{
Exception = ex,
Message = "Failed to write log file"
});
}
finally
{
Interlocked.Exchange(ref _isWriting, 0);
} }
} }
private void AppendMessage(StringBuilder sb, LogMessage msg) private void AppendMessage(StringBuilder sb, LogMessage msg)
{ {
// Ensure correlation id exists (once per scope) // Ensure correlation id exists (once per async context)
if (IncludeCorrelationId) if (IncludeCorrelationId)
{ {
var cid = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); var cid = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
_context.Set("CorrelationId", cid); _context.Set("CorrelationId", cid);
} }
// 1. Append the already-formatted message FIRST
sb.Append(msg.Message); sb.Append(msg.Message);
// 2. Append context AFTER the message
var ctx = _context.GetAll(); var ctx = _context.GetAll();
if (ctx.Count > 0) if (ctx.Count > 0)
{ {
@@ -121,19 +107,26 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
sb.Append(' '); sb.Append(' ');
} }
sb.Append(kv.Key) sb.Append(kv.Key).Append('=').Append(kv.Value);
.Append('=')
.Append(kv.Value);
first = false; first = false;
} }
sb.Append(']'); sb.Append(']');
} }
// 3. End the line
sb.AppendLine(); sb.AppendLine();
} }
public void InitializeCurrentFile()
{
if (!string.IsNullOrEmpty(_logFile))
{
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 sb, CancellationToken token) private async Task FlushAsync(string file, StringBuilder sb, CancellationToken token)
{ {
if (sb.Length == 0) if (sb.Length == 0)
@@ -152,14 +145,16 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
64 * 1024, 64 * 1024,
useAsync: true); useAsync: true);
using var writer = new StreamWriter(fs); using var writer = new StreamWriter(fs, Encoding.UTF8);
var text = sb.ToString(); var text = sb.ToString();
await writer.WriteAsync(text).ConfigureAwait(false); await writer.WriteAsync(text).ConfigureAwait(false);
_currentFileSize += Encoding.UTF8.GetByteCount(text); _fileSizes.AddOrUpdate(file, Encoding.UTF8.GetByteCount(text), (_, old) => old + Encoding.UTF8.GetByteCount(text));
_currentFileSize = _fileSizes[file];
sb.Clear(); sb.Clear();
if (_currentFileSize >= _maxFileSize) if (_fileSizes[file] >= _maxFileSize)
{ {
await RollOverAsync(file).ConfigureAwait(false); await RollOverAsync(file).ConfigureAwait(false);
} }
@@ -195,8 +190,13 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
} }
} }
File.Move(file, Path.Combine(dir, $"{name}.1{ext}")); var firstRoll = Path.Combine(dir, $"{name}.1{ext}");
_currentFileSize = 0; if (File.Exists(file))
{
File.Move(file, firstRoll);
}
_fileSizes[file] = 0;
} }
finally finally
{ {
@@ -206,15 +206,8 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
private void InitializeFile(string file) private void InitializeFile(string file)
{ {
if (_logFile == file) _fileSizes.TryAdd(file, File.Exists(file) ? new FileInfo(file).Length : 0);
{ _buffers.TryAdd(file, new StringBuilder(4096));
return;
}
_logFile = file;
_currentFileSize = File.Exists(file)
? new FileInfo(file).Length
: 0;
} }
private (int Year, int Month, int Day) GetGrouping(LogMessage m) => private (int Year, int Month, int Day) GetGrouping(LogMessage m) =>
@@ -225,7 +218,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
? Path.Combine(_path, $"{g.Year:0000}{g.Month:00}{g.Day:00}.log") ? Path.Combine(_path, $"{g.Year:0000}{g.Month:00}{g.Day:00}.log")
: Path.Combine(_path, $"{_fileNamePrefix}_{g.Year:0000}{g.Month:00}{g.Day:00}.log"); : Path.Combine(_path, $"{_fileNamePrefix}_{g.Year:0000}{g.Month:00}{g.Day:00}.log");
protected void DeleteOldLogFiles() private void DeleteOldLogFiles()
{ {
if (_maxRetainedFiles <= 0) if (_maxRetainedFiles <= 0)
{ {
@@ -234,13 +227,24 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
var files = new DirectoryInfo(_path) var files = new DirectoryInfo(_path)
.GetFiles($"{_fileNamePrefix}*") .GetFiles($"{_fileNamePrefix}*")
.Where(f => !f.FullName.Equals(_logFile, StringComparison.OrdinalIgnoreCase)) .OrderByDescending(f =>
.OrderByDescending(f => f.CreationTimeUtc) {
// Parse date from filename instead of CreationTimeUtc
var name = Path.GetFileNameWithoutExtension(f.Name);
var parts = name.Split('_');
var datePart = parts.Length > 1 ? parts[1] : parts[0];
if (DateTime.TryParseExact(datePart, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out var dt))
{
return dt;
}
return DateTime.MinValue;
})
.Skip(_maxRetainedFiles); .Skip(_maxRetainedFiles);
foreach (var f in files) foreach (var f in files)
{ {
f.Delete(); try { f.Delete(); } catch { }
} }
} }
} }

View File

@@ -1,8 +1,11 @@
using System; using EonaCat.Logger.EonaCatCoreLogger.Models;
using EonaCat.Logger.EonaCatCoreLogger.Models;
using EonaCat.Logger.Extensions; using EonaCat.Logger.Extensions;
using EonaCat.Logger.Managers; using EonaCat.Logger.Managers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace EonaCat.Logger.EonaCatCoreLogger.Internal namespace EonaCat.Logger.EonaCatCoreLogger.Internal
{ {
@@ -26,7 +29,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal
{ {
get get
{ {
// Avoid DateTimeOffset.Now if UseLocalTime is false
return _settings.UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow; return _settings.UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
} }
} }
@@ -63,36 +65,52 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal
} }
var timestamp = Now; var timestamp = Now;
LogInternal(timestamp, logLevel, rawMessage, exception); LogInternalAsync(timestamp, logLevel, rawMessage, exception).ConfigureAwait(false);
} }
private void LogInternal( private async Task LogInternalAsync(
DateTimeOffset timestamp, DateTimeOffset timestamp,
LogLevel logLevel, LogLevel logLevel,
string message, string message,
Exception exception) Exception exception)
{ {
string formatted = LogHelper.FormatMessageWithHeader( try
_settings,
logLevel.FromLogLevel(),
message,
timestamp.DateTime,
_category);
var writtenMessage = _provider.AddMessage(timestamp, formatted, _category);
var onLogEvent = _settings.OnLogEvent;
if (onLogEvent != null)
{ {
onLogEvent(new EonaCatLogMessage string formatted = LogHelper.FormatMessageWithHeader(
_settings,
logLevel.FromLogLevel(),
message,
timestamp.DateTime,
_category);
var writtenMessage = _provider.AddMessage(timestamp, formatted, _category);
var onLogEvent = _settings.OnLogEvent;
if (onLogEvent != null)
{ {
DateTime = timestamp.DateTime, onLogEvent(new EonaCatLogMessage
Message = writtenMessage, {
LogType = logLevel.FromLogLevel(), DateTime = timestamp.DateTime,
Category = _category, Message = writtenMessage,
Exception = exception, LogType = logLevel.FromLogLevel(),
Origin = string.IsNullOrWhiteSpace(_settings.LogOrigin) ? "BatchingLogger" : _settings.LogOrigin Category = _category,
}); Exception = exception,
Origin = string.IsNullOrWhiteSpace(_settings.LogOrigin) ? "BatchingLogger" : _settings.LogOrigin
});
}
await _provider.WriteMessagesAsync(new List<LogMessage>
{
new LogMessage
{
Timestamp = timestamp,
Message = formatted
}
}, CancellationToken.None).ConfigureAwait(false);
}
catch (Exception ex)
{
Console.WriteLine($"Logging error: {ex.Message}");
} }
} }

View File

@@ -22,17 +22,17 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options) protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options)
{ {
var o = options.Value ?? throw new ArgumentNullException(nameof(options)); var currentOptions = options.Value ?? throw new ArgumentNullException(nameof(options));
if (o.FlushPeriod <= TimeSpan.Zero) if (currentOptions.FlushPeriod <= TimeSpan.Zero)
{ {
throw new ArgumentOutOfRangeException(nameof(o.FlushPeriod)); throw new ArgumentOutOfRangeException(nameof(currentOptions.FlushPeriod));
} }
_batchSize = o.BatchSize > 0 ? o.BatchSize : 100; _batchSize = currentOptions.BatchSize > 0 ? currentOptions.BatchSize : 100;
_queue = new BlockingCollection<LogMessage>(new ConcurrentQueue<LogMessage>()); _queue = new BlockingCollection<LogMessage>(new ConcurrentQueue<LogMessage>());
if (o is FileLoggerOptions file) if (currentOptions is FileLoggerOptions file)
{ {
UseLocalTime = file.UseLocalTime; UseLocalTime = file.UseLocalTime;
UseMask = file.UseMask; UseMask = file.UseMask;
@@ -72,7 +72,7 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
public ILogger CreateLogger(string categoryName) public ILogger CreateLogger(string categoryName)
=> new BatchingLogger(this, categoryName, LoggerSettings); => new BatchingLogger(this, categoryName, LoggerSettings);
protected abstract Task WriteMessagesAsync( internal abstract Task WriteMessagesAsync(
IReadOnlyList<LogMessage> messages, IReadOnlyList<LogMessage> messages,
CancellationToken token); CancellationToken token);
@@ -99,7 +99,7 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
}; };
} }
private async void ProcessLoop() private async Task ProcessLoop()
{ {
var batch = new List<LogMessage>(_batchSize); var batch = new List<LogMessage>(_batchSize);
@@ -109,32 +109,45 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
{ {
batch.Add(item); batch.Add(item);
if (batch.Count < _batchSize) if (batch.Count >= _batchSize)
{ {
continue; await FlushBatchAsync(batch);
} }
await WriteMessagesAsync(batch, _cts.Token).ConfigureAwait(false);
batch.Clear();
} }
if (batch.Count > 0) if (batch.Count > 0)
{ {
await WriteMessagesAsync(batch, _cts.Token).ConfigureAwait(false); await FlushBatchAsync(batch);
} }
} }
catch (OperationCanceledException) catch (OperationCanceledException)
{ {
// normal shutdown if (batch.Count > 0)
{
await FlushBatchAsync(batch);
}
} }
catch (Exception ex) catch (Exception ex)
{ {
// last-resort logging
Console.Error.WriteLine(ex); Console.Error.WriteLine(ex);
} }
} }
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);
}
finally
{
batch.Clear();
}
}
public void Dispose() public void Dispose()
{ {

View File

@@ -48,7 +48,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, public void Log<TState>(LogLevel logLevel, EventId eventId, TState state,
Exception exception, Func<TState, Exception, string> formatter) Exception exception, Func<TState, Exception, string> formatter)
{ {
if (!IsEnabled(logLevel) || formatter == null) return; if (!IsEnabled(logLevel) || formatter == null)
{
return;
}
try try
{ {

View File

@@ -55,9 +55,23 @@ namespace EonaCat.Logger.Managers
public ILogger Logger { get; private set; } public ILogger Logger { get; private set; }
public bool IsRunning { get; private set; } public bool IsRunning { get; private set; }
public string CurrentLogFile => LoggerProvider is FileLoggerProvider fileLoggerProvider public string CurrentLogFile
? fileLoggerProvider.LogFile {
: string.Empty; get
{
if (LoggerProvider is FileLoggerProvider fileLoggerProvider)
{
// Ensure log file is initialized
if (string.IsNullOrEmpty(fileLoggerProvider.LogFile))
{
fileLoggerProvider.InitializeCurrentFile();
}
return fileLoggerProvider.LogFile;
}
return string.Empty;
}
}
private DateTime CurrentDateTime => Settings.UseLocalTime ? DateTime.Now : DateTime.UtcNow; private DateTime CurrentDateTime => Settings.UseLocalTime ? DateTime.Now : DateTime.UtcNow;
@@ -104,11 +118,17 @@ namespace EonaCat.Logger.Managers
return; return;
} }
if (!IsRunning)
{
await StartNewLogAsync().ConfigureAwait(false);
}
await InternalWriteAsync(CurrentDateTime, message, logType, writeToConsole, await InternalWriteAsync(CurrentDateTime, message, logType, writeToConsole,
customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL) customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
public async Task StartNewLogAsync() public async Task StartNewLogAsync()
{ {
if (_isDisposing || _tokenSource.IsCancellationRequested) if (_isDisposing || _tokenSource.IsCancellationRequested)
@@ -121,10 +141,17 @@ namespace EonaCat.Logger.Managers
await StopLoggingAsync().ConfigureAwait(false); await StopLoggingAsync().ConfigureAwait(false);
} }
IsRunning = true;
CreateLogger(); CreateLogger();
// Ensure log file exists
Directory.CreateDirectory(Settings.FileLoggerOptions.LogDirectory); Directory.CreateDirectory(Settings.FileLoggerOptions.LogDirectory);
if (LoggerProvider is FileLoggerProvider fileProvider)
{
fileProvider.InitializeCurrentFile();
}
_logDate = CurrentDateTime; _logDate = CurrentDateTime;
IsRunning = true;
} }
@@ -291,8 +318,8 @@ namespace EonaCat.Logger.Managers
_tokenSource?.Dispose(); _tokenSource?.Dispose();
_tokenSource = null; _tokenSource = null;
LoggerProvider?.Dispose(); try { LoggerProvider?.Dispose(); } catch { }
LoggerFactory?.Dispose(); try { LoggerFactory?.Dispose(); } catch { }
} }
} }

View File

@@ -33,7 +33,7 @@
PatternDetectionInterval = TimeSpan.FromMinutes(3) PatternDetectionInterval = TimeSpan.FromMinutes(3)
}; };
MemoryGuard.Start(_config); //MemoryGuard.Start(_config);
var builder = WebApplication.CreateBuilder(args); var builder = WebApplication.CreateBuilder(args);
int onLogCounter = 0; int onLogCounter = 0;
@@ -94,6 +94,7 @@
// Create the adapter // Create the adapter
var adapter = new LogCentralEonaCatAdapter(logger.LoggerSettings, logClient); var adapter = new LogCentralEonaCatAdapter(logger.LoggerSettings, logClient);
await LogManager.Instance.WriteAsync("LogCentral adapter initialized", ELogType.INFO).ConfigureAwait(false);
// Now all EonaCat.Logger logs will be sent to LogCentral automatically // Now all EonaCat.Logger logs will be sent to LogCentral automatically
await logger.LogAsync("This is a test log message sent to LogCentral!", ELogType.INFO).ConfigureAwait(false); await logger.LogAsync("This is a test log message sent to LogCentral!", ELogType.INFO).ConfigureAwait(false);