Files
EonaCat.LogStack/EonaCat.LogStack/EonaCatLogger.cs
T
EonaCatandJeroen Saey dae624fd45 Added more stats
Added more dependency injection
Made README.md better
2026-06-11 08:17:29 +02:00

563 lines
20 KiB
C#

using EonaCat.LogStack.Analytics;
using EonaCat.LogStack.Boosters;
using EonaCat.LogStack.Core;
using EonaCat.LogStack.Flows;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
// 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.
namespace EonaCat.LogStack
{
/// <summary>
/// EonaCat logger with flow-based architecture, booster, and pre-build modifier hook.
/// Designed for zero-allocation logging paths and superior memory efficiency.
/// </summary>
public sealed class EonaCatLogStack : IAsyncDisposable
{
private readonly string _category;
private readonly List<IFlow> _flows = new List<IFlow>();
private readonly List<IBooster> _boosters = new List<IBooster>();
private readonly ConcurrentBag<IFlow> _concurrentFlows = new ConcurrentBag<IFlow>();
private readonly LogLevel _minimumLevel;
private readonly TimestampMode _timestampMode;
private volatile bool _isDisposed;
private long _totalLoggedCount;
private long _totalDroppedCount;
private long _totalExceptionsCount;
private long _totalByteCount;
private readonly Stopwatch _startTime = Stopwatch.StartNew();
// Per-level counters
private long _traceCount;
private long _debugCount;
private long _informationCount;
private long _warningCount;
private long _errorCount;
private long _criticalCount;
// Metrics tracking
private readonly LoggingMetrics _metrics = new LoggingMetrics();
private readonly ConcurrentDictionary<string, PerFlowStatistics> _flowStats = new();
private readonly List<ActionRef<LogEventBuilder>> _modifiers = new List<ActionRef<LogEventBuilder>>();
public delegate void ActionRef<T>(ref T item);
private readonly object _modifiersLock = new object();
// Channel-based async pipeline
private Channel<LogEvent>? _asyncChannel;
private Task? _asyncConsumer;
private CancellationTokenSource? _asyncCts;
// Dynamic level controller
private volatile DynamicLevelController? _dynamicLevel;
public event EventHandler<LogMessage> OnLog;
/// <summary>
/// Creates a new logger instance
/// </summary>
public EonaCatLogStack(string category = "Application",
LogLevel minimumLevel = LogLevel.Trace,
TimestampMode timestampMode = TimestampMode.Utc)
{
_category = category ?? throw new ArgumentNullException(nameof(category));
_minimumLevel = minimumLevel;
_timestampMode = timestampMode;
}
/// <summary>
/// Enables a Channel-based async dispatch pipeline for zero-blocking logging.
/// Events are enqueued and consumed by a dedicated background Task.
/// </summary>
/// <param name="capacity">Bounded channel capacity (0 = unbounded).</param>
public EonaCatLogStack UseAsyncPipeline(int capacity = 65536)
{
if (_asyncChannel != null)
{
return this;
}
_asyncCts = new CancellationTokenSource();
_asyncChannel = capacity > 0
? Channel.CreateBounded<LogEvent>(new BoundedChannelOptions(capacity)
{
FullMode = BoundedChannelFullMode.DropOldest,
SingleReader = true,
AllowSynchronousContinuations = false
})
: Channel.CreateUnbounded<LogEvent>(new UnboundedChannelOptions
{
SingleReader = true,
AllowSynchronousContinuations = false
});
_asyncConsumer = Task.Run(() => ConsumeChannelAsync(_asyncCts.Token));
return this;
}
/// <summary>
/// Attaches a <see cref="DynamicLevelController"/> so the minimum level
/// can be changed at runtime without restarting the application.
/// </summary>
public EonaCatLogStack UseDynamicLevel(DynamicLevelController controller)
{
_dynamicLevel = controller ?? throw new ArgumentNullException(nameof(controller));
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private LogLevel EffectiveMinLevel() =>
_dynamicLevel != null ? _dynamicLevel.CurrentLevel : _minimumLevel;
/// <summary>
/// Adds a flow (output destination) to this logger
/// </summary>
public EonaCatLogStack AddFlow(IFlow flow)
{
if (flow == null)
{
throw new ArgumentNullException(nameof(flow));
}
lock (_flows) { _flows.Add(flow); }
_concurrentFlows.Add(flow);
return this;
}
/// <summary>
/// Adds a booster to this logger
/// </summary>
public EonaCatLogStack AddBooster(IBooster booster)
{
if (booster == null)
{
throw new ArgumentNullException(nameof(booster));
}
lock (_boosters) { _boosters.Add(booster); }
return this;
}
/// <summary>
/// Removes a flow by name
/// </summary>
public EonaCatLogStack RemoveFlow(string name)
{
lock (_flows) { _flows.RemoveAll(f => f.Name == name); }
return this;
}
/// <summary>
/// Removes a booster by name
/// </summary>
public EonaCatLogStack RemoveBooster(string name)
{
lock (_boosters) { _boosters.RemoveAll(b => b.Name == name); }
return this;
}
/// <summary>
/// Adds a modifier to run before building the LogEvent.
/// Return false to cancel logging.
/// </summary>
public EonaCatLogStack AddModifier(ActionRef<LogEventBuilder> modifier)
{
if (modifier == null)
{
throw new ArgumentNullException(nameof(modifier));
}
lock (_modifiersLock) { _modifiers.Add(modifier); }
return this;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(string message, LogLevel level = LogLevel.Information)
{
if (_isDisposed || level < EffectiveMinLevel())
{
return;
}
TrackLevel(level);
var builder = new LogEventBuilder()
.WithLevel(level)
.WithCategory(_category)
.WithMessage(message)
.WithTimestamp(GetTimestamp());
ProcessLogEvent(ref builder);
}
/// <summary>
/// Logs using a structured message template, binding named holes to the provided args.
/// E.g. <c>LogTemplate(LogLevel.Information, "User {UserId} logged in from {Ip}", userId, ip)</c>
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogTemplate(LogLevel level, string template, params object?[] args)
{
if (_isDisposed || level < EffectiveMinLevel())
{
return;
}
var parsed = MessageTemplate.FromCache(template);
var builder = new LogEventBuilder()
.WithLevel(level)
.WithCategory(_category)
.WithTimestamp(GetTimestamp());
var rendered = parsed.RenderInto(args, builder);
builder.WithMessage(rendered);
ProcessLogEvent(ref builder);
}
/// <summary>
/// Logs using a structured message template with an exception.
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void LogTemplate(LogLevel level, Exception exception, string template, params object?[] args)
{
if (_isDisposed || level < EffectiveMinLevel())
{
return;
}
var parsed = MessageTemplate.FromCache(template);
var builder = new LogEventBuilder()
.WithLevel(level)
.WithCategory(_category)
.WithException(exception)
.WithTimestamp(GetTimestamp());
var rendered = parsed.RenderInto(args, builder);
builder.WithMessage(rendered);
ProcessLogEvent(ref builder);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(LogLevel level, Exception exception, string message)
{
if (_isDisposed || level < EffectiveMinLevel())
{
return;
}
TrackLevel(level);
if (exception != null)
{
Interlocked.Increment(ref _totalExceptionsCount);
}
var builder = new LogEventBuilder()
.WithLevel(level)
.WithCategory(_category)
.WithMessage(message)
.WithException(exception)
.WithTimestamp(GetTimestamp());
ProcessLogEvent(ref builder);
OnLog?.Invoke(this, new LogMessage
{
Level = level,
Exception = exception,
Message = message,
Category = _category,
Origin = null
});
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void TrackLevel(LogLevel level)
{
switch (level)
{
case LogLevel.Trace:
Interlocked.Increment(ref _traceCount);
break;
case LogLevel.Debug:
Interlocked.Increment(ref _debugCount);
break;
case LogLevel.Information:
Interlocked.Increment(ref _informationCount);
break;
case LogLevel.Warning:
Interlocked.Increment(ref _warningCount);
break;
case LogLevel.Error:
Interlocked.Increment(ref _errorCount);
break;
case LogLevel.Critical:
Interlocked.Increment(ref _criticalCount);
break;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(LogLevel level, string message, params (string Key, object Value)[] properties)
{
if (_isDisposed || level < EffectiveMinLevel())
{
return;
}
TrackLevel(level);
var builder = new LogEventBuilder()
.WithLevel(level)
.WithCategory(_category)
.WithMessage(message)
.WithTimestamp(GetTimestamp());
foreach (var (key, value) in properties)
{
builder.WithProperty(key, value);
}
ProcessLogEvent(ref builder);
}
private void Write(LogLevel level, string template, params object[] args)
{
Log(level, string.Format(template, args));
}
private void Write(LogLevel level, Exception ex, string template, params object[] args)
{
Log(level, ex, string.Format(template, args));
}
public void Trace(string template, params object[] args) => Write(LogLevel.Trace, template, args);
public void Debug(string template, params object[] args) => Write(LogLevel.Debug, template, args);
public void Information(string template, params object[] args) => Write(LogLevel.Information, template, args);
public void Warning(string template, params object[] args) => Write(LogLevel.Warning, template, args);
public void Warning(Exception ex, string template, params object[] args) => Write(LogLevel.Warning, ex, template, args);
public void Error(string template, params object[] args) => Write(LogLevel.Error, template, args);
public void Error(Exception ex, string template, params object[] args) => Write(LogLevel.Error, ex, template, args);
public void Critical(string template, params object[] args) => Write(LogLevel.Critical, template, args);
public void Critical(Exception ex, string template, params object[] args) => Write(LogLevel.Critical, ex, template, args);
public void LogTrace(string template, params object[] args) => Write(LogLevel.Trace, template, args);
public void LogDebug(string template, params object[] args) => Write(LogLevel.Debug, template, args);
public void LogInformation(string template, params object[] args) => Write(LogLevel.Information, template, args);
public void LogWarning(string template, params object[] args) => Write(LogLevel.Warning, template, args);
public void LogWarning(Exception ex, string template, params object[] args) => Write(LogLevel.Warning, ex, template, args);
public void LogError(string template, params object[] args) => Write(LogLevel.Error, template, args);
public void LogError(Exception ex, string template, params object[] args) => Write(LogLevel.Error, ex, template, args);
public void LogCritical(string template, params object[] args) => Write(LogLevel.Critical, template, args);
public void LogCritical(Exception ex, string template, params object[] args) => Write(LogLevel.Critical, ex, template, args);
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessLogEvent(ref LogEventBuilder builder)
{
// Apply boosters
lock (_boosters)
{
foreach (var booster in _boosters)
{
try
{
if (!booster.Boost(ref builder))
{
return; // filtered out
}
}
catch { }
}
}
// Apply modifiers
foreach (var mod in _modifiers)
{
try
{
mod(ref builder);
}
catch { }
}
var logEvent = builder.Build();
Interlocked.Increment(ref _totalLoggedCount);
// Async channel pipeline
if (_asyncChannel != null)
{
if (!_asyncChannel.Writer.TryWrite(logEvent))
{
Interlocked.Increment(ref _totalDroppedCount);
}
return;
}
// Synchronous blast to flows
DispatchToFlows(logEvent);
}
private void DispatchToFlows(LogEvent logEvent)
{
foreach (var flow in _concurrentFlows)
{
try
{
var result = flow.BlastAsync(logEvent).GetAwaiter().GetResult();
if (result == WriteResult.Dropped)
{
Interlocked.Increment(ref _totalDroppedCount);
}
}
catch { }
}
}
private async Task ConsumeChannelAsync(CancellationToken cancellationToken)
{
var reader = _asyncChannel!.Reader;
try
{
await foreach (var logEvent in reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
var flows = _concurrentFlows;
foreach (var flow in flows)
{
try
{
var result = await flow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
if (result == WriteResult.Dropped)
{
Interlocked.Increment(ref _totalDroppedCount);
}
}
catch { }
}
}
}
catch (OperationCanceledException) { }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private long GetTimestamp()
{
switch (_timestampMode)
{
case TimestampMode.Local: return DateTime.Now.Ticks;
case TimestampMode.HighPrecision: return System.Diagnostics.Stopwatch.GetTimestamp();
default: return DateTime.UtcNow.Ticks;
}
}
public async Task FlushAsync(CancellationToken cancellationToken = default)
{
var tasks = _concurrentFlows.Select(f => f.FlushAsync(cancellationToken));
await Task.WhenAll(tasks).ConfigureAwait(false);
}
public LoggerDiagnostics GetDiagnostics()
{
var flowDiagnostics = _concurrentFlows
.Select(f => f is FlowBase fb ? fb.GetDiagnostics() : null)
.Where(d => d != null)
.ToList();
return new LoggerDiagnostics
{
Category = _category,
MinimumLevel = _minimumLevel,
TotalLogged = Interlocked.Read(ref _totalLoggedCount),
TotalDropped = Interlocked.Read(ref _totalDroppedCount),
TotalExceptions = Interlocked.Read(ref _totalExceptionsCount),
FlowCount = _flows.Count,
BoosterCount = _boosters.Count,
Flows = flowDiagnostics
};
}
/// <summary>
/// Gets detailed metrics about the logger
/// </summary>
public LoggerMetrics GetMetrics()
{
var elapsedMs = _startTime.ElapsedMilliseconds;
var writesPerSecond = elapsedMs > 0 ? (_totalLoggedCount * 1000.0) / elapsedMs : 0;
return new LoggerMetrics
{
TotalLogged = Interlocked.Read(ref _totalLoggedCount),
TotalDropped = Interlocked.Read(ref _totalDroppedCount),
TotalExceptions = Interlocked.Read(ref _totalExceptionsCount),
TotalBytes = Interlocked.Read(ref _totalByteCount),
WritesPerSecond = writesPerSecond,
TraceCount = Interlocked.Read(ref _traceCount),
DebugCount = Interlocked.Read(ref _debugCount),
InformationCount = Interlocked.Read(ref _informationCount),
WarningCount = Interlocked.Read(ref _warningCount),
ErrorCount = Interlocked.Read(ref _errorCount),
CriticalCount = Interlocked.Read(ref _criticalCount),
UptimeMilliseconds = elapsedMs,
FlowMetrics = _flowStats.Values.Select(s => s.GetSnapshot()).ToList()
};
}
/// <summary>
/// Gets the metrics collector for analytics
/// </summary>
public LoggingMetrics GetMetricsCollector() => _metrics;
public async ValueTask DisposeAsync()
{
if (_isDisposed)
{
return;
}
_isDisposed = true;
// Drain and stop the async channel pipeline if active
if (_asyncChannel != null)
{
_asyncChannel.Writer.TryComplete();
_asyncCts?.Cancel();
try { if (_asyncConsumer != null) { await _asyncConsumer.ConfigureAwait(false); } } catch { }
}
await FlushAsync().ConfigureAwait(false);
var disposeTasks = _concurrentFlows.Select(f => f.DisposeAsync().AsTask());
await Task.WhenAll(disposeTasks).ConfigureAwait(false);
_asyncCts?.Dispose();
GC.SuppressFinalize(this);
}
}
}