Updated (added more header options)

This commit is contained in:
2026-01-30 20:40:41 +01:00
parent f18456e263
commit 04a807df4f
9 changed files with 512 additions and 390 deletions

View File

@@ -25,7 +25,7 @@
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="EonaCat.Logger" Version="1.5.5" /> <PackageReference Include="EonaCat.Logger" Version="1.5.7" />
<PackageReference Include="System.Net.Http.Json" Version="10.0.1" /> <PackageReference Include="System.Net.Http.Json" Version="10.0.1" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

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.5.7</Version> <Version>1.5.8</Version>
<FileVersion>1.5.7</FileVersion> <FileVersion>1.5.8</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.5.7+{chash:10}.{c:ymd}</EVRevisionFormat> <EVRevisionFormat>1.5.8+{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

@@ -53,7 +53,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;
}
if (IncludeCorrelationId) if (IncludeCorrelationId)
{ {
@@ -71,17 +73,25 @@ namespace EonaCat.Logger.EonaCatCoreLogger
}; };
foreach (var kvp in _context.GetAll()) foreach (var kvp in _context.GetAll())
{
logParts.Add($"`{kvp.Key}`: {kvp.Value}"); logParts.Add($"`{kvp.Key}`: {kvp.Value}");
}
if (exception != null) if (exception != null)
{
logParts.Add($"Exception: {exception}"); logParts.Add($"Exception: {exception}");
}
// Limit queue size to prevent memory growth // Limit queue size to prevent memory growth
if (_messageQueue.Count < 1000) if (_messageQueue.Count < 1000)
{
_messageQueue.Enqueue(string.Join("\n", logParts)); _messageQueue.Enqueue(string.Join("\n", logParts));
}
else else
{
OnException?.Invoke(this, new Exception("DiscordLogger queue overflow")); OnException?.Invoke(this, new Exception("DiscordLogger queue overflow"));
} }
}
private async Task FlushLoopAsync(CancellationToken token) private async Task FlushLoopAsync(CancellationToken token)
{ {
@@ -107,7 +117,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
private async Task FlushBufferAsync(CancellationToken token) private async Task FlushBufferAsync(CancellationToken token)
{ {
if (!await _flushLock.WaitAsync(0, token).ConfigureAwait(false)) if (!await _flushLock.WaitAsync(0, token).ConfigureAwait(false))
{
return; return;
}
try try
{ {

View File

@@ -54,8 +54,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal
? exception.FormatExceptionToMessage() + Environment.NewLine ? exception.FormatExceptionToMessage() + Environment.NewLine
: formatter(state, exception); : formatter(state, exception);
message = LogHelper.FormatMessageWithHeader(_loggerSettings, logLevel.FromLogLevel(), message, timestamp.DateTime, category) message = LogHelper.FormatMessageWithHeader(_loggerSettings, logLevel.FromLogLevel(), message, timestamp.DateTime, category) + Environment.NewLine;
+ Environment.NewLine;
var currentMessage = new EonaCatLogMessage var currentMessage = new EonaCatLogMessage
{ {

View File

@@ -1,12 +1,11 @@
using System; using EonaCat.Logger.Managers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.SymbolStore;
using System.Threading;
using System.Threading.Tasks;
using EonaCat.Logger.Managers;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace EonaCat.Logger.EonaCatCoreLogger.Internal; namespace EonaCat.Logger.EonaCatCoreLogger.Internal;
// 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.
@@ -14,21 +13,22 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal;
public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
{ {
private readonly int _batchSize; private int _batchSize;
private readonly Channel<LogMessage> _channel =
Channel.CreateUnbounded<LogMessage>(new UnboundedChannelOptions
{
SingleReader = true,
SingleWriter = false
});
private readonly List<LogMessage> _currentBatch = new();
private CancellationTokenSource _cancellationTokenSource; private CancellationTokenSource _cancellationTokenSource;
private LoggerSettings _loggerSettings;
private ConcurrentQueue<LogMessage> _messageQueue;
private Task _outputTask; private Task _outputTask;
private object _writeLock = new object(); private bool _isDisposed;
private bool _isDisposing;
protected string Category;
protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options) protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options)
{ {
var loggerOptions = options.Value; var loggerOptions = options.Value ?? throw new ArgumentNullException(nameof(options));
if (loggerOptions.FlushPeriod <= TimeSpan.Zero) if (loggerOptions.FlushPeriod <= TimeSpan.Zero)
{ {
@@ -42,9 +42,8 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
UseMask = fileLoggerOptions.UseMask; UseMask = fileLoggerOptions.UseMask;
} }
_batchSize = loggerOptions.BatchSize; _batchSize = loggerOptions.BatchSize > 0 ? loggerOptions.BatchSize : 100;
StartAsync();
StartAsync().ConfigureAwait(false);
} }
protected DateTimeOffset CurrentDateTimeOffset => UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow; protected DateTimeOffset CurrentDateTimeOffset => UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
@@ -70,73 +69,56 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
set => _loggerSettings = value; set => _loggerSettings = value;
} }
private SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1, 1);
public bool IsStarted { get; set; } public bool IsStarted { get; set; }
public bool UseMask { get; set; } public bool UseMask { get; set; }
public async void Dispose() private LoggerSettings _loggerSettings;
{
while (!_messageQueue.IsEmpty)
{
// Do nothing, just wait for the queue to be empty
await Task.Delay(1000);
}
_isDisposing = true;
await StopAsync().ConfigureAwait(false);
GC.SuppressFinalize(this);
}
public ILogger CreateLogger(string categoryName) public ILogger CreateLogger(string categoryName)
{ {
Category = categoryName;
return new BatchingLogger(this, categoryName, LoggerSettings); return new BatchingLogger(this, categoryName, LoggerSettings);
} }
protected abstract Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken token); protected abstract Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken token);
private async Task ProcessLogQueueAsync(object state)
{
while (!_cancellationTokenSource.IsCancellationRequested)
{
var limit = _batchSize <= 0 ? int.MaxValue : _batchSize;
while (limit > 0 && _messageQueue.TryDequeue(out var message))
{
_currentBatch.Add(message);
limit--;
}
if (_currentBatch.Count > 0) private async Task ProcessLogQueueAsync()
{ {
var batchSize = _batchSize > 0 ? _batchSize : 100;
var batch = new List<LogMessage>(batchSize);
try try
{ {
if (_isDisposing) var token = _cancellationTokenSource.Token;
while (await _channel.Reader.WaitToReadAsync(token).ConfigureAwait(false))
{ {
return; batch.Clear();
while (batch.Count < batchSize && _channel.Reader.TryRead(out var message))
{
batch.Add(message);
} }
await _writeSemaphore.WaitAsync(); if (batch.Count > 0)
await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token).ConfigureAwait(false);
_currentBatch.Clear();
}
catch
{ {
// ignored await WriteMessagesAsync(batch, token).ConfigureAwait(false);
} }
finally }
}
catch (OperationCanceledException)
{ {
_writeSemaphore.Release(); // normal shutdown
}
catch (Exception)
{
throw;
} }
} }
await Task.Delay(500);
}
}
protected async Task WriteStartMessage() protected async Task WriteStartMessage()
{ {
var message = LogHelper.GetStartupMessage(); var message = LogHelper.GetStartupMessage();
await WriteMessagesAsync(new List<LogMessage> { CreateLoggerMessage(message, CurrentDateTimeOffset) }, _cancellationTokenSource.Token).ConfigureAwait(false); var token = _cancellationTokenSource?.Token ?? CancellationToken.None;
await WriteMessagesAsync(new List<LogMessage> { CreateLoggerMessage(message, CurrentDateTimeOffset) }, token).ConfigureAwait(false);
} }
private LogMessage CreateLoggerMessage(string message, DateTimeOffset currentDateTimeOffset) private LogMessage CreateLoggerMessage(string message, DateTimeOffset currentDateTimeOffset)
@@ -320,46 +302,93 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
internal string AddMessage(DateTimeOffset timestamp, string message) internal string AddMessage(DateTimeOffset timestamp, string message)
{ {
var result = CreateLoggerMessage(message, timestamp); var result = CreateLoggerMessage(message, timestamp);
_messageQueue.Enqueue(result); if (!_channel.Writer.TryWrite(result))
{
_ = _channel.Writer.WriteAsync(result);
}
return result.Message; return result.Message;
} }
private Task StartAsync() private void StartAsync()
{ {
IsStarted = true; if (_cancellationTokenSource != null)
_messageQueue = new ConcurrentQueue<LogMessage>(); {
_cancellationTokenSource = new CancellationTokenSource(); return;
}
_outputTask = Task.Factory.StartNew( _cancellationTokenSource = new CancellationTokenSource();
ProcessLogQueueAsync, _outputTask = Task.Run(ProcessLogQueueAsync);
null, IsStarted = true;
TaskCreationOptions.LongRunning);
return Task.CompletedTask;
} }
private async Task StopAsync() private async Task StopAsync()
{ {
_cancellationTokenSource.Cancel(); if (_cancellationTokenSource == null)
{
return;
}
try try
{ {
while (_outputTask.Status != TaskStatus.RanToCompletion && _outputTask.Status != TaskStatus.Canceled) _cancellationTokenSource.Cancel();
_channel.Writer.Complete();
if (_outputTask != null)
{ {
await _outputTask.ConfigureAwait(false); await _outputTask.ConfigureAwait(false);
await Task.Delay(100);
} }
} }
catch (TaskCanceledException) catch (OperationCanceledException)
{ {
// expected on shutdown
} }
catch (AggregateException exception) when (exception.InnerExceptions.Count == 1 && catch (AggregateException exception) when (exception.InnerExceptions.Count == 1 && exception.InnerExceptions[0] is TaskCanceledException)
exception.InnerExceptions[0] is TaskCanceledException)
{ {
// Do nothing
}
finally
{
try
{
_cancellationTokenSource.Dispose();
}
catch
{
// Do nothing
}
_cancellationTokenSource = null;
_outputTask = null;
IsStarted = false;
} }
} }
~BatchingLoggerProvider() public void Dispose()
{ {
Dispose(); Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
if (disposing)
{
try
{
StopAsync().GetAwaiter().GetResult();
}
catch
{
// Do nothing
}
}
} }
} }

