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
|
try
|
||||||
{
|
{
|
||||||
if (IsJson(rawMessage))
|
if (IsJson(rawMessage))
|
||||||
|
{
|
||||||
return ParseJson(rawMessage, remoteIp);
|
return ParseJson(rawMessage, remoteIp);
|
||||||
|
}
|
||||||
|
|
||||||
return ParseSyslogAdvanced(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>
|
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||||
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
|
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
|
||||||
<PackageIconUrl />
|
<PackageIconUrl />
|
||||||
<FileVersion>0.0.8</FileVersion>
|
<FileVersion>0.0.9</FileVersion>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
@@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<EVRevisionFormat>0.0.8+{chash:10}.{c:ymd}</EVRevisionFormat>
|
<EVRevisionFormat>0.0.9+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||||
<EVDefault>true</EVDefault>
|
<EVDefault>true</EVDefault>
|
||||||
<EVInfo>true</EVInfo>
|
<EVInfo>true</EVInfo>
|
||||||
<EVTagMatch>v[0-9]*</EVTagMatch>
|
<EVTagMatch>v[0-9]*</EVTagMatch>
|
||||||
@@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>0.0.8</Version>
|
<Version>0.0.9</Version>
|
||||||
<PackageId>EonaCat.LogStack</PackageId>
|
<PackageId>EonaCat.LogStack</PackageId>
|
||||||
<Product>EonaCat.LogStack</Product>
|
<Product>EonaCat.LogStack</Product>
|
||||||
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl>
|
<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>
|
</PropertyGroup>
|
||||||
</Target>
|
</Target>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Remove="Examples\**" />
|
||||||
|
<EmbeddedResource Remove="Examples\**" />
|
||||||
|
<None Remove="Examples\**" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Remove="icon.png" />
|
<None Remove="icon.png" />
|
||||||
<None Include="..\LICENSE">
|
<None Include="..\LICENSE">
|
||||||
@@ -91,4 +97,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
<PackagePath>\</PackagePath>
|
<PackagePath>\</PackagePath>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="EonaCatLoggerCore\Examples\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -1,12 +1,15 @@
|
|||||||
using EonaCat.LogStack.Boosters;
|
using EonaCat.LogStack.Analytics;
|
||||||
|
using EonaCat.LogStack.Boosters;
|
||||||
using EonaCat.LogStack.Core;
|
using EonaCat.LogStack.Core;
|
||||||
using EonaCat.LogStack.Flows;
|
using EonaCat.LogStack.Flows;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
@@ -30,12 +33,35 @@ namespace EonaCat.LogStack
|
|||||||
private volatile bool _isDisposed;
|
private volatile bool _isDisposed;
|
||||||
private long _totalLoggedCount;
|
private long _totalLoggedCount;
|
||||||
private long _totalDroppedCount;
|
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>>();
|
private readonly List<ActionRef<LogEventBuilder>> _modifiers = new List<ActionRef<LogEventBuilder>>();
|
||||||
public delegate void ActionRef<T>(ref T item);
|
public delegate void ActionRef<T>(ref T item);
|
||||||
|
|
||||||
private readonly object _modifiersLock = new object();
|
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;
|
public event EventHandler<LogMessage> OnLog;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -50,6 +76,50 @@ namespace EonaCat.LogStack
|
|||||||
_timestampMode = timestampMode;
|
_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>
|
/// <summary>
|
||||||
/// Adds a flow (output destination) to this logger
|
/// Adds a flow (output destination) to this logger
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -115,11 +185,12 @@ namespace EonaCat.LogStack
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void Log(string message, LogLevel level = LogLevel.Information)
|
public void Log(string message, LogLevel level = LogLevel.Information)
|
||||||
{
|
{
|
||||||
if (_isDisposed || level < _minimumLevel)
|
if (_isDisposed || level < EffectiveMinLevel())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TrackLevel(level);
|
||||||
var builder = new LogEventBuilder()
|
var builder = new LogEventBuilder()
|
||||||
.WithLevel(level)
|
.WithLevel(level)
|
||||||
.WithCategory(_category)
|
.WithCategory(_category)
|
||||||
@@ -129,14 +200,68 @@ namespace EonaCat.LogStack
|
|||||||
ProcessLogEvent(ref builder);
|
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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void Log(LogLevel level, Exception exception, string message)
|
public void Log(LogLevel level, Exception exception, string message)
|
||||||
{
|
{
|
||||||
if (_isDisposed || level < _minimumLevel)
|
if (_isDisposed || level < EffectiveMinLevel())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TrackLevel(level);
|
||||||
|
if (exception != null)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _totalExceptionsCount);
|
||||||
|
}
|
||||||
|
|
||||||
var builder = new LogEventBuilder()
|
var builder = new LogEventBuilder()
|
||||||
.WithLevel(level)
|
.WithLevel(level)
|
||||||
.WithCategory(_category)
|
.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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
public void Log(LogLevel level, string message, params (string Key, object Value)[] properties)
|
public void Log(LogLevel level, string message, params (string Key, object Value)[] properties)
|
||||||
{
|
{
|
||||||
if (_isDisposed || level < _minimumLevel)
|
if (_isDisposed || level < EffectiveMinLevel())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TrackLevel(level);
|
||||||
var builder = new LogEventBuilder()
|
var builder = new LogEventBuilder()
|
||||||
.WithLevel(level)
|
.WithLevel(level)
|
||||||
.WithCategory(_category)
|
.WithCategory(_category)
|
||||||
@@ -256,7 +408,23 @@ namespace EonaCat.LogStack
|
|||||||
var logEvent = builder.Build();
|
var logEvent = builder.Build();
|
||||||
Interlocked.Increment(ref _totalLoggedCount);
|
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)
|
foreach (var flow in _concurrentFlows)
|
||||||
{
|
{
|
||||||
try
|
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)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private long GetTimestamp()
|
private long GetTimestamp()
|
||||||
{
|
{
|
||||||
@@ -301,12 +494,45 @@ namespace EonaCat.LogStack
|
|||||||
MinimumLevel = _minimumLevel,
|
MinimumLevel = _minimumLevel,
|
||||||
TotalLogged = Interlocked.Read(ref _totalLoggedCount),
|
TotalLogged = Interlocked.Read(ref _totalLoggedCount),
|
||||||
TotalDropped = Interlocked.Read(ref _totalDroppedCount),
|
TotalDropped = Interlocked.Read(ref _totalDroppedCount),
|
||||||
|
TotalExceptions = Interlocked.Read(ref _totalExceptionsCount),
|
||||||
FlowCount = _flows.Count,
|
FlowCount = _flows.Count,
|
||||||
BoosterCount = _boosters.Count,
|
BoosterCount = _boosters.Count,
|
||||||
Flows = flowDiagnostics
|
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()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (_isDisposed)
|
if (_isDisposed)
|
||||||
@@ -316,11 +542,20 @@ namespace EonaCat.LogStack
|
|||||||
|
|
||||||
_isDisposed = true;
|
_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);
|
await FlushAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
var disposeTasks = _concurrentFlows.Select(f => f.DisposeAsync().AsTask());
|
var disposeTasks = _concurrentFlows.Select(f => f.DisposeAsync().AsTask());
|
||||||
await Task.WhenAll(disposeTasks).ConfigureAwait(false);
|
await Task.WhenAll(disposeTasks).ConfigureAwait(false);
|
||||||
|
|
||||||
|
_asyncCts?.Dispose();
|
||||||
GC.SuppressFinalize(this);
|
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 EonaCat.LogStack.Core;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
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
|
namespace EonaCat.LogStack.EonaCatLogStackCore
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Comprehensive logging statistics with per-level and per-category metrics
|
||||||
|
/// </summary>
|
||||||
public struct LogStats
|
public struct LogStats
|
||||||
{
|
{
|
||||||
|
// Basic counters
|
||||||
public long Written;
|
public long Written;
|
||||||
public long Dropped;
|
public long Dropped;
|
||||||
public long Rotations;
|
public long Rotations;
|
||||||
public long BytesWritten;
|
public long BytesWritten;
|
||||||
public double WritesPerSecond;
|
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)
|
public LogStats(long written, long dropped, long rotations, long bytesWritten, double writesPerSecond)
|
||||||
{
|
{
|
||||||
Written = written;
|
Written = written;
|
||||||
@@ -18,6 +43,19 @@ namespace EonaCat.LogStack.EonaCatLogStackCore
|
|||||||
Rotations = rotations;
|
Rotations = rotations;
|
||||||
BytesWritten = bytesWritten;
|
BytesWritten = bytesWritten;
|
||||||
WritesPerSecond = writesPerSecond;
|
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
|
/// Gets diagnostics information about the logger
|
||||||
/// </summary>
|
/// </summary>
|
||||||
LoggerDiagnostics GetDiagnostics();
|
LoggerDiagnostics GetDiagnostics();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets detailed metrics about the logger
|
||||||
|
/// </summary>
|
||||||
|
LoggerMetrics GetMetrics();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -124,6 +129,19 @@ public sealed class LoggerFactory : ILoggerFactory
|
|||||||
return _logStack.GetDiagnostics();
|
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>
|
/// <summary>
|
||||||
/// Disposes all loggers and the underlying log stack
|
/// Disposes all loggers and the underlying log stack
|
||||||
/// </summary>
|
/// </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.Core;
|
||||||
|
using EonaCat.LogStack.Logging;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace EonaCat.LogStack.Extensions;
|
namespace EonaCat.LogStack.Extensions;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for integrating EonaCat LogStack with HostApplicationBuilder
|
||||||
|
/// </summary>
|
||||||
public static class HostApplicationBuilderExtensions
|
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)
|
||||||
{
|
{
|
||||||
|
if (builder == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException(nameof(builder));
|
||||||
|
}
|
||||||
|
|
||||||
builder.Services.AddEonaCatLogging(configure ?? (_ => { }));
|
builder.Services.AddEonaCatLogging(configure ?? (_ => { }));
|
||||||
return builder;
|
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);
|
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 TimestampMode _timestampMode = TimestampMode.Utc;
|
||||||
private readonly List<IFlow> _flows = new();
|
private readonly List<IFlow> _flows = new();
|
||||||
private readonly List<IBooster> _boosters = new();
|
private readonly List<IBooster> _boosters = new();
|
||||||
|
private DynamicLevelController? _dynamicLevel;
|
||||||
|
private bool _useAsyncPipeline;
|
||||||
|
private int _asyncPipelineCapacity = 65536;
|
||||||
|
|
||||||
public event EventHandler<LogMessage> OnLog;
|
public event EventHandler<LogMessage> OnLog;
|
||||||
|
|
||||||
@@ -991,6 +994,16 @@ public sealed class LogBuilder
|
|||||||
logger.AddBooster(booster);
|
logger.AddBooster(booster);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (_dynamicLevel != null)
|
||||||
|
{
|
||||||
|
logger.UseDynamicLevel(_dynamicLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_useAsyncPipeline)
|
||||||
|
{
|
||||||
|
logger.UseAsyncPipeline(_asyncPipelineCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
return logger;
|
return logger;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1080,4 +1093,113 @@ public sealed class LogBuilder
|
|||||||
|
|
||||||
lock (_flows) { _flows.RemoveAll(f => f.Name == name); }
|
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 LogLevel MinimumLevel { get; set; }
|
||||||
public long TotalLogged { get; set; }
|
public long TotalLogged { get; set; }
|
||||||
public long TotalDropped { get; set; }
|
public long TotalDropped { get; set; }
|
||||||
|
public long TotalExceptions { get; set; }
|
||||||
public int FlowCount { get; set; }
|
public int FlowCount { get; set; }
|
||||||
public int BoosterCount { get; set; }
|
public int BoosterCount { get; set; }
|
||||||
public List<FlowDiagnostics> Flows { 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