563 lines
20 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|