View File

@@ -32,7 +32,7 @@ public static class ExceptionExtensions
var sb = new StringBuilderChill(); var sb = new StringBuilderChill();
sb.AppendLine(); sb.AppendLine();
sb.AppendLine($"--- Exception details provided by {DllInfo.ApplicationName} ---"); sb.AppendLine($"--- Exception details provided by {DllInfo.ApplicationName} on {Environment.MachineName} ---");
if (!string.IsNullOrEmpty(module)) if (!string.IsNullOrEmpty(module))
{ {
sb.AppendLine(" Module : " + module); sb.AppendLine(" Module : " + module);

View File

@@ -1,14 +1,16 @@
using System; using EonaCat.Json;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using EonaCat.Json;
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;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Net;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
// 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.
@@ -23,6 +25,34 @@ public class ErrorMessage
public static class LogHelper public static class LogHelper
{ {
public sealed class HeaderContext
{
public DateTime Timestamp { get; set; }
public string TimestampFormat { get; set; }
public string HostName { get; set; }
public string Category { get; set; }
public ELogType LogType { get; set; }
public LoggerSettings Settings { get; set; }
public string EnvironmentName { get; set; }
}
internal static Dictionary<string, Func<HeaderContext, string>> TokenResolvers =
new(StringComparer.OrdinalIgnoreCase)
{
["ts"] = ctx => ctx.Timestamp.ToString(ctx.TimestampFormat),
["host"] = ctx => ctx.HostName,
["machine"] = ctx => Environment.MachineName,
["category"] = ctx => ctx.Category,
["thread"] = ctx => Environment.CurrentManagedThreadId.ToString(),
["process"] = ctx => Process.GetCurrentProcess().ProcessName,
["pid"] = ctx => Process.GetCurrentProcess().Id.ToString(),
["sev"] = ctx => ctx.LogType.ToString(),
["user"] = ctx => Environment.UserName,
["env"] = ctx => ctx.EnvironmentName,
["newline"] = _ => Environment.NewLine
};
internal static event EventHandler<ErrorMessage> OnException; internal static event EventHandler<ErrorMessage> OnException;
internal static event EventHandler<ErrorMessage> OnLogLevelDisabled; internal static event EventHandler<ErrorMessage> OnLogLevelDisabled;
@@ -73,34 +103,71 @@ public static class LogHelper
return syslogMessage; return syslogMessage;
} }
private static string ResolveHeader(string format, HeaderContext ctx)
internal static string FormatMessageWithHeader(LoggerSettings settings, ELogType logType, string currentMessage,
DateTime dateTime, string category = null)
{ {
if (string.IsNullOrWhiteSpace(currentMessage)) if (string.IsNullOrWhiteSpace(format))
{ {
return currentMessage; return string.Empty;
} }
category ??= "General"; return Regex.Replace(format, @"\{([^}]+)\}", match =>
var sb = new StringBuilder(settings?.HeaderFormat);
var timestamp = dateTime.ToString(settings?.TimestampFormat ?? "yyyy-MM-dd HH:mm:ss");
var timeLabel = settings?.UseLocalTime ?? false ? "[LOCAL]" : "[UTC]";
sb.Replace("{ts}", $"{timestamp} {timeLabel}")
.Replace("{host}", $"[Host:{HostName}]")
.Replace("{category}", $"[Category:{category}]")
.Replace("{thread}", $"[ThreadId:{Environment.CurrentManagedThreadId}]")
.Replace("{sev}", $"[{logType}]");
if (sb.Length > 0)
{ {
sb.Append(" "); var token = match.Groups[1].Value;
var parts = token.Split(new[] { ':' }, 2);
var key = parts[0];
if (!TokenResolvers.TryGetValue(key, out var resolver))
{
return match.Value;
} }
sb.Append(currentMessage);
return sb.ToString(); if (key.Equals("ts", StringComparison.OrdinalIgnoreCase) && parts.Length == 2)
{
ctx.TimestampFormat = parts[1];
} }
return resolver(ctx);
});
}
internal static string FormatMessageWithHeader(
LoggerSettings settings,
ELogType logType,
string message,
DateTime dateTime,
string category = null)
{
if (string.IsNullOrWhiteSpace(message))
{
return message;
}
var ctx = new HeaderContext
{
Timestamp = dateTime,
TimestampFormat = settings?.TimestampFormat ?? "yyyy-MM-dd HH:mm:ss",
HostName = HostName,
Category = category ?? "General",
LogType = logType,
Settings = settings,
EnvironmentName = settings?.EnvironmentName
};
string header = settings?.CustomHeaderFormatter != null
? settings.CustomHeaderFormatter(ctx)
: ResolveHeader(settings?.HeaderFormat, ctx);
if (!string.IsNullOrEmpty(header))
{
header += " ";
}
return header + message;
}
internal static void SendToConsole(LoggerSettings settings, ELogType logType, string message) internal static void SendToConsole(LoggerSettings settings, ELogType logType, string message)
{ {
if (settings == null || string.IsNullOrWhiteSpace(message)) if (settings == null || string.IsNullOrWhiteSpace(message))

View File

@@ -1,11 +1,12 @@
using System; using EonaCat.Json.Linq;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using EonaCat.Json.Linq;
using EonaCat.Logger.EonaCatCoreLogger; using EonaCat.Logger.EonaCatCoreLogger;
using EonaCat.Logger.EonaCatCoreLogger.Models; using EonaCat.Logger.EonaCatCoreLogger.Models;
using EonaCat.Logger.Servers.GrayLog; using EonaCat.Logger.Servers.GrayLog;
using System;
using System.Collections.Generic;
using System.Net.Sockets;
using System.Text.RegularExpressions;
using static EonaCat.Logger.Managers.LogHelper;
namespace EonaCat.Logger.Managers; namespace EonaCat.Logger.Managers;
// 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.
@@ -60,6 +61,10 @@ public class LoggerSettings
} }
} }
public Func<HeaderContext, string> CustomHeaderFormatter { get; set; }
public string EnvironmentName { get; set; }
/// <summary> /// <summary>
/// Timestamp format. /// Timestamp format.
/// </summary> /// </summary>

View File

@@ -38,6 +38,16 @@ public class Logger
}, },
}; };
LoggerSettings.CustomHeaderFormatter = ctx =>
{
if (ctx.LogType == ELogType.Error)
{
return $"{ctx.Timestamp:HH:mm:ss} [{ctx.LogType}]";
}
return $"{ctx.Timestamp:HH:mm:ss} [{ctx.LogType}]";
};
_logManager = new LogManager(LoggerSettings); _logManager = new LogManager(LoggerSettings);
_logManager.Settings.TypesToLog.Clear(); _logManager.Settings.TypesToLog.Clear();
_logManager.Settings.LogInfo(); _logManager.Settings.LogInfo();