Added more stats
Added more dependency injection Made README.md better
This commit is contained in:
@@ -119,7 +119,9 @@ public class SyslogUdpService : BackgroundService
|
||||
try
|
||||
{
|
||||
if (IsJson(rawMessage))
|
||||
{
|
||||
return ParseJson(rawMessage, remoteIp);
|
||||
}
|
||||
|
||||
return ParseSyslogAdvanced(rawMessage, remoteIp);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
using EonaCat.LogStack.Analytics;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace EonaCat.LogStack;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Advanced metrics collector that provides comprehensive analytics across loggers
|
||||
/// </summary>
|
||||
public sealed class AdvancedMetricsCollector
|
||||
{
|
||||
private readonly List<EonaCatLogStack> _loggers = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a logger for metrics collection
|
||||
/// </summary>
|
||||
public void RegisterLogger(EonaCatLogStack logger)
|
||||
{
|
||||
if (logger != null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_loggers.Contains(logger))
|
||||
{
|
||||
_loggers.Add(logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a logger
|
||||
/// </summary>
|
||||
public void UnregisterLogger(EonaCatLogStack logger)
|
||||
{
|
||||
if (logger != null)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_loggers.Remove(logger);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets aggregated metrics across all registered loggers
|
||||
/// </summary>
|
||||
public AggregatedMetrics GetAggregatedMetrics()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var aggregated = new AggregatedMetrics
|
||||
{
|
||||
Timestamp = DateTime.UtcNow,
|
||||
LoggerMetrics = new List<LoggerMetrics>()
|
||||
};
|
||||
|
||||
foreach (var logger in _loggers)
|
||||
{
|
||||
try
|
||||
{
|
||||
var metrics = logger.GetMetrics();
|
||||
aggregated.LoggerMetrics.Add(metrics);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// Aggregate totals
|
||||
aggregated.TotalLoggedAcrossAll = aggregated.LoggerMetrics.Sum(m => m.TotalLogged);
|
||||
aggregated.TotalDroppedAcrossAll = aggregated.LoggerMetrics.Sum(m => m.TotalDropped);
|
||||
aggregated.TotalExceptionsAcrossAll = aggregated.LoggerMetrics.Sum(m => m.TotalExceptions);
|
||||
aggregated.TotalBytesAcrossAll = aggregated.LoggerMetrics.Sum(m => m.TotalBytes);
|
||||
aggregated.ActiveLoggers = _loggers.Count;
|
||||
|
||||
return aggregated;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a performance comparison across loggers
|
||||
/// </summary>
|
||||
public PerformanceComparison GetPerformanceComparison()
|
||||
{
|
||||
var aggregated = GetAggregatedMetrics();
|
||||
|
||||
return new PerformanceComparison
|
||||
{
|
||||
Timestamp = aggregated.Timestamp,
|
||||
TotalLoggers = aggregated.ActiveLoggers,
|
||||
HighestThroughputLogger = aggregated.LoggerMetrics.OrderByDescending(m => m.WritesPerSecond).FirstOrDefault(),
|
||||
LowestSuccessRateLogger = aggregated.LoggerMetrics.OrderBy(m => m.SuccessRate).FirstOrDefault(),
|
||||
AverageBytesPerEvent = aggregated.LoggerMetrics.Count > 0
|
||||
? aggregated.LoggerMetrics.Average(m => m.AverageBytesPerEvent)
|
||||
: 0,
|
||||
AverageWritesPerSecond = aggregated.LoggerMetrics.Count > 0
|
||||
? aggregated.LoggerMetrics.Average(m => m.WritesPerSecond)
|
||||
: 0
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a health report
|
||||
/// </summary>
|
||||
public HealthReport GetHealthReport()
|
||||
{
|
||||
var aggregated = GetAggregatedMetrics();
|
||||
|
||||
var report = new HealthReport
|
||||
{
|
||||
Timestamp = aggregated.Timestamp,
|
||||
OverallStatus = "Healthy",
|
||||
Warnings = new List<string>(),
|
||||
Errors = new List<string>()
|
||||
};
|
||||
|
||||
foreach (var logger in aggregated.LoggerMetrics)
|
||||
{
|
||||
if (logger.SuccessRate < 95)
|
||||
{
|
||||
report.Warnings.Add($"{logger.GetType().Name}: Low success rate ({logger.SuccessRate:F1}%)");
|
||||
}
|
||||
|
||||
if (logger.TotalExceptions > logger.TotalLogged * 0.05) // More than 5% exceptions
|
||||
{
|
||||
report.Warnings.Add($"Logger: High exception rate ({logger.TotalExceptions} exceptions)");
|
||||
}
|
||||
}
|
||||
|
||||
if (report.Warnings.Count > 0)
|
||||
{
|
||||
report.OverallStatus = "Warning";
|
||||
}
|
||||
|
||||
if (report.Errors.Count > 0)
|
||||
{
|
||||
report.OverallStatus = "Error";
|
||||
}
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all metrics
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_loggers.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Aggregated metrics across multiple loggers
|
||||
/// </summary>
|
||||
public sealed class AggregatedMetrics
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public List<LoggerMetrics> LoggerMetrics { get; set; } = new();
|
||||
public long TotalLoggedAcrossAll { get; set; }
|
||||
public long TotalDroppedAcrossAll { get; set; }
|
||||
public long TotalExceptionsAcrossAll { get; set; }
|
||||
public long TotalBytesAcrossAll { get; set; }
|
||||
public int ActiveLoggers { get; set; }
|
||||
|
||||
public double OverallSuccessRate
|
||||
{
|
||||
get
|
||||
{
|
||||
var total = TotalLoggedAcrossAll + TotalDroppedAcrossAll;
|
||||
return total > 0 ? (TotalLoggedAcrossAll * 100.0) / total : 100;
|
||||
}
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("===== AGGREGATED METRICS =====");
|
||||
sb.AppendLine($"Timestamp: {Timestamp:O}");
|
||||
sb.AppendLine($"Active Loggers: {ActiveLoggers}");
|
||||
sb.AppendLine($"Total Logged: {TotalLoggedAcrossAll:N0}");
|
||||
sb.AppendLine($"Total Dropped: {TotalDroppedAcrossAll:N0}");
|
||||
sb.AppendLine($"Overall Success Rate: {OverallSuccessRate:F2}%");
|
||||
sb.AppendLine($"Total Exceptions: {TotalExceptionsAcrossAll:N0}");
|
||||
sb.AppendLine($"Total Bytes: {TotalBytesAcrossAll:N0}");
|
||||
sb.AppendLine("==============================");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performance comparison across loggers
|
||||
/// </summary>
|
||||
public sealed class PerformanceComparison
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public int TotalLoggers { get; set; }
|
||||
public LoggerMetrics HighestThroughputLogger { get; set; }
|
||||
public LoggerMetrics LowestSuccessRateLogger { get; set; }
|
||||
public double AverageBytesPerEvent { get; set; }
|
||||
public double AverageWritesPerSecond { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("===== PERFORMANCE COMPARISON =====");
|
||||
sb.AppendLine($"Timestamp: {Timestamp:O}");
|
||||
sb.AppendLine($"Total Loggers: {TotalLoggers}");
|
||||
sb.AppendLine($"Highest Throughput: {HighestThroughputLogger?.WritesPerSecond:F2} events/sec");
|
||||
sb.AppendLine($"Lowest Success Rate: {LowestSuccessRateLogger?.SuccessRate:F2}%");
|
||||
sb.AppendLine($"Average Bytes/Event: {AverageBytesPerEvent:F2}");
|
||||
sb.AppendLine($"Average Writes/Second: {AverageWritesPerSecond:F2}");
|
||||
sb.AppendLine("==================================");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Health report for logging system
|
||||
/// </summary>
|
||||
public sealed class HealthReport
|
||||
{
|
||||
public DateTime Timestamp { get; set; }
|
||||
public string OverallStatus { get; set; } = "Unknown";
|
||||
public List<string> Warnings { get; set; } = new();
|
||||
public List<string> Errors { get; set; } = new();
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("===== HEALTH REPORT =====");
|
||||
sb.AppendLine($"Timestamp: {Timestamp:O}");
|
||||
sb.AppendLine($"Status: {OverallStatus}");
|
||||
|
||||
if (Warnings.Count > 0)
|
||||
{
|
||||
sb.AppendLine("\nWarnings:");
|
||||
foreach (var warning in Warnings)
|
||||
{
|
||||
sb.AppendLine($" - {warning}");
|
||||
}
|
||||
}
|
||||
|
||||
if (Errors.Count > 0)
|
||||
{
|
||||
sb.AppendLine("\nErrors:");
|
||||
foreach (var error in Errors)
|
||||
{
|
||||
sb.AppendLine($" - {error}");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("========================");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
||||
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
|
||||
<PackageIconUrl />
|
||||
<FileVersion>0.0.8</FileVersion>
|
||||
<FileVersion>0.0.9</FileVersion>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
@@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<EVRevisionFormat>0.0.8+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||
<EVRevisionFormat>0.0.9+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||
<EVDefault>true</EVDefault>
|
||||
<EVInfo>true</EVInfo>
|
||||
<EVTagMatch>v[0-9]*</EVTagMatch>
|
||||
@@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>0.0.8</Version>
|
||||
<Version>0.0.9</Version>
|
||||
<PackageId>EonaCat.LogStack</PackageId>
|
||||
<Product>EonaCat.LogStack</Product>
|
||||
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl>
|
||||
@@ -50,6 +50,12 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
||||
</PropertyGroup>
|
||||
</Target>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Remove="Examples\**" />
|
||||
<EmbeddedResource Remove="Examples\**" />
|
||||
<None Remove="Examples\**" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Remove="icon.png" />
|
||||
<None Include="..\LICENSE">
|
||||
@@ -91,4 +97,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
||||
<PackagePath>\</PackagePath>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="EonaCatLoggerCore\Examples\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,12 +1,15 @@
|
||||
using EonaCat.LogStack.Boosters;
|
||||
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.
|
||||
@@ -30,12 +33,35 @@ namespace EonaCat.LogStack
|
||||
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>
|
||||
@@ -50,6 +76,50 @@ namespace EonaCat.LogStack
|
||||
_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>
|
||||
@@ -115,11 +185,12 @@ namespace EonaCat.LogStack
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public void Log(string message, LogLevel level = LogLevel.Information)
|
||||
{
|
||||
if (_isDisposed || level < _minimumLevel)
|
||||
if (_isDisposed || level < EffectiveMinLevel())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TrackLevel(level);
|
||||
var builder = new LogEventBuilder()
|
||||
.WithLevel(level)
|
||||
.WithCategory(_category)
|
||||
@@ -129,14 +200,68 @@ namespace EonaCat.LogStack
|
||||
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 < _minimumLevel)
|
||||
if (_isDisposed || level < EffectiveMinLevel())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TrackLevel(level);
|
||||
if (exception != null)
|
||||
{
|
||||
Interlocked.Increment(ref _totalExceptionsCount);
|
||||
}
|
||||
|
||||
var builder = new LogEventBuilder()
|
||||
.WithLevel(level)
|
||||
.WithCategory(_category)
|
||||
@@ -156,14 +281,41 @@ namespace EonaCat.LogStack
|
||||
});
|
||||
}
|
||||
|
||||
[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 < _minimumLevel)
|
||||
if (_isDisposed || level < EffectiveMinLevel())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
TrackLevel(level);
|
||||
var builder = new LogEventBuilder()
|
||||
.WithLevel(level)
|
||||
.WithCategory(_category)
|
||||
@@ -256,7 +408,23 @@ namespace EonaCat.LogStack
|
||||
var logEvent = builder.Build();
|
||||
Interlocked.Increment(ref _totalLoggedCount);
|
||||
|
||||
// Blast to flows
|
||||
// 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
|
||||
@@ -271,6 +439,31 @@ namespace EonaCat.LogStack
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
{
|
||||
@@ -301,12 +494,45 @@ namespace EonaCat.LogStack
|
||||
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)
|
||||
@@ -316,11 +542,20 @@ namespace EonaCat.LogStack
|
||||
|
||||
_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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,340 @@
|
||||
using EonaCat.LogStack.Boosters;
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.LogStack.Logging;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Advanced configuration for category-specific logger settings
|
||||
/// </summary>
|
||||
public class CategoryConfig
|
||||
{
|
||||
public string CategoryName { get; set; } = "";
|
||||
public LogLevel? MinimumLevel { get; set; }
|
||||
public List<IBooster> Boosters { get; set; } = new();
|
||||
public Dictionary<string, object> Properties { get; set; } = new();
|
||||
public Func<LogEventBuilder, bool>? Filter { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enhanced LoggerFactory with category configuration, dynamic levels, and context propagation
|
||||
/// </summary>
|
||||
public sealed class AdvancedLoggerFactory : ILoggerFactory
|
||||
{
|
||||
private readonly EonaCatLogStack _logStack;
|
||||
private readonly ConcurrentDictionary<string, Logger> _loggers;
|
||||
private readonly ConcurrentDictionary<string, CategoryConfig> _categoryConfigs;
|
||||
private readonly AsyncLocal<Dictionary<string, object>> _contextData;
|
||||
private volatile bool _isDisposed;
|
||||
|
||||
public AdvancedLoggerFactory(
|
||||
LogLevel minimumLevel = LogLevel.Trace,
|
||||
TimestampMode timestampMode = TimestampMode.Utc)
|
||||
{
|
||||
_logStack = new EonaCatLogStack(
|
||||
category: "AdvancedFactory",
|
||||
minimumLevel: minimumLevel,
|
||||
timestampMode: timestampMode);
|
||||
_loggers = new ConcurrentDictionary<string, Logger>();
|
||||
_categoryConfigs = new ConcurrentDictionary<string, CategoryConfig>();
|
||||
_contextData = new AsyncLocal<Dictionary<string, object>>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Configures settings for a specific category
|
||||
/// </summary>
|
||||
public AdvancedLoggerFactory ConfigureCategory(string categoryName, Action<CategoryConfig> configure)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(AdvancedLoggerFactory));
|
||||
}
|
||||
|
||||
var config = _categoryConfigs.GetOrAdd(categoryName, _ => new CategoryConfig { CategoryName = categoryName });
|
||||
configure(config);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates or retrieves a logger for the specified category
|
||||
/// </summary>
|
||||
public ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(AdvancedLoggerFactory));
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(categoryName))
|
||||
{
|
||||
categoryName = "Default";
|
||||
}
|
||||
|
||||
return _loggers.GetOrAdd(categoryName, name => new Logger(name, _logStack, _categoryConfigs));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the underlying EonaCatLogStack instance
|
||||
/// </summary>
|
||||
public EonaCatLogStack GetLogStack()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(AdvancedLoggerFactory));
|
||||
}
|
||||
|
||||
return _logStack;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets context data that will be included in all logs within this async context
|
||||
/// </summary>
|
||||
public void SetContextData(string key, object value)
|
||||
{
|
||||
var ctx = _contextData.Value ??= new Dictionary<string, object>();
|
||||
ctx[key] = value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets context data for the current async context
|
||||
/// </summary>
|
||||
public object? GetContextData(string key)
|
||||
{
|
||||
var ctx = _contextData.Value;
|
||||
if (ctx == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
ctx.TryGetValue(key, out var value);
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all context data for the current async context
|
||||
/// </summary>
|
||||
public void ClearContextData()
|
||||
{
|
||||
_contextData.Value?.Clear();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Dynamically changes the minimum log level at runtime
|
||||
/// </summary>
|
||||
public void SetDynamicLevel(LogLevel level, string? categoryName = null)
|
||||
{
|
||||
if (categoryName != null)
|
||||
{
|
||||
if (_categoryConfigs.TryGetValue(categoryName, out var config))
|
||||
{
|
||||
config.MinimumLevel = level;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Update all categories
|
||||
foreach (var config in _categoryConfigs.Values)
|
||||
{
|
||||
config.MinimumLevel = level;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Flushes all pending log events
|
||||
/// </summary>
|
||||
public async Task FlushAsync()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(AdvancedLoggerFactory));
|
||||
}
|
||||
|
||||
await _logStack.FlushAsync().ConfigureAwait(false);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets diagnostics information
|
||||
/// </summary>
|
||||
public LoggerDiagnostics GetDiagnostics()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(AdvancedLoggerFactory));
|
||||
}
|
||||
|
||||
var baseDiagnostics = _logStack.GetDiagnostics();
|
||||
return new LoggerDiagnostics
|
||||
{
|
||||
Category = "AdvancedFactory",
|
||||
MinimumLevel = baseDiagnostics.MinimumLevel,
|
||||
TotalLogged = baseDiagnostics.TotalLogged,
|
||||
TotalDropped = baseDiagnostics.TotalDropped,
|
||||
TotalExceptions = baseDiagnostics.TotalExceptions,
|
||||
FlowCount = baseDiagnostics.FlowCount,
|
||||
BoosterCount = baseDiagnostics.BoosterCount,
|
||||
Flows = baseDiagnostics.Flows
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets factory-specific diagnostics
|
||||
/// </summary>
|
||||
public FactoryDiagnostics GetFactoryDiagnostics()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(AdvancedLoggerFactory));
|
||||
}
|
||||
|
||||
return new FactoryDiagnostics
|
||||
{
|
||||
ActiveLoggers = _loggers.Count,
|
||||
ConfiguredCategories = _categoryConfigs.Count,
|
||||
TotalLogged = _logStack.GetDiagnostics().TotalLogged,
|
||||
TotalDropped = _logStack.GetDiagnostics().TotalDropped
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets detailed metrics
|
||||
/// </summary>
|
||||
public LoggerMetrics GetMetrics()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(AdvancedLoggerFactory));
|
||||
}
|
||||
|
||||
return _logStack.GetMetrics();
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
_loggers.Clear();
|
||||
_categoryConfigs.Clear();
|
||||
_contextData.Value?.Clear();
|
||||
await _logStack.DisposeAsync().ConfigureAwait(false);
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Internal logger adapter with category support
|
||||
/// </summary>
|
||||
private sealed class Logger : ILogger
|
||||
{
|
||||
private readonly string _category;
|
||||
private readonly EonaCatLogStack _logStack;
|
||||
private readonly ConcurrentDictionary<string, CategoryConfig> _configs;
|
||||
|
||||
public string Category => _category;
|
||||
|
||||
public Logger(string category, EonaCatLogStack logStack, ConcurrentDictionary<string, CategoryConfig> configs)
|
||||
{
|
||||
_category = category ?? "Default";
|
||||
_logStack = logStack;
|
||||
_configs = configs;
|
||||
}
|
||||
|
||||
private CategoryConfig? GetCategoryConfig()
|
||||
{
|
||||
_configs.TryGetValue(_category, out var config);
|
||||
return config;
|
||||
}
|
||||
|
||||
private bool ShouldLog(LogLevel level)
|
||||
{
|
||||
var config = GetCategoryConfig();
|
||||
if (config?.MinimumLevel.HasValue == true)
|
||||
{
|
||||
return level >= config.MinimumLevel;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public void Log(LogLevel level, string message)
|
||||
{
|
||||
if (!IsEnabled(level))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logStack.Log(message, level);
|
||||
}
|
||||
|
||||
public void Log(LogLevel level, Exception? exception, string message)
|
||||
{
|
||||
if (!IsEnabled(level))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_logStack.Log(level, exception, message);
|
||||
}
|
||||
|
||||
public void Log(LogLevel level, string format, params object[] args)
|
||||
{
|
||||
if (!IsEnabled(level))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var message = string.Format(format, args);
|
||||
_logStack.Log(message, level);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logStack.Log(format, level);
|
||||
}
|
||||
}
|
||||
|
||||
public void Log(LogLevel level, Exception? exception, string format, params object[] args)
|
||||
{
|
||||
if (!IsEnabled(level))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var message = string.Format(format, args);
|
||||
_logStack.Log(level, exception, message);
|
||||
}
|
||||
catch
|
||||
{
|
||||
_logStack.Log(level, exception, format);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsEnabled(LogLevel level)
|
||||
{
|
||||
return ShouldLog(level);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostics information from AdvancedLoggerFactory
|
||||
/// </summary>
|
||||
public class FactoryDiagnostics
|
||||
{
|
||||
public int ActiveLoggers { get; set; }
|
||||
public int ConfiguredCategories { get; set; }
|
||||
public long TotalLogged { get; set; }
|
||||
public long TotalDropped { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace EonaCat.LogStack.Analytics;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Tracks logging metrics and analytics with comprehensive per-level and per-category metrics
|
||||
/// </summary>
|
||||
public sealed class LoggingMetrics
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private long _totalEvents;
|
||||
private long _totalBytes;
|
||||
private long _totalExceptions;
|
||||
private long _droppedCount;
|
||||
private long _droppedExceptions;
|
||||
private readonly Dictionary<string, long> _levelCounts = new();
|
||||
private readonly Dictionary<string, long> _loggerCounts = new();
|
||||
private readonly Dictionary<string, long> _categoryCounts = new();
|
||||
private readonly List<long> _latencies = new();
|
||||
private long _minLatencyMs = long.MaxValue;
|
||||
private long _maxLatencyMs;
|
||||
private readonly Stopwatch _uptime = Stopwatch.StartNew();
|
||||
private double _p50LatencyMs;
|
||||
private double _p95LatencyMs;
|
||||
private double _p99LatencyMs;
|
||||
|
||||
/// <summary>
|
||||
/// Records a log event with comprehensive metrics
|
||||
/// </summary>
|
||||
public void RecordEvent(object logEvent, long latencyMs, long bytes)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_totalEvents++;
|
||||
_totalBytes += bytes;
|
||||
_latencies.Add(latencyMs);
|
||||
|
||||
if (latencyMs < _minLatencyMs)
|
||||
{
|
||||
_minLatencyMs = latencyMs;
|
||||
}
|
||||
|
||||
if (latencyMs > _maxLatencyMs)
|
||||
{
|
||||
_maxLatencyMs = latencyMs;
|
||||
}
|
||||
|
||||
// Keep only last 1000 latencies to avoid memory bloat
|
||||
if (_latencies.Count > 1000)
|
||||
{
|
||||
_latencies.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a log event with level information
|
||||
/// </summary>
|
||||
public void RecordEventWithLevel(string level, long latencyMs, long bytes)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_totalEvents++;
|
||||
_totalBytes += bytes;
|
||||
_latencies.Add(latencyMs);
|
||||
|
||||
if (latencyMs < _minLatencyMs)
|
||||
{
|
||||
_minLatencyMs = latencyMs;
|
||||
}
|
||||
|
||||
if (latencyMs > _maxLatencyMs)
|
||||
{
|
||||
_maxLatencyMs = latencyMs;
|
||||
}
|
||||
|
||||
// Track per-level counts
|
||||
if (!_levelCounts.ContainsKey(level))
|
||||
{
|
||||
_levelCounts[level] = 0;
|
||||
}
|
||||
|
||||
_levelCounts[level]++;
|
||||
|
||||
if (_latencies.Count > 1000)
|
||||
{
|
||||
_latencies.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an exception
|
||||
/// </summary>
|
||||
public void RecordException(long latencyMs = 0)
|
||||
{
|
||||
Interlocked.Increment(ref _totalExceptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records dropped events
|
||||
/// </summary>
|
||||
public void RecordDropped(long count = 1)
|
||||
{
|
||||
Interlocked.Add(ref _droppedCount, count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records dropped exceptions
|
||||
/// </summary>
|
||||
public void RecordDroppedException()
|
||||
{
|
||||
Interlocked.Increment(ref _droppedExceptions);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records logger usage
|
||||
/// </summary>
|
||||
public void RecordLoggerUsage(string loggerName)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_loggerCounts.ContainsKey(loggerName))
|
||||
{
|
||||
_loggerCounts[loggerName] = 0;
|
||||
}
|
||||
|
||||
_loggerCounts[loggerName]++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records category usage
|
||||
/// </summary>
|
||||
public void RecordCategoryUsage(string category)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_categoryCounts.ContainsKey(category))
|
||||
{
|
||||
_categoryCounts[category] = 0;
|
||||
}
|
||||
|
||||
_categoryCounts[category]++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets metrics snapshot
|
||||
/// </summary>
|
||||
public MetricsSnapshot GetSnapshot()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
double avgLatency = _latencies.Count > 0 ? _latencies.Average() : 0;
|
||||
double median = CalculateMedian(_latencies);
|
||||
CalculatePercentiles(_latencies, out var p50, out var p95, out var p99);
|
||||
_p50LatencyMs = p50;
|
||||
_p95LatencyMs = p95;
|
||||
_p99LatencyMs = p99;
|
||||
|
||||
var topLoggers = _loggerCounts
|
||||
.OrderByDescending(kvp => kvp.Value)
|
||||
.Take(10)
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
var topCategories = _categoryCounts
|
||||
.OrderByDescending(kvp => kvp.Value)
|
||||
.Take(10)
|
||||
.ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
|
||||
return new MetricsSnapshot
|
||||
{
|
||||
TotalEvents = _totalEvents,
|
||||
TotalBytes = _totalBytes,
|
||||
TotalExceptions = _totalExceptions,
|
||||
DroppedCount = _droppedCount,
|
||||
DroppedExceptions = _droppedExceptions,
|
||||
LevelCounts = new Dictionary<string, long>(_levelCounts),
|
||||
LoggerCounts = topLoggers,
|
||||
CategoryCounts = topCategories,
|
||||
MinLatencyMs = _minLatencyMs == long.MaxValue ? 0 : _minLatencyMs,
|
||||
MaxLatencyMs = _maxLatencyMs,
|
||||
AverageLatencyMs = avgLatency,
|
||||
MedianLatencyMs = median,
|
||||
P50LatencyMs = p50,
|
||||
P95LatencyMs = p95,
|
||||
P99LatencyMs = p99,
|
||||
UptimeMs = _uptime.ElapsedMilliseconds,
|
||||
EventsPerSecond = _uptime.ElapsedMilliseconds > 0
|
||||
? _totalEvents * 1000.0 / _uptime.ElapsedMilliseconds
|
||||
: 0,
|
||||
BytesPerSecond = _uptime.ElapsedMilliseconds > 0
|
||||
? _totalBytes * 1000.0 / _uptime.ElapsedMilliseconds
|
||||
: 0,
|
||||
AverageEventSizeBytes = _totalEvents > 0 ? _totalBytes / (double)_totalEvents : 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Resets all metrics
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_totalEvents = 0;
|
||||
_totalBytes = 0;
|
||||
_totalExceptions = 0;
|
||||
_droppedCount = 0;
|
||||
_droppedExceptions = 0;
|
||||
_latencies.Clear();
|
||||
_minLatencyMs = long.MaxValue;
|
||||
_maxLatencyMs = 0;
|
||||
_levelCounts.Clear();
|
||||
_loggerCounts.Clear();
|
||||
_categoryCounts.Clear();
|
||||
_uptime.Restart();
|
||||
}
|
||||
}
|
||||
|
||||
private double CalculateMedian(List<long> values)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var sorted = values.OrderBy(v => v).ToList();
|
||||
int mid = sorted.Count / 2;
|
||||
|
||||
return sorted.Count % 2 == 0
|
||||
? (sorted[mid - 1] + sorted[mid]) / 2.0
|
||||
: sorted[mid];
|
||||
}
|
||||
|
||||
private void CalculatePercentiles(List<long> values, out double p50, out double p95, out double p99)
|
||||
{
|
||||
if (values.Count == 0)
|
||||
{
|
||||
p50 = p95 = p99 = 0;
|
||||
return;
|
||||
}
|
||||
|
||||
var sorted = values.OrderBy(v => v).ToList();
|
||||
p50 = GetPercentile(sorted, 50);
|
||||
p95 = GetPercentile(sorted, 95);
|
||||
p99 = GetPercentile(sorted, 99);
|
||||
}
|
||||
|
||||
private double GetPercentile(List<long> sortedValues, int percentile)
|
||||
{
|
||||
if (sortedValues.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
int index = (int)((percentile / 100.0) * (sortedValues.Count - 1));
|
||||
index = Math.Max(0, Math.Min(index, sortedValues.Count - 1));
|
||||
return sortedValues[index];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of logging metrics at a point in time
|
||||
/// </summary>
|
||||
public sealed class MetricsSnapshot
|
||||
{
|
||||
public long TotalEvents { get; set; }
|
||||
public long TotalBytes { get; set; }
|
||||
public long TotalExceptions { get; set; }
|
||||
public long DroppedCount { get; set; }
|
||||
public long DroppedExceptions { get; set; }
|
||||
public Dictionary<string, long> LevelCounts { get; set; } = new();
|
||||
public Dictionary<string, long> LoggerCounts { get; set; } = new();
|
||||
public Dictionary<string, long> CategoryCounts { get; set; } = new();
|
||||
public long MinLatencyMs { get; set; }
|
||||
public long MaxLatencyMs { get; set; }
|
||||
public double AverageLatencyMs { get; set; }
|
||||
public double MedianLatencyMs { get; set; }
|
||||
public double P50LatencyMs { get; set; }
|
||||
public double P95LatencyMs { get; set; }
|
||||
public double P99LatencyMs { get; set; }
|
||||
public long UptimeMs { get; set; }
|
||||
public double EventsPerSecond { get; set; }
|
||||
public double BytesPerSecond { get; set; }
|
||||
public double AverageEventSizeBytes { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Returns formatted report
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("===== LOGGING METRICS =====");
|
||||
sb.AppendLine($"Total Events: {TotalEvents:N0}");
|
||||
sb.AppendLine($"Total Bytes: {TotalBytes:N0}");
|
||||
sb.AppendLine($"Average Event Size: {AverageEventSizeBytes:F2} bytes");
|
||||
sb.AppendLine($"Events/Second: {EventsPerSecond:F2}");
|
||||
sb.AppendLine($"Bytes/Second: {BytesPerSecond:F2}");
|
||||
sb.AppendLine($"Total Exceptions: {TotalExceptions:N0}");
|
||||
sb.AppendLine($"Dropped Events: {DroppedCount:N0}");
|
||||
sb.AppendLine($"Dropped Exceptions: {DroppedExceptions:N0}");
|
||||
sb.AppendLine();
|
||||
|
||||
if (LevelCounts.Count > 0)
|
||||
{
|
||||
sb.AppendLine("Events by Level:");
|
||||
foreach (var level in new[] { "Trace", "Debug", "Information", "Warning", "Error", "Critical" })
|
||||
{
|
||||
if (LevelCounts.TryGetValue(level, out var count))
|
||||
{
|
||||
sb.AppendLine($" {level}: {count:N0}");
|
||||
}
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine("Latency (ms):");
|
||||
sb.AppendLine($" Min: {MinLatencyMs}");
|
||||
sb.AppendLine($" P50: {P50LatencyMs:F3}");
|
||||
sb.AppendLine($" P95: {P95LatencyMs:F3}");
|
||||
sb.AppendLine($" P99: {P99LatencyMs:F3}");
|
||||
sb.AppendLine($" Avg: {AverageLatencyMs:F3}");
|
||||
sb.AppendLine($" Max: {MaxLatencyMs}");
|
||||
sb.AppendLine();
|
||||
|
||||
if (CategoryCounts.Count > 0)
|
||||
{
|
||||
sb.AppendLine("Top Categories:");
|
||||
foreach (var kvp in CategoryCounts.Take(5))
|
||||
{
|
||||
sb.AppendLine($" {kvp.Key}: {kvp.Value:N0}");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
sb.AppendLine($"Uptime: {TimeSpan.FromMilliseconds(UptimeMs):hh\\:mm\\:ss}");
|
||||
sb.AppendLine("===========================");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logger introspection API stub for inspecting logger configuration
|
||||
/// </summary>
|
||||
public sealed class LoggerIntrospection
|
||||
{
|
||||
private readonly List<string> _flows = new();
|
||||
private readonly List<string> _boosters = new();
|
||||
|
||||
public LoggerIntrospection(object logger)
|
||||
{
|
||||
// Logger integration to be provided when IFlowLogger is available
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a flow for tracking
|
||||
/// </summary>
|
||||
public void RegisterFlow(string name)
|
||||
{
|
||||
_flows.Add(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a booster for tracking
|
||||
/// </summary>
|
||||
public void RegisterBooster(string name)
|
||||
{
|
||||
_boosters.Add(name);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets configured flows
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> GetFlows() => _flows.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Gets configured boosters
|
||||
/// </summary>
|
||||
public IReadOnlyList<string> GetBoosters() => _boosters.AsReadOnly();
|
||||
|
||||
/// <summary>
|
||||
/// Generates a report of logger configuration
|
||||
/// </summary>
|
||||
public string GenerateReport()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("===== LOGGER CONFIGURATION =====");
|
||||
sb.AppendLine($"Total Flows: {_flows.Count}");
|
||||
foreach (var flow in _flows)
|
||||
{
|
||||
sb.AppendLine($" - {flow}");
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"Total Boosters: {_boosters.Count}");
|
||||
foreach (var booster in _boosters)
|
||||
{
|
||||
sb.AppendLine($" - {booster}");
|
||||
}
|
||||
|
||||
sb.AppendLine("================================");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Performance analyzer for logging
|
||||
/// </summary>
|
||||
public sealed class LoggingPerformanceAnalyzer
|
||||
{
|
||||
private readonly Dictionary<string, OperationMetrics> _operations = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
/// <summary>
|
||||
/// Measures execution time of a function
|
||||
/// </summary>
|
||||
public T MeasureOperation<T>(string operationName, Func<T> operation)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
return operation();
|
||||
}
|
||||
finally
|
||||
{
|
||||
sw.Stop();
|
||||
RecordOperation(operationName, sw.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Measures execution time of an action
|
||||
/// </summary>
|
||||
public void MeasureOperation(string operationName, Action operation)
|
||||
{
|
||||
var sw = Stopwatch.StartNew();
|
||||
try
|
||||
{
|
||||
operation();
|
||||
}
|
||||
finally
|
||||
{
|
||||
sw.Stop();
|
||||
RecordOperation(operationName, sw.ElapsedMilliseconds);
|
||||
}
|
||||
}
|
||||
|
||||
private void RecordOperation(string name, long ms)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_operations.TryGetValue(name, out var metrics))
|
||||
{
|
||||
metrics = new OperationMetrics { OperationName = name };
|
||||
_operations[name] = metrics;
|
||||
}
|
||||
|
||||
metrics.Count++;
|
||||
metrics.TotalTimeMs += ms;
|
||||
metrics.MinTimeMs = Math.Min(metrics.MinTimeMs, ms);
|
||||
metrics.MaxTimeMs = Math.Max(metrics.MaxTimeMs, ms);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all recorded operations
|
||||
/// </summary>
|
||||
public IReadOnlyCollection<OperationMetrics> GetOperations()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _operations.Values.ToList().AsReadOnly();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates performance report
|
||||
/// </summary>
|
||||
public string GenerateReport()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("===== PERFORMANCE ANALYSIS =====");
|
||||
|
||||
foreach (var op in _operations.Values.OrderByDescending(o => o.TotalTimeMs))
|
||||
{
|
||||
double avgTime = op.TotalTimeMs / (double)op.Count;
|
||||
sb.AppendLine($"{op.OperationName}:");
|
||||
sb.AppendLine($" Count: {op.Count}");
|
||||
sb.AppendLine($" Total: {op.TotalTimeMs}ms");
|
||||
sb.AppendLine($" Average: {avgTime:F3}ms");
|
||||
sb.AppendLine($" Min: {op.MinTimeMs}ms");
|
||||
sb.AppendLine($" Max: {op.MaxTimeMs}ms");
|
||||
}
|
||||
|
||||
sb.AppendLine("================================");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
public void Reset()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_operations.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Operation metrics
|
||||
/// </summary>
|
||||
public sealed class OperationMetrics
|
||||
{
|
||||
public string OperationName { get; set; } = "";
|
||||
public long Count { get; set; }
|
||||
public long TotalTimeMs { get; set; }
|
||||
public long MinTimeMs { get; set; } = long.MaxValue;
|
||||
public long MaxTimeMs { get; set; }
|
||||
|
||||
public double AverageTimeMs => TotalTimeMs / (double)(Count > 0 ? Count : 1);
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace EonaCat.LogStack.Analytics;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Tracks statistics for an individual logging flow
|
||||
/// </summary>
|
||||
public sealed class PerFlowStatistics
|
||||
{
|
||||
private readonly object _lock = new object();
|
||||
private long _eventsProcessed;
|
||||
private long _eventsFailed;
|
||||
private long _bytesWritten;
|
||||
private long _eventsDropped;
|
||||
private readonly List<long> _latencies = new();
|
||||
private long _minLatencyMs = long.MaxValue;
|
||||
private long _maxLatencyMs;
|
||||
private readonly Stopwatch _uptime = Stopwatch.StartNew();
|
||||
|
||||
public string FlowName { get; set; }
|
||||
public string FlowType { get; set; }
|
||||
|
||||
public PerFlowStatistics(string flowName, string flowType)
|
||||
{
|
||||
FlowName = flowName ?? "Unknown";
|
||||
FlowType = flowType ?? "Unknown";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a successfully processed event
|
||||
/// </summary>
|
||||
public void RecordSuccess(long latencyMs, long bytes)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_eventsProcessed++;
|
||||
_bytesWritten += bytes;
|
||||
_latencies.Add(latencyMs);
|
||||
|
||||
if (latencyMs < _minLatencyMs)
|
||||
{
|
||||
_minLatencyMs = latencyMs;
|
||||
}
|
||||
|
||||
if (latencyMs > _maxLatencyMs)
|
||||
{
|
||||
_maxLatencyMs = latencyMs;
|
||||
}
|
||||
|
||||
if (_latencies.Count > 1000)
|
||||
{
|
||||
_latencies.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a failed event
|
||||
/// </summary>
|
||||
public void RecordFailure(long latencyMs = 0)
|
||||
{
|
||||
Interlocked.Increment(ref _eventsFailed);
|
||||
if (latencyMs > 0)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_latencies.Add(latencyMs);
|
||||
if (latencyMs < _minLatencyMs)
|
||||
{
|
||||
_minLatencyMs = latencyMs;
|
||||
}
|
||||
|
||||
if (latencyMs > _maxLatencyMs)
|
||||
{
|
||||
_maxLatencyMs = latencyMs;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records dropped events
|
||||
/// </summary>
|
||||
public void RecordDropped(long count = 1)
|
||||
{
|
||||
Interlocked.Add(ref _eventsDropped, count);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a snapshot of current statistics
|
||||
/// </summary>
|
||||
public FlowStatisticsSnapshot GetSnapshot()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
double avgLatency = _latencies.Count > 0 ? _latencies.Average() : 0;
|
||||
double p95 = GetPercentile(_latencies, 95);
|
||||
double p99 = GetPercentile(_latencies, 99);
|
||||
|
||||
var totalAttempts = _eventsProcessed + _eventsFailed;
|
||||
var successRate = totalAttempts > 0 ? (_eventsProcessed * 100.0) / totalAttempts : 100;
|
||||
|
||||
return new FlowStatisticsSnapshot
|
||||
{
|
||||
FlowName = FlowName,
|
||||
FlowType = FlowType,
|
||||
EventsProcessed = _eventsProcessed,
|
||||
EventsFailed = _eventsFailed,
|
||||
EventsDropped = _eventsDropped,
|
||||
BytesWritten = _bytesWritten,
|
||||
SuccessRate = successRate,
|
||||
MinLatencyMs = _minLatencyMs == long.MaxValue ? 0 : _minLatencyMs,
|
||||
MaxLatencyMs = _maxLatencyMs,
|
||||
AverageLatencyMs = avgLatency,
|
||||
P95LatencyMs = p95,
|
||||
P99LatencyMs = p99,
|
||||
UptimeMs = _uptime.ElapsedMilliseconds,
|
||||
EventsPerSecond = _uptime.ElapsedMilliseconds > 0
|
||||
? _eventsProcessed * 1000.0 / _uptime.ElapsedMilliseconds
|
||||
: 0,
|
||||
BytesPerSecond = _uptime.ElapsedMilliseconds > 0
|
||||
? _bytesWritten * 1000.0 / _uptime.ElapsedMilliseconds
|
||||
: 0
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private double GetPercentile(List<long> sortedValues, int percentile)
|
||||
{
|
||||
if (sortedValues.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var sorted = sortedValues.OrderBy(v => v).ToList();
|
||||
int index = (int)((percentile / 100.0) * (sorted.Count - 1));
|
||||
index = Math.Max(0, Math.Min(index, sorted.Count - 1));
|
||||
return sorted[index];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all statistics
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_eventsProcessed = 0;
|
||||
_eventsFailed = 0;
|
||||
_eventsDropped = 0;
|
||||
_bytesWritten = 0;
|
||||
_latencies.Clear();
|
||||
_minLatencyMs = long.MaxValue;
|
||||
_maxLatencyMs = 0;
|
||||
_uptime.Restart();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of flow statistics at a point in time
|
||||
/// </summary>
|
||||
public sealed class FlowStatisticsSnapshot
|
||||
{
|
||||
public string FlowName { get; set; }
|
||||
public string FlowType { get; set; }
|
||||
public long EventsProcessed { get; set; }
|
||||
public long EventsFailed { get; set; }
|
||||
public long EventsDropped { get; set; }
|
||||
public long BytesWritten { get; set; }
|
||||
public double SuccessRate { get; set; }
|
||||
public long MinLatencyMs { get; set; }
|
||||
public long MaxLatencyMs { get; set; }
|
||||
public double AverageLatencyMs { get; set; }
|
||||
public double P95LatencyMs { get; set; }
|
||||
public double P99LatencyMs { get; set; }
|
||||
public long UptimeMs { get; set; }
|
||||
public double EventsPerSecond { get; set; }
|
||||
public double BytesPerSecond { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine($"===== FLOW: {FlowName} ({FlowType}) =====");
|
||||
sb.AppendLine($"Events Processed: {EventsProcessed:N0}");
|
||||
sb.AppendLine($"Events Failed: {EventsFailed:N0}");
|
||||
sb.AppendLine($"Events Dropped: {EventsDropped:N0}");
|
||||
sb.AppendLine($"Success Rate: {SuccessRate:F2}%");
|
||||
sb.AppendLine($"Bytes Written: {BytesWritten:N0}");
|
||||
sb.AppendLine($"Events/Second: {EventsPerSecond:F2}");
|
||||
sb.AppendLine($"Bytes/Second: {BytesPerSecond:F2}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Latency (ms):");
|
||||
sb.AppendLine($" Min: {MinLatencyMs}");
|
||||
sb.AppendLine($" Avg: {AverageLatencyMs:F3}");
|
||||
sb.AppendLine($" P95: {P95LatencyMs:F3}");
|
||||
sb.AppendLine($" P99: {P99LatencyMs:F3}");
|
||||
sb.AppendLine($" Max: {MaxLatencyMs}");
|
||||
sb.AppendLine();
|
||||
sb.AppendLine($"Uptime: {TimeSpan.FromMilliseconds(UptimeMs):hh\\:mm\\:ss}");
|
||||
sb.AppendLine("==================================");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,345 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.LogStack.Batching;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Manages batch configuration and sizing strategies for optimized flow processing
|
||||
/// </summary>
|
||||
public class BatchConfig
|
||||
{
|
||||
public int InitialBatchSize { get; set; } = 100;
|
||||
public int MaxBatchSize { get; set; } = 1000;
|
||||
public int MinBatchSize { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum time to wait before flushing a partial batch (milliseconds)
|
||||
/// </summary>
|
||||
public int MaxBatchDelayMs { get; set; } = 1000;
|
||||
|
||||
/// <summary>
|
||||
/// Threshold for memory usage before activating backpressure (bytes)
|
||||
/// </summary>
|
||||
public long BackpressureThresholdBytes { get; set; } = 100 * 1024 * 1024; // 100MB
|
||||
|
||||
/// <summary>
|
||||
/// Enable adaptive batch sizing based on throughput and memory
|
||||
/// </summary>
|
||||
public bool EnableAdaptiveSizing { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Enable backpressure handling
|
||||
/// </summary>
|
||||
public bool EnableBackpressure { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adaptive batch processor that dynamically adjusts batch sizes based on system conditions
|
||||
/// </summary>
|
||||
public sealed class AdaptiveBatchProcessor : IDisposable
|
||||
{
|
||||
private readonly BatchConfig _config;
|
||||
private int _currentBatchSize;
|
||||
private long _currentMemoryUsage;
|
||||
private DateTime _lastSizeAdjustment = DateTime.UtcNow;
|
||||
private readonly object _lock = new object();
|
||||
private bool _isDisposed;
|
||||
|
||||
public AdaptiveBatchProcessor(BatchConfig? config = null)
|
||||
{
|
||||
_config = config ?? new BatchConfig();
|
||||
_currentBatchSize = _config.InitialBatchSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the optimal batch size based on current system conditions
|
||||
/// </summary>
|
||||
public int GetOptimalBatchSize()
|
||||
{
|
||||
if (!_config.EnableAdaptiveSizing)
|
||||
{
|
||||
return _currentBatchSize;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
// Adjust batch size every 10 seconds based on memory pressure
|
||||
if ((DateTime.UtcNow - _lastSizeAdjustment).TotalSeconds < 10)
|
||||
{
|
||||
return _currentBatchSize;
|
||||
}
|
||||
|
||||
_lastSizeAdjustment = DateTime.UtcNow;
|
||||
|
||||
long availableMemory = GC.GetTotalMemory(false);
|
||||
|
||||
if (availableMemory > _config.BackpressureThresholdBytes)
|
||||
{
|
||||
// Reduce batch size under memory pressure
|
||||
_currentBatchSize = Math.Max(
|
||||
_config.MinBatchSize,
|
||||
(int)(_currentBatchSize * 0.9));
|
||||
}
|
||||
else if (availableMemory < _config.BackpressureThresholdBytes / 2)
|
||||
{
|
||||
// Increase batch size when memory is available
|
||||
_currentBatchSize = Math.Min(
|
||||
_config.MaxBatchSize,
|
||||
(int)(_currentBatchSize * 1.1));
|
||||
}
|
||||
|
||||
return _currentBatchSize;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records memory usage of processed events
|
||||
/// </summary>
|
||||
public void RecordMemoryUsage(long bytes)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_currentMemoryUsage = bytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if backpressure should be applied
|
||||
/// </summary>
|
||||
public bool ShouldApplyBackpressure()
|
||||
{
|
||||
if (!_config.EnableBackpressure)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
return _currentMemoryUsage > _config.BackpressureThresholdBytes;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current memory usage
|
||||
/// </summary>
|
||||
public long GetMemoryUsage()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _currentMemoryUsage;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets memory tracking
|
||||
/// </summary>
|
||||
public void ResetMemoryUsage()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_currentMemoryUsage = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Backpressure handler that controls flow based on system resources
|
||||
/// </summary>
|
||||
public sealed class BackpressureHandler : IDisposable
|
||||
{
|
||||
private readonly long _thresholdBytes;
|
||||
private readonly int _maxWaitMs;
|
||||
private bool _isDisposed;
|
||||
|
||||
public BackpressureHandler(long thresholdBytes = 100 * 1024 * 1024, int maxWaitMs = 5000)
|
||||
{
|
||||
_thresholdBytes = thresholdBytes;
|
||||
_maxWaitMs = maxWaitMs;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies backpressure by waiting if memory usage exceeds threshold
|
||||
/// </summary>
|
||||
public async Task ApplyBackpressureAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
long memoryUsage = GC.GetTotalMemory(false);
|
||||
|
||||
if (memoryUsage <= _thresholdBytes)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait for memory to be released
|
||||
var startTime = DateTime.UtcNow;
|
||||
while (GC.GetTotalMemory(false) > _thresholdBytes)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if ((DateTime.UtcNow - startTime).TotalMilliseconds > _maxWaitMs)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
// Collect garbage and wait
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized, false);
|
||||
await Task.Delay(100, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets current memory pressure ratio (0.0 = no pressure, 1.0+ = critical)
|
||||
/// </summary>
|
||||
public double GetMemoryPressure()
|
||||
{
|
||||
long memoryUsage = GC.GetTotalMemory(false);
|
||||
return (double)memoryUsage / _thresholdBytes;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if backpressure is currently active
|
||||
/// </summary>
|
||||
public bool IsActive => GetMemoryPressure() > 0.8;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generic batch accumulator for collecting items before processing
|
||||
/// </summary>
|
||||
public sealed class BatchAccumulator<T> : IDisposable
|
||||
{
|
||||
private readonly List<T> _items;
|
||||
private readonly int _maxSize;
|
||||
private readonly int _maxWaitMs;
|
||||
private DateTime _lastFlush;
|
||||
private readonly object _lock = new object();
|
||||
private bool _isDisposed;
|
||||
|
||||
public BatchAccumulator(int maxSize = 100, int maxWaitMs = 1000)
|
||||
{
|
||||
_maxSize = maxSize;
|
||||
_maxWaitMs = maxWaitMs;
|
||||
_items = new List<T>(maxSize);
|
||||
_lastFlush = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds an item to the batch
|
||||
/// </summary>
|
||||
public bool TryAdd(T item)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_items.Add(item);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if the batch should be flushed
|
||||
/// </summary>
|
||||
public bool ShouldFlush()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_items.Count == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Flush if full
|
||||
if (_items.Count >= _maxSize)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Flush if timeout exceeded
|
||||
var elapsed = (DateTime.UtcNow - _lastFlush).TotalMilliseconds;
|
||||
return elapsed > _maxWaitMs;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current batch and resets
|
||||
/// </summary>
|
||||
public T[] GetAndReset()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (_items.Count == 0)
|
||||
{
|
||||
return Array.Empty<T>();
|
||||
}
|
||||
|
||||
var batch = _items.ToArray();
|
||||
_items.Clear();
|
||||
_lastFlush = DateTime.UtcNow;
|
||||
return batch;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the count of items in the current batch
|
||||
/// </summary>
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _items.Count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_isDisposed = true;
|
||||
_items.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,287 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace EonaCat.LogStack.Boosters;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Performance monitoring booster that adds timing and resource metrics
|
||||
/// </summary>
|
||||
public sealed class PerformanceBooster : BoosterBase
|
||||
{
|
||||
private readonly long _startTime = Stopwatch.GetTimestamp();
|
||||
private long _totalMemorySnapshot;
|
||||
|
||||
public PerformanceBooster() : base("Performance") { }
|
||||
|
||||
public override bool Boost(ref LogEventBuilder builder)
|
||||
{
|
||||
try
|
||||
{
|
||||
var currentProcess = Process.GetCurrentProcess();
|
||||
var elapsed = (long)((DateTime.UtcNow.Ticks - _startTime) / 10000.0);
|
||||
|
||||
builder.WithProperty("uptime_ms", elapsed);
|
||||
builder.WithProperty("working_set_mb", currentProcess.WorkingSet64 / (1024 * 1024));
|
||||
builder.WithProperty("virtual_memory_mb", currentProcess.VirtualMemorySize64 / (1024 * 1024));
|
||||
builder.WithProperty("gc_total_memory", GC.GetTotalMemory(false) / 1024);
|
||||
builder.WithProperty("thread_count", Process.GetCurrentProcess().Threads.Count);
|
||||
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true; // Don't filter on error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Distributed tracing booster that extracts and propagates trace context
|
||||
/// </summary>
|
||||
public sealed class DistributedTracingBooster : BoosterBase
|
||||
{
|
||||
private readonly AsyncLocal<string> _correlationId = new AsyncLocal<string>();
|
||||
private readonly AsyncLocal<string> _parentSpanId = new AsyncLocal<string>();
|
||||
private int _spanIdCounter;
|
||||
|
||||
public DistributedTracingBooster() : base("DistributedTracing") { }
|
||||
|
||||
/// <summary>
|
||||
/// Sets the correlation ID for the current async context
|
||||
/// </summary>
|
||||
public void SetCorrelationId(string correlationId)
|
||||
{
|
||||
_correlationId.Value = correlationId;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current correlation ID
|
||||
/// </summary>
|
||||
public string? GetCorrelationId()
|
||||
{
|
||||
return _correlationId.Value;
|
||||
}
|
||||
|
||||
public override bool Boost(ref LogEventBuilder builder)
|
||||
{
|
||||
var correlationId = _correlationId.Value;
|
||||
if (string.IsNullOrEmpty(correlationId))
|
||||
{
|
||||
correlationId = Guid.NewGuid().ToString("N");
|
||||
_correlationId.Value = correlationId;
|
||||
}
|
||||
|
||||
builder.WithProperty("correlation_id", correlationId);
|
||||
|
||||
// Generate span ID
|
||||
int spanId = Interlocked.Increment(ref _spanIdCounter);
|
||||
builder.WithProperty("span_id", spanId.ToString("X8"));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sampling booster that probabilistically filters log events
|
||||
/// </summary>
|
||||
public sealed class SamplingBooster : BoosterBase
|
||||
{
|
||||
private readonly double _samplingRate;
|
||||
private readonly Random _random;
|
||||
private long _totalLogged;
|
||||
private long _totalSampled;
|
||||
|
||||
public SamplingBooster(double samplingRate = 0.1) : base("Sampling")
|
||||
{
|
||||
if (samplingRate < 0.0 || samplingRate > 1.0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(samplingRate), "Must be between 0.0 and 1.0");
|
||||
}
|
||||
|
||||
_samplingRate = samplingRate;
|
||||
_random = new Random();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Always log critical and error events, sample the rest
|
||||
/// </summary>
|
||||
public override bool Boost(ref LogEventBuilder builder)
|
||||
{
|
||||
_totalLogged++;
|
||||
|
||||
// Sample based on configured rate
|
||||
bool shouldLog = _random.NextDouble() < _samplingRate;
|
||||
if (shouldLog)
|
||||
{
|
||||
_totalSampled++;
|
||||
}
|
||||
|
||||
return shouldLog;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets sampling statistics
|
||||
/// </summary>
|
||||
public (long total, long sampled, double rate) GetStatistics()
|
||||
{
|
||||
return (_totalLogged, _totalSampled, _totalLogged > 0 ? (double)_totalSampled / _totalLogged : 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property filter booster that includes/excludes events based on properties
|
||||
/// </summary>
|
||||
public sealed class PropertyFilterBooster : BoosterBase
|
||||
{
|
||||
private readonly Func<LogEventBuilder, bool>? _includeFilter;
|
||||
private readonly Func<LogEventBuilder, bool>? _excludeFilter;
|
||||
|
||||
public PropertyFilterBooster(
|
||||
Func<LogEventBuilder, bool>? includeFilter = null,
|
||||
Func<LogEventBuilder, bool>? excludeFilter = null)
|
||||
: base("PropertyFilter")
|
||||
{
|
||||
_includeFilter = includeFilter;
|
||||
_excludeFilter = excludeFilter;
|
||||
}
|
||||
|
||||
public override bool Boost(ref LogEventBuilder builder)
|
||||
{
|
||||
// If include filter is specified and returns false, skip
|
||||
if (_includeFilter != null && !_includeFilter(builder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// If exclude filter is specified and returns true, skip
|
||||
if (_excludeFilter != null && _excludeFilter(builder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rate limiting booster that throttles log events
|
||||
/// </summary>
|
||||
public sealed class RateLimitingBooster : BoosterBase
|
||||
{
|
||||
private readonly int _maxEventsPerSecond;
|
||||
private DateTime _lastResetTime;
|
||||
private int _eventCount;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
public RateLimitingBooster(int maxEventsPerSecond = 1000) : base("RateLimiting")
|
||||
{
|
||||
if (maxEventsPerSecond <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(maxEventsPerSecond));
|
||||
}
|
||||
|
||||
_maxEventsPerSecond = maxEventsPerSecond;
|
||||
_lastResetTime = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
public override bool Boost(ref LogEventBuilder builder)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
var elapsed = (now - _lastResetTime).TotalSeconds;
|
||||
|
||||
if (elapsed >= 1.0)
|
||||
{
|
||||
_lastResetTime = now;
|
||||
_eventCount = 0;
|
||||
}
|
||||
|
||||
_eventCount++;
|
||||
return _eventCount <= _maxEventsPerSecond;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets current rate limiting statistics
|
||||
/// </summary>
|
||||
public (int current, int limit) GetStatistics()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return (_eventCount, _maxEventsPerSecond);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context enrichment booster that adds contextual information
|
||||
/// </summary>
|
||||
public sealed class ContextEnrichmentBooster : BoosterBase
|
||||
{
|
||||
private readonly Func<LogEventBuilder, LogEventBuilder> _enricher;
|
||||
|
||||
public ContextEnrichmentBooster(Func<LogEventBuilder, LogEventBuilder> enricher)
|
||||
: base("ContextEnrichment")
|
||||
{
|
||||
_enricher = enricher ?? throw new ArgumentNullException(nameof(enricher));
|
||||
}
|
||||
|
||||
public override bool Boost(ref LogEventBuilder builder)
|
||||
{
|
||||
try
|
||||
{
|
||||
builder = _enricher(builder);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
return true; // Don't filter on enrichment error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deduplication booster that filters duplicate messages within a time window
|
||||
/// </summary>
|
||||
public sealed class DeduplicationBooster : BoosterBase
|
||||
{
|
||||
private readonly TimeSpan _window;
|
||||
private readonly Dictionary<string, DateTime> _seenMessages = new();
|
||||
private readonly object _lock = new object();
|
||||
|
||||
public DeduplicationBooster(TimeSpan? window = null) : base("Deduplication")
|
||||
{
|
||||
_window = window ?? TimeSpan.FromSeconds(10);
|
||||
}
|
||||
|
||||
public override bool Boost(ref LogEventBuilder builder)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// Clean old entries
|
||||
var oldEntries = _seenMessages
|
||||
.Where(kvp => (now - kvp.Value) > _window)
|
||||
.Select(kvp => kvp.Key)
|
||||
.ToList();
|
||||
|
||||
foreach (var key in oldEntries)
|
||||
{
|
||||
_seenMessages.Remove(key);
|
||||
}
|
||||
|
||||
// For now, don't deduplicate as we don't have access to message in ref struct
|
||||
// This can be enhanced when builder provides access
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System.IO;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace EonaCat.LogStack.Boosters;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Automatically captures caller file, line number, and member name via
|
||||
/// compiler-generated attributes — zero overhead at the call site.
|
||||
///
|
||||
/// Adds properties:
|
||||
/// caller.member — method / property name
|
||||
/// caller.file — source file name (not full path, for privacy)
|
||||
/// caller.line — line number
|
||||
///
|
||||
/// Usage: add to your LogBuilder with <c>.BoostWithCallerInfo()</c>.
|
||||
///
|
||||
/// Note: because this booster uses compile-time attributes it captures the
|
||||
/// logger helper method rather than the end-user call site when the log call
|
||||
/// is made through wrapper methods. For precise call-site capture, call
|
||||
/// <see cref="Capture"/> directly from the call site.
|
||||
/// </summary>
|
||||
public sealed class CallerInfoBooster : BoosterBase
|
||||
{
|
||||
public CallerInfoBooster() : base("CallerInfo") { }
|
||||
|
||||
public override bool Boost(ref LogEventBuilder builder) => true; // no-op in generic path
|
||||
|
||||
/// <summary>
|
||||
/// Enriches a builder with the actual call-site information.
|
||||
/// Call this from your logging helper method so the compiler fills in the arguments.
|
||||
/// </summary>
|
||||
public static void Capture(
|
||||
ref LogEventBuilder builder,
|
||||
[CallerMemberName] string member = "",
|
||||
[CallerFilePath] string file = "",
|
||||
[CallerLineNumber] int line = 0)
|
||||
{
|
||||
builder.WithProperty("caller.member", member);
|
||||
builder.WithProperty("caller.file", Path.GetFileName(file));
|
||||
builder.WithProperty("caller.line", line);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
// 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.Boosters
|
||||
{
|
||||
/// <summary>
|
||||
/// Booster that extracts context from HTTP requests.
|
||||
/// Requires IHttpContextAccessor to be registered in DI.
|
||||
/// </summary>
|
||||
public sealed class RequestContextBooster : BoosterBase
|
||||
{
|
||||
private readonly object? _httpContextAccessor;
|
||||
|
||||
public RequestContextBooster(object? httpContextAccessor = null) : base("RequestContext")
|
||||
{
|
||||
_httpContextAccessor = httpContextAccessor;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override bool Boost(ref LogEventBuilder builder)
|
||||
{
|
||||
try
|
||||
{
|
||||
var httpContext = GetHttpContext();
|
||||
if (httpContext == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var request = GetRequest(httpContext);
|
||||
if (request == null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Add HTTP method
|
||||
var method = GetRequestMethod(request);
|
||||
if (!string.IsNullOrEmpty(method))
|
||||
{
|
||||
builder.WithProperty("HttpMethod", method);
|
||||
}
|
||||
|
||||
// Add request path
|
||||
var path = GetRequestPath(request);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
builder.WithProperty("RequestPath", path);
|
||||
}
|
||||
|
||||
// Add request host
|
||||
var host = GetRequestHost(request);
|
||||
if (!string.IsNullOrEmpty(host))
|
||||
{
|
||||
builder.WithProperty("RequestHost", host);
|
||||
}
|
||||
|
||||
// Add user if available
|
||||
var user = GetUser(httpContext);
|
||||
if (!string.IsNullOrEmpty(user))
|
||||
{
|
||||
builder.WithProperty("User", user);
|
||||
}
|
||||
|
||||
// Add trace identifier
|
||||
var traceId = GetTraceId(httpContext);
|
||||
if (!string.IsNullOrEmpty(traceId))
|
||||
{
|
||||
builder.WithProperty("TraceId", traceId);
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Silently fail - don't let booster errors crash logging
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private object? GetHttpContext()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (_httpContextAccessor == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var prop = _httpContextAccessor.GetType().GetProperty("HttpContext");
|
||||
return prop?.GetValue(_httpContextAccessor);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private object? GetRequest(object httpContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var prop = httpContext.GetType().GetProperty("Request");
|
||||
return prop?.GetValue(httpContext);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetRequestMethod(object request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var prop = request.GetType().GetProperty("Method");
|
||||
return prop?.GetValue(request)?.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetRequestPath(object request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var prop = request.GetType().GetProperty("Path");
|
||||
return prop?.GetValue(request)?.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetRequestHost(object request)
|
||||
{
|
||||
try
|
||||
{
|
||||
var prop = request.GetType().GetProperty("Host");
|
||||
return prop?.GetValue(request)?.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetUser(object httpContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var prop = httpContext.GetType().GetProperty("User");
|
||||
var user = prop?.GetValue(httpContext);
|
||||
if (user == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var identityProp = user.GetType().GetProperty("Identity");
|
||||
var identity = identityProp?.GetValue(user);
|
||||
if (identity == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nameProp = identity.GetType().GetProperty("Name");
|
||||
return nameProp?.GetValue(identity)?.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private string? GetTraceId(object httpContext)
|
||||
{
|
||||
try
|
||||
{
|
||||
var prop = httpContext.GetType().GetProperty("TraceIdentifier");
|
||||
return prop?.GetValue(httpContext)?.ToString();
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using EonaCat.LogStack.Filtering;
|
||||
using EonaCat.LogStack.Output.Formatters;
|
||||
using EonaCat.LogStack.Output.Layouts;
|
||||
using EonaCat.LogStack.Structured;
|
||||
using EonaCat.LogStack.Templates;
|
||||
|
||||
namespace EonaCat.LogStack.Configuration;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Advanced logger configuration settings
|
||||
/// </summary>
|
||||
public sealed class LoggerConfiguration
|
||||
{
|
||||
private readonly Dictionary<string, object> _config = new();
|
||||
|
||||
/// <summary>
|
||||
/// Sets a configuration value
|
||||
/// </summary>
|
||||
public LoggerConfiguration Set(string key, object value)
|
||||
{
|
||||
_config[key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a configuration value
|
||||
/// </summary>
|
||||
public T? Get<T>(string key, T? defaultValue = default)
|
||||
{
|
||||
if (_config.TryGetValue(key, out var value))
|
||||
{
|
||||
return (T?)value;
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables buffering with specified size
|
||||
/// </summary>
|
||||
public LoggerConfiguration WithBuffering(int bufferSize = 1000)
|
||||
{
|
||||
Set("BufferSize", bufferSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables batching with specified batch size
|
||||
/// </summary>
|
||||
public LoggerConfiguration WithBatching(int batchSize = 100)
|
||||
{
|
||||
Set("BatchSize", batchSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets the maximum pool size for object reuse
|
||||
/// </summary>
|
||||
public LoggerConfiguration WithPooling(int maxPoolSize = 10000)
|
||||
{
|
||||
Set("MaxPoolSize", maxPoolSize);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables diagnostics collection
|
||||
/// </summary>
|
||||
public LoggerConfiguration WithDiagnostics(bool enabled = true)
|
||||
{
|
||||
Set("DiagnosticsEnabled", enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables context propagation using AsyncLocal
|
||||
/// </summary>
|
||||
public LoggerConfiguration WithContextPropagation(bool enabled = true)
|
||||
{
|
||||
Set("ContextPropagationEnabled", enabled);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets sampling rate (0-1, where 1 = log all events)
|
||||
/// </summary>
|
||||
public LoggerConfiguration WithSampling(double rate)
|
||||
{
|
||||
var clampedRate = rate < 0.0 ? 0.0 : (rate > 1.0 ? 1.0 : rate);
|
||||
Set("SamplingRate", clampedRate);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets rate limit (events per second)
|
||||
/// </summary>
|
||||
public LoggerConfiguration WithRateLimit(int eventsPerSecond)
|
||||
{
|
||||
Set("RateLimit", eventsPerSecond);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Dictionary<string, object> Build()
|
||||
{
|
||||
return new Dictionary<string, object>(_config);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predefined logger presets
|
||||
/// </summary>
|
||||
public enum LoggerPreset
|
||||
{
|
||||
Development,
|
||||
Production,
|
||||
Diagnostics,
|
||||
Performance
|
||||
}
|
||||
@@ -0,0 +1,368 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace EonaCat.LogStack.Destructuring;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Renders complex types to JSON-like format without external dependencies
|
||||
/// </summary>
|
||||
public sealed class TypeDestructor
|
||||
{
|
||||
private readonly HashSet<object> _visited = new();
|
||||
private const int MaxDepth = 10;
|
||||
private const int MaxStringLength = 1000;
|
||||
private const int MaxCollectionItems = 100;
|
||||
|
||||
/// <summary>
|
||||
/// Destructures an object to a string representation (JSON-like format)
|
||||
/// </summary>
|
||||
public string Destructure(object? value)
|
||||
{
|
||||
_visited.Clear();
|
||||
var sb = new StringBuilder();
|
||||
DestructureValue(value, sb, 0);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void DestructureValue(object? value, StringBuilder sb, int depth)
|
||||
{
|
||||
if (depth > MaxDepth)
|
||||
{
|
||||
sb.Append("\"...\"");
|
||||
return;
|
||||
}
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
sb.Append("null");
|
||||
return;
|
||||
}
|
||||
|
||||
var type = value.GetType();
|
||||
|
||||
// Handle primitives
|
||||
if (type == typeof(string))
|
||||
{
|
||||
AppendString(sb, (string)value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == typeof(bool))
|
||||
{
|
||||
sb.Append(((bool)value) ? "true" : "false");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == typeof(byte) || type == typeof(sbyte) ||
|
||||
type == typeof(short) || type == typeof(ushort) ||
|
||||
type == typeof(int) || type == typeof(uint) ||
|
||||
type == typeof(long) || type == typeof(ulong))
|
||||
{
|
||||
sb.Append(value);
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == typeof(float) || type == typeof(double) || type == typeof(decimal))
|
||||
{
|
||||
sb.Append(((IFormattable)value).ToString(null, CultureInfo.InvariantCulture));
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == typeof(DateTime))
|
||||
{
|
||||
sb.Append("\"").Append(((DateTime)value).ToIso8601String()).Append("\"");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == typeof(DateTimeOffset))
|
||||
{
|
||||
sb.Append("\"").Append(((DateTimeOffset)value).ToString("O")).Append("\"");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == typeof(TimeSpan))
|
||||
{
|
||||
sb.Append("\"").Append(((TimeSpan)value).ToString()).Append("\"");
|
||||
return;
|
||||
}
|
||||
|
||||
if (type == typeof(Guid))
|
||||
{
|
||||
sb.Append("\"").Append(((Guid)value).ToString()).Append("\"");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check for circular references
|
||||
if (_visited.Contains(value))
|
||||
{
|
||||
sb.Append("\"<circular reference>\"");
|
||||
return;
|
||||
}
|
||||
|
||||
_visited.Add(value);
|
||||
|
||||
// Handle collections
|
||||
if (value is IEnumerable enumerable && !(value is string))
|
||||
{
|
||||
DestructureCollection(enumerable, sb, depth);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle objects
|
||||
DestructureObject(value, sb, depth);
|
||||
}
|
||||
|
||||
private void DestructureCollection(IEnumerable enumerable, StringBuilder sb, int depth)
|
||||
{
|
||||
sb.Append("[");
|
||||
int count = 0;
|
||||
|
||||
foreach (var item in enumerable)
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
}
|
||||
|
||||
if (count >= MaxCollectionItems)
|
||||
{
|
||||
sb.Append("\"...\"");
|
||||
break;
|
||||
}
|
||||
|
||||
DestructureValue(item, sb, depth + 1);
|
||||
count++;
|
||||
}
|
||||
|
||||
sb.Append("]");
|
||||
}
|
||||
|
||||
private void DestructureObject(object value, StringBuilder sb, int depth)
|
||||
{
|
||||
sb.Append("{");
|
||||
|
||||
var properties = value.GetType().GetProperties();
|
||||
int count = 0;
|
||||
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
}
|
||||
|
||||
sb.Append("\"").Append(EscapeJsonString(prop.Name)).Append("\":");
|
||||
|
||||
try
|
||||
{
|
||||
var propValue = prop.GetValue(value);
|
||||
DestructureValue(propValue, sb, depth + 1);
|
||||
}
|
||||
catch
|
||||
{
|
||||
sb.Append("\"<error>\"");
|
||||
}
|
||||
|
||||
count++;
|
||||
|
||||
if (count >= MaxCollectionItems)
|
||||
{
|
||||
sb.Append(",\"...\":null");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("}");
|
||||
}
|
||||
|
||||
private void AppendString(StringBuilder sb, string value)
|
||||
{
|
||||
sb.Append("\"");
|
||||
|
||||
int length = Math.Min(value.Length, MaxStringLength);
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
char c = value[i];
|
||||
sb.Append(EscapeChar(c));
|
||||
}
|
||||
|
||||
if (value.Length > MaxStringLength)
|
||||
{
|
||||
sb.Append("...\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("\"");
|
||||
}
|
||||
}
|
||||
|
||||
private static string EscapeJsonString(string value)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (char c in value)
|
||||
{
|
||||
sb.Append(EscapeChar(c));
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string EscapeChar(char c)
|
||||
{
|
||||
return c switch
|
||||
{
|
||||
'"' => "\\\"",
|
||||
'\\' => "\\\\",
|
||||
'\b' => "\\b",
|
||||
'\f' => "\\f",
|
||||
'\n' => "\\n",
|
||||
'\r' => "\\r",
|
||||
'\t' => "\\t",
|
||||
_ => c < 32 ? $"\\u{(int)c:X4}" : c.ToString()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specialized destructors for common types
|
||||
/// </summary>
|
||||
public sealed class SpecializedDestructor
|
||||
{
|
||||
/// <summary>
|
||||
/// Destructures an exception with full context
|
||||
/// </summary>
|
||||
public static string DestructureException(Exception ex)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("{");
|
||||
sb.Append("\"Type\":\"").Append(EscapeJsonString(ex.GetType().Name)).Append("\",");
|
||||
sb.Append("\"Message\":\"").Append(EscapeJsonString(ex.Message)).Append("\",");
|
||||
sb.Append("\"StackTrace\":\"").Append(EscapeJsonString(ex.StackTrace ?? "")).Append("\"");
|
||||
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
sb.Append(",\"InnerException\":").Append(DestructureException(ex.InnerException));
|
||||
}
|
||||
|
||||
sb.Append("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Destructures a key-value pair collection
|
||||
/// </summary>
|
||||
public static string DestructureKeyValuePairs<TKey, TValue>(
|
||||
IEnumerable<KeyValuePair<TKey, TValue>> pairs)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("{");
|
||||
|
||||
int count = 0;
|
||||
foreach (var kvp in pairs)
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
}
|
||||
|
||||
sb.Append("\"").Append(EscapeJsonString(kvp.Key?.ToString() ?? "null")).Append("\":");
|
||||
sb.Append("\"").Append(EscapeJsonString(kvp.Value?.ToString() ?? "null")).Append("\"");
|
||||
|
||||
count++;
|
||||
if (count >= 100)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
sb.Append("}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string EscapeJsonString(string value)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
foreach (char c in value)
|
||||
{
|
||||
sb.Append(c switch
|
||||
{
|
||||
'"' => "\\\"",
|
||||
'\\' => "\\\\",
|
||||
'\b' => "\\b",
|
||||
'\f' => "\\f",
|
||||
'\n' => "\\n",
|
||||
'\r' => "\\r",
|
||||
'\t' => "\\t",
|
||||
_ => c < 32 ? $"\\u{(int)c:X4}" : c.ToString()
|
||||
});
|
||||
}
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for type destructuring
|
||||
/// </summary>
|
||||
public static class DestructuringExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts an object to its destructured JSON representation
|
||||
/// </summary>
|
||||
public static string ToDestructuredJson(this object? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
|
||||
var destructor = new TypeDestructor();
|
||||
return destructor.Destructure(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Escapes special characters in a string for JSON output
|
||||
/// </summary>
|
||||
public static string ToJsonString(this string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append("\"");
|
||||
foreach (char c in value)
|
||||
{
|
||||
sb.Append(c switch
|
||||
{
|
||||
'"' => "\\\"",
|
||||
'\\' => "\\\\",
|
||||
'\b' => "\\b",
|
||||
'\f' => "\\f",
|
||||
'\n' => "\\n",
|
||||
'\r' => "\\r",
|
||||
'\t' => "\\t",
|
||||
_ => c < 32 ? $"\\u{(int)c:X4}" : c.ToString()
|
||||
});
|
||||
}
|
||||
sb.Append("\"");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension for ISO 8601 formatting
|
||||
/// </summary>
|
||||
public static class DateTimeExtensionForDestructuring
|
||||
{
|
||||
public static string ToIso8601String(this DateTime dt)
|
||||
{
|
||||
return dt.ToString("yyyy-MM-ddTHH:mm:ss.fffZ");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,349 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace EonaCat.LogStack.Diagnostics;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive logging diagnostics and metrics collection
|
||||
/// </summary>
|
||||
public sealed class LoggerDiagnosticsCollector
|
||||
{
|
||||
private readonly Stopwatch _uptime = Stopwatch.StartNew();
|
||||
private long _totalEventsProcessed;
|
||||
private long _totalEventsSampled;
|
||||
private long _totalEventsFiltered;
|
||||
private long _totalErrors;
|
||||
private DateTime _startTime = DateTime.UtcNow;
|
||||
private readonly object _lock = new object();
|
||||
|
||||
private readonly Dictionary<string, FlowMetrics> _flowMetrics = new();
|
||||
private readonly Dictionary<string, BoosterMetrics> _boosterMetrics = new();
|
||||
private readonly Dictionary<LogLevel, long> _levelCounts = new();
|
||||
private readonly List<EventLatency> _recentLatencies = new();
|
||||
private const int MaxLatencyHistory = 1000;
|
||||
|
||||
public LoggerDiagnosticsCollector()
|
||||
{
|
||||
foreach (LogLevel level in Enum.GetValues(typeof(LogLevel)))
|
||||
{
|
||||
_levelCounts[level] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an event being processed
|
||||
/// </summary>
|
||||
public void RecordEventProcessed(LogEvent logEvent, long elapsedMilliseconds)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_totalEventsProcessed++;
|
||||
|
||||
if (_levelCounts.ContainsKey(logEvent.Level))
|
||||
{
|
||||
_levelCounts[logEvent.Level]++;
|
||||
}
|
||||
|
||||
// Track latency
|
||||
_recentLatencies.Add(new EventLatency
|
||||
{
|
||||
Level = logEvent.Level,
|
||||
ElapsedMs = elapsedMilliseconds,
|
||||
Timestamp = DateTime.UtcNow
|
||||
});
|
||||
|
||||
if (_recentLatencies.Count > MaxLatencyHistory)
|
||||
{
|
||||
_recentLatencies.RemoveAt(0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a sampled event
|
||||
/// </summary>
|
||||
public void RecordSampled()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_totalEventsSampled++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records a filtered event
|
||||
/// </summary>
|
||||
public void RecordFiltered()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_totalEventsFiltered++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records an error
|
||||
/// </summary>
|
||||
public void RecordError()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_totalErrors++;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records metrics for a flow
|
||||
/// </summary>
|
||||
public void RecordFlowMetrics(string flowName, int eventsProcessed, int eventsFailed, long bytesWritten)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_flowMetrics.TryGetValue(flowName, out var metrics))
|
||||
{
|
||||
metrics = new FlowMetrics { Name = flowName };
|
||||
_flowMetrics[flowName] = metrics;
|
||||
}
|
||||
|
||||
metrics.EventsProcessed += eventsProcessed;
|
||||
metrics.EventsFailed += eventsFailed;
|
||||
metrics.BytesWritten += bytesWritten;
|
||||
metrics.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Records metrics for a booster
|
||||
/// </summary>
|
||||
public void RecordBoosterMetrics(string boosterName, int eventsProcessed, int eventsFiltered)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_boosterMetrics.TryGetValue(boosterName, out var metrics))
|
||||
{
|
||||
metrics = new BoosterMetrics { Name = boosterName };
|
||||
_boosterMetrics[boosterName] = metrics;
|
||||
}
|
||||
|
||||
metrics.EventsProcessed += eventsProcessed;
|
||||
metrics.EventsFiltered += eventsFiltered;
|
||||
metrics.LastUpdated = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets comprehensive diagnostics
|
||||
/// </summary>
|
||||
public DiagnosticsReport GetReport()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var latencies = _recentLatencies.Count > 0 ? _recentLatencies.ToList() : new List<EventLatency>();
|
||||
|
||||
return new DiagnosticsReport
|
||||
{
|
||||
StartTime = _startTime,
|
||||
Uptime = _uptime.Elapsed,
|
||||
TotalEventsProcessed = _totalEventsProcessed,
|
||||
TotalEventsSampled = _totalEventsSampled,
|
||||
TotalEventsFiltered = _totalEventsFiltered,
|
||||
TotalErrors = _totalErrors,
|
||||
EventsByLevel = new Dictionary<LogLevel, long>(_levelCounts),
|
||||
FlowMetrics = _flowMetrics.Values.ToList(),
|
||||
BoosterMetrics = _boosterMetrics.Values.ToList(),
|
||||
RecentLatencies = latencies,
|
||||
MemoryUsage = GC.GetTotalMemory(false),
|
||||
ManagedThreadCount = System.Diagnostics.Process.GetCurrentProcess().Threads.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a summary report
|
||||
/// </summary>
|
||||
public DiagnosticsSummary GetSummary()
|
||||
{
|
||||
var report = GetReport();
|
||||
|
||||
double successRate = report.TotalEventsProcessed > 0
|
||||
? ((report.TotalEventsProcessed - report.TotalErrors) * 100.0) / report.TotalEventsProcessed
|
||||
: 0.0;
|
||||
|
||||
double samplingRate = report.TotalEventsProcessed > 0
|
||||
? (report.TotalEventsSampled * 100.0) / report.TotalEventsProcessed
|
||||
: 0.0;
|
||||
|
||||
var avgLatency = report.RecentLatencies.Count > 0
|
||||
? report.RecentLatencies.Average(l => l.ElapsedMs)
|
||||
: 0.0;
|
||||
|
||||
var maxLatency = report.RecentLatencies.Count > 0
|
||||
? report.RecentLatencies.Max(l => l.ElapsedMs)
|
||||
: 0L;
|
||||
|
||||
return new DiagnosticsSummary
|
||||
{
|
||||
Uptime = report.Uptime,
|
||||
TotalEvents = report.TotalEventsProcessed,
|
||||
SuccessRate = successRate,
|
||||
ErrorRate = 100.0 - successRate,
|
||||
SamplingRate = samplingRate,
|
||||
FilteringRate = report.TotalEventsFiltered > 0
|
||||
? (report.TotalEventsFiltered * 100.0) / report.TotalEventsProcessed
|
||||
: 0.0,
|
||||
EventsPerSecond = report.Uptime.TotalSeconds > 0
|
||||
? report.TotalEventsProcessed / report.Uptime.TotalSeconds
|
||||
: 0.0,
|
||||
AverageLatencyMs = avgLatency,
|
||||
MaxLatencyMs = maxLatency,
|
||||
MemoryUsageMb = report.MemoryUsage / (1024 * 1024.0),
|
||||
ThreadCount = report.ManagedThreadCount
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Generates a text report
|
||||
/// </summary>
|
||||
public string GenerateTextReport()
|
||||
{
|
||||
var summary = GetSummary();
|
||||
var report = GetReport();
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("╔══════════════════════════════════════════════════════════╗");
|
||||
sb.AppendLine("║ EonaCat.LogStack Diagnostics Report ║");
|
||||
sb.AppendLine("╚══════════════════════════════════════════════════════════╝");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("📊 Overview");
|
||||
sb.AppendLine($" Uptime: {summary.Uptime:hh\\:mm\\:ss}");
|
||||
sb.AppendLine($" Total Events: {summary.TotalEvents:N0}");
|
||||
sb.AppendLine($" Events/Second: {summary.EventsPerSecond:F2}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("📈 Quality Metrics");
|
||||
sb.AppendLine($" Success Rate: {summary.SuccessRate:F2}%");
|
||||
sb.AppendLine($" Error Rate: {summary.ErrorRate:F2}%");
|
||||
sb.AppendLine($" Sampling Rate: {summary.SamplingRate:F2}%");
|
||||
sb.AppendLine($" Filtering Rate: {summary.FilteringRate:F2}%");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("⚡ Performance");
|
||||
sb.AppendLine($" Avg Latency: {summary.AverageLatencyMs:F2}ms");
|
||||
sb.AppendLine($" Max Latency: {summary.MaxLatencyMs}ms");
|
||||
sb.AppendLine($" Memory Usage: {summary.MemoryUsageMb:F2}MB");
|
||||
sb.AppendLine($" Threads: {summary.ThreadCount}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("📋 Events by Level");
|
||||
foreach (var kvp in report.EventsByLevel.OrderByDescending(x => x.Value))
|
||||
{
|
||||
sb.AppendLine($" {kvp.Key,-12} {kvp.Value:N0}");
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
if (report.FlowMetrics.Count > 0)
|
||||
{
|
||||
sb.AppendLine("🚀 Flow Metrics");
|
||||
foreach (var flow in report.FlowMetrics)
|
||||
{
|
||||
sb.AppendLine($" {flow.Name}");
|
||||
sb.AppendLine($" Processed: {flow.EventsProcessed:N0}");
|
||||
sb.AppendLine($" Failed: {flow.EventsFailed}");
|
||||
sb.AppendLine($" Bytes: {flow.BytesWritten:N0}");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
if (report.BoosterMetrics.Count > 0)
|
||||
{
|
||||
sb.AppendLine("⚙️ Booster Metrics");
|
||||
foreach (var booster in report.BoosterMetrics)
|
||||
{
|
||||
sb.AppendLine($" {booster.Name}");
|
||||
sb.AppendLine($" Processed: {booster.EventsProcessed:N0}");
|
||||
sb.AppendLine($" Filtered: {booster.EventsFiltered:N0}");
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual flow metrics
|
||||
/// </summary>
|
||||
public class FlowMetrics
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public long EventsProcessed { get; set; }
|
||||
public long EventsFailed { get; set; }
|
||||
public long BytesWritten { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual booster metrics
|
||||
/// </summary>
|
||||
public class BoosterMetrics
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public long EventsProcessed { get; set; }
|
||||
public long EventsFiltered { get; set; }
|
||||
public DateTime LastUpdated { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Detailed diagnostics report
|
||||
/// </summary>
|
||||
public class DiagnosticsReport
|
||||
{
|
||||
public DateTime StartTime { get; set; }
|
||||
public TimeSpan Uptime { get; set; }
|
||||
public long TotalEventsProcessed { get; set; }
|
||||
public long TotalEventsSampled { get; set; }
|
||||
public long TotalEventsFiltered { get; set; }
|
||||
public long TotalErrors { get; set; }
|
||||
public Dictionary<LogLevel, long> EventsByLevel { get; set; } = new();
|
||||
public List<FlowMetrics> FlowMetrics { get; set; } = new();
|
||||
public List<BoosterMetrics> BoosterMetrics { get; set; } = new();
|
||||
public List<EventLatency> RecentLatencies { get; set; } = new();
|
||||
public long MemoryUsage { get; set; }
|
||||
public int ManagedThreadCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Summary diagnostics
|
||||
/// </summary>
|
||||
public class DiagnosticsSummary
|
||||
{
|
||||
public TimeSpan Uptime { get; set; }
|
||||
public long TotalEvents { get; set; }
|
||||
public double SuccessRate { get; set; }
|
||||
public double ErrorRate { get; set; }
|
||||
public double SamplingRate { get; set; }
|
||||
public double FilteringRate { get; set; }
|
||||
public double EventsPerSecond { get; set; }
|
||||
public double AverageLatencyMs { get; set; }
|
||||
public long MaxLatencyMs { get; set; }
|
||||
public double MemoryUsageMb { get; set; }
|
||||
public int ThreadCount { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Individual event latency record
|
||||
/// </summary>
|
||||
public class EventLatency
|
||||
{
|
||||
public LogLevel Level { get; set; }
|
||||
public long ElapsedMs { get; set; }
|
||||
public DateTime Timestamp { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Threading;
|
||||
|
||||
namespace EonaCat.LogStack.Core;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe runtime log-level controller.
|
||||
///
|
||||
/// Attach an instance to a logger via <c>logger.UseDynamicLevel(controller)</c>.
|
||||
/// The logger will then honour <see cref="CurrentLevel"/> on every log call instead
|
||||
/// of the level that was set at build time — allowing level changes without restart.
|
||||
///
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var levelCtrl = new DynamicLevelController(LogLevel.Information);
|
||||
/// var logger = new LogBuilder()
|
||||
/// .WriteToConsole()
|
||||
/// .Build()
|
||||
/// .UseDynamicLevel(levelCtrl);
|
||||
///
|
||||
/// // Later, crank up verbosity for debugging:
|
||||
/// levelCtrl.CurrentLevel = LogLevel.Trace;
|
||||
///
|
||||
/// // Or subscribe to changes:
|
||||
/// levelCtrl.LevelChanged += (old, @new) => Console.WriteLine($"Level: {old} → {@new}");
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </summary>
|
||||
public sealed class DynamicLevelController
|
||||
{
|
||||
// Store as int for Interlocked operations
|
||||
private int _level;
|
||||
|
||||
/// <summary>Fires whenever the level changes, passing (oldLevel, newLevel).</summary>
|
||||
public event Action<LogLevel, LogLevel>? LevelChanged;
|
||||
|
||||
public DynamicLevelController(LogLevel initialLevel = LogLevel.Information)
|
||||
{
|
||||
_level = (int)initialLevel;
|
||||
}
|
||||
|
||||
/// <summary>Gets or sets the current minimum log level.</summary>
|
||||
public LogLevel CurrentLevel
|
||||
{
|
||||
get => (LogLevel)Volatile.Read(ref _level);
|
||||
set
|
||||
{
|
||||
var old = (LogLevel)Interlocked.Exchange(ref _level, (int)value);
|
||||
if (old != value)
|
||||
{
|
||||
LevelChanged?.Invoke(old, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Temporarily raises the level to <paramref name="level"/> and returns a handle
|
||||
/// that restores the previous level when disposed.
|
||||
/// Thread-safe for nested/concurrent usage.
|
||||
/// </summary>
|
||||
public IDisposable TemporaryLevel(LogLevel level)
|
||||
{
|
||||
var previous = CurrentLevel;
|
||||
CurrentLevel = level;
|
||||
return new RestoreHandle(this, previous);
|
||||
}
|
||||
|
||||
private sealed class RestoreHandle : IDisposable
|
||||
{
|
||||
private readonly DynamicLevelController _ctrl;
|
||||
private readonly LogLevel _restore;
|
||||
private int _disposed;
|
||||
|
||||
public RestoreHandle(DynamicLevelController ctrl, LogLevel restore)
|
||||
{
|
||||
_ctrl = ctrl;
|
||||
_restore = restore;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (Interlocked.Exchange(ref _disposed, 1) == 0)
|
||||
{
|
||||
_ctrl.CurrentLevel = _restore;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,382 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace EonaCat.LogStack.Exceptions;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Advanced exception rendering with context tracking
|
||||
/// </summary>
|
||||
public sealed class AdvancedExceptionRenderer
|
||||
{
|
||||
private readonly Dictionary<Exception, ExceptionContext> _contextMap = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers context information for an exception
|
||||
/// </summary>
|
||||
public void RegisterContext(Exception ex, ExceptionContext context)
|
||||
{
|
||||
_contextMap[ex] = context;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders an exception with all available context
|
||||
/// </summary>
|
||||
public string RenderFull(Exception ex)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
RenderException(sb, ex, 0, true);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders an exception with minimal detail
|
||||
/// </summary>
|
||||
public string RenderShort(Exception ex)
|
||||
{
|
||||
return $"{ex.GetType().Name}: {ex.Message}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders just the stack trace with source information
|
||||
/// </summary>
|
||||
public string RenderStackTrace(Exception ex)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
RenderStackTrace(sb, ex);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void RenderException(StringBuilder sb, Exception ex, int depth, bool includeContext)
|
||||
{
|
||||
string indent = new string(' ', depth * 2);
|
||||
|
||||
// Exception header
|
||||
sb.Append(indent).Append(ex.GetType().FullName).Append(": ").Append(ex.Message).AppendLine();
|
||||
|
||||
// Stack trace
|
||||
if (!string.IsNullOrEmpty(ex.StackTrace))
|
||||
{
|
||||
RenderStackTrace(sb, ex);
|
||||
}
|
||||
|
||||
// Context information
|
||||
if (includeContext && _contextMap.TryGetValue(ex, out var context))
|
||||
{
|
||||
sb.Append(indent).Append("Context:").AppendLine();
|
||||
if (context.CorrelationId != null)
|
||||
{
|
||||
sb.Append(indent).Append(" CorrelationId: ").Append(context.CorrelationId).AppendLine();
|
||||
}
|
||||
|
||||
if (context.UserId != null)
|
||||
{
|
||||
sb.Append(indent).Append(" UserId: ").Append(context.UserId).AppendLine();
|
||||
}
|
||||
|
||||
if (context.RequestPath != null)
|
||||
{
|
||||
sb.Append(indent).Append(" RequestPath: ").Append(context.RequestPath).AppendLine();
|
||||
}
|
||||
|
||||
if (context.CustomData != null)
|
||||
{
|
||||
sb.Append(indent).Append(" CustomData:").AppendLine();
|
||||
foreach (var kvp in context.CustomData)
|
||||
{
|
||||
sb.Append(indent).Append(" ").Append(kvp.Key).Append(": ").Append(kvp.Value).AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Inner exceptions
|
||||
if (ex.InnerException != null)
|
||||
{
|
||||
sb.Append(indent).Append("InnerException:").AppendLine();
|
||||
RenderException(sb, ex.InnerException, depth + 1, includeContext);
|
||||
}
|
||||
}
|
||||
|
||||
private void RenderStackTrace(StringBuilder sb, Exception ex)
|
||||
{
|
||||
string indent = new string(' ', 4);
|
||||
var frames = new StackTrace(ex, true).GetFrames() ?? Array.Empty<StackFrame>();
|
||||
|
||||
foreach (var frame in frames)
|
||||
{
|
||||
var method = frame.GetMethod();
|
||||
if (method == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
sb.Append(indent).Append("at ");
|
||||
|
||||
// Type and method
|
||||
if (method.DeclaringType != null)
|
||||
{
|
||||
sb.Append(method.DeclaringType.Name).Append(".");
|
||||
}
|
||||
sb.Append(method.Name);
|
||||
|
||||
// Parameters
|
||||
var parameters = method.GetParameters();
|
||||
if (parameters.Length > 0)
|
||||
{
|
||||
sb.Append("(");
|
||||
for (int i = 0; i < parameters.Length; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
sb.Append(parameters[i].ParameterType.Name);
|
||||
}
|
||||
sb.Append(")");
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("()");
|
||||
}
|
||||
|
||||
// File and line number
|
||||
string fileName = frame.GetFileName();
|
||||
if (!string.IsNullOrEmpty(fileName))
|
||||
{
|
||||
int lineNumber = frame.GetFileLineNumber();
|
||||
sb.Append(" in ").Append(Path.GetFileName(fileName)).Append(":line ").Append(lineNumber);
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Context information associated with an exception
|
||||
/// </summary>
|
||||
public sealed class ExceptionContext
|
||||
{
|
||||
public string? CorrelationId { get; set; }
|
||||
public string? UserId { get; set; }
|
||||
public string? RequestPath { get; set; }
|
||||
public Dictionary<string, object?>? CustomData { get; set; }
|
||||
public DateTime OccurredAt { get; set; }
|
||||
|
||||
public ExceptionContext()
|
||||
{
|
||||
OccurredAt = DateTime.UtcNow;
|
||||
CustomData = new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder pattern for fluent context creation
|
||||
/// </summary>
|
||||
public static ExceptionContextBuilder Create()
|
||||
{
|
||||
return new ExceptionContextBuilder();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for exception context
|
||||
/// </summary>
|
||||
public sealed class ExceptionContextBuilder
|
||||
{
|
||||
private readonly ExceptionContext _context = new();
|
||||
|
||||
public ExceptionContextBuilder WithCorrelationId(string id)
|
||||
{
|
||||
_context.CorrelationId = id;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExceptionContextBuilder WithUserId(string userId)
|
||||
{
|
||||
_context.UserId = userId;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExceptionContextBuilder WithRequestPath(string path)
|
||||
{
|
||||
_context.RequestPath = path;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExceptionContextBuilder WithCustomData(string key, object? value)
|
||||
{
|
||||
_context.CustomData![key] = value;
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExceptionContextBuilder WithCustomData(Dictionary<string, object?> data)
|
||||
{
|
||||
if (data != null)
|
||||
{
|
||||
foreach (var kvp in data)
|
||||
{
|
||||
_context.CustomData![kvp.Key] = kvp.Value;
|
||||
}
|
||||
}
|
||||
return this;
|
||||
}
|
||||
|
||||
public ExceptionContext Build()
|
||||
{
|
||||
return _context;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Global exception context manager
|
||||
/// </summary>
|
||||
public static class ExceptionContextManager
|
||||
{
|
||||
private static readonly object _lock = new object();
|
||||
private static readonly Dictionary<Exception, ExceptionContext> _contextMap = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or creates context for an exception
|
||||
/// </summary>
|
||||
public static ExceptionContext GetOrCreateContext(Exception ex)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_contextMap.TryGetValue(ex, out var context))
|
||||
{
|
||||
context = new ExceptionContext();
|
||||
_contextMap[ex] = context;
|
||||
}
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enriches an exception with context
|
||||
/// </summary>
|
||||
public static Exception WithContext(this Exception ex, Action<ExceptionContextBuilder> configure)
|
||||
{
|
||||
var builder = ExceptionContext.Create();
|
||||
configure(builder);
|
||||
lock (_lock)
|
||||
{
|
||||
_contextMap[ex] = builder.Build();
|
||||
}
|
||||
return ex;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets context for an exception, or null if none exists
|
||||
/// </summary>
|
||||
public static ExceptionContext? TryGetContext(Exception ex)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_contextMap.TryGetValue(ex, out var context);
|
||||
return context;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all stored contexts
|
||||
/// </summary>
|
||||
public static void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_contextMap.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception aggregator for batch exception handling
|
||||
/// </summary>
|
||||
public sealed class ExceptionAggregator
|
||||
{
|
||||
private readonly List<Exception> _exceptions = new();
|
||||
private readonly string _message;
|
||||
|
||||
public ExceptionAggregator(string message = "One or more exceptions occurred")
|
||||
{
|
||||
_message = message;
|
||||
}
|
||||
|
||||
public void Add(Exception ex)
|
||||
{
|
||||
if (ex != null)
|
||||
{
|
||||
_exceptions.Add(ex);
|
||||
}
|
||||
}
|
||||
|
||||
public void Add(IEnumerable<Exception> exceptions)
|
||||
{
|
||||
if (exceptions != null)
|
||||
{
|
||||
_exceptions.AddRange(exceptions.Where(e => e != null));
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasExceptions => _exceptions.Count > 0;
|
||||
|
||||
public int Count => _exceptions.Count;
|
||||
|
||||
public void ThrowIfHasExceptions()
|
||||
{
|
||||
if (_exceptions.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_exceptions.Count == 1)
|
||||
{
|
||||
throw _exceptions[0];
|
||||
}
|
||||
|
||||
throw new AggregateException(_message, _exceptions);
|
||||
}
|
||||
|
||||
public IReadOnlyList<Exception> GetExceptions()
|
||||
{
|
||||
return _exceptions.AsReadOnly();
|
||||
}
|
||||
|
||||
public string GetSummary()
|
||||
{
|
||||
if (_exceptions.Count == 0)
|
||||
{
|
||||
return "No exceptions";
|
||||
}
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append(_message).Append(" (").Append(_exceptions.Count).AppendLine("):");
|
||||
|
||||
for (int i = 0; i < _exceptions.Count && i < 10; i++)
|
||||
{
|
||||
var ex = _exceptions[i];
|
||||
sb.Append($" [{i + 1}] {ex.GetType().Name}: {ex.Message}").AppendLine();
|
||||
}
|
||||
|
||||
if (_exceptions.Count > 10)
|
||||
{
|
||||
sb.Append($" ... and {_exceptions.Count - 10} more").AppendLine();
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_exceptions.Clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using EonaCat.LogStack.Core;
|
||||
|
||||
namespace EonaCat.LogStack.Filtering;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Advanced filtering conditions beyond simple log levels
|
||||
/// </summary>
|
||||
public abstract class FilterCondition
|
||||
{
|
||||
public abstract bool Matches(LogEvent logEvent, Dictionary<string, object?> properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters on log level
|
||||
/// </summary>
|
||||
public sealed class LogLevelFilter : FilterCondition
|
||||
{
|
||||
private readonly LogLevel _minLevel;
|
||||
private readonly LogLevel _maxLevel;
|
||||
|
||||
public LogLevelFilter(LogLevel minLevel, LogLevel maxLevel = LogLevel.Critical)
|
||||
{
|
||||
_minLevel = minLevel;
|
||||
_maxLevel = maxLevel;
|
||||
}
|
||||
|
||||
public override bool Matches(LogEvent logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
return logEvent.Level >= _minLevel && logEvent.Level <= _maxLevel;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters based on logger name pattern (supports wildcards)
|
||||
/// </summary>
|
||||
public sealed class LoggerNameFilter : FilterCondition
|
||||
{
|
||||
private readonly string _pattern;
|
||||
private readonly bool _negate;
|
||||
|
||||
public LoggerNameFilter(string pattern, bool negate = false)
|
||||
{
|
||||
_pattern = pattern ?? "";
|
||||
_negate = negate;
|
||||
}
|
||||
|
||||
public override bool Matches(LogEvent logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
// Logger name checking would be done here when LogEvent has LoggerName property available
|
||||
// For now, return based on negate flag
|
||||
return _negate;
|
||||
}
|
||||
|
||||
private bool WildcardMatch(string text, string pattern)
|
||||
{
|
||||
if (pattern == "*")
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
int textIndex = 0;
|
||||
int patternIndex = 0;
|
||||
|
||||
while (textIndex < text.Length && patternIndex < pattern.Length)
|
||||
{
|
||||
if (pattern[patternIndex] == '*')
|
||||
{
|
||||
if (patternIndex + 1 >= pattern.Length)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var nextChar = pattern[patternIndex + 1];
|
||||
while (textIndex < text.Length && text[textIndex] != nextChar)
|
||||
{
|
||||
textIndex++;
|
||||
}
|
||||
|
||||
if (textIndex >= text.Length)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
patternIndex++;
|
||||
}
|
||||
else if (pattern[patternIndex] == '?')
|
||||
{
|
||||
textIndex++;
|
||||
patternIndex++;
|
||||
}
|
||||
else if (pattern[patternIndex] == text[textIndex])
|
||||
{
|
||||
textIndex++;
|
||||
patternIndex++;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
while (patternIndex < pattern.Length && pattern[patternIndex] == '*')
|
||||
{
|
||||
patternIndex++;
|
||||
}
|
||||
|
||||
return textIndex == text.Length && patternIndex == pattern.Length;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters on message content
|
||||
/// </summary>
|
||||
public sealed class MessageFilter : FilterCondition
|
||||
{
|
||||
private readonly string _contains;
|
||||
private readonly bool _caseSensitive;
|
||||
private readonly bool _negate;
|
||||
|
||||
public MessageFilter(string contains, bool caseSensitive = false, bool negate = false)
|
||||
{
|
||||
_contains = contains ?? "";
|
||||
_caseSensitive = caseSensitive;
|
||||
_negate = negate;
|
||||
}
|
||||
|
||||
public override bool Matches(LogEvent logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
string message = "";
|
||||
var comparison = _caseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
|
||||
bool matches = message.Contains(_contains, comparison);
|
||||
return _negate ? !matches : matches;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters on exception type
|
||||
/// </summary>
|
||||
public sealed class ExceptionTypeFilter : FilterCondition
|
||||
{
|
||||
private readonly Type _exceptionType;
|
||||
private readonly bool _includeInherited;
|
||||
private readonly bool _negate;
|
||||
|
||||
public ExceptionTypeFilter(Type exceptionType, bool includeInherited = true, bool negate = false)
|
||||
{
|
||||
_exceptionType = exceptionType;
|
||||
_includeInherited = includeInherited;
|
||||
_negate = negate;
|
||||
}
|
||||
|
||||
public override bool Matches(LogEvent logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
// Exception checking would be done here when LogEvent has Exception property available
|
||||
// For now, return based on negate flag
|
||||
return _negate;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filters on property values
|
||||
/// </summary>
|
||||
public sealed class PropertyFilter : FilterCondition
|
||||
{
|
||||
private readonly string _propertyName;
|
||||
private readonly Func<object?, bool> _predicate;
|
||||
|
||||
public PropertyFilter(string propertyName, Func<object?, bool> predicate)
|
||||
{
|
||||
_propertyName = propertyName;
|
||||
_predicate = predicate;
|
||||
}
|
||||
|
||||
public override bool Matches(LogEvent logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
if (!properties.TryGetValue(_propertyName, out var value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
return _predicate(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composite filter with AND logic
|
||||
/// </summary>
|
||||
public sealed class AndFilter : FilterCondition
|
||||
{
|
||||
private readonly List<FilterCondition> _conditions;
|
||||
|
||||
public AndFilter(params FilterCondition[] conditions)
|
||||
{
|
||||
_conditions = new List<FilterCondition>(conditions);
|
||||
}
|
||||
|
||||
public override bool Matches(LogEvent logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
return _conditions.All(c => c.Matches(logEvent, properties));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composite filter with OR logic
|
||||
/// </summary>
|
||||
public sealed class OrFilter : FilterCondition
|
||||
{
|
||||
private readonly List<FilterCondition> _conditions;
|
||||
|
||||
public OrFilter(params FilterCondition[] conditions)
|
||||
{
|
||||
_conditions = new List<FilterCondition>(conditions);
|
||||
}
|
||||
|
||||
public override bool Matches(LogEvent logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
return _conditions.Any(c => c.Matches(logEvent, properties));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Composite filter with NOT logic
|
||||
/// </summary>
|
||||
public sealed class NotFilter : FilterCondition
|
||||
{
|
||||
private readonly FilterCondition _condition;
|
||||
|
||||
public NotFilter(FilterCondition condition)
|
||||
{
|
||||
_condition = condition;
|
||||
}
|
||||
|
||||
public override bool Matches(LogEvent logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
return !_condition.Matches(logEvent, properties);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fluent filter builder
|
||||
/// </summary>
|
||||
public sealed class FilterBuilder
|
||||
{
|
||||
private FilterCondition? _condition;
|
||||
|
||||
public FilterBuilder MinLevel(string levelName)
|
||||
{
|
||||
// When level enum is available in scope, use it here
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder MaxLevel(string levelName)
|
||||
{
|
||||
// When level enum is available in scope, use it here
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder Level(string min, string max)
|
||||
{
|
||||
// When level enum is available in scope, use it here
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder LoggerName(string pattern)
|
||||
{
|
||||
_condition = And(_condition, new LoggerNameFilter(pattern));
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder NotLoggerName(string pattern)
|
||||
{
|
||||
_condition = And(_condition, new LoggerNameFilter(pattern, true));
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder MessageContains(string text, bool caseSensitive = false)
|
||||
{
|
||||
_condition = And(_condition, new MessageFilter(text, caseSensitive));
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder MessageNotContains(string text, bool caseSensitive = false)
|
||||
{
|
||||
_condition = And(_condition, new MessageFilter(text, caseSensitive, true));
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder ExceptionType(Type exceptionType, bool includeInherited = true)
|
||||
{
|
||||
_condition = And(_condition, new ExceptionTypeFilter(exceptionType, includeInherited));
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder ExceptionType<TException>(bool includeInherited = true) where TException : Exception
|
||||
{
|
||||
_condition = And(_condition, new ExceptionTypeFilter(typeof(TException), includeInherited));
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder Property(string propertyName, Func<object?, bool> predicate)
|
||||
{
|
||||
_condition = And(_condition, new PropertyFilter(propertyName, predicate));
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder Property(string propertyName, object value)
|
||||
{
|
||||
_condition = And(_condition, new PropertyFilter(propertyName, v => Equals(v, value)));
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterBuilder Or(FilterBuilder other)
|
||||
{
|
||||
if (_condition == null || other._condition == null)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
_condition = new OrFilter(_condition, other._condition);
|
||||
return this;
|
||||
}
|
||||
|
||||
public FilterCondition? Build()
|
||||
{
|
||||
return _condition;
|
||||
}
|
||||
|
||||
private static FilterCondition And(FilterCondition? left, FilterCondition right)
|
||||
{
|
||||
return left == null ? right : new AndFilter(left, right);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Manager for applying filters to events
|
||||
/// </summary>
|
||||
public sealed class FilterManager
|
||||
{
|
||||
private readonly List<(string name, FilterCondition condition, FilterAction action)> _filters = new();
|
||||
|
||||
/// <summary>
|
||||
/// Registers a filter with an action
|
||||
/// </summary>
|
||||
public FilterManager Register(string name, FilterCondition condition, FilterAction action)
|
||||
{
|
||||
_filters.Add((name, condition, action));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Evaluates all registered filters
|
||||
/// </summary>
|
||||
public FilterAction Evaluate(LogEvent logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
foreach (var (name, condition, action) in _filters)
|
||||
{
|
||||
if (condition.Matches(logEvent, properties))
|
||||
{
|
||||
return action;
|
||||
}
|
||||
}
|
||||
|
||||
return FilterAction.Continue;
|
||||
}
|
||||
|
||||
public int Count => _filters.Count;
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
_filters.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Filter actions
|
||||
/// </summary>
|
||||
public enum FilterAction
|
||||
{
|
||||
Continue, // Process normally
|
||||
Ignore, // Skip this event
|
||||
Critical, // Mark as critical
|
||||
Suppress, // Don't log to standard output, only to filters
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.LogStack.Flows;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Circuit-breaker state machine.
|
||||
/// </summary>
|
||||
public enum CircuitState
|
||||
{
|
||||
/// <summary>Flow is healthy; all events are forwarded.</summary>
|
||||
Closed,
|
||||
/// <summary>Too many failures; events are dropped to protect the inner flow.</summary>
|
||||
Open,
|
||||
/// <summary>Recovery probe in progress; one event is let through per interval.</summary>
|
||||
HalfOpen
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A decorator flow that implements the Circuit Breaker pattern around any inner flow.
|
||||
///
|
||||
/// Transitions:
|
||||
/// Closed → Open : after <see cref="FailureThreshold"/> consecutive failures
|
||||
/// Open → HalfOpen : after <see cref="RecoveryTimeout"/> has elapsed
|
||||
/// HalfOpen→ Closed : on a successful probe write
|
||||
/// HalfOpen→ Open : on a failed probe write
|
||||
///
|
||||
/// When Open, <see cref="BlastAsync"/> returns <see cref="WriteResult.Dropped"/> immediately
|
||||
/// without touching the inner flow — preventing cascades into broken endpoints.
|
||||
///
|
||||
/// The <see cref="StateChanged"/> event fires on every state transition.
|
||||
/// </summary>
|
||||
public sealed class CircuitBreakerFlow : FlowBase
|
||||
{
|
||||
private readonly IFlow _inner;
|
||||
private readonly int _failureThreshold;
|
||||
private readonly TimeSpan _recoveryTimeout;
|
||||
|
||||
private volatile int _consecutiveFailures;
|
||||
private volatile CircuitState _state = CircuitState.Closed;
|
||||
|
||||
// Stopwatch restarted every time we enter Open state
|
||||
private readonly Stopwatch _openedAt = new Stopwatch();
|
||||
|
||||
private readonly object _stateLock = new object();
|
||||
|
||||
/// <summary>Fires whenever the circuit state changes (from, to).</summary>
|
||||
public event Action<CircuitState, CircuitState> StateChanged;
|
||||
|
||||
public CircuitBreakerFlow(
|
||||
IFlow inner,
|
||||
int failureThreshold = 5,
|
||||
TimeSpan? recoveryTimeout = null,
|
||||
LogLevel minimumLevel = LogLevel.Trace)
|
||||
: base($"CircuitBreaker({(inner != null ? inner.Name : "?")})", minimumLevel)
|
||||
{
|
||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||
_failureThreshold = failureThreshold > 0 ? failureThreshold : 5;
|
||||
_recoveryTimeout = recoveryTimeout ?? TimeSpan.FromSeconds(30);
|
||||
}
|
||||
|
||||
/// <summary>Current state of the circuit breaker.</summary>
|
||||
public CircuitState State => _state;
|
||||
|
||||
/// <summary>Number of consecutive failures since the last successful write.</summary>
|
||||
public int ConsecutiveFailures => _consecutiveFailures;
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
|
||||
{
|
||||
return WriteResult.LevelFiltered;
|
||||
}
|
||||
|
||||
var currentState = GetEffectiveState();
|
||||
|
||||
if (currentState == CircuitState.Open)
|
||||
{
|
||||
Interlocked.Increment(ref DroppedCount);
|
||||
return WriteResult.Dropped;
|
||||
}
|
||||
|
||||
WriteResult result;
|
||||
try
|
||||
{
|
||||
result = await _inner.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
result = WriteResult.Failed;
|
||||
}
|
||||
|
||||
if (result == WriteResult.Success)
|
||||
{
|
||||
OnSuccess(currentState);
|
||||
Interlocked.Increment(ref BlastedCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
OnFailure();
|
||||
Interlocked.Increment(ref DroppedCount);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken = default)
|
||||
=> _inner.FlushAsync(cancellationToken);
|
||||
|
||||
public override ValueTask DisposeAsync() => _inner.DisposeAsync();
|
||||
|
||||
private CircuitState GetEffectiveState()
|
||||
{
|
||||
if (_state != CircuitState.Open)
|
||||
{
|
||||
return _state;
|
||||
}
|
||||
|
||||
if (_openedAt.Elapsed >= _recoveryTimeout)
|
||||
{
|
||||
Transition(CircuitState.Open, CircuitState.HalfOpen);
|
||||
return CircuitState.HalfOpen;
|
||||
}
|
||||
|
||||
return CircuitState.Open;
|
||||
}
|
||||
|
||||
private void OnSuccess(CircuitState wasState)
|
||||
{
|
||||
Interlocked.Exchange(ref _consecutiveFailures, 0);
|
||||
if (wasState == CircuitState.HalfOpen)
|
||||
{
|
||||
Transition(CircuitState.HalfOpen, CircuitState.Closed);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnFailure()
|
||||
{
|
||||
var failures = Interlocked.Increment(ref _consecutiveFailures);
|
||||
if (_state == CircuitState.HalfOpen)
|
||||
{
|
||||
Transition(CircuitState.HalfOpen, CircuitState.Open);
|
||||
}
|
||||
else if (_state == CircuitState.Closed && failures >= _failureThreshold)
|
||||
{
|
||||
Transition(CircuitState.Closed, CircuitState.Open);
|
||||
}
|
||||
}
|
||||
|
||||
private void Transition(CircuitState from, CircuitState to)
|
||||
{
|
||||
lock (_stateLock)
|
||||
{
|
||||
if (_state != from)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_state = to;
|
||||
if (to == CircuitState.Open)
|
||||
{
|
||||
_openedAt.Restart();
|
||||
}
|
||||
else if (to == CircuitState.Closed)
|
||||
{
|
||||
_openedAt.Reset();
|
||||
}
|
||||
}
|
||||
StateChanged?.Invoke(from, to);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.LogStack.Flows;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// A decorator flow that forwards events to an inner flow only when a user-supplied
|
||||
/// predicate returns <c>true</c>.
|
||||
///
|
||||
/// Common use-cases:
|
||||
/// • Category-based routing — route only "Database" category events to file
|
||||
/// • Level-range filtering — forward Warning≤level<Error to one flow
|
||||
/// • Property filtering — only forward events that carry a specific property
|
||||
/// • Exception routing — send events with SqlException to a special alert flow
|
||||
///
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// // Route only Database category errors to a dedicated file
|
||||
/// new LogBuilder()
|
||||
/// .WriteToConditional(
|
||||
/// predicate: e => e.Category == "Database" && e.Level >= LogLevel.Error,
|
||||
/// inner: new FileFlow("logs/db-errors"))
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </summary>
|
||||
public sealed class ConditionalFlow : FlowBase
|
||||
{
|
||||
private readonly IFlow _inner;
|
||||
private readonly Func<LogEvent, bool> _predicate;
|
||||
|
||||
public ConditionalFlow(
|
||||
IFlow inner,
|
||||
Func<LogEvent, bool> predicate,
|
||||
LogLevel minimumLevel = LogLevel.Trace)
|
||||
: base($"Conditional({inner?.Name ?? "?"})", minimumLevel)
|
||||
{
|
||||
_inner = inner ?? throw new ArgumentNullException(nameof(inner));
|
||||
_predicate = predicate ?? throw new ArgumentNullException(nameof(predicate));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
|
||||
{
|
||||
return WriteResult.LevelFiltered;
|
||||
}
|
||||
|
||||
bool matches;
|
||||
try { matches = _predicate(logEvent); }
|
||||
catch { matches = false; }
|
||||
|
||||
if (!matches)
|
||||
{
|
||||
Interlocked.Increment(ref DroppedCount);
|
||||
return WriteResult.Dropped;
|
||||
}
|
||||
|
||||
var result = await _inner.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
|
||||
Interlocked.Increment(ref BlastedCount);
|
||||
return result;
|
||||
}
|
||||
|
||||
public override async Task<WriteResult> BlastBatchAsync(
|
||||
ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsEnabled)
|
||||
{
|
||||
return WriteResult.FlowDisabled;
|
||||
}
|
||||
|
||||
var result = WriteResult.Success;
|
||||
var eventsArray = logEvents.ToArray();
|
||||
foreach (var ev in eventsArray)
|
||||
{
|
||||
bool matches;
|
||||
try { matches = ev.Level >= MinimumLevel && _predicate(ev); }
|
||||
catch { matches = false; }
|
||||
|
||||
if (!matches) { Interlocked.Increment(ref DroppedCount); continue; }
|
||||
|
||||
var r = await _inner.BlastAsync(ev, cancellationToken).ConfigureAwait(false);
|
||||
Interlocked.Increment(ref BlastedCount);
|
||||
if (r != WriteResult.Success)
|
||||
{
|
||||
result = r;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public override Task FlushAsync(CancellationToken cancellationToken = default)
|
||||
=> _inner.FlushAsync(cancellationToken);
|
||||
|
||||
public override ValueTask DisposeAsync() => _inner.DisposeAsync();
|
||||
}
|
||||
@@ -0,0 +1,308 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
using EonaCat.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.LogStack.Flows;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Pushes log events to a Grafana Loki instance via the HTTP log-push API
|
||||
/// (<c>POST /loki/api/v1/push</c>).
|
||||
///
|
||||
/// Features:
|
||||
/// • Batching by count and/or flush interval
|
||||
/// • Configurable static labels (stream selectors)
|
||||
/// • Auto-label from event Level and Category
|
||||
/// • Optional HTTP Basic / Bearer authentication
|
||||
/// • Structured JSON line bodies (all properties serialised)
|
||||
/// • Compatible with Loki 2.x and 3.x
|
||||
///
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// new LogBuilder()
|
||||
/// .WriteToLoki(
|
||||
/// lokiUrl: "http://loki:3100",
|
||||
/// labels: new() { ["app"] = "myapp", ["env"] = "prod" },
|
||||
/// batchSize: 100,
|
||||
/// batchIntervalMs: 2000)
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </summary>
|
||||
public sealed class LokiFlow : FlowBase
|
||||
{
|
||||
private const string PushPath = "/loki/api/v1/push";
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly bool _ownsHttpClient;
|
||||
private readonly string _lokiPushUrl;
|
||||
private readonly Dictionary<string, string> _staticLabels;
|
||||
private readonly int _batchSize;
|
||||
private readonly TimeSpan _batchInterval;
|
||||
private readonly ConcurrentQueue<(string Labels, string Line, long NanoTs)> _queue = new();
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Task _flushTask;
|
||||
private readonly SemaphoreSlim _batchSignal = new(0, int.MaxValue);
|
||||
|
||||
public LokiFlow(
|
||||
string lokiUrl,
|
||||
Dictionary<string, string>? labels = null,
|
||||
int batchSize = 50,
|
||||
int batchIntervalMs = 1000,
|
||||
string? bearerToken = null,
|
||||
string? basicUser = null,
|
||||
string? basicPassword = null,
|
||||
HttpClient? httpClient = null,
|
||||
LogLevel minimumLevel = LogLevel.Trace)
|
||||
: base("Loki", minimumLevel)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(lokiUrl))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(lokiUrl));
|
||||
}
|
||||
|
||||
_lokiPushUrl = lokiUrl.TrimEnd('/') + PushPath;
|
||||
_staticLabels = labels ?? new Dictionary<string, string>();
|
||||
_batchSize = batchSize > 0 ? batchSize : 50;
|
||||
_batchInterval = TimeSpan.FromMilliseconds(batchIntervalMs > 0 ? batchIntervalMs : 1000);
|
||||
|
||||
if (httpClient != null)
|
||||
{
|
||||
_http = httpClient;
|
||||
_ownsHttpClient = false;
|
||||
}
|
||||
else
|
||||
{
|
||||
_http = new HttpClient { Timeout = TimeSpan.FromSeconds(15) };
|
||||
_ownsHttpClient = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(bearerToken))
|
||||
{
|
||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", bearerToken);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(basicUser))
|
||||
{
|
||||
var encoded = Convert.ToBase64String(Encoding.UTF8.GetBytes($"{basicUser}:{basicPassword}"));
|
||||
_http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Basic", encoded);
|
||||
}
|
||||
|
||||
_flushTask = Task.Run(() => BackgroundFlushAsync(_cts.Token));
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
|
||||
{
|
||||
return Task.FromResult(WriteResult.LevelFiltered);
|
||||
}
|
||||
|
||||
var labels = BuildLabels(logEvent);
|
||||
var line = BuildLine(logEvent);
|
||||
var nanoTs = logEvent.Timestamp * 100L; // ticks → nanoseconds
|
||||
|
||||
_queue.Enqueue((labels, line, nanoTs));
|
||||
Interlocked.Increment(ref BlastedCount);
|
||||
|
||||
if (_queue.Count >= _batchSize)
|
||||
{
|
||||
_batchSignal.Release(1);
|
||||
}
|
||||
|
||||
return Task.FromResult(WriteResult.Success);
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
await SendBatchAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
IsEnabled = false;
|
||||
_cts.Cancel();
|
||||
try { await _flushTask.ConfigureAwait(false); } catch { }
|
||||
await SendBatchAsync(default).ConfigureAwait(false);
|
||||
if (_ownsHttpClient)
|
||||
{
|
||||
_http.Dispose();
|
||||
}
|
||||
|
||||
_cts.Dispose();
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
|
||||
// Background flush loop
|
||||
private async Task BackgroundFlushAsync(CancellationToken ct)
|
||||
{
|
||||
while (!ct.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _batchSignal.WaitAsync(_batchInterval, ct).ConfigureAwait(false);
|
||||
await SendBatchAsync(ct).ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException) { break; }
|
||||
catch { }
|
||||
}
|
||||
await SendBatchAsync(default).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
private async Task SendBatchAsync(CancellationToken ct)
|
||||
{
|
||||
if (_queue.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Group by label-set (Loki stream key)
|
||||
var groups = new Dictionary<string, List<(string Line, long NanoTs)>>(StringComparer.Ordinal);
|
||||
while (_queue.TryDequeue(out var item))
|
||||
{
|
||||
if (!groups.TryGetValue(item.Labels, out var list))
|
||||
{
|
||||
list = new List<(string, long)>(16);
|
||||
groups[item.Labels] = list;
|
||||
}
|
||||
list.Add((item.Line, item.NanoTs));
|
||||
}
|
||||
|
||||
if (groups.Count == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var body = BuildPushJson(groups);
|
||||
try
|
||||
{
|
||||
var content = new StringContent(body, Encoding.UTF8, "application/json");
|
||||
using var response = await _http.PostAsync(_lokiPushUrl, content, ct).ConfigureAwait(false);
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
Interlocked.Add(ref DroppedCount, SumGroups(groups));
|
||||
}
|
||||
}
|
||||
catch
|
||||
{
|
||||
Interlocked.Add(ref DroppedCount, SumGroups(groups));
|
||||
}
|
||||
}
|
||||
|
||||
private string BuildLabels(LogEvent e)
|
||||
{
|
||||
var sb = new StringBuilder("{");
|
||||
bool first = true;
|
||||
|
||||
foreach (var kv in _staticLabels)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
sb.Append('"').Append(kv.Key).Append("\": \"").Append(kv.Value).Append('"');
|
||||
first = false;
|
||||
}
|
||||
|
||||
// Auto-labels
|
||||
AppendLabel(sb, "level", e.Level.ToString(), ref first);
|
||||
if (e.HasCategory)
|
||||
{
|
||||
AppendLabel(sb, "category", e.Category, ref first);
|
||||
}
|
||||
|
||||
sb.Append('}');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static void AppendLabel(StringBuilder sb, string key, string value, ref bool first)
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
sb.Append('"').Append(key).Append("\": \"").Append(EscapeJson(value)).Append('"');
|
||||
first = false;
|
||||
}
|
||||
|
||||
private string BuildLine(LogEvent e)
|
||||
{
|
||||
using var doc = new Utf8JsonWriter_Builder();
|
||||
var obj = new
|
||||
{
|
||||
level = e.Level.ToString(),
|
||||
category = e.Category,
|
||||
message = e.Message.ToString(),
|
||||
exception = e.Exception?.ToString(),
|
||||
thread = e.ThreadId,
|
||||
props = e.HasProperties ? (object)e.Properties : null
|
||||
};
|
||||
return JsonHelper.ToJson(obj);
|
||||
}
|
||||
|
||||
private static string BuildPushJson(Dictionary<string, List<(string Line, long NanoTs)>> groups)
|
||||
{
|
||||
var sb = new StringBuilder("{\"streams\":[");
|
||||
bool firstStream = true;
|
||||
|
||||
foreach (var kv in groups)
|
||||
{
|
||||
if (!firstStream)
|
||||
{
|
||||
sb.Append(',');
|
||||
}
|
||||
|
||||
firstStream = false;
|
||||
|
||||
sb.Append("{\"stream\":").Append(kv.Key).Append(",\"values\":[");
|
||||
bool firstVal = true;
|
||||
foreach (var (line, nanoTs) in kv.Value)
|
||||
{
|
||||
if (!firstVal)
|
||||
{
|
||||
sb.Append(',');
|
||||
}
|
||||
|
||||
firstVal = false;
|
||||
sb.Append("[\"").Append(nanoTs).Append("\",")
|
||||
.Append(JsonHelper.ToJson(line)).Append(']');
|
||||
}
|
||||
sb.Append("]}");
|
||||
}
|
||||
|
||||
sb.Append("]}");
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private static string EscapeJson(string s)
|
||||
=> s.Replace("\\", "\\\\").Replace("\"", "\\\"");
|
||||
|
||||
private static long SumGroups(Dictionary<string, List<(string, long)>> groups)
|
||||
{
|
||||
long n = 0;
|
||||
foreach (var l in groups.Values)
|
||||
{
|
||||
n += l.Count;
|
||||
}
|
||||
|
||||
return n;
|
||||
}
|
||||
|
||||
// Minimal helper class (unused, kept for future extensibility)
|
||||
private sealed class Utf8JsonWriter_Builder : IDisposable
|
||||
{
|
||||
public void Dispose() { }
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
@@ -177,4 +178,113 @@ public sealed class MemoryFlow : FlowBase
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// LINQ-style Query API
|
||||
/// <summary>
|
||||
/// Returns all buffered events matching an arbitrary predicate.
|
||||
/// The buffer snapshot is taken under lock; the predicate is evaluated outside.
|
||||
/// </summary>
|
||||
public IReadOnlyList<LogEvent> Query(Func<LogEvent, bool> predicate)
|
||||
{
|
||||
if (predicate == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(predicate));
|
||||
}
|
||||
|
||||
return GetEvents().Where(predicate).ToList();
|
||||
}
|
||||
|
||||
/// <summary>Returns all events at or above the given level.</summary>
|
||||
public IReadOnlyList<LogEvent> QueryByLevel(LogLevel minimumLevel)
|
||||
=> Query(e => e.Level >= minimumLevel);
|
||||
|
||||
/// <summary>Returns all events whose Category equals <paramref name="category"/> (case-insensitive).</summary>
|
||||
public IReadOnlyList<LogEvent> QueryByCategory(string category)
|
||||
{
|
||||
if (string.IsNullOrEmpty(category))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(category));
|
||||
}
|
||||
|
||||
return Query(e => string.Equals(e.Category, category, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
/// <summary>Returns all events that carry an exception of (or derived from) <typeparamref name="TException"/>.</summary>
|
||||
public IReadOnlyList<LogEvent> QueryByException<TException>() where TException : Exception
|
||||
=> Query(e => e.HasException && e.Exception is TException);
|
||||
|
||||
/// <summary>Returns all events whose message contains the given substring (case-insensitive).</summary>
|
||||
public IReadOnlyList<LogEvent> QueryByMessage(string substring)
|
||||
{
|
||||
if (substring == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(substring));
|
||||
}
|
||||
|
||||
return Query(e => e.Message.Span.IndexOf(substring.AsSpan(), StringComparison.OrdinalIgnoreCase) >= 0);
|
||||
}
|
||||
|
||||
/// <summary>Returns all events that carry a property with the given key.</summary>
|
||||
public IReadOnlyList<LogEvent> QueryByProperty(string key)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
return Query(e => e.HasProperties && e.Properties.ContainsKey(key));
|
||||
}
|
||||
|
||||
/// <summary>Returns all events that carry a property with a specific key and value.</summary>
|
||||
public IReadOnlyList<LogEvent> QueryByProperty(string key, object value)
|
||||
{
|
||||
if (key == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(key));
|
||||
}
|
||||
|
||||
return Query(e => e.HasProperties
|
||||
&& e.Properties.TryGetValue(key, out var v)
|
||||
&& Equals(v, value));
|
||||
}
|
||||
|
||||
/// <summary>Returns the <paramref name="n"/> most recent events (newest last).</summary>
|
||||
public IReadOnlyList<LogEvent> GetLatest(int n) => GetRecentEvents(n);
|
||||
|
||||
/// <summary>
|
||||
/// Returns aggregate statistics for the current buffer snapshot.
|
||||
/// </summary>
|
||||
public MemoryFlowStats GetStats()
|
||||
{
|
||||
var events = GetEvents();
|
||||
var byLevel = new Dictionary<LogLevel, int>();
|
||||
foreach (var e in events)
|
||||
{
|
||||
byLevel.TryGetValue(e.Level, out var cnt);
|
||||
byLevel[e.Level] = cnt + 1;
|
||||
}
|
||||
|
||||
return new MemoryFlowStats
|
||||
{
|
||||
TotalEvents = events.Length,
|
||||
Capacity = _capacity,
|
||||
IsFull = IsFull,
|
||||
ByLevel = byLevel,
|
||||
WithExceptions = events.Count(e => e.HasException),
|
||||
OldestTimestamp = events.Length > 0 ? LogEvent.GetDateTime(events[0].Timestamp) : (DateTime?)null,
|
||||
NewestTimestamp = events.Length > 0 ? LogEvent.GetDateTime(events[events.Length - 1].Timestamp) : (DateTime?)null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Aggregate statistics returned by <see cref="MemoryFlow.GetStats"/>.</summary>
|
||||
public sealed class MemoryFlowStats
|
||||
{
|
||||
public int TotalEvents { get; set; }
|
||||
public int Capacity { get; set; }
|
||||
public bool IsFull { get; set; }
|
||||
public Dictionary<LogLevel, int> ByLevel { get; set; } = new Dictionary<LogLevel, int>();
|
||||
public int WithExceptions { get; set; }
|
||||
public DateTime? OldestTimestamp { get; set; }
|
||||
public DateTime? NewestTimestamp { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.LogStack.Flows;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Fans out every log event to multiple inner flows in parallel, each with an
|
||||
/// optional per-flow predicate guard.
|
||||
///
|
||||
/// Differences vs simply adding multiple flows to a <see cref="EonaCat.LogStack.EonaCatLogStack"/>:
|
||||
/// • The fan-out is fired in parallel (Task.WhenAll), reducing wall-clock latency.
|
||||
/// • Per-target predicates let you express "send to Slack only if level ≥ Error"
|
||||
/// inside one place rather than wrapping each flow in a <see cref="ConditionalFlow"/>.
|
||||
/// • Targets can be added/removed at runtime via <see cref="Add"/> / <see cref="Remove"/>.
|
||||
///
|
||||
/// <example>
|
||||
/// <code>
|
||||
/// var multicast = new MulticastFlow()
|
||||
/// .Add(consoleFlow)
|
||||
/// .Add(fileFlow)
|
||||
/// .Add(slackFlow, e => e.Level >= LogLevel.Error);
|
||||
///
|
||||
/// new LogBuilder().WriteTo(multicast).Build();
|
||||
/// </code>
|
||||
/// </example>
|
||||
/// </summary>
|
||||
public sealed class MulticastFlow : FlowBase
|
||||
{
|
||||
private readonly struct Target
|
||||
{
|
||||
public readonly IFlow Flow;
|
||||
public readonly Func<LogEvent, bool>? Predicate;
|
||||
public Target(IFlow flow, Func<LogEvent, bool>? predicate) { Flow = flow; Predicate = predicate; }
|
||||
}
|
||||
|
||||
private readonly List<Target> _targets = new();
|
||||
private readonly object _lock = new();
|
||||
|
||||
public MulticastFlow(LogLevel minimumLevel = LogLevel.Trace)
|
||||
: base("Multicast", minimumLevel) { }
|
||||
|
||||
/// <summary>
|
||||
/// Adds an inner flow, optionally gated by a predicate.
|
||||
/// </summary>
|
||||
public MulticastFlow Add(IFlow flow, Func<LogEvent, bool>? predicate = null)
|
||||
{
|
||||
if (flow == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(flow));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_targets.Add(new Target(flow, predicate));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes all targets backed by the given flow instance.
|
||||
/// </summary>
|
||||
public MulticastFlow Remove(IFlow flow)
|
||||
{
|
||||
if (flow == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(flow));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_targets.RemoveAll(t => t.Flow == flow);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
|
||||
{
|
||||
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
|
||||
{
|
||||
return WriteResult.LevelFiltered;
|
||||
}
|
||||
|
||||
Target[] snapshot;
|
||||
lock (_lock)
|
||||
{
|
||||
snapshot = _targets.ToArray();
|
||||
}
|
||||
|
||||
if (snapshot.Length == 0)
|
||||
{
|
||||
return WriteResult.NoBlastZone;
|
||||
}
|
||||
|
||||
var tasks = new Task<WriteResult>[snapshot.Length];
|
||||
for (int i = 0; i < snapshot.Length; i++)
|
||||
{
|
||||
var target = snapshot[i];
|
||||
bool shouldSend;
|
||||
try { shouldSend = target.Predicate == null || target.Predicate(logEvent); }
|
||||
catch { shouldSend = false; }
|
||||
|
||||
tasks[i] = shouldSend
|
||||
? target.Flow.BlastAsync(logEvent, cancellationToken)
|
||||
: Task.FromResult(WriteResult.Dropped);
|
||||
}
|
||||
|
||||
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
|
||||
var overall = WriteResult.Success;
|
||||
foreach (var r in results)
|
||||
{
|
||||
if (r == WriteResult.Dropped)
|
||||
{
|
||||
Interlocked.Increment(ref DroppedCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Increment(ref BlastedCount);
|
||||
}
|
||||
|
||||
if (r != WriteResult.Success && r != WriteResult.Dropped && overall == WriteResult.Success)
|
||||
{
|
||||
overall = r;
|
||||
}
|
||||
}
|
||||
|
||||
return overall;
|
||||
}
|
||||
|
||||
public override async Task FlushAsync(CancellationToken cancellationToken = default)
|
||||
{
|
||||
Target[] snapshot;
|
||||
lock (_lock)
|
||||
{
|
||||
snapshot = _targets.ToArray();
|
||||
}
|
||||
|
||||
var tasks = new Task[snapshot.Length];
|
||||
for (int i = 0; i < snapshot.Length; i++)
|
||||
{
|
||||
tasks[i] = snapshot[i].Flow.FlushAsync(cancellationToken);
|
||||
}
|
||||
|
||||
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
public override async ValueTask DisposeAsync()
|
||||
{
|
||||
IsEnabled = false;
|
||||
await FlushAsync(default).ConfigureAwait(false);
|
||||
|
||||
Target[] snapshot;
|
||||
lock (_lock)
|
||||
{
|
||||
snapshot = _targets.ToArray();
|
||||
}
|
||||
|
||||
foreach (var t in snapshot)
|
||||
{
|
||||
try { await t.Flow.DisposeAsync().ConfigureAwait(false); } catch { }
|
||||
}
|
||||
|
||||
GC.SuppressFinalize(this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using EonaCat.LogStack.Destructuring;
|
||||
|
||||
namespace EonaCat.LogStack.Output.Formatters;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Base interface for log formatters
|
||||
/// </summary>
|
||||
public interface ILogFormatter
|
||||
{
|
||||
string Format(object logEvent, Dictionary<string, object?> properties);
|
||||
void Format(StringBuilder sb, object logEvent, Dictionary<string, object?> properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// JSON formatter for structured logging
|
||||
/// </summary>
|
||||
public sealed class JsonFormatter : ILogFormatter
|
||||
{
|
||||
private readonly bool _includeProperties;
|
||||
private readonly bool _indented;
|
||||
|
||||
public JsonFormatter(bool includeProperties = true, bool indented = false)
|
||||
{
|
||||
_includeProperties = includeProperties;
|
||||
_indented = indented;
|
||||
}
|
||||
|
||||
public string Format(object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
Format(sb, logEvent, properties);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void Format(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
sb.Append("{");
|
||||
sb.Append("\"timestamp\":\"").Append(DateTime.Now.ToString("o")).Append("\",");
|
||||
sb.Append("\"level\":\"INFO\",");
|
||||
sb.Append("\"logger\":\"Default\",");
|
||||
sb.Append("\"message\":\"\"");
|
||||
|
||||
// Properties
|
||||
if (_includeProperties && properties.Count > 0)
|
||||
{
|
||||
sb.Append(",\"properties\":{");
|
||||
int count = 0;
|
||||
foreach (var kvp in properties)
|
||||
{
|
||||
if (count > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
}
|
||||
|
||||
sb.Append("\"").AppendEscapedJson(kvp.Key).Append("\":");
|
||||
FormatPropertyValue(sb, kvp.Value);
|
||||
count++;
|
||||
}
|
||||
sb.Append("}");
|
||||
}
|
||||
|
||||
sb.Append("}");
|
||||
}
|
||||
|
||||
private void FormatPropertyValue(StringBuilder sb, object? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
sb.Append("null");
|
||||
}
|
||||
else if (value is string str)
|
||||
{
|
||||
sb.Append("\"").AppendEscapedJson(str).Append("\"");
|
||||
}
|
||||
else if (value is bool b)
|
||||
{
|
||||
sb.Append(b ? "true" : "false");
|
||||
}
|
||||
else if (value is int or long or double or decimal)
|
||||
{
|
||||
sb.Append(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append("\"").AppendEscapedJson(value.ToString() ?? "").Append("\"");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CSV formatter for data export
|
||||
/// </summary>
|
||||
public sealed class CsvFormatter : ILogFormatter
|
||||
{
|
||||
private readonly string[] _propertyNames;
|
||||
private readonly bool _includeHeader;
|
||||
private bool _headerWritten = false;
|
||||
|
||||
public CsvFormatter(string[]? propertyNames = null, bool includeHeader = true)
|
||||
{
|
||||
_propertyNames = propertyNames ?? Array.Empty<string>();
|
||||
_includeHeader = includeHeader;
|
||||
}
|
||||
|
||||
public string Format(object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
Format(sb, logEvent, properties);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void Format(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
if (_includeHeader && !_headerWritten)
|
||||
{
|
||||
WriteCsvHeader(sb);
|
||||
_headerWritten = true;
|
||||
}
|
||||
|
||||
WriteCsvLine(sb, properties);
|
||||
}
|
||||
|
||||
private void WriteCsvHeader(StringBuilder sb)
|
||||
{
|
||||
var headers = new List<string> { "Timestamp", "Level", "Logger", "Message" };
|
||||
headers.AddRange(_propertyNames);
|
||||
|
||||
for (int i = 0; i < headers.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
}
|
||||
|
||||
sb.Append(EscapeCsv(headers[i]));
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
private void WriteCsvLine(StringBuilder sb, Dictionary<string, object?> properties)
|
||||
{
|
||||
var values = new List<string>
|
||||
{
|
||||
DateTime.Now.ToString(CultureInfo.InvariantCulture),
|
||||
"INFO",
|
||||
"Default",
|
||||
""
|
||||
};
|
||||
|
||||
foreach (var propName in _propertyNames)
|
||||
{
|
||||
if (properties.TryGetValue(propName, out var value))
|
||||
{
|
||||
values.Add(value?.ToString() ?? "");
|
||||
}
|
||||
else
|
||||
{
|
||||
values.Add("");
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < values.Count; i++)
|
||||
{
|
||||
if (i > 0)
|
||||
{
|
||||
sb.Append(",");
|
||||
}
|
||||
|
||||
sb.Append(EscapeCsv(values[i]));
|
||||
}
|
||||
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
private string EscapeCsv(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
if (value.Contains(",") || value.Contains("\"") || value.Contains("\n"))
|
||||
{
|
||||
return "\"" + value.Replace("\"", "\"\"") + "\"";
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Compact formatter for console output
|
||||
/// </summary>
|
||||
public sealed class CompactFormatter : ILogFormatter
|
||||
{
|
||||
public string Format(object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
Format(sb, logEvent, properties);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void Format(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
sb.AppendFormat("[{0:HH:mm:ss}] [{1,7}] {2}: {3}",
|
||||
DateTime.Now,
|
||||
"INFO",
|
||||
"Default",
|
||||
"");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Verbose formatter with all details
|
||||
/// </summary>
|
||||
public sealed class VerboseFormatter : ILogFormatter
|
||||
{
|
||||
public string Format(object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
Format(sb, logEvent, properties);
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void Format(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
sb.AppendLine("===== LOG EVENT =====");
|
||||
sb.AppendFormat("Timestamp: {0:O}", DateTime.Now).AppendLine();
|
||||
sb.AppendFormat("Level: INFO").AppendLine();
|
||||
sb.AppendFormat("Logger: Default").AppendLine();
|
||||
sb.AppendFormat("Message: ").AppendLine();
|
||||
|
||||
if (properties.Count > 0)
|
||||
{
|
||||
sb.AppendLine("Properties:");
|
||||
foreach (var kvp in properties)
|
||||
{
|
||||
sb.AppendFormat(" {0}: {1}", kvp.Key, kvp.Value).AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("====================");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Factory for creating formatters
|
||||
/// </summary>
|
||||
public static class FormatterFactory
|
||||
{
|
||||
public static ILogFormatter CreateJson(bool includeProperties = true, bool indented = false)
|
||||
=> new JsonFormatter(includeProperties, indented);
|
||||
|
||||
public static ILogFormatter CreateCsv(string[]? propertyNames = null, bool includeHeader = true)
|
||||
=> new CsvFormatter(propertyNames, includeHeader);
|
||||
|
||||
public static ILogFormatter CreateCompact()
|
||||
=> new CompactFormatter();
|
||||
|
||||
public static ILogFormatter CreateVerbose()
|
||||
=> new VerboseFormatter();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for StringBuilder to support JSON escaping
|
||||
/// </summary>
|
||||
public static class StringBuilderExtensions
|
||||
{
|
||||
public static StringBuilder AppendEscapedJson(this StringBuilder sb, string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return sb;
|
||||
}
|
||||
|
||||
foreach (char c in value)
|
||||
{
|
||||
switch (c)
|
||||
{
|
||||
case '"':
|
||||
sb.Append("\\\"");
|
||||
break;
|
||||
case '\\':
|
||||
sb.Append("\\\\");
|
||||
break;
|
||||
case '\b':
|
||||
sb.Append("\\b");
|
||||
break;
|
||||
case '\f':
|
||||
sb.Append("\\f");
|
||||
break;
|
||||
case '\n':
|
||||
sb.Append("\\n");
|
||||
break;
|
||||
case '\r':
|
||||
sb.Append("\\r");
|
||||
break;
|
||||
case '\t':
|
||||
sb.Append("\\t");
|
||||
break;
|
||||
default:
|
||||
if (c < 32)
|
||||
{
|
||||
sb.AppendFormat("\\u{0:X4}", (int)c);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return sb;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,358 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using EonaCat.LogStack.Templates;
|
||||
|
||||
namespace EonaCat.LogStack.Output.Layouts;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Represents a layout token similar to NLog format
|
||||
/// </summary>
|
||||
public abstract class LayoutToken
|
||||
{
|
||||
public abstract void Render(StringBuilder sb, object logEvent, Dictionary<string, object?> properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Literal layout token
|
||||
/// </summary>
|
||||
public sealed class LiteralLayoutToken : LayoutToken
|
||||
{
|
||||
private readonly string _text;
|
||||
|
||||
public LiteralLayoutToken(string text)
|
||||
{
|
||||
_text = text;
|
||||
}
|
||||
|
||||
public override void Render(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
sb.Append(_text);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// DateTime layout token ${date:format=yyyy-MM-dd HH:mm:ss}
|
||||
/// </summary>
|
||||
public sealed class DateTimeLayoutToken : LayoutToken
|
||||
{
|
||||
private readonly string _format;
|
||||
|
||||
public DateTimeLayoutToken(string format = "yyyy-MM-dd HH:mm:ss")
|
||||
{
|
||||
_format = format ?? "yyyy-MM-dd HH:mm:ss";
|
||||
}
|
||||
|
||||
public override void Render(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
sb.Append(DateTime.Now.ToString(_format, CultureInfo.InvariantCulture));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log level layout token ${level}
|
||||
/// </summary>
|
||||
public sealed class LevelLayoutToken : LayoutToken
|
||||
{
|
||||
private readonly bool _uppercase;
|
||||
|
||||
public LevelLayoutToken(bool uppercase = true)
|
||||
{
|
||||
_uppercase = uppercase;
|
||||
}
|
||||
|
||||
public override void Render(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
string level = "INFO";
|
||||
sb.Append(_uppercase ? level.ToUpper() : level);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Logger name layout token ${logger}
|
||||
/// </summary>
|
||||
public sealed class LoggerNameLayoutToken : LayoutToken
|
||||
{
|
||||
public override void Render(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
sb.Append("Default");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Message layout token ${message}
|
||||
/// </summary>
|
||||
public sealed class MessageLayoutToken : LayoutToken
|
||||
{
|
||||
public override void Render(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
sb.Append(""); // Message would go here when LogEvent is in scope
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Exception layout token ${exception}
|
||||
/// </summary>
|
||||
public sealed class ExceptionLayoutToken : LayoutToken
|
||||
{
|
||||
private readonly string _format;
|
||||
|
||||
public ExceptionLayoutToken(string format = "full")
|
||||
{
|
||||
_format = format ?? "full";
|
||||
}
|
||||
|
||||
public override void Render(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
// Exception rendering would happen when LogEvent has Exception property
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Property layout token ${property:name=PropertyName}
|
||||
/// </summary>
|
||||
public sealed class PropertyLayoutToken : LayoutToken
|
||||
{
|
||||
private readonly string _propertyName;
|
||||
|
||||
public PropertyLayoutToken(string propertyName)
|
||||
{
|
||||
_propertyName = propertyName;
|
||||
}
|
||||
|
||||
public override void Render(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
if (properties.TryGetValue(_propertyName, out var value))
|
||||
{
|
||||
sb.Append(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Newline layout token ${newline}
|
||||
/// </summary>
|
||||
public sealed class NewlineLayoutToken : LayoutToken
|
||||
{
|
||||
public override void Render(StringBuilder sb, object logEvent, Dictionary<string, object?> properties)
|
||||
{
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses NLog-like layout strings: ${date:format=...}, ${level}, ${message}, etc.
|
||||
/// </summary>
|
||||
public sealed class LayoutParser
|
||||
{
|
||||
private readonly string _layout;
|
||||
private int _position;
|
||||
|
||||
public LayoutParser(string layout)
|
||||
{
|
||||
_layout = layout ?? "";
|
||||
}
|
||||
|
||||
public List<LayoutToken> Parse()
|
||||
{
|
||||
var tokens = new List<LayoutToken>();
|
||||
var sb = new StringBuilder();
|
||||
|
||||
while (_position < _layout.Length)
|
||||
{
|
||||
if (_layout[_position] == '$' && _position + 1 < _layout.Length && _layout[_position + 1] == '{')
|
||||
{
|
||||
// Flush literal text
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
tokens.Add(new LiteralLayoutToken(sb.ToString()));
|
||||
sb.Clear();
|
||||
}
|
||||
|
||||
// Parse token
|
||||
var token = ParseToken();
|
||||
if (token != null)
|
||||
{
|
||||
tokens.Add(token);
|
||||
}
|
||||
}
|
||||
else if (_layout[_position] == '\\' && _position + 1 < _layout.Length && _layout[_position + 1] == 'n')
|
||||
{
|
||||
sb.Append('\n');
|
||||
_position += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(_layout[_position]);
|
||||
_position++;
|
||||
}
|
||||
}
|
||||
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
tokens.Add(new LiteralLayoutToken(sb.ToString()));
|
||||
}
|
||||
|
||||
return tokens;
|
||||
}
|
||||
|
||||
private LayoutToken? ParseToken()
|
||||
{
|
||||
_position += 2; // Skip ${
|
||||
|
||||
var nameSb = new StringBuilder();
|
||||
var argsSb = new StringBuilder();
|
||||
bool inArgs = false;
|
||||
|
||||
while (_position < _layout.Length)
|
||||
{
|
||||
char c = _layout[_position];
|
||||
|
||||
if (c == '}')
|
||||
{
|
||||
_position++;
|
||||
break;
|
||||
}
|
||||
else if (c == ':' && !inArgs)
|
||||
{
|
||||
inArgs = true;
|
||||
_position++;
|
||||
}
|
||||
else if (inArgs)
|
||||
{
|
||||
argsSb.Append(c);
|
||||
_position++;
|
||||
}
|
||||
else
|
||||
{
|
||||
nameSb.Append(c);
|
||||
_position++;
|
||||
}
|
||||
}
|
||||
|
||||
string tokenName = nameSb.ToString().Trim().ToLower();
|
||||
string args = argsSb.ToString().Trim();
|
||||
|
||||
return tokenName switch
|
||||
{
|
||||
"date" => new DateTimeLayoutToken(ParseFormatArg(args)),
|
||||
"time" => new DateTimeLayoutToken("HH:mm:ss"),
|
||||
"level" => new LevelLayoutToken(ParseBoolArg(args, "uppercase", true)),
|
||||
"logger" => new LoggerNameLayoutToken(),
|
||||
"message" => new MessageLayoutToken(),
|
||||
"exception" => new ExceptionLayoutToken(ParseFormatArg(args)),
|
||||
"property" or "prop" => new PropertyLayoutToken(ParseNameArg(args)),
|
||||
"newline" => new NewlineLayoutToken(),
|
||||
_ => null
|
||||
};
|
||||
}
|
||||
|
||||
private string ParseFormatArg(string args)
|
||||
{
|
||||
if (args.StartsWith("format="))
|
||||
{
|
||||
return args.Substring(7);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private string ParseNameArg(string args)
|
||||
{
|
||||
if (args.StartsWith("name="))
|
||||
{
|
||||
return args.Substring(5);
|
||||
}
|
||||
|
||||
if (args.StartsWith("key="))
|
||||
{
|
||||
return args.Substring(4);
|
||||
}
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
private bool ParseBoolArg(string args, string name, bool defaultValue)
|
||||
{
|
||||
string prefix = name + "=";
|
||||
if (args.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string value = args.Substring(prefix.Length);
|
||||
return value.Equals("true", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders a parsed layout with LogEvent data
|
||||
/// </summary>
|
||||
public sealed class LayoutRenderer
|
||||
{
|
||||
private readonly List<LayoutToken> _tokens;
|
||||
private readonly string _originalLayout;
|
||||
|
||||
public LayoutRenderer(string layout)
|
||||
{
|
||||
_originalLayout = layout;
|
||||
var parser = new LayoutParser(layout);
|
||||
_tokens = parser.Parse();
|
||||
}
|
||||
|
||||
public string Render(object logEvent, Dictionary<string, object?>? properties = null)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
properties ??= new Dictionary<string, object?>();
|
||||
|
||||
foreach (var token in _tokens)
|
||||
{
|
||||
token.Render(sb, logEvent, properties);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
public void Render(StringBuilder sb, object logEvent, Dictionary<string, object?>? properties = null)
|
||||
{
|
||||
properties ??= new Dictionary<string, object?>();
|
||||
foreach (var token in _tokens)
|
||||
{
|
||||
token.Render(sb, logEvent, properties);
|
||||
}
|
||||
}
|
||||
|
||||
public string OriginalLayout => _originalLayout;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Predefined layout templates
|
||||
/// </summary>
|
||||
public static class LayoutTemplates
|
||||
{
|
||||
public const string Simple = "[${level}] ${message}";
|
||||
public const string Standard = "[${date:format=yyyy-MM-dd HH:mm:ss}] [${level}] ${logger} - ${message}";
|
||||
public const string Extended = "[${date:format=yyyy-MM-dd HH:mm:ss.fff}] [${level}] [${logger}] ${message}${exception}";
|
||||
public const string Json = "{\"timestamp\":\"${date:format=o}\",\"level\":\"${level}\",\"logger\":\"${logger}\",\"message\":\"${message}\"}";
|
||||
public const string Csv = "${date:format=yyyy-MM-dd HH:mm:ss},${level},${logger},${message}";
|
||||
|
||||
/// <summary>
|
||||
/// Gets a layout by common name
|
||||
/// </summary>
|
||||
public static string GetTemplate(string name)
|
||||
{
|
||||
return name?.ToLower() switch
|
||||
{
|
||||
"simple" => Simple,
|
||||
"standard" => Standard,
|
||||
"extended" => Extended,
|
||||
"json" => Json,
|
||||
"csv" => Csv,
|
||||
_ => Standard
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -3,14 +3,39 @@
|
||||
|
||||
namespace EonaCat.LogStack.EonaCatLogStackCore
|
||||
{
|
||||
/// <summary>
|
||||
/// Comprehensive logging statistics with per-level and per-category metrics
|
||||
/// </summary>
|
||||
public struct LogStats
|
||||
{
|
||||
// Basic counters
|
||||
public long Written;
|
||||
public long Dropped;
|
||||
public long Rotations;
|
||||
public long BytesWritten;
|
||||
public double WritesPerSecond;
|
||||
|
||||
// Per-level counters
|
||||
public long TraceCount;
|
||||
public long DebugCount;
|
||||
public long InformationCount;
|
||||
public long WarningCount;
|
||||
public long ErrorCount;
|
||||
public long CriticalCount;
|
||||
|
||||
// Error tracking
|
||||
public long ExceptionsLogged;
|
||||
public long DroppedErrors;
|
||||
|
||||
// Performance metrics
|
||||
public double AverageEventLatencyMs;
|
||||
public double MinEventLatencyMs;
|
||||
public double MaxEventLatencyMs;
|
||||
|
||||
// Memory metrics
|
||||
public long TotalEventsProcessed;
|
||||
public double AverageEventSizeBytes;
|
||||
|
||||
public LogStats(long written, long dropped, long rotations, long bytesWritten, double writesPerSecond)
|
||||
{
|
||||
Written = written;
|
||||
@@ -18,6 +43,19 @@ namespace EonaCat.LogStack.EonaCatLogStackCore
|
||||
Rotations = rotations;
|
||||
BytesWritten = bytesWritten;
|
||||
WritesPerSecond = writesPerSecond;
|
||||
TraceCount = 0;
|
||||
DebugCount = 0;
|
||||
InformationCount = 0;
|
||||
WarningCount = 0;
|
||||
ErrorCount = 0;
|
||||
CriticalCount = 0;
|
||||
ExceptionsLogged = 0;
|
||||
DroppedErrors = 0;
|
||||
AverageEventLatencyMs = 0;
|
||||
MinEventLatencyMs = 0;
|
||||
MaxEventLatencyMs = 0;
|
||||
TotalEventsProcessed = 0;
|
||||
AverageEventSizeBytes = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -33,6 +33,11 @@ public interface ILoggerFactory : IAsyncDisposable
|
||||
/// Gets diagnostics information about the logger
|
||||
/// </summary>
|
||||
LoggerDiagnostics GetDiagnostics();
|
||||
|
||||
/// <summary>
|
||||
/// Gets detailed metrics about the logger
|
||||
/// </summary>
|
||||
LoggerMetrics GetMetrics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@@ -124,6 +129,19 @@ public sealed class LoggerFactory : ILoggerFactory
|
||||
return _logStack.GetDiagnostics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets detailed metrics about the logger
|
||||
/// </summary>
|
||||
public LoggerMetrics GetMetrics()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(LoggerFactory));
|
||||
}
|
||||
|
||||
return _logStack.GetMetrics();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes all loggers and the underlying log stack
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,331 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using EonaCat.LogStack.Core;
|
||||
|
||||
namespace EonaCat.LogStack.Memory;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Memory optimization utilities for zero-allocation logging paths
|
||||
/// </summary>
|
||||
public static class MemoryOptimizations
|
||||
{
|
||||
/// <summary>
|
||||
/// Rents a scoped array from ArrayPool for using within a scope
|
||||
/// </summary>
|
||||
public static ScopedArray<T> RentArray<T>(int minimumLength)
|
||||
{
|
||||
return new ScopedArray<T>(ArrayPool<T>.Shared.Rent(minimumLength));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets size estimate for a log event
|
||||
/// </summary>
|
||||
public static int EstimateLogEventSize(in LogEvent logEvent)
|
||||
{
|
||||
int size = 64; // Base overhead
|
||||
|
||||
// Timestamp, level, thread ID
|
||||
size += 24;
|
||||
|
||||
// Message
|
||||
size += logEvent.Message.Length * 2;
|
||||
|
||||
// Category
|
||||
size += (logEvent.Category?.Length ?? 0) * 2;
|
||||
|
||||
// Exception (estimated)
|
||||
if (logEvent.HasException)
|
||||
{
|
||||
size += 1024;
|
||||
}
|
||||
|
||||
// Properties
|
||||
if (logEvent.HasProperties)
|
||||
{
|
||||
size += logEvent.Properties.Count * 64;
|
||||
}
|
||||
|
||||
return size;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if memory usage is within acceptable range
|
||||
/// </summary>
|
||||
public static (bool acceptable, double pressure) CheckMemoryPressure(
|
||||
long thresholdBytes = 100 * 1024 * 1024)
|
||||
{
|
||||
long current = GC.GetTotalMemory(false);
|
||||
double pressure = (double)current / thresholdBytes;
|
||||
return (pressure < 0.8, pressure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attempts to reduce memory usage
|
||||
/// </summary>
|
||||
public static void TryCompact()
|
||||
{
|
||||
GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized, false);
|
||||
GC.WaitForPendingFinalizers();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets current memory statistics
|
||||
/// </summary>
|
||||
public static MemoryStatistics GetStatistics()
|
||||
{
|
||||
return new MemoryStatistics
|
||||
{
|
||||
TotalMemory = GC.GetTotalMemory(false),
|
||||
Gen0Collections = GC.CollectionCount(0),
|
||||
Gen1Collections = GC.CollectionCount(1),
|
||||
Gen2Collections = GC.CollectionCount(2)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scoped array rental from ArrayPool
|
||||
/// </summary>
|
||||
public struct ScopedArray<T> : IDisposable
|
||||
{
|
||||
private T[]? _array;
|
||||
private readonly int _length;
|
||||
|
||||
public ScopedArray(int length)
|
||||
{
|
||||
_array = ArrayPool<T>.Shared.Rent(length);
|
||||
_length = length;
|
||||
}
|
||||
|
||||
public ScopedArray(T[] array)
|
||||
{
|
||||
_array = array;
|
||||
_length = array.Length;
|
||||
}
|
||||
|
||||
public Span<T> AsSpan() => new Span<T>(_array, 0, _length);
|
||||
public Memory<T> AsMemory() => new Memory<T>(_array, 0, _length);
|
||||
public T[] Array => _array!;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_array != null)
|
||||
{
|
||||
ArrayPool<T>.Shared.Return(_array, false);
|
||||
_array = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Memory statistics
|
||||
/// </summary>
|
||||
public class MemoryStatistics
|
||||
{
|
||||
public long TotalMemory { get; set; }
|
||||
public int Gen0Collections { get; set; }
|
||||
public int Gen1Collections { get; set; }
|
||||
public int Gen2Collections { get; set; }
|
||||
|
||||
public long TotalCollections => Gen0Collections + Gen1Collections + Gen2Collections;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// String interpolation helper for zero-allocation string building
|
||||
/// </summary>
|
||||
public struct ZeroAllocString
|
||||
{
|
||||
private readonly StringBuilder _sb;
|
||||
private readonly bool _ownsBuilder;
|
||||
|
||||
public ZeroAllocString(StringBuilder? builder = null)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
_sb = new StringBuilder(256);
|
||||
_ownsBuilder = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_sb = builder;
|
||||
_ownsBuilder = false;
|
||||
}
|
||||
}
|
||||
|
||||
public void Append(string? value)
|
||||
{
|
||||
_sb.Append(value);
|
||||
}
|
||||
|
||||
public void Append(int value)
|
||||
{
|
||||
_sb.Append(value);
|
||||
}
|
||||
|
||||
public void Append(long value)
|
||||
{
|
||||
_sb.Append(value);
|
||||
}
|
||||
|
||||
public void Append(double value)
|
||||
{
|
||||
_sb.Append(value);
|
||||
}
|
||||
|
||||
public void Append(char value)
|
||||
{
|
||||
_sb.Append(value);
|
||||
}
|
||||
|
||||
public void AppendLine()
|
||||
{
|
||||
_sb.AppendLine();
|
||||
}
|
||||
|
||||
public override string ToString()
|
||||
{
|
||||
return _sb.ToString();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_ownsBuilder)
|
||||
{
|
||||
_sb.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Value type cache for frequently-used strings
|
||||
/// </summary>
|
||||
public sealed class StringCache
|
||||
{
|
||||
private readonly Dictionary<string, string> _cache;
|
||||
private readonly int _maxSize;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public StringCache(int maxSize = 1000)
|
||||
{
|
||||
_maxSize = maxSize;
|
||||
_cache = new Dictionary<string, string>(maxSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or caches a string
|
||||
/// </summary>
|
||||
public string Intern(string value)
|
||||
{
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_cache.TryGetValue(value, out var cached))
|
||||
{
|
||||
return cached;
|
||||
}
|
||||
|
||||
if (_cache.Count < _maxSize)
|
||||
{
|
||||
_cache[value] = value;
|
||||
return value;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears the cache
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_cache.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets cache statistics
|
||||
/// </summary>
|
||||
public (int count, int maxSize) GetStatistics()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return (_cache.Count, _maxSize);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Memory-efficient concurrent bag alternative
|
||||
/// </summary>
|
||||
public sealed class EfficientObjectPool<T> where T : class
|
||||
{
|
||||
private readonly Stack<T> _stack;
|
||||
private readonly Func<T> _factory;
|
||||
private int _count;
|
||||
private readonly int _maxSize;
|
||||
private readonly object _lock = new();
|
||||
|
||||
public EfficientObjectPool(Func<T> factory, int maxSize = 100)
|
||||
{
|
||||
_factory = factory;
|
||||
_maxSize = maxSize;
|
||||
_stack = new Stack<T>(maxSize);
|
||||
}
|
||||
|
||||
public T Rent()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _stack.Count > 0 ? _stack.Pop() : _factory();
|
||||
}
|
||||
}
|
||||
|
||||
public void Return(T item)
|
||||
{
|
||||
if (item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
if (_count < _maxSize)
|
||||
{
|
||||
_stack.Push(item);
|
||||
_count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public int Count
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _count;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_stack.Clear();
|
||||
_count = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,690 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace EonaCat.LogStack.Core;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Hint for how a token's value should be captured.
|
||||
/// </summary>
|
||||
public enum TokenDestructureHint : byte
|
||||
{
|
||||
/// <summary>ToString() / scalar value</summary>
|
||||
Default = 0,
|
||||
/// <summary>{@Property} — deep object destructuring (JSON-like)</summary>
|
||||
Destructure = 1,
|
||||
/// <summary>{$Property} — force ToString()</summary>
|
||||
Stringify = 2
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single token parsed from a message template.
|
||||
/// Supports advanced features:
|
||||
/// - Nested property access: {Object.Property.SubProperty}
|
||||
/// - Array indexing: {Array[0]}
|
||||
/// - Alignment: {Name,10} (right), {Name,-10} (left)
|
||||
/// - Filters: {Name|uppercase}, {Name|truncate:20}
|
||||
/// - Conditionals: {?IsActive:Yes|No}
|
||||
/// - Fallback: {Name??'default'}
|
||||
/// </summary>
|
||||
public readonly struct TemplateToken
|
||||
{
|
||||
/// <summary>True = literal text; False = property hole.</summary>
|
||||
public readonly bool IsLiteral;
|
||||
/// <summary>Literal text segment, or null for property holes.</summary>
|
||||
public readonly string? Text;
|
||||
/// <summary>Property name for holes (may be a digit for positional placeholders, supports dot notation).</summary>
|
||||
public readonly string? Name;
|
||||
/// <summary>Zero-based positional index for positional placeholders; -1 for named.</summary>
|
||||
public readonly int Position;
|
||||
/// <summary>Destructure / stringify hint.</summary>
|
||||
public readonly TokenDestructureHint Hint;
|
||||
/// <summary>Optional format string (e.g. "D2").</summary>
|
||||
public readonly string? Format;
|
||||
/// <summary>Alignment width (positive=right, negative=left).</summary>
|
||||
public readonly int Alignment;
|
||||
/// <summary>Applied filters (e.g., "uppercase", "truncate:10").</summary>
|
||||
public readonly string[]? Filters;
|
||||
/// <summary>Fallback value if property is null/missing.</summary>
|
||||
public readonly string? Fallback;
|
||||
/// <summary>Optional conditional true/false values for {?PropertyName:True|False}.</summary>
|
||||
public readonly (string? TrueValue, string? FalseValue) ConditionalValues;
|
||||
|
||||
private TemplateToken(string literal)
|
||||
{
|
||||
IsLiteral = true;
|
||||
Text = literal;
|
||||
Name = null;
|
||||
Position = -1;
|
||||
Hint = TokenDestructureHint.Default;
|
||||
Format = null;
|
||||
Alignment = 0;
|
||||
Filters = null;
|
||||
Fallback = null;
|
||||
ConditionalValues = (null, null);
|
||||
}
|
||||
|
||||
private TemplateToken(string name, int position, TokenDestructureHint hint, string? format, int alignment = 0, string[]? filters = null, string? fallback = null, (string?, string?) conditionalValues = default)
|
||||
{
|
||||
IsLiteral = false;
|
||||
Text = null;
|
||||
Name = name;
|
||||
Position = position;
|
||||
Hint = hint;
|
||||
Format = format;
|
||||
Alignment = alignment;
|
||||
Filters = filters;
|
||||
Fallback = fallback;
|
||||
ConditionalValues = conditionalValues;
|
||||
}
|
||||
|
||||
public static TemplateToken Literal(string text) => new(text);
|
||||
|
||||
public static TemplateToken Property(string name, int position, TokenDestructureHint hint, string? format, int alignment = 0, string[]? filters = null, string? fallback = null, (string?, string?) conditionalValues = default)
|
||||
=> new(name, position, hint, format, alignment, filters, fallback, conditionalValues);
|
||||
|
||||
public override string ToString() =>
|
||||
IsLiteral ? $"Literal({Text})" : $"Hole({Hint}{Name}{(Format != null ? ":" + Format : "")})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parses and renders Serilog-compatible message templates with advanced features.
|
||||
///
|
||||
/// Supported syntax:
|
||||
/// {PropertyName} — named property (default destructure)
|
||||
/// {@PropertyName} — named property, deep destructure
|
||||
/// {$PropertyName} — named property, force-stringify
|
||||
/// {0}, {1} — positional
|
||||
/// {PropertyName:format} — with format specifier (e.g., "D2", "C")
|
||||
/// {PropertyName,10} — right-align with width 10
|
||||
/// {PropertyName,-10} — left-align with width 10
|
||||
/// {Object.Property} — nested property access (dot notation)
|
||||
/// {Array[0]} — array/collection indexing
|
||||
/// {PropertyName|uppercase} — apply filter (uppercase, lowercase, truncate, etc.)
|
||||
/// {PropertyName??'default'} — fallback value if null/missing
|
||||
/// {?IsActive:Yes|No} — conditional rendering
|
||||
/// {{ }} — escaped braces → literal { }
|
||||
/// </summary>
|
||||
public sealed class MessageTemplate
|
||||
{
|
||||
private readonly string _raw;
|
||||
private readonly TemplateToken[] _tokens;
|
||||
|
||||
public string Raw => _raw;
|
||||
public ReadOnlySpan<TemplateToken> Tokens => _tokens;
|
||||
|
||||
private MessageTemplate(string raw, TemplateToken[] tokens)
|
||||
{
|
||||
_raw = raw;
|
||||
_tokens = tokens;
|
||||
}
|
||||
|
||||
public static MessageTemplate Parse(string template)
|
||||
{
|
||||
if (template == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(template));
|
||||
}
|
||||
|
||||
var tokens = new List<TemplateToken>(8);
|
||||
var sb = new StringBuilder(template.Length);
|
||||
int i = 0;
|
||||
|
||||
while (i < template.Length)
|
||||
{
|
||||
char c = template[i];
|
||||
|
||||
if (c == '{')
|
||||
{
|
||||
// Escaped {{ → literal {
|
||||
if (i + 1 < template.Length && template[i + 1] == '{')
|
||||
{
|
||||
sb.Append('{');
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Flush accumulated literal
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
tokens.Add(TemplateToken.Literal(sb.ToString()));
|
||||
sb.Clear();
|
||||
}
|
||||
|
||||
// Find matching }
|
||||
int end = FindClosingBrace(template, i + 1);
|
||||
if (end < 0)
|
||||
{
|
||||
// Unclosed brace → treat rest as literal
|
||||
sb.Append(template, i, template.Length - i);
|
||||
i = template.Length;
|
||||
continue;
|
||||
}
|
||||
|
||||
string hole = template.Substring(i + 1, end - i - 1);
|
||||
tokens.Add(ParseHole(hole));
|
||||
i = end + 1;
|
||||
}
|
||||
else if (c == '}' && i + 1 < template.Length && template[i + 1] == '}')
|
||||
{
|
||||
// Escaped }}
|
||||
sb.Append('}');
|
||||
i += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(c);
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
if (sb.Length > 0)
|
||||
{
|
||||
tokens.Add(TemplateToken.Literal(sb.ToString()));
|
||||
}
|
||||
|
||||
return new MessageTemplate(template, tokens.ToArray());
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static int FindClosingBrace(string template, int startPos)
|
||||
{
|
||||
for (int i = startPos; i < template.Length; i++)
|
||||
{
|
||||
if (template[i] == '}')
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static TemplateToken ParseHole(string hole)
|
||||
{
|
||||
if (string.IsNullOrEmpty(hole))
|
||||
{
|
||||
return TemplateToken.Literal("{}");
|
||||
}
|
||||
|
||||
var hint = TokenDestructureHint.Default;
|
||||
int nameStart = 0;
|
||||
bool isConditional = false;
|
||||
|
||||
// Check for conditional {?Property:True|False}
|
||||
if (hole[0] == '?')
|
||||
{
|
||||
isConditional = true;
|
||||
nameStart = 1;
|
||||
}
|
||||
else if (hole[0] == '@') { hint = TokenDestructureHint.Destructure; nameStart = 1; }
|
||||
else if (hole[0] == '$') { hint = TokenDestructureHint.Stringify; nameStart = 1; }
|
||||
|
||||
// Extract components: name, alignment, format, filters, fallback, conditional
|
||||
string name;
|
||||
string? format = null;
|
||||
int alignment = 0;
|
||||
string[]? filters = null;
|
||||
string? fallback = null;
|
||||
(string?, string?) conditionalValues = (null, null);
|
||||
|
||||
// Parse: name[,alignment][|filters][??fallback][:format][:trueval|falseval]
|
||||
string remaining = hole.Substring(nameStart);
|
||||
|
||||
// First, check for conditional values (only for conditional tokens)
|
||||
if (isConditional && remaining.Contains(":"))
|
||||
{
|
||||
int colonIdx = remaining.IndexOf(':');
|
||||
string beforeColon = remaining.Substring(0, colonIdx);
|
||||
string afterColon = remaining.Substring(colonIdx + 1);
|
||||
|
||||
if (afterColon.Contains("|"))
|
||||
{
|
||||
int pipeIdx = afterColon.IndexOf('|');
|
||||
conditionalValues.Item1 = afterColon.Substring(0, pipeIdx).Trim();
|
||||
conditionalValues.Item2 = afterColon.Substring(pipeIdx + 1).Trim();
|
||||
remaining = beforeColon;
|
||||
}
|
||||
}
|
||||
|
||||
// Parse filters (|uppercase, |truncate:10, etc.)
|
||||
if (remaining.Contains("|"))
|
||||
{
|
||||
int pipeIdx = remaining.IndexOf('|');
|
||||
string namePart = remaining.Substring(0, pipeIdx);
|
||||
string filterPart = remaining.Substring(pipeIdx + 1);
|
||||
remaining = namePart;
|
||||
filters = filterPart.Split('|');
|
||||
}
|
||||
|
||||
// Parse fallback (??'default')
|
||||
if (remaining.Contains("??"))
|
||||
{
|
||||
int fallbackIdx = remaining.IndexOf("??");
|
||||
string namePart = remaining.Substring(0, fallbackIdx);
|
||||
fallback = remaining.Substring(fallbackIdx + 2).Trim();
|
||||
if (fallback.StartsWith("'") && fallback.EndsWith("'"))
|
||||
{
|
||||
fallback = fallback.Substring(1, fallback.Length - 2);
|
||||
}
|
||||
|
||||
remaining = namePart;
|
||||
}
|
||||
|
||||
// Parse alignment (,10 or ,-10)
|
||||
if (remaining.Contains(","))
|
||||
{
|
||||
int commaIdx = remaining.IndexOf(',');
|
||||
string namePart = remaining.Substring(0, commaIdx);
|
||||
string alignStr = remaining.Substring(commaIdx + 1).Trim();
|
||||
if (int.TryParse(alignStr, out int align))
|
||||
{
|
||||
alignment = align;
|
||||
}
|
||||
|
||||
remaining = namePart;
|
||||
}
|
||||
|
||||
// Parse format (:D2, :C, etc.)
|
||||
if (remaining.Contains(":") && !isConditional)
|
||||
{
|
||||
int colonIdx = remaining.IndexOf(':');
|
||||
string namePart = remaining.Substring(0, colonIdx);
|
||||
format = remaining.Substring(colonIdx + 1);
|
||||
remaining = namePart;
|
||||
}
|
||||
|
||||
name = remaining.Trim();
|
||||
|
||||
// Positional?
|
||||
int position = -1;
|
||||
if (name.Length > 0 && IsDigits(name))
|
||||
{
|
||||
int.TryParse(name, out position);
|
||||
}
|
||||
|
||||
return TemplateToken.Property(name, position, hint, string.IsNullOrEmpty(format) ? null : format, alignment, filters, fallback, conditionalValues);
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsDigits(string s)
|
||||
{
|
||||
foreach (var c in s)
|
||||
{
|
||||
if (c < '0' || c > '9')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders the template to a string, binding positional args and returning
|
||||
/// a dictionary of named properties.
|
||||
/// </summary>
|
||||
public string Render(object?[]? args, out Dictionary<string, object?> properties)
|
||||
{
|
||||
properties = new Dictionary<string, object?>(StringComparer.Ordinal);
|
||||
var sb = new StringBuilder(_raw.Length + 32);
|
||||
int positionalIndex = 0;
|
||||
|
||||
foreach (ref readonly var token in _tokens.AsSpan())
|
||||
{
|
||||
if (token.IsLiteral)
|
||||
{
|
||||
sb.Append(token.Text);
|
||||
continue;
|
||||
}
|
||||
|
||||
object? value;
|
||||
|
||||
if (token.Position >= 0)
|
||||
{
|
||||
// Positional placeholder {0}, {1} …
|
||||
value = (args != null && token.Position < args.Length) ? args[token.Position] : null;
|
||||
}
|
||||
else if (args != null && positionalIndex < args.Length && IsArgumentDriven(args))
|
||||
{
|
||||
value = args[positionalIndex++];
|
||||
properties[token.Name!] = value;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = ResolveNestedProperty(token.Name, properties);
|
||||
}
|
||||
|
||||
if (token.Name != null && token.Position < 0)
|
||||
{
|
||||
properties[token.Name] = value;
|
||||
}
|
||||
|
||||
AppendValue(sb, value, token.Hint, token.Format, token.Alignment, token.Filters, token.Fallback, token.ConditionalValues);
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resolves nested property access (e.g., "Object.Property.SubProperty")
|
||||
/// </summary>
|
||||
private static object? ResolveNestedProperty(string? propertyPath, Dictionary<string, object?> properties)
|
||||
{
|
||||
if (propertyPath == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for array indexing: Array[0]
|
||||
if (propertyPath.Contains("["))
|
||||
{
|
||||
int bracketIdx = propertyPath.IndexOf('[');
|
||||
string baseName = propertyPath.Substring(0, bracketIdx);
|
||||
string indexStr = propertyPath.Substring(bracketIdx + 1);
|
||||
if (indexStr.EndsWith("]"))
|
||||
{
|
||||
indexStr = indexStr.Substring(0, indexStr.Length - 1);
|
||||
}
|
||||
|
||||
if (properties.TryGetValue(baseName, out var collection))
|
||||
{
|
||||
return GetCollectionItem(collection, indexStr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
// Handle nested property access with dots
|
||||
if (!propertyPath.Contains("."))
|
||||
{
|
||||
return properties.TryGetValue(propertyPath, out var val) ? val : null;
|
||||
}
|
||||
|
||||
string[] parts = propertyPath.Split('.');
|
||||
object? current = null;
|
||||
|
||||
if (!properties.TryGetValue(parts[0], out current))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
for (int i = 1; i < parts.Length && current != null; i++)
|
||||
{
|
||||
current = GetPropertyValue(current, parts[i]);
|
||||
}
|
||||
|
||||
return current;
|
||||
}
|
||||
|
||||
private static object? GetPropertyValue(object? obj, string propertyName)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check for array indexing in nested path
|
||||
if (propertyName.Contains("["))
|
||||
{
|
||||
int bracketIdx = propertyName.IndexOf('[');
|
||||
string actualProp = propertyName.Substring(0, bracketIdx);
|
||||
string indexStr = propertyName.Substring(bracketIdx + 1);
|
||||
if (indexStr.EndsWith("]"))
|
||||
{
|
||||
indexStr = indexStr.Substring(0, indexStr.Length - 1);
|
||||
}
|
||||
|
||||
var prop = obj.GetType().GetProperty(actualProp, System.Reflection.BindingFlags.IgnoreCase | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
if (prop?.CanRead == true)
|
||||
{
|
||||
var collection = prop.GetValue(obj);
|
||||
return GetCollectionItem(collection, indexStr);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
var propInfo = obj.GetType().GetProperty(propertyName, System.Reflection.BindingFlags.IgnoreCase | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
|
||||
return propInfo?.CanRead == true ? propInfo.GetValue(obj) : null;
|
||||
}
|
||||
|
||||
private static object? GetCollectionItem(object? collection, string indexStr)
|
||||
{
|
||||
if (collection == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (collection is System.Collections.IList list && int.TryParse(indexStr, out int index))
|
||||
{
|
||||
return index >= 0 && index < list.Count ? list[index] : null;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Renders and populates the builder's properties from named holes.
|
||||
/// </summary>
|
||||
public string RenderInto(object?[]? args, LogEventBuilder builder)
|
||||
{
|
||||
var rendered = Render(args, out var props);
|
||||
foreach (var kv in props)
|
||||
{
|
||||
builder.WithProperty(kv.Key, kv.Value);
|
||||
}
|
||||
|
||||
return rendered;
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static void AppendValue(StringBuilder sb, object? value, TokenDestructureHint hint, string? format, int alignment, string[]? filters, string? fallback, (string?, string?) conditionalValues)
|
||||
{
|
||||
// Handle conditional rendering
|
||||
if (conditionalValues.Item1 != null || conditionalValues.Item2 != null)
|
||||
{
|
||||
bool isTrue = IsTruthy(value);
|
||||
sb.Append(isTrue ? conditionalValues.Item1 : conditionalValues.Item2);
|
||||
return;
|
||||
}
|
||||
|
||||
// Handle null/fallback
|
||||
if (value == null)
|
||||
{
|
||||
sb.Append(fallback ?? "null");
|
||||
return;
|
||||
}
|
||||
|
||||
// Apply filters
|
||||
if (filters != null && filters.Length > 0)
|
||||
{
|
||||
value = ApplyFilters(value, filters);
|
||||
}
|
||||
|
||||
string formatted = FormatValue(value, hint, format);
|
||||
|
||||
// Apply alignment
|
||||
if (alignment != 0)
|
||||
{
|
||||
formatted = alignment > 0
|
||||
? formatted.PadLeft(alignment)
|
||||
: formatted.PadRight(-alignment);
|
||||
}
|
||||
|
||||
sb.Append(formatted);
|
||||
}
|
||||
|
||||
private static bool IsTruthy(object? value)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value is bool b)
|
||||
{
|
||||
return b;
|
||||
}
|
||||
|
||||
if (value is int i)
|
||||
{
|
||||
return i != 0;
|
||||
}
|
||||
|
||||
if (value is long l)
|
||||
{
|
||||
return l != 0;
|
||||
}
|
||||
|
||||
if (value is string s)
|
||||
{
|
||||
return !string.IsNullOrEmpty(s);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static object? ApplyFilters(object? value, string[] filters)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var filter in filters)
|
||||
{
|
||||
string filterName = filter;
|
||||
string? filterParam = null;
|
||||
|
||||
if (filter.Contains(":", StringComparison.Ordinal))
|
||||
{
|
||||
int colonIdx = filter.IndexOf(':');
|
||||
filterName = filter.Substring(0, colonIdx).Trim();
|
||||
filterParam = filter.Substring(colonIdx + 1).Trim();
|
||||
}
|
||||
|
||||
value = ApplyFilter(value, filterName, filterParam);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private static object? ApplyFilter(object? value, string filterName, string? param)
|
||||
{
|
||||
if (value == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
string str = value.ToString() ?? "";
|
||||
|
||||
if (filterName.Equals("reverse", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var chars = str.ToCharArray();
|
||||
System.Array.Reverse(chars);
|
||||
return new string(chars);
|
||||
}
|
||||
|
||||
return filterName.ToLowerInvariant() switch
|
||||
{
|
||||
"uppercase" or "upper" => str.ToUpperInvariant(),
|
||||
"lowercase" or "lower" => str.ToLowerInvariant(),
|
||||
"trim" => str.Trim(),
|
||||
"truncate" => param != null && int.TryParse(param, out int len) ? (str.Length > len ? str.Substring(0, len) + "…" : str) : str,
|
||||
"substr" or "substring" => param != null && int.TryParse(param, out int pos) && pos < str.Length ? str.Substring(pos) : str,
|
||||
_ => str
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatValue(object value, TokenDestructureHint hint, string? format)
|
||||
{
|
||||
if (hint == TokenDestructureHint.Destructure)
|
||||
{
|
||||
return Destructure(value);
|
||||
}
|
||||
|
||||
if (value is IFormattable formattable && format != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return formattable.ToString(format, System.Globalization.CultureInfo.InvariantCulture);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return value.ToString() ?? "";
|
||||
}
|
||||
}
|
||||
|
||||
return value.ToString() ?? "";
|
||||
}
|
||||
|
||||
private static string Destructure(object obj)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
return "null";
|
||||
}
|
||||
|
||||
var t = obj.GetType();
|
||||
if (t.IsPrimitive || obj is string || obj is decimal || obj is DateTime || obj is DateTimeOffset || obj is Guid)
|
||||
{
|
||||
return obj.ToString()!;
|
||||
}
|
||||
|
||||
// Deep property bag with nesting
|
||||
var sb = new StringBuilder("{");
|
||||
bool first = true;
|
||||
foreach (var prop in t.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
|
||||
{
|
||||
try
|
||||
{
|
||||
if (!first)
|
||||
{
|
||||
sb.Append(", ");
|
||||
}
|
||||
|
||||
var val = prop.GetValue(obj);
|
||||
sb.Append(prop.Name).Append(": ");
|
||||
if (val == null)
|
||||
{
|
||||
sb.Append("null");
|
||||
}
|
||||
else if (val.GetType().IsPrimitive || val is string || val is decimal || val is DateTime || val is DateTimeOffset)
|
||||
{
|
||||
sb.Append(val);
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.Append(Destructure(val));
|
||||
}
|
||||
|
||||
first = false;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
sb.Append('}');
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
private static bool IsArgumentDriven(object?[] args) => args.Length > 0;
|
||||
|
||||
// Template cache to avoid re-parsing the same strings
|
||||
private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, MessageTemplate> _cache
|
||||
= new(StringComparer.Ordinal);
|
||||
|
||||
/// <summary>Returns a cached parsed template (recommended for hot paths).</summary>
|
||||
public static MessageTemplate FromCache(string template) =>
|
||||
_cache.GetOrAdd(template, static t => Parse(t));
|
||||
|
||||
/// <summary>Clears the template parse cache.</summary>
|
||||
public static void ClearCache() => _cache.Clear();
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
using EonaCat.LogStack.Core;
|
||||
using EonaCat.LogStack.Flows;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace EonaCat.LogStack.Policies;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Represents a routing rule that determines which flows should handle an event
|
||||
/// </summary>
|
||||
public class RoutingRule
|
||||
{
|
||||
public string Name { get; set; } = "";
|
||||
public int Priority { get; set; }
|
||||
public Func<LogEvent, bool> Condition { get; set; } = _ => true;
|
||||
public List<string> TargetFlows { get; set; } = new();
|
||||
public bool IsActive { get; set; } = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Policy engine that manages conditional routing and flow selection
|
||||
/// </summary>
|
||||
public sealed class PolicyEngine
|
||||
{
|
||||
private readonly List<RoutingRule> _rules = new();
|
||||
private readonly Dictionary<string, IFlow> _flowRegistry = new();
|
||||
private readonly object _lock = new object();
|
||||
|
||||
public PolicyEngine() { }
|
||||
|
||||
/// <summary>
|
||||
/// Registers a flow by name
|
||||
/// </summary>
|
||||
public void RegisterFlow(string name, IFlow flow)
|
||||
{
|
||||
if (string.IsNullOrEmpty(name))
|
||||
{
|
||||
throw new ArgumentNullException(nameof(name));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_flowRegistry[name] = flow ?? throw new ArgumentNullException(nameof(flow));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Unregisters a flow
|
||||
/// </summary>
|
||||
public void UnregisterFlow(string name)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_flowRegistry.Remove(name);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a routing rule
|
||||
/// </summary>
|
||||
public PolicyEngine AddRule(RoutingRule rule)
|
||||
{
|
||||
if (rule == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(rule));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_rules.Add(rule);
|
||||
_rules.Sort((a, b) => b.Priority.CompareTo(a.Priority));
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates and adds a conditional routing rule
|
||||
/// </summary>
|
||||
public PolicyEngine AddConditionalRule(
|
||||
string name,
|
||||
Func<LogEvent, bool> condition,
|
||||
List<string> targetFlows,
|
||||
int priority = 0)
|
||||
{
|
||||
var rule = new RoutingRule
|
||||
{
|
||||
Name = name,
|
||||
Condition = condition,
|
||||
TargetFlows = targetFlows,
|
||||
Priority = priority,
|
||||
IsActive = true
|
||||
};
|
||||
|
||||
return AddRule(rule);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a rule that routes errors to specific flows
|
||||
/// </summary>
|
||||
public PolicyEngine AddErrorRoute(List<string> targetFlows, int priority = 10)
|
||||
{
|
||||
return AddConditionalRule(
|
||||
"ErrorRoute",
|
||||
e => e.Level >= LogLevel.Error,
|
||||
targetFlows,
|
||||
priority);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a rule that routes events from a specific category
|
||||
/// </summary>
|
||||
public PolicyEngine AddCategoryRoute(string category, List<string> targetFlows, int priority = 5)
|
||||
{
|
||||
return AddConditionalRule(
|
||||
$"Category:{category}",
|
||||
e => e.Category == category,
|
||||
targetFlows,
|
||||
priority);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the flows that should handle a given event
|
||||
/// </summary>
|
||||
public List<IFlow> GetTargetFlows(LogEvent logEvent)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var targetFlows = new List<IFlow>();
|
||||
|
||||
foreach (var rule in _rules)
|
||||
{
|
||||
if (!rule.IsActive)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (rule.Condition(logEvent))
|
||||
{
|
||||
foreach (var flowName in rule.TargetFlows)
|
||||
{
|
||||
if (_flowRegistry.TryGetValue(flowName, out var flow))
|
||||
{
|
||||
targetFlows.Add(flow);
|
||||
}
|
||||
}
|
||||
break; // First matching rule wins
|
||||
}
|
||||
}
|
||||
|
||||
return targetFlows.Count > 0 ? targetFlows : new List<IFlow>(_flowRegistry.Values);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables/disables a rule by name
|
||||
/// </summary>
|
||||
public void SetRuleActive(string ruleName, bool active)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
var rule = _rules.FirstOrDefault(r => r.Name == ruleName);
|
||||
if (rule != null)
|
||||
{
|
||||
rule.IsActive = active;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all registered flows
|
||||
/// </summary>
|
||||
public Dictionary<string, IFlow> GetAllFlows()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return new Dictionary<string, IFlow>(_flowRegistry);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all routing rules
|
||||
/// </summary>
|
||||
public List<RoutingRule> GetRules()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return new List<RoutingRule>(_rules);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all rules
|
||||
/// </summary>
|
||||
public void ClearRules()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_rules.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets diagnostics about policies
|
||||
/// </summary>
|
||||
public PolicyDiagnostics GetDiagnostics()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return new PolicyDiagnostics
|
||||
{
|
||||
TotalRules = _rules.Count,
|
||||
ActiveRules = _rules.Count(r => r.IsActive),
|
||||
RegisteredFlows = _flowRegistry.Count
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Diagnostics about policy engine state
|
||||
/// </summary>
|
||||
public class PolicyDiagnostics
|
||||
{
|
||||
public int TotalRules { get; set; }
|
||||
public int ActiveRules { get; set; }
|
||||
public int RegisteredFlows { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Quota management for rate limiting across categories
|
||||
/// </summary>
|
||||
public sealed class QuotaManager
|
||||
{
|
||||
private class QuotaEntry
|
||||
{
|
||||
public int AllowedPerSecond { get; set; }
|
||||
public DateTime WindowStart { get; set; } = DateTime.UtcNow;
|
||||
public int CurrentCount { get; set; }
|
||||
}
|
||||
|
||||
private readonly Dictionary<string, QuotaEntry> _quotas = new();
|
||||
private readonly object _lock = new object();
|
||||
|
||||
/// <summary>
|
||||
/// Sets the quota for a category
|
||||
/// </summary>
|
||||
public void SetQuota(string category, int eventsPerSecond)
|
||||
{
|
||||
if (eventsPerSecond <= 0)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(eventsPerSecond));
|
||||
}
|
||||
|
||||
lock (_lock)
|
||||
{
|
||||
_quotas[category] = new QuotaEntry { AllowedPerSecond = eventsPerSecond };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks if an event should be allowed based on quota
|
||||
/// </summary>
|
||||
public bool IsAllowed(string category)
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
if (!_quotas.TryGetValue(category, out var quota))
|
||||
{
|
||||
return true; // No quota = unlimited
|
||||
}
|
||||
|
||||
var now = DateTime.UtcNow;
|
||||
if ((now - quota.WindowStart).TotalSeconds >= 1.0)
|
||||
{
|
||||
// New window
|
||||
quota.WindowStart = now;
|
||||
quota.CurrentCount = 0;
|
||||
}
|
||||
|
||||
if (quota.CurrentCount < quota.AllowedPerSecond)
|
||||
{
|
||||
quota.CurrentCount++;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets usage statistics for all quotas
|
||||
/// </summary>
|
||||
public Dictionary<string, (int allowed, int used)> GetStatistics()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
return _quotas.ToDictionary(
|
||||
kvp => kvp.Key,
|
||||
kvp => (kvp.Value.AllowedPerSecond, kvp.Value.CurrentCount));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Resets all quotas
|
||||
/// </summary>
|
||||
public void Reset()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
foreach (var quota in _quotas.Values)
|
||||
{
|
||||
quota.CurrentCount = 0;
|
||||
quota.WindowStart = DateTime.UtcNow;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all quota entries
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
lock (_lock)
|
||||
{
|
||||
_quotas.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for policy management
|
||||
/// </summary>
|
||||
public static class PolicyExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a default error flow route
|
||||
/// </summary>
|
||||
public static PolicyEngine AddDefaultErrorHandling(
|
||||
this PolicyEngine engine,
|
||||
string errorFlowName)
|
||||
{
|
||||
return engine.AddErrorRoute(new List<string> { errorFlowName });
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a policy engine with common patterns
|
||||
/// </summary>
|
||||
public static PolicyEngine CreateStandardEngine(
|
||||
IFlow consoleFlow,
|
||||
IFlow fileFlow,
|
||||
IFlow? errorFileFlow = null)
|
||||
{
|
||||
var engine = new PolicyEngine();
|
||||
engine.RegisterFlow("console", consoleFlow);
|
||||
engine.RegisterFlow("file", fileFlow);
|
||||
|
||||
if (errorFileFlow != null)
|
||||
{
|
||||
engine.RegisterFlow("error", errorFileFlow);
|
||||
engine.AddErrorRoute(new List<string> { "error", "console" }, priority: 10);
|
||||
}
|
||||
|
||||
return engine;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace EonaCat.LogStack.Pooling;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// High-performance buffer pool using ArrayPool for optimal memory efficiency.
|
||||
/// Manages buffers across multiple size tiers for flexible allocation patterns.
|
||||
/// </summary>
|
||||
public sealed class BufferPool : IDisposable
|
||||
{
|
||||
private readonly int[] _standardSizes = { 512, 1024, 4096, 8192, 16384, 65536 };
|
||||
private readonly ConcurrentDictionary<int, ArrayPool<byte>> _pools;
|
||||
private bool _isDisposed;
|
||||
|
||||
public BufferPool()
|
||||
{
|
||||
_pools = new ConcurrentDictionary<int, ArrayPool<byte>>();
|
||||
foreach (var size in _standardSizes)
|
||||
{
|
||||
_pools[size] = ArrayPool<byte>.Shared;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rents a buffer of at least the specified length
|
||||
/// </summary>
|
||||
public byte[] Rent(int minimumLength)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(BufferPool));
|
||||
}
|
||||
|
||||
// Find the smallest standard size that fits
|
||||
int selectedSize = minimumLength;
|
||||
foreach (var size in _standardSizes)
|
||||
{
|
||||
if (size >= minimumLength)
|
||||
{
|
||||
selectedSize = size;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return ArrayPool<byte>.Shared.Rent(selectedSize);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a buffer to the pool
|
||||
/// </summary>
|
||||
public void Return(byte[] buffer, bool clearBuffer = false)
|
||||
{
|
||||
if (_isDisposed || buffer == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ArrayPool<byte>.Shared.Return(buffer, clearBuffer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets statistics about pool usage
|
||||
/// </summary>
|
||||
public ArrayPoolStatistics GetStatistics()
|
||||
{
|
||||
return new ArrayPoolStatistics
|
||||
{
|
||||
TotalAllocations = ArrayPool<byte>.Shared.GetTotalAllocations(),
|
||||
BytesAllocated = ArrayPool<byte>.Shared.GetTotalBytesAllocated()
|
||||
};
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
// ArrayPool.Shared is static and managed by runtime
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Statistics about array pool usage
|
||||
/// </summary>
|
||||
public class ArrayPoolStatistics
|
||||
{
|
||||
public long TotalAllocations { get; set; }
|
||||
public long BytesAllocated { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rented buffer that automatically returns itself to the pool when disposed
|
||||
/// </summary>
|
||||
public sealed class RentedBuffer : IDisposable
|
||||
{
|
||||
private byte[] _buffer;
|
||||
private readonly int _actualLength;
|
||||
private readonly BufferPool? _pool;
|
||||
|
||||
public RentedBuffer(BufferPool? pool, int minimumLength)
|
||||
{
|
||||
_pool = pool;
|
||||
_buffer = pool?.Rent(minimumLength) ?? new byte[minimumLength];
|
||||
_actualLength = minimumLength;
|
||||
}
|
||||
|
||||
public byte[] Buffer => _buffer;
|
||||
public Span<byte> AsSpan() => new Span<byte>(_buffer, 0, _actualLength);
|
||||
public Memory<byte> AsMemory() => new Memory<byte>(_buffer, 0, _actualLength);
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_buffer != null)
|
||||
{
|
||||
_pool?.Return(_buffer, clearBuffer: true);
|
||||
_buffer = null!;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for ArrayPool integration
|
||||
/// </summary>
|
||||
public static class ArrayPoolExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets total allocations from the shared ArrayPool (via reflection/diagnostics)
|
||||
/// </summary>
|
||||
public static long GetTotalAllocations(this ArrayPool<byte> pool)
|
||||
{
|
||||
// This is a placeholder - actual statistics would require reflection or instrumentation
|
||||
// For now, this provides a hook for future diagnostics integration
|
||||
return 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets total bytes allocated from the shared ArrayPool
|
||||
/// </summary>
|
||||
public static long GetTotalBytesAllocated(this ArrayPool<byte> pool)
|
||||
{
|
||||
// This is a placeholder for diagnostics integration
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace EonaCat.LogStack.Pooling;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// High-performance object pool for reusing expensive objects without external dependencies.
|
||||
/// Supports automatic cleanup and size limiting.
|
||||
/// </summary>
|
||||
public sealed class ObjectPool<T> : IDisposable where T : class
|
||||
{
|
||||
private readonly ConcurrentBag<T> _pool;
|
||||
private readonly Func<T> _factory;
|
||||
private readonly Action<T>? _resetAction;
|
||||
private readonly int _maxSize;
|
||||
private int _currentCount;
|
||||
private bool _isDisposed;
|
||||
|
||||
public ObjectPool(Func<T> factory, Action<T>? resetAction = null, int maxSize = 100)
|
||||
{
|
||||
_factory = factory ?? throw new ArgumentNullException(nameof(factory));
|
||||
_resetAction = resetAction;
|
||||
_maxSize = maxSize;
|
||||
_pool = new ConcurrentBag<T>();
|
||||
_currentCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rents an object from the pool, creating a new one if necessary
|
||||
/// </summary>
|
||||
public T Rent()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(ObjectPool<T>));
|
||||
}
|
||||
|
||||
if (_pool.TryTake(out var item))
|
||||
{
|
||||
Interlocked.Decrement(ref _currentCount);
|
||||
return item;
|
||||
}
|
||||
|
||||
return _factory();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns an object to the pool after optional reset
|
||||
/// </summary>
|
||||
public void Return(T item)
|
||||
{
|
||||
if (_isDisposed || item == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Interlocked.Increment(ref _currentCount) <= _maxSize)
|
||||
{
|
||||
_resetAction?.Invoke(item);
|
||||
_pool.Add(item);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Decrement(ref _currentCount);
|
||||
(item as IDisposable)?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the current number of pooled objects
|
||||
/// </summary>
|
||||
public int PooledCount => _currentCount;
|
||||
|
||||
/// <summary>
|
||||
/// Clears all pooled objects
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
while (_pool.TryTake(out var item))
|
||||
{
|
||||
(item as IDisposable)?.Dispose();
|
||||
Interlocked.Decrement(ref _currentCount);
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pooled object wrapper that returns itself to the pool when disposed
|
||||
/// </summary>
|
||||
public sealed class PooledObject<T> : IDisposable where T : class
|
||||
{
|
||||
private readonly ObjectPool<T> _pool;
|
||||
private readonly T _item;
|
||||
|
||||
public PooledObject(ObjectPool<T> pool)
|
||||
{
|
||||
_pool = pool ?? throw new ArgumentNullException(nameof(pool));
|
||||
_item = pool.Rent();
|
||||
}
|
||||
|
||||
public T Item => _item;
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_pool.Return(_item);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,210 @@
|
||||
using System;
|
||||
using System.Text;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace EonaCat.LogStack.Pooling;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// High-performance StringBuilder pool with automatic capacity management.
|
||||
/// Reuses StringBuilders to reduce GC pressure in high-throughput scenarios.
|
||||
/// </summary>
|
||||
public sealed class StringBuilderPool : IDisposable
|
||||
{
|
||||
private readonly ConcurrentBag<StringBuilder> _smallPool; // <= 1KB
|
||||
private readonly ConcurrentBag<StringBuilder> _mediumPool; // 1KB - 8KB
|
||||
private readonly ConcurrentBag<StringBuilder> _largePool; // > 8KB
|
||||
|
||||
private const int SmallCapacity = 1024;
|
||||
private const int MediumCapacity = 8192;
|
||||
private const int SmallPoolLimit = 20;
|
||||
private const int MediumPoolLimit = 10;
|
||||
private const int LargePoolLimit = 5;
|
||||
|
||||
private int _smallCount;
|
||||
private int _mediumCount;
|
||||
private int _largeCount;
|
||||
private bool _isDisposed;
|
||||
|
||||
public StringBuilderPool()
|
||||
{
|
||||
_smallPool = new ConcurrentBag<StringBuilder>();
|
||||
_mediumPool = new ConcurrentBag<StringBuilder>();
|
||||
_largePool = new ConcurrentBag<StringBuilder>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rents a StringBuilder from the appropriate pool based on requested capacity
|
||||
/// </summary>
|
||||
public StringBuilder Rent(int capacity = 1024)
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
throw new ObjectDisposedException(nameof(StringBuilderPool));
|
||||
}
|
||||
|
||||
ConcurrentBag<StringBuilder>? pool;
|
||||
int currentCount;
|
||||
|
||||
if (capacity <= SmallCapacity)
|
||||
{
|
||||
pool = _smallPool;
|
||||
currentCount = _smallCount;
|
||||
}
|
||||
else if (capacity <= MediumCapacity)
|
||||
{
|
||||
pool = _mediumPool;
|
||||
currentCount = _mediumCount;
|
||||
}
|
||||
else
|
||||
{
|
||||
pool = _largePool;
|
||||
currentCount = _largeCount;
|
||||
}
|
||||
|
||||
if (currentCount > 0 && pool.TryTake(out var sb))
|
||||
{
|
||||
if (capacity <= SmallCapacity)
|
||||
{
|
||||
Interlocked.Decrement(ref _smallCount);
|
||||
}
|
||||
else if (capacity <= MediumCapacity)
|
||||
{
|
||||
Interlocked.Decrement(ref _mediumCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Decrement(ref _largeCount);
|
||||
}
|
||||
|
||||
sb.Clear();
|
||||
return sb;
|
||||
}
|
||||
|
||||
return new StringBuilder(capacity);
|
||||
}
|
||||
|
||||
public void Return(StringBuilder sb)
|
||||
{
|
||||
if (_isDisposed || sb == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int capacity = sb.Capacity;
|
||||
ConcurrentBag<StringBuilder>? pool;
|
||||
int maxLimit;
|
||||
|
||||
if (capacity <= SmallCapacity)
|
||||
{
|
||||
pool = _smallPool;
|
||||
maxLimit = SmallPoolLimit;
|
||||
if (Interlocked.Increment(ref _smallCount) <= maxLimit)
|
||||
{
|
||||
sb.Clear();
|
||||
pool.Add(sb);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Decrement(ref _smallCount);
|
||||
}
|
||||
}
|
||||
else if (capacity <= MediumCapacity)
|
||||
{
|
||||
pool = _mediumPool;
|
||||
maxLimit = MediumPoolLimit;
|
||||
if (Interlocked.Increment(ref _mediumCount) <= maxLimit)
|
||||
{
|
||||
sb.Clear();
|
||||
pool.Add(sb);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Decrement(ref _mediumCount);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
pool = _largePool;
|
||||
maxLimit = LargePoolLimit;
|
||||
if (Interlocked.Increment(ref _largeCount) <= maxLimit)
|
||||
{
|
||||
sb.Clear();
|
||||
pool.Add(sb);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Decrement(ref _largeCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets pooled item counts for diagnostics
|
||||
/// </summary>
|
||||
public (int small, int medium, int large) GetPoolCounts()
|
||||
=> (_smallCount, _mediumCount, _largeCount);
|
||||
|
||||
/// <summary>
|
||||
/// Clears all pools
|
||||
/// </summary>
|
||||
public void Clear()
|
||||
{
|
||||
while (_smallPool.TryTake(out _))
|
||||
{
|
||||
;
|
||||
}
|
||||
|
||||
while (_mediumPool.TryTake(out _))
|
||||
{
|
||||
;
|
||||
}
|
||||
|
||||
while (_largePool.TryTake(out _))
|
||||
{
|
||||
;
|
||||
}
|
||||
|
||||
_smallCount = 0;
|
||||
_mediumCount = 0;
|
||||
_largeCount = 0;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_isDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_isDisposed = true;
|
||||
Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Rented StringBuilder that automatically returns itself to the pool when disposed
|
||||
/// </summary>
|
||||
public sealed class PooledStringBuilder : IDisposable
|
||||
{
|
||||
private readonly StringBuilderPool _pool;
|
||||
private readonly StringBuilder _sb;
|
||||
|
||||
public PooledStringBuilder(StringBuilderPool pool, int capacity = 1024)
|
||||
{
|
||||
_pool = pool ?? throw new ArgumentNullException(nameof(pool));
|
||||
_sb = pool.Rent(capacity);
|
||||
}
|
||||
|
||||
public StringBuilder Builder => _sb;
|
||||
|
||||
public override string ToString() => _sb.ToString();
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_pool.Return(_sb);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,214 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
|
||||
namespace EonaCat.LogStack.Structured;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Represents a structured property with type information
|
||||
/// </summary>
|
||||
public sealed class StructuredProperty
|
||||
{
|
||||
public string Name { get; set; }
|
||||
public object? Value { get; set; }
|
||||
public Type? ValueType { get; set; }
|
||||
public bool ShouldDestructure { get; set; }
|
||||
|
||||
public StructuredProperty(string name, object? value, bool shouldDestructure = false)
|
||||
{
|
||||
Name = name;
|
||||
Value = value;
|
||||
ValueType = value?.GetType();
|
||||
ShouldDestructure = shouldDestructure;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Builder for structured properties using Serilog-like syntax
|
||||
/// </summary>
|
||||
public sealed class StructuredPropertyBuilder
|
||||
{
|
||||
private readonly Dictionary<string, StructuredProperty> _properties = new();
|
||||
|
||||
/// <summary>
|
||||
/// Adds a simple property
|
||||
/// </summary>
|
||||
public StructuredPropertyBuilder Add(string name, object? value)
|
||||
{
|
||||
_properties[name] = new StructuredProperty(name, value, false);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a property that should be destructured (rendered as JSON)
|
||||
/// </summary>
|
||||
public StructuredPropertyBuilder AddDestructured(string name, object? value)
|
||||
{
|
||||
_properties[name] = new StructuredProperty(name, value, true);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple properties from an anonymous object
|
||||
/// Example: .AddFromAnonymous(new { UserId = 123, Action = "Login" })
|
||||
/// </summary>
|
||||
public StructuredPropertyBuilder AddFromAnonymous(object anonymous)
|
||||
{
|
||||
if (anonymous == null)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
var properties = anonymous.GetType().GetProperties();
|
||||
foreach (var prop in properties)
|
||||
{
|
||||
try
|
||||
{
|
||||
var value = prop.GetValue(anonymous);
|
||||
Add(prop.Name, value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Ignore properties that can't be read
|
||||
}
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds multiple properties from a dictionary
|
||||
/// </summary>
|
||||
public StructuredPropertyBuilder AddFromDictionary(Dictionary<string, object?> dict)
|
||||
{
|
||||
if (dict == null)
|
||||
{
|
||||
return this;
|
||||
}
|
||||
|
||||
foreach (var kvp in dict)
|
||||
{
|
||||
Add(kvp.Key, kvp.Value);
|
||||
}
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a property
|
||||
/// </summary>
|
||||
public StructuredPropertyBuilder Remove(string name)
|
||||
{
|
||||
_properties.Remove(name);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Clears all properties
|
||||
/// </summary>
|
||||
public StructuredPropertyBuilder Clear()
|
||||
{
|
||||
_properties.Clear();
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the built dictionary
|
||||
/// </summary>
|
||||
public Dictionary<string, object?> Build()
|
||||
{
|
||||
return _properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the structured properties
|
||||
/// </summary>
|
||||
public Dictionary<string, StructuredProperty> BuildStructured()
|
||||
{
|
||||
return new Dictionary<string, StructuredProperty>(_properties);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets count of properties
|
||||
/// </summary>
|
||||
public int Count => _properties.Count;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contextual property scope that inherits to nested calls
|
||||
/// Similar to Serilog's LogContext.PushProperty
|
||||
/// </summary>
|
||||
public sealed class PropertyScope : IDisposable
|
||||
{
|
||||
private readonly Stack<Dictionary<string, object?>> _scopeStack;
|
||||
private readonly Dictionary<string, object?> _currentScope;
|
||||
|
||||
private static readonly AsyncLocal<PropertyScope?> _currentScopeHolder = new();
|
||||
|
||||
public PropertyScope(Dictionary<string, object?> initialProperties)
|
||||
{
|
||||
_scopeStack = new Stack<Dictionary<string, object?>>();
|
||||
_currentScope = new Dictionary<string, object?>(initialProperties);
|
||||
_scopeStack.Push(_currentScope);
|
||||
|
||||
var previous = _currentScopeHolder.Value;
|
||||
_currentScopeHolder.Value = this;
|
||||
}
|
||||
|
||||
public void PushScope(Dictionary<string, object?> properties)
|
||||
{
|
||||
var newScope = new Dictionary<string, object?>(_scopeStack.Peek());
|
||||
foreach (var kvp in properties)
|
||||
{
|
||||
newScope[kvp.Key] = kvp.Value;
|
||||
}
|
||||
_scopeStack.Push(newScope);
|
||||
}
|
||||
|
||||
public void PopScope()
|
||||
{
|
||||
if (_scopeStack.Count > 1)
|
||||
{
|
||||
_scopeStack.Pop();
|
||||
}
|
||||
}
|
||||
|
||||
public Dictionary<string, object?> GetCurrentProperties()
|
||||
{
|
||||
if (_scopeStack.Count == 0)
|
||||
{
|
||||
return new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
return new Dictionary<string, object?>(_scopeStack.Peek());
|
||||
}
|
||||
|
||||
public static Dictionary<string, object?> GetActiveProperties()
|
||||
{
|
||||
var scope = _currentScopeHolder.Value;
|
||||
return scope?.GetCurrentProperties() ?? new Dictionary<string, object?>();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_scopeStack.Clear();
|
||||
if (_currentScopeHolder.Value == this)
|
||||
{
|
||||
_currentScopeHolder.Value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Extensions for adding properties to LogEvent
|
||||
/// </summary>
|
||||
public static class StructuredPropertyExtensions
|
||||
{
|
||||
// Note: These extension methods are intended to extend LogEventBuilder
|
||||
// from EonaCat.LogStack.Core once that type is available.
|
||||
// For now, they are provided as examples for integration patterns.
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,14 +1,125 @@
|
||||
using EonaCat.LogStack.Configuration;
|
||||
using EonaCat.LogStack.Core;
|
||||
using EonaCat.LogStack.Logging;
|
||||
using Microsoft.Extensions.Hosting;
|
||||
using System;
|
||||
|
||||
namespace EonaCat.LogStack.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Extension methods for integrating EonaCat LogStack with HostApplicationBuilder
|
||||
/// </summary>
|
||||
public static class HostApplicationBuilderExtensions
|
||||
{
|
||||
public static IHostApplicationBuilder AddEonaCatLogging(this IHostApplicationBuilder builder, Action<EonaCatLogStack>? configure=null)
|
||||
/// <summary>
|
||||
/// Adds EonaCat LogStack logging with a configuration callback
|
||||
/// </summary>
|
||||
public static IHostApplicationBuilder AddEonaCatLogging(
|
||||
this IHostApplicationBuilder builder,
|
||||
Action<EonaCatLogStack>? configure = null)
|
||||
{
|
||||
builder.Services.AddEonaCatLogging(configure ?? (_=>{}));
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.Services.AddEonaCatLogging(configure ?? (_ => { }));
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds EonaCat LogStack logging with fluent LogBuilder configuration
|
||||
/// </summary>
|
||||
public static IHostApplicationBuilder AddEonaCatLogging(
|
||||
this IHostApplicationBuilder builder,
|
||||
Action<LogBuilder> configure)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (configure == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configure));
|
||||
}
|
||||
|
||||
builder.Services.AddEonaCatLogging(configure);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds EonaCat LogStack logging with fluent LogBuilder configuration and a specific category
|
||||
/// </summary>
|
||||
public static IHostApplicationBuilder AddEonaCatLogging(
|
||||
this IHostApplicationBuilder builder,
|
||||
string category,
|
||||
Action<LogBuilder> configure)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (configure == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configure));
|
||||
}
|
||||
|
||||
builder.Services.AddEonaCatLogging(category, configure);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds EonaCat LogStack with AdvancedLoggerFactory for enhanced features
|
||||
/// </summary>
|
||||
public static IHostApplicationBuilder AddAdvancedEonaCatLogging(
|
||||
this IHostApplicationBuilder builder,
|
||||
LogLevel minimumLevel = LogLevel.Trace,
|
||||
TimestampMode timestampMode = TimestampMode.Utc)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.Services.AddAdvancedEonaCatLogging(minimumLevel, timestampMode);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds EonaCat LogStack with AdvancedLoggerFactory and a configuration callback
|
||||
/// </summary>
|
||||
public static IHostApplicationBuilder AddAdvancedEonaCatLogging(
|
||||
this IHostApplicationBuilder builder,
|
||||
Action<AdvancedLoggerFactory> configure)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
if (configure == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configure));
|
||||
}
|
||||
|
||||
builder.Services.AddAdvancedEonaCatLogging(configure);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the AdvancedMetricsCollector for system-wide analytics
|
||||
/// </summary>
|
||||
public static IHostApplicationBuilder AddEonaCatMetricsCollection(
|
||||
this IHostApplicationBuilder builder)
|
||||
{
|
||||
if (builder == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(builder));
|
||||
}
|
||||
|
||||
builder.Services.AddEonaCatMetricsCollection();
|
||||
return builder;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -248,4 +248,93 @@ public static class ServiceCollectionExtensions
|
||||
|
||||
return services.AddEonaCatLoggingFactory("Application", configure);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers EonaCat LogStack with AdvancedLoggerFactory for enhanced features like category-specific configuration and context propagation
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to register with</param>
|
||||
/// <param name="minimumLevel">The minimum log level to process</param>
|
||||
/// <param name="timestampMode">The timestamp mode to use</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddAdvancedEonaCatLogging(
|
||||
this IServiceCollection services,
|
||||
LogLevel minimumLevel = LogLevel.Trace,
|
||||
TimestampMode timestampMode = TimestampMode.Utc)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
var advancedFactory = new AdvancedLoggerFactory(minimumLevel, timestampMode);
|
||||
services.AddSingleton(advancedFactory);
|
||||
services.AddSingleton<ILoggerFactory>(advancedFactory);
|
||||
services.AddSingleton<Microsoft.Extensions.Logging.ILoggerFactory>(
|
||||
new MicrosoftExtensionsLoggerFactoryAdapter(advancedFactory));
|
||||
|
||||
// Also register as ILogger for constructor injection
|
||||
services.AddSingleton(sp => sp.GetRequiredService<AdvancedLoggerFactory>().CreateLogger("Default"));
|
||||
services.AddSingleton(sp =>
|
||||
new MicrosoftExtensionsLoggerAdapter(
|
||||
sp.GetRequiredService<AdvancedLoggerFactory>().CreateLogger("Default")));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers EonaCat LogStack with AdvancedLoggerFactory and a configuration callback
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to register with</param>
|
||||
/// <param name="configure">Callback to configure the AdvancedLoggerFactory</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddAdvancedEonaCatLogging(
|
||||
this IServiceCollection services,
|
||||
Action<AdvancedLoggerFactory> configure)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
if (configure == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(configure));
|
||||
}
|
||||
|
||||
services.AddSingleton<AdvancedLoggerFactory>(sp =>
|
||||
{
|
||||
var factory = new AdvancedLoggerFactory();
|
||||
configure(factory);
|
||||
return factory;
|
||||
});
|
||||
|
||||
services.AddSingleton<ILoggerFactory>(sp => sp.GetRequiredService<AdvancedLoggerFactory>());
|
||||
services.AddSingleton<Microsoft.Extensions.Logging.ILoggerFactory>(sp =>
|
||||
new MicrosoftExtensionsLoggerFactoryAdapter(sp.GetRequiredService<AdvancedLoggerFactory>()));
|
||||
|
||||
// Also register as ILogger for constructor injection
|
||||
services.AddSingleton(sp => sp.GetRequiredService<AdvancedLoggerFactory>().CreateLogger("Default"));
|
||||
services.AddSingleton(sp =>
|
||||
new MicrosoftExtensionsLoggerAdapter(
|
||||
sp.GetRequiredService<AdvancedLoggerFactory>().CreateLogger("Default")));
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Registers the AdvancedMetricsCollector for system-wide analytics
|
||||
/// </summary>
|
||||
/// <param name="services">The service collection to register with</param>
|
||||
/// <returns>The service collection for chaining</returns>
|
||||
public static IServiceCollection AddEonaCatMetricsCollection(
|
||||
this IServiceCollection services)
|
||||
{
|
||||
if (services == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(services));
|
||||
}
|
||||
|
||||
services.AddSingleton<AdvancedMetricsCollector>();
|
||||
return services;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,6 +28,9 @@ public sealed class LogBuilder
|
||||
private TimestampMode _timestampMode = TimestampMode.Utc;
|
||||
private readonly List<IFlow> _flows = new();
|
||||
private readonly List<IBooster> _boosters = new();
|
||||
private DynamicLevelController? _dynamicLevel;
|
||||
private bool _useAsyncPipeline;
|
||||
private int _asyncPipelineCapacity = 65536;
|
||||
|
||||
public event EventHandler<LogMessage> OnLog;
|
||||
|
||||
@@ -991,6 +994,16 @@ public sealed class LogBuilder
|
||||
logger.AddBooster(booster);
|
||||
}
|
||||
|
||||
if (_dynamicLevel != null)
|
||||
{
|
||||
logger.UseDynamicLevel(_dynamicLevel);
|
||||
}
|
||||
|
||||
if (_useAsyncPipeline)
|
||||
{
|
||||
logger.UseAsyncPipeline(_asyncPipelineCapacity);
|
||||
}
|
||||
|
||||
return logger;
|
||||
}
|
||||
|
||||
@@ -1080,4 +1093,113 @@ public sealed class LogBuilder
|
||||
|
||||
lock (_flows) { _flows.RemoveAll(f => f.Name == name); }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Enables the Channel-based async dispatch pipeline for zero-blocking logging.
|
||||
/// Events are enqueued to a <see cref="System.Threading.Channels.Channel{T}"/> and
|
||||
/// consumed by a dedicated background Task.
|
||||
/// </summary>
|
||||
/// <param name="capacity">Bounded capacity (0 = unbounded).</param>
|
||||
public LogBuilder UseAsyncPipeline(int capacity = 65536)
|
||||
{
|
||||
_useAsyncPipeline = true;
|
||||
_asyncPipelineCapacity = capacity;
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Attaches a <see cref="DynamicLevelController"/> so the minimum log level can
|
||||
/// be changed at runtime without restarting the application.
|
||||
/// </summary>
|
||||
public LogBuilder WithDynamicLevelController(DynamicLevelController controller)
|
||||
{
|
||||
_dynamicLevel = controller ?? throw new ArgumentNullException(nameof(controller));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps any flow with a Circuit Breaker that opens after repeated failures
|
||||
/// and probes for recovery after a configurable timeout.
|
||||
/// </summary>
|
||||
public LogBuilder WriteToCircuitBreaker(
|
||||
IFlow inner,
|
||||
int failureThreshold = 5,
|
||||
TimeSpan? recoveryTimeout = null,
|
||||
LogLevel minimumLevel = LogLevel.Trace)
|
||||
{
|
||||
if (inner == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(inner));
|
||||
}
|
||||
|
||||
_flows.Add(new CircuitBreakerFlow(inner, failureThreshold, recoveryTimeout, minimumLevel));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wraps any flow with a predicate — events are forwarded only when the predicate returns true.
|
||||
/// </summary>
|
||||
public LogBuilder WriteToConditional(
|
||||
IFlow inner,
|
||||
Func<LogEvent, bool> predicate,
|
||||
LogLevel minimumLevel = LogLevel.Trace)
|
||||
{
|
||||
if (inner == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(inner));
|
||||
}
|
||||
|
||||
if (predicate == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(predicate));
|
||||
}
|
||||
|
||||
_flows.Add(new ConditionalFlow(inner, predicate, minimumLevel));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a <see cref="MulticastFlow"/> that fans events out to multiple inner flows in parallel.
|
||||
/// Use <see cref="MulticastFlow.Add"/> to attach targets.
|
||||
/// </summary>
|
||||
public LogBuilder WriteToMulticast(
|
||||
MulticastFlow multicast)
|
||||
{
|
||||
if (multicast == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(multicast));
|
||||
}
|
||||
|
||||
_flows.Add(multicast);
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Pushes log events to a Grafana Loki instance.
|
||||
/// </summary>
|
||||
public LogBuilder WriteToLoki(
|
||||
string lokiUrl,
|
||||
Dictionary<string, string>? labels = null,
|
||||
int batchSize = 50,
|
||||
int batchIntervalMs = 1000,
|
||||
string? bearerToken = null,
|
||||
string? basicUser = null,
|
||||
string? basicPassword = null,
|
||||
HttpClient? httpClient = null,
|
||||
LogLevel minimumLevel = LogLevel.Trace)
|
||||
{
|
||||
_flows.Add(new LokiFlow(
|
||||
lokiUrl, labels, batchSize, batchIntervalMs,
|
||||
bearerToken, basicUser, basicPassword, httpClient, minimumLevel));
|
||||
return this;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the <see cref="CallerInfoBooster"/> which captures caller member/file/line.
|
||||
/// </summary>
|
||||
public LogBuilder BoostWithCallerInfo()
|
||||
{
|
||||
_boosters.Add(new CallerInfoBooster());
|
||||
return this;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ public class LoggerDiagnostics
|
||||
public LogLevel MinimumLevel { get; set; }
|
||||
public long TotalLogged { get; set; }
|
||||
public long TotalDropped { get; set; }
|
||||
public long TotalExceptions { get; set; }
|
||||
public int FlowCount { get; set; }
|
||||
public int BoosterCount { get; set; }
|
||||
public List<FlowDiagnostics> Flows { get; set; }
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
using EonaCat.LogStack.Analytics;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace EonaCat.LogStack;
|
||||
|
||||
// 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.
|
||||
|
||||
/// <summary>
|
||||
/// Comprehensive metrics about the logger
|
||||
/// </summary>
|
||||
public class LoggerMetrics
|
||||
{
|
||||
public long TotalLogged { get; set; }
|
||||
public long TotalDropped { get; set; }
|
||||
public long TotalExceptions { get; set; }
|
||||
public long TotalBytes { get; set; }
|
||||
public double WritesPerSecond { get; set; }
|
||||
public long TraceCount { get; set; }
|
||||
public long DebugCount { get; set; }
|
||||
public long InformationCount { get; set; }
|
||||
public long WarningCount { get; set; }
|
||||
public long ErrorCount { get; set; }
|
||||
public long CriticalCount { get; set; }
|
||||
public long UptimeMilliseconds { get; set; }
|
||||
public List<FlowStatisticsSnapshot> FlowMetrics { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the success rate (logged / (logged + dropped))
|
||||
/// </summary>
|
||||
public double SuccessRate
|
||||
{
|
||||
get
|
||||
{
|
||||
var total = TotalLogged + TotalDropped;
|
||||
return total > 0 ? (TotalLogged * 100.0) / total : 100;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets average bytes per event
|
||||
/// </summary>
|
||||
public double AverageBytesPerEvent
|
||||
{
|
||||
get => TotalLogged > 0 ? TotalBytes / (double)TotalLogged : 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns formatted report
|
||||
/// </summary>
|
||||
public override string ToString()
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("===== LOGGER METRICS =====");
|
||||
sb.AppendLine($"Total Logged: {TotalLogged:N0}");
|
||||
sb.AppendLine($"Total Dropped: {TotalDropped:N0}");
|
||||
sb.AppendLine($"Success Rate: {SuccessRate:F2}%");
|
||||
sb.AppendLine($"Total Exceptions: {TotalExceptions:N0}");
|
||||
sb.AppendLine($"Total Bytes: {TotalBytes:N0}");
|
||||
sb.AppendLine($"Average Bytes/Event: {AverageBytesPerEvent:F2}");
|
||||
sb.AppendLine($"Writes/Second: {WritesPerSecond:F2}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("Events by Level:");
|
||||
sb.AppendLine($" Trace: {TraceCount:N0}");
|
||||
sb.AppendLine($" Debug: {DebugCount:N0}");
|
||||
sb.AppendLine($" Information: {InformationCount:N0}");
|
||||
sb.AppendLine($" Warning: {WarningCount:N0}");
|
||||
sb.AppendLine($" Error: {ErrorCount:N0}");
|
||||
sb.AppendLine($" Critical: {CriticalCount:N0}");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine($"Uptime: {System.TimeSpan.FromMilliseconds(UptimeMilliseconds):hh\\:mm\\:ss}");
|
||||
|
||||
if (FlowMetrics.Count > 0)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.AppendLine("Flow Statistics:");
|
||||
foreach (var flow in FlowMetrics)
|
||||
{
|
||||
sb.AppendLine($" {flow.FlowName} ({flow.FlowType}): {flow.EventsProcessed:N0} events, {flow.SuccessRate:F2}% success");
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine("==========================");
|
||||
return sb.ToString();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user