Added more stats

Added more dependency injection
Made README.md better
This commit is contained in:
2026-06-11 08:17:29 +02:00
committed by Jeroen Saey
parent 001ed93ebb
commit dae624fd45
40 changed files with 11622 additions and 222 deletions
@@ -118,9 +118,11 @@ 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);
} }
catch (Exception ex) catch (Exception ex)
@@ -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();
}
}
+12 -3
View File
@@ -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>
+244 -9
View File
@@ -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,21 +408,62 @@ 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
{ {
var result = flow.BlastAsync(logEvent).GetAwaiter().GetResult(); var result = flow.BlastAsync(logEvent).GetAwaiter().GetResult();
if (result == WriteResult.Dropped) if (result == WriteResult.Dropped)
{ {
Interlocked.Increment(ref _totalDroppedCount); Interlocked.Increment(ref _totalDroppedCount);
} }
} }
catch { } catch { }
} }
} }
private async Task ConsumeChannelAsync(CancellationToken cancellationToken)
{
var reader = _asyncChannel!.Reader;
try
{
await foreach (var logEvent in reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{
var flows = _concurrentFlows;
foreach (var flow in flows)
{
try
{
var result = await flow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
if (result == WriteResult.Dropped)
{
Interlocked.Increment(ref _totalDroppedCount);
}
}
catch { }
}
}
}
catch (OperationCanceledException) { }
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [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&lt;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" &amp;&amp; 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)
{ {
builder.Services.AddEonaCatLogging(configure ?? (_=>{})); if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddEonaCatLogging(configure ?? (_ => { }));
return builder;
}
/// <summary>
/// Adds EonaCat LogStack logging with fluent LogBuilder configuration
/// </summary>
public static IHostApplicationBuilder AddEonaCatLogging(
this IHostApplicationBuilder builder,
Action<LogBuilder> configure)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
builder.Services.AddEonaCatLogging(configure);
return builder;
}
/// <summary>
/// Adds EonaCat LogStack logging with fluent LogBuilder configuration and a specific category
/// </summary>
public static IHostApplicationBuilder AddEonaCatLogging(
this IHostApplicationBuilder builder,
string category,
Action<LogBuilder> configure)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
builder.Services.AddEonaCatLogging(category, configure);
return builder;
}
/// <summary>
/// Adds EonaCat LogStack with AdvancedLoggerFactory for enhanced features
/// </summary>
public static IHostApplicationBuilder AddAdvancedEonaCatLogging(
this IHostApplicationBuilder builder,
LogLevel minimumLevel = LogLevel.Trace,
TimestampMode timestampMode = TimestampMode.Utc)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddAdvancedEonaCatLogging(minimumLevel, timestampMode);
return builder;
}
/// <summary>
/// Adds EonaCat LogStack with AdvancedLoggerFactory and a configuration callback
/// </summary>
public static IHostApplicationBuilder AddAdvancedEonaCatLogging(
this IHostApplicationBuilder builder,
Action<AdvancedLoggerFactory> configure)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
builder.Services.AddAdvancedEonaCatLogging(configure);
return builder;
}
/// <summary>
/// Adds the AdvancedMetricsCollector for system-wide analytics
/// </summary>
public static IHostApplicationBuilder AddEonaCatMetricsCollection(
this IHostApplicationBuilder builder)
{
if (builder == null)
{
throw new ArgumentNullException(nameof(builder));
}
builder.Services.AddEonaCatMetricsCollection();
return builder; 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;
}
} }
+123 -1
View File
@@ -28,7 +28,10 @@ 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;
public LogBuilder(string category = "Application") public LogBuilder(string category = "Application")
@@ -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;
}
} }
+1
View File
@@ -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; }
+90
View File
@@ -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();
}
}
+2187 -203
View File
File diff suppressed because it is too large Load Diff