diff --git a/EonaCat.LogStack.Status/Services/SyslogService.cs b/EonaCat.LogStack.Status/Services/SyslogService.cs
index f4ce85a..69cc630 100644
--- a/EonaCat.LogStack.Status/Services/SyslogService.cs
+++ b/EonaCat.LogStack.Status/Services/SyslogService.cs
@@ -118,9 +118,11 @@ public class SyslogUdpService : BackgroundService
{
try
{
- if (IsJson(rawMessage))
- return ParseJson(rawMessage, remoteIp);
-
+ if (IsJson(rawMessage))
+ {
+ return ParseJson(rawMessage, remoteIp);
+ }
+
return ParseSyslogAdvanced(rawMessage, remoteIp);
}
catch (Exception ex)
diff --git a/EonaCat.LogStack/AdvancedMetricsCollector.cs b/EonaCat.LogStack/AdvancedMetricsCollector.cs
new file mode 100644
index 0000000..37017de
--- /dev/null
+++ b/EonaCat.LogStack/AdvancedMetricsCollector.cs
@@ -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.
+
+///
+/// Advanced metrics collector that provides comprehensive analytics across loggers
+///
+public sealed class AdvancedMetricsCollector
+{
+ private readonly List _loggers = new();
+ private readonly object _lock = new();
+
+ ///
+ /// Registers a logger for metrics collection
+ ///
+ public void RegisterLogger(EonaCatLogStack logger)
+ {
+ if (logger != null)
+ {
+ lock (_lock)
+ {
+ if (!_loggers.Contains(logger))
+ {
+ _loggers.Add(logger);
+ }
+ }
+ }
+ }
+
+ ///
+ /// Unregisters a logger
+ ///
+ public void UnregisterLogger(EonaCatLogStack logger)
+ {
+ if (logger != null)
+ {
+ lock (_lock)
+ {
+ _loggers.Remove(logger);
+ }
+ }
+ }
+
+ ///
+ /// Gets aggregated metrics across all registered loggers
+ ///
+ public AggregatedMetrics GetAggregatedMetrics()
+ {
+ lock (_lock)
+ {
+ var aggregated = new AggregatedMetrics
+ {
+ Timestamp = DateTime.UtcNow,
+ LoggerMetrics = new List()
+ };
+
+ 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;
+ }
+ }
+
+ ///
+ /// Gets a performance comparison across loggers
+ ///
+ 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
+ };
+ }
+
+ ///
+ /// Gets a health report
+ ///
+ public HealthReport GetHealthReport()
+ {
+ var aggregated = GetAggregatedMetrics();
+
+ var report = new HealthReport
+ {
+ Timestamp = aggregated.Timestamp,
+ OverallStatus = "Healthy",
+ Warnings = new List(),
+ Errors = new List()
+ };
+
+ 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;
+ }
+
+ ///
+ /// Resets all metrics
+ ///
+ public void Reset()
+ {
+ lock (_lock)
+ {
+ _loggers.Clear();
+ }
+ }
+}
+
+///
+/// Aggregated metrics across multiple loggers
+///
+public sealed class AggregatedMetrics
+{
+ public DateTime Timestamp { get; set; }
+ public List 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();
+ }
+}
+
+///
+/// Performance comparison across loggers
+///
+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();
+ }
+}
+
+///
+/// Health report for logging system
+///
+public sealed class HealthReport
+{
+ public DateTime Timestamp { get; set; }
+ public string OverallStatus { get; set; } = "Unknown";
+ public List Warnings { get; set; } = new();
+ public List 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();
+ }
+}
diff --git a/EonaCat.LogStack/EonaCat.LogStack.csproj b/EonaCat.LogStack/EonaCat.LogStack.csproj
index f63570b..6768bb8 100644
--- a/EonaCat.LogStack/EonaCat.LogStack.csproj
+++ b/EonaCat.LogStack/EonaCat.LogStack.csproj
@@ -14,7 +14,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
EonaCat (Jeroen Saey)
EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey
- 0.0.8
+ 0.0.9
README.md
True
LICENSE
@@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
- 0.0.8+{chash:10}.{c:ymd}
+ 0.0.9+{chash:10}.{c:ymd}
true
true
v[0-9]*
@@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
- 0.0.8
+ 0.0.9
EonaCat.LogStack
EonaCat.LogStack
https://git.saey.me/EonaCat/EonaCat.LogStack
@@ -50,6 +50,12 @@ It features a rich fluent API for routing log events to dozens of destinations f
+
+
+
+
+
+
@@ -91,4 +97,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
\
+
+
+
\ No newline at end of file
diff --git a/EonaCat.LogStack/EonaCatLogger.cs b/EonaCat.LogStack/EonaCatLogger.cs
index 594bdbc..aa44544 100644
--- a/EonaCat.LogStack/EonaCatLogger.cs
+++ b/EonaCat.LogStack/EonaCatLogger.cs
@@ -1,12 +1,15 @@
-using EonaCat.LogStack.Boosters;
+using EonaCat.LogStack.Analytics;
+using EonaCat.LogStack.Boosters;
using EonaCat.LogStack.Core;
using EonaCat.LogStack.Flows;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
+using System.Diagnostics;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Threading;
+using System.Threading.Channels;
using System.Threading.Tasks;
// This file is part of the EonaCat project(s) which is released under the Apache License.
@@ -30,12 +33,35 @@ namespace EonaCat.LogStack
private volatile bool _isDisposed;
private long _totalLoggedCount;
private long _totalDroppedCount;
+ private long _totalExceptionsCount;
+ private long _totalByteCount;
+ private readonly Stopwatch _startTime = Stopwatch.StartNew();
+
+ // Per-level counters
+ private long _traceCount;
+ private long _debugCount;
+ private long _informationCount;
+ private long _warningCount;
+ private long _errorCount;
+ private long _criticalCount;
+
+ // Metrics tracking
+ private readonly LoggingMetrics _metrics = new LoggingMetrics();
+ private readonly ConcurrentDictionary _flowStats = new();
private readonly List> _modifiers = new List>();
public delegate void ActionRef(ref T item);
private readonly object _modifiersLock = new object();
+ // Channel-based async pipeline
+ private Channel? _asyncChannel;
+ private Task? _asyncConsumer;
+ private CancellationTokenSource? _asyncCts;
+
+ // Dynamic level controller
+ private volatile DynamicLevelController? _dynamicLevel;
+
public event EventHandler OnLog;
///
@@ -50,6 +76,50 @@ namespace EonaCat.LogStack
_timestampMode = timestampMode;
}
+ ///
+ /// Enables a Channel-based async dispatch pipeline for zero-blocking logging.
+ /// Events are enqueued and consumed by a dedicated background Task.
+ ///
+ /// Bounded channel capacity (0 = unbounded).
+ public EonaCatLogStack UseAsyncPipeline(int capacity = 65536)
+ {
+ if (_asyncChannel != null)
+ {
+ return this;
+ }
+
+ _asyncCts = new CancellationTokenSource();
+ _asyncChannel = capacity > 0
+ ? Channel.CreateBounded(new BoundedChannelOptions(capacity)
+ {
+ FullMode = BoundedChannelFullMode.DropOldest,
+ SingleReader = true,
+ AllowSynchronousContinuations = false
+ })
+ : Channel.CreateUnbounded(new UnboundedChannelOptions
+ {
+ SingleReader = true,
+ AllowSynchronousContinuations = false
+ });
+
+ _asyncConsumer = Task.Run(() => ConsumeChannelAsync(_asyncCts.Token));
+ return this;
+ }
+
+ ///
+ /// Attaches a so the minimum level
+ /// can be changed at runtime without restarting the application.
+ ///
+ 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;
+
///
/// Adds a flow (output destination) to this logger
///
@@ -115,11 +185,12 @@ namespace EonaCat.LogStack
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(string message, LogLevel level = LogLevel.Information)
{
- if (_isDisposed || level < _minimumLevel)
+ if (_isDisposed || level < EffectiveMinLevel())
{
return;
}
+ TrackLevel(level);
var builder = new LogEventBuilder()
.WithLevel(level)
.WithCategory(_category)
@@ -129,14 +200,68 @@ namespace EonaCat.LogStack
ProcessLogEvent(ref builder);
}
+ ///
+ /// Logs using a structured message template, binding named holes to the provided args.
+ /// E.g. LogTemplate(LogLevel.Information, "User {UserId} logged in from {Ip}", userId, ip)
+ ///
+ [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);
+ }
+
+ ///
+ /// Logs using a structured message template with an exception.
+ ///
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ public void LogTemplate(LogLevel level, Exception exception, string template, params object?[] args)
+ {
+ if (_isDisposed || level < EffectiveMinLevel())
+ {
+ return;
+ }
+
+ var parsed = MessageTemplate.FromCache(template);
+ var builder = new LogEventBuilder()
+ .WithLevel(level)
+ .WithCategory(_category)
+ .WithException(exception)
+ .WithTimestamp(GetTimestamp());
+
+ var rendered = parsed.RenderInto(args, builder);
+ builder.WithMessage(rendered);
+
+ ProcessLogEvent(ref builder);
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(LogLevel level, Exception exception, string message)
{
- if (_isDisposed || level < _minimumLevel)
+ if (_isDisposed || level < EffectiveMinLevel())
{
return;
}
+ TrackLevel(level);
+ if (exception != null)
+ {
+ Interlocked.Increment(ref _totalExceptionsCount);
+ }
+
var builder = new LogEventBuilder()
.WithLevel(level)
.WithCategory(_category)
@@ -156,14 +281,41 @@ namespace EonaCat.LogStack
});
}
+ [MethodImpl(MethodImplOptions.AggressiveInlining)]
+ private void TrackLevel(LogLevel level)
+ {
+ switch (level)
+ {
+ case LogLevel.Trace:
+ Interlocked.Increment(ref _traceCount);
+ break;
+ case LogLevel.Debug:
+ Interlocked.Increment(ref _debugCount);
+ break;
+ case LogLevel.Information:
+ Interlocked.Increment(ref _informationCount);
+ break;
+ case LogLevel.Warning:
+ Interlocked.Increment(ref _warningCount);
+ break;
+ case LogLevel.Error:
+ Interlocked.Increment(ref _errorCount);
+ break;
+ case LogLevel.Critical:
+ Interlocked.Increment(ref _criticalCount);
+ break;
+ }
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Log(LogLevel level, string message, params (string Key, object Value)[] properties)
{
- if (_isDisposed || level < _minimumLevel)
+ if (_isDisposed || level < EffectiveMinLevel())
{
return;
}
+ TrackLevel(level);
var builder = new LogEventBuilder()
.WithLevel(level)
.WithCategory(_category)
@@ -256,21 +408,62 @@ namespace EonaCat.LogStack
var logEvent = builder.Build();
Interlocked.Increment(ref _totalLoggedCount);
- // Blast to flows
+ // Async channel pipeline
+ if (_asyncChannel != null)
+ {
+ if (!_asyncChannel.Writer.TryWrite(logEvent))
+ {
+ Interlocked.Increment(ref _totalDroppedCount);
+ }
+
+ return;
+ }
+
+ // Synchronous blast to flows
+ DispatchToFlows(logEvent);
+ }
+
+ private void DispatchToFlows(LogEvent logEvent)
+ {
foreach (var flow in _concurrentFlows)
{
try
{
var result = flow.BlastAsync(logEvent).GetAwaiter().GetResult();
- if (result == WriteResult.Dropped)
- {
- Interlocked.Increment(ref _totalDroppedCount);
- }
+ if (result == WriteResult.Dropped)
+ {
+ Interlocked.Increment(ref _totalDroppedCount);
+ }
}
catch { }
}
}
+ private async Task ConsumeChannelAsync(CancellationToken cancellationToken)
+ {
+ var reader = _asyncChannel!.Reader;
+ try
+ {
+ await foreach (var logEvent in reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
+ {
+ var flows = _concurrentFlows;
+ foreach (var flow in flows)
+ {
+ try
+ {
+ var result = await flow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
+ if (result == WriteResult.Dropped)
+ {
+ Interlocked.Increment(ref _totalDroppedCount);
+ }
+ }
+ catch { }
+ }
+ }
+ }
+ catch (OperationCanceledException) { }
+ }
+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private long GetTimestamp()
{
@@ -301,12 +494,45 @@ namespace EonaCat.LogStack
MinimumLevel = _minimumLevel,
TotalLogged = Interlocked.Read(ref _totalLoggedCount),
TotalDropped = Interlocked.Read(ref _totalDroppedCount),
+ TotalExceptions = Interlocked.Read(ref _totalExceptionsCount),
FlowCount = _flows.Count,
BoosterCount = _boosters.Count,
Flows = flowDiagnostics
};
}
+ ///
+ /// Gets detailed metrics about the logger
+ ///
+ 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()
+ };
+ }
+
+ ///
+ /// Gets the metrics collector for analytics
+ ///
+ public LoggingMetrics GetMetricsCollector() => _metrics;
+
+
public async ValueTask DisposeAsync()
{
if (_isDisposed)
@@ -316,11 +542,20 @@ namespace EonaCat.LogStack
_isDisposed = true;
+ // Drain and stop the async channel pipeline if active
+ if (_asyncChannel != null)
+ {
+ _asyncChannel.Writer.TryComplete();
+ _asyncCts?.Cancel();
+ try { if (_asyncConsumer != null) { await _asyncConsumer.ConfigureAwait(false); } } catch { }
+ }
+
await FlushAsync().ConfigureAwait(false);
var disposeTasks = _concurrentFlows.Select(f => f.DisposeAsync().AsTask());
await Task.WhenAll(disposeTasks).ConfigureAwait(false);
+ _asyncCts?.Dispose();
GC.SuppressFinalize(this);
}
}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/AdvancedLoggerFactory.cs b/EonaCat.LogStack/EonaCatLoggerCore/AdvancedLoggerFactory.cs
new file mode 100644
index 0000000..7c77371
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/AdvancedLoggerFactory.cs
@@ -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.
+
+///
+/// Advanced configuration for category-specific logger settings
+///
+public class CategoryConfig
+{
+ public string CategoryName { get; set; } = "";
+ public LogLevel? MinimumLevel { get; set; }
+ public List Boosters { get; set; } = new();
+ public Dictionary Properties { get; set; } = new();
+ public Func? Filter { get; set; }
+}
+
+///
+/// Enhanced LoggerFactory with category configuration, dynamic levels, and context propagation
+///
+public sealed class AdvancedLoggerFactory : ILoggerFactory
+{
+ private readonly EonaCatLogStack _logStack;
+ private readonly ConcurrentDictionary _loggers;
+ private readonly ConcurrentDictionary _categoryConfigs;
+ private readonly AsyncLocal> _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();
+ _categoryConfigs = new ConcurrentDictionary();
+ _contextData = new AsyncLocal>();
+ }
+
+ ///
+ /// Configures settings for a specific category
+ ///
+ public AdvancedLoggerFactory ConfigureCategory(string categoryName, Action configure)
+ {
+ if (_isDisposed)
+ {
+ throw new ObjectDisposedException(nameof(AdvancedLoggerFactory));
+ }
+
+ var config = _categoryConfigs.GetOrAdd(categoryName, _ => new CategoryConfig { CategoryName = categoryName });
+ configure(config);
+ return this;
+ }
+
+ ///
+ /// Creates or retrieves a logger for the specified category
+ ///
+ 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));
+ }
+
+ ///
+ /// Gets the underlying EonaCatLogStack instance
+ ///
+ public EonaCatLogStack GetLogStack()
+ {
+ if (_isDisposed)
+ {
+ throw new ObjectDisposedException(nameof(AdvancedLoggerFactory));
+ }
+
+ return _logStack;
+ }
+
+ ///
+ /// Sets context data that will be included in all logs within this async context
+ ///
+ public void SetContextData(string key, object value)
+ {
+ var ctx = _contextData.Value ??= new Dictionary();
+ ctx[key] = value;
+ }
+
+ ///
+ /// Gets context data for the current async context
+ ///
+ public object? GetContextData(string key)
+ {
+ var ctx = _contextData.Value;
+ if (ctx == null)
+ {
+ return null;
+ }
+
+ ctx.TryGetValue(key, out var value);
+ return value;
+ }
+
+ ///
+ /// Clears all context data for the current async context
+ ///
+ public void ClearContextData()
+ {
+ _contextData.Value?.Clear();
+ }
+
+ ///
+ /// Dynamically changes the minimum log level at runtime
+ ///
+ 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;
+ }
+ }
+ }
+
+ ///
+ /// Flushes all pending log events
+ ///
+ public async Task FlushAsync()
+ {
+ if (_isDisposed)
+ {
+ throw new ObjectDisposedException(nameof(AdvancedLoggerFactory));
+ }
+
+ await _logStack.FlushAsync().ConfigureAwait(false);
+ }
+
+ ///
+ /// Gets diagnostics information
+ ///
+ 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
+ };
+ }
+
+ ///
+ /// Gets factory-specific diagnostics
+ ///
+ 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
+ };
+ }
+
+ ///
+ /// Gets detailed metrics
+ ///
+ 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);
+ }
+
+ ///
+ /// Internal logger adapter with category support
+ ///
+ private sealed class Logger : ILogger
+ {
+ private readonly string _category;
+ private readonly EonaCatLogStack _logStack;
+ private readonly ConcurrentDictionary _configs;
+
+ public string Category => _category;
+
+ public Logger(string category, EonaCatLogStack logStack, ConcurrentDictionary 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);
+ }
+ }
+}
+
+///
+/// Diagnostics information from AdvancedLoggerFactory
+///
+public class FactoryDiagnostics
+{
+ public int ActiveLoggers { get; set; }
+ public int ConfiguredCategories { get; set; }
+ public long TotalLogged { get; set; }
+ public long TotalDropped { get; set; }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Analytics/LoggingMetrics.cs b/EonaCat.LogStack/EonaCatLoggerCore/Analytics/LoggingMetrics.cs
new file mode 100644
index 0000000..4a77cd9
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Analytics/LoggingMetrics.cs
@@ -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.
+
+///
+/// Tracks logging metrics and analytics with comprehensive per-level and per-category metrics
+///
+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 _levelCounts = new();
+ private readonly Dictionary _loggerCounts = new();
+ private readonly Dictionary _categoryCounts = new();
+ private readonly List _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;
+
+ ///
+ /// Records a log event with comprehensive metrics
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// Records a log event with level information
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// Records an exception
+ ///
+ public void RecordException(long latencyMs = 0)
+ {
+ Interlocked.Increment(ref _totalExceptions);
+ }
+
+ ///
+ /// Records dropped events
+ ///
+ public void RecordDropped(long count = 1)
+ {
+ Interlocked.Add(ref _droppedCount, count);
+ }
+
+ ///
+ /// Records dropped exceptions
+ ///
+ public void RecordDroppedException()
+ {
+ Interlocked.Increment(ref _droppedExceptions);
+ }
+
+ ///
+ /// Records logger usage
+ ///
+ public void RecordLoggerUsage(string loggerName)
+ {
+ lock (_lock)
+ {
+ if (!_loggerCounts.ContainsKey(loggerName))
+ {
+ _loggerCounts[loggerName] = 0;
+ }
+
+ _loggerCounts[loggerName]++;
+ }
+ }
+
+ ///
+ /// Records category usage
+ ///
+ public void RecordCategoryUsage(string category)
+ {
+ lock (_lock)
+ {
+ if (!_categoryCounts.ContainsKey(category))
+ {
+ _categoryCounts[category] = 0;
+ }
+
+ _categoryCounts[category]++;
+ }
+ }
+
+ ///
+ /// Gets metrics snapshot
+ ///
+ 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(_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
+ };
+ }
+ }
+
+
+ ///
+ /// Resets all metrics
+ ///
+ 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 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 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 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];
+ }
+}
+
+///
+/// Snapshot of logging metrics at a point in time
+///
+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 LevelCounts { get; set; } = new();
+ public Dictionary LoggerCounts { get; set; } = new();
+ public Dictionary 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; }
+
+ ///
+ /// Returns formatted report
+ ///
+ 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();
+ }
+}
+
+///
+/// Logger introspection API stub for inspecting logger configuration
+///
+public sealed class LoggerIntrospection
+{
+ private readonly List _flows = new();
+ private readonly List _boosters = new();
+
+ public LoggerIntrospection(object logger)
+ {
+ // Logger integration to be provided when IFlowLogger is available
+ }
+
+ ///
+ /// Adds a flow for tracking
+ ///
+ public void RegisterFlow(string name)
+ {
+ _flows.Add(name);
+ }
+
+ ///
+ /// Adds a booster for tracking
+ ///
+ public void RegisterBooster(string name)
+ {
+ _boosters.Add(name);
+ }
+
+ ///
+ /// Gets configured flows
+ ///
+ public IReadOnlyList GetFlows() => _flows.AsReadOnly();
+
+ ///
+ /// Gets configured boosters
+ ///
+ public IReadOnlyList GetBoosters() => _boosters.AsReadOnly();
+
+ ///
+ /// Generates a report of logger configuration
+ ///
+ 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();
+ }
+}
+
+///
+/// Performance analyzer for logging
+///
+public sealed class LoggingPerformanceAnalyzer
+{
+ private readonly Dictionary _operations = new();
+ private readonly object _lock = new();
+
+ ///
+ /// Measures execution time of a function
+ ///
+ public T MeasureOperation(string operationName, Func operation)
+ {
+ var sw = Stopwatch.StartNew();
+ try
+ {
+ return operation();
+ }
+ finally
+ {
+ sw.Stop();
+ RecordOperation(operationName, sw.ElapsedMilliseconds);
+ }
+ }
+
+ ///
+ /// Measures execution time of an action
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Gets all recorded operations
+ ///
+ public IReadOnlyCollection GetOperations()
+ {
+ lock (_lock)
+ {
+ return _operations.Values.ToList().AsReadOnly();
+ }
+ }
+
+ ///
+ /// Generates performance report
+ ///
+ 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();
+ }
+ }
+}
+
+///
+/// Operation metrics
+///
+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);
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Analytics/PerFlowStatistics.cs b/EonaCat.LogStack/EonaCatLoggerCore/Analytics/PerFlowStatistics.cs
new file mode 100644
index 0000000..77c93ef
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Analytics/PerFlowStatistics.cs
@@ -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.
+
+///
+/// Tracks statistics for an individual logging flow
+///
+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 _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";
+ }
+
+ ///
+ /// Records a successfully processed event
+ ///
+ 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);
+ }
+ }
+ }
+
+ ///
+ /// Records a failed event
+ ///
+ 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;
+ }
+ }
+ }
+ }
+
+ ///
+ /// Records dropped events
+ ///
+ public void RecordDropped(long count = 1)
+ {
+ Interlocked.Add(ref _eventsDropped, count);
+ }
+
+ ///
+ /// Gets a snapshot of current statistics
+ ///
+ 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 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];
+ }
+
+ ///
+ /// Resets all statistics
+ ///
+ public void Reset()
+ {
+ lock (_lock)
+ {
+ _eventsProcessed = 0;
+ _eventsFailed = 0;
+ _eventsDropped = 0;
+ _bytesWritten = 0;
+ _latencies.Clear();
+ _minLatencyMs = long.MaxValue;
+ _maxLatencyMs = 0;
+ _uptime.Restart();
+ }
+ }
+}
+
+///
+/// Snapshot of flow statistics at a point in time
+///
+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();
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Batching/BatchProcessor.cs b/EonaCat.LogStack/EonaCatLoggerCore/Batching/BatchProcessor.cs
new file mode 100644
index 0000000..2deb31d
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Batching/BatchProcessor.cs
@@ -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.
+
+///
+/// Manages batch configuration and sizing strategies for optimized flow processing
+///
+public class BatchConfig
+{
+ public int InitialBatchSize { get; set; } = 100;
+ public int MaxBatchSize { get; set; } = 1000;
+ public int MinBatchSize { get; set; } = 10;
+
+ ///
+ /// Maximum time to wait before flushing a partial batch (milliseconds)
+ ///
+ public int MaxBatchDelayMs { get; set; } = 1000;
+
+ ///
+ /// Threshold for memory usage before activating backpressure (bytes)
+ ///
+ public long BackpressureThresholdBytes { get; set; } = 100 * 1024 * 1024; // 100MB
+
+ ///
+ /// Enable adaptive batch sizing based on throughput and memory
+ ///
+ public bool EnableAdaptiveSizing { get; set; } = true;
+
+ ///
+ /// Enable backpressure handling
+ ///
+ public bool EnableBackpressure { get; set; } = true;
+}
+
+///
+/// Adaptive batch processor that dynamically adjusts batch sizes based on system conditions
+///
+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;
+ }
+
+ ///
+ /// Gets the optimal batch size based on current system conditions
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Records memory usage of processed events
+ ///
+ public void RecordMemoryUsage(long bytes)
+ {
+ lock (_lock)
+ {
+ _currentMemoryUsage = bytes;
+ }
+ }
+
+ ///
+ /// Checks if backpressure should be applied
+ ///
+ public bool ShouldApplyBackpressure()
+ {
+ if (!_config.EnableBackpressure)
+ {
+ return false;
+ }
+
+ lock (_lock)
+ {
+ return _currentMemoryUsage > _config.BackpressureThresholdBytes;
+ }
+ }
+
+ ///
+ /// Gets the current memory usage
+ ///
+ public long GetMemoryUsage()
+ {
+ lock (_lock)
+ {
+ return _currentMemoryUsage;
+ }
+ }
+
+ ///
+ /// Resets memory tracking
+ ///
+ public void ResetMemoryUsage()
+ {
+ lock (_lock)
+ {
+ _currentMemoryUsage = 0;
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _isDisposed = true;
+ }
+}
+
+///
+/// Backpressure handler that controls flow based on system resources
+///
+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;
+ }
+
+ ///
+ /// Applies backpressure by waiting if memory usage exceeds threshold
+ ///
+ 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);
+ }
+ }
+
+ ///
+ /// Gets current memory pressure ratio (0.0 = no pressure, 1.0+ = critical)
+ ///
+ public double GetMemoryPressure()
+ {
+ long memoryUsage = GC.GetTotalMemory(false);
+ return (double)memoryUsage / _thresholdBytes;
+ }
+
+ ///
+ /// Checks if backpressure is currently active
+ ///
+ public bool IsActive => GetMemoryPressure() > 0.8;
+
+ public void Dispose()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _isDisposed = true;
+ }
+}
+
+///
+/// Generic batch accumulator for collecting items before processing
+///
+public sealed class BatchAccumulator : IDisposable
+{
+ private readonly List _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(maxSize);
+ _lastFlush = DateTime.UtcNow;
+ }
+
+ ///
+ /// Adds an item to the batch
+ ///
+ public bool TryAdd(T item)
+ {
+ lock (_lock)
+ {
+ if (_isDisposed)
+ {
+ return false;
+ }
+
+ _items.Add(item);
+ return true;
+ }
+ }
+
+ ///
+ /// Checks if the batch should be flushed
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Gets the current batch and resets
+ ///
+ public T[] GetAndReset()
+ {
+ lock (_lock)
+ {
+ if (_items.Count == 0)
+ {
+ return Array.Empty();
+ }
+
+ var batch = _items.ToArray();
+ _items.Clear();
+ _lastFlush = DateTime.UtcNow;
+ return batch;
+ }
+ }
+
+ ///
+ /// Gets the count of items in the current batch
+ ///
+ public int Count
+ {
+ get
+ {
+ lock (_lock)
+ {
+ return _items.Count;
+ }
+ }
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ lock (_lock)
+ {
+ _isDisposed = true;
+ _items.Clear();
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AdvancedBoosters.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AdvancedBoosters.cs
new file mode 100644
index 0000000..7e7d73f
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AdvancedBoosters.cs
@@ -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.
+
+///
+/// Performance monitoring booster that adds timing and resource metrics
+///
+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
+ }
+ }
+}
+
+///
+/// Distributed tracing booster that extracts and propagates trace context
+///
+public sealed class DistributedTracingBooster : BoosterBase
+{
+ private readonly AsyncLocal _correlationId = new AsyncLocal();
+ private readonly AsyncLocal _parentSpanId = new AsyncLocal();
+ private int _spanIdCounter;
+
+ public DistributedTracingBooster() : base("DistributedTracing") { }
+
+ ///
+ /// Sets the correlation ID for the current async context
+ ///
+ public void SetCorrelationId(string correlationId)
+ {
+ _correlationId.Value = correlationId;
+ }
+
+ ///
+ /// Gets the current correlation ID
+ ///
+ 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;
+ }
+}
+
+///
+/// Sampling booster that probabilistically filters log events
+///
+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();
+ }
+
+ ///
+ /// Always log critical and error events, sample the rest
+ ///
+ public override bool Boost(ref LogEventBuilder builder)
+ {
+ _totalLogged++;
+
+ // Sample based on configured rate
+ bool shouldLog = _random.NextDouble() < _samplingRate;
+ if (shouldLog)
+ {
+ _totalSampled++;
+ }
+
+ return shouldLog;
+ }
+
+ ///
+ /// Gets sampling statistics
+ ///
+ public (long total, long sampled, double rate) GetStatistics()
+ {
+ return (_totalLogged, _totalSampled, _totalLogged > 0 ? (double)_totalSampled / _totalLogged : 0.0);
+ }
+}
+
+///
+/// Property filter booster that includes/excludes events based on properties
+///
+public sealed class PropertyFilterBooster : BoosterBase
+{
+ private readonly Func? _includeFilter;
+ private readonly Func? _excludeFilter;
+
+ public PropertyFilterBooster(
+ Func? includeFilter = null,
+ Func? 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;
+ }
+}
+
+///
+/// Rate limiting booster that throttles log events
+///
+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;
+ }
+ }
+
+ ///
+ /// Gets current rate limiting statistics
+ ///
+ public (int current, int limit) GetStatistics()
+ {
+ lock (_lock)
+ {
+ return (_eventCount, _maxEventsPerSecond);
+ }
+ }
+}
+
+///
+/// Context enrichment booster that adds contextual information
+///
+public sealed class ContextEnrichmentBooster : BoosterBase
+{
+ private readonly Func _enricher;
+
+ public ContextEnrichmentBooster(Func 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
+ }
+ }
+}
+
+///
+/// Deduplication booster that filters duplicate messages within a time window
+///
+public sealed class DeduplicationBooster : BoosterBase
+{
+ private readonly TimeSpan _window;
+ private readonly Dictionary _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;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CallerInfoBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CallerInfoBooster.cs
new file mode 100644
index 0000000..e4e6d35
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/CallerInfoBooster.cs
@@ -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.
+
+///
+/// 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 .BoostWithCallerInfo().
+///
+/// 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
+/// directly from the call site.
+///
+public sealed class CallerInfoBooster : BoosterBase
+{
+ public CallerInfoBooster() : base("CallerInfo") { }
+
+ public override bool Boost(ref LogEventBuilder builder) => true; // no-op in generic path
+
+ ///
+ /// Enriches a builder with the actual call-site information.
+ /// Call this from your logging helper method so the compiler fills in the arguments.
+ ///
+ 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);
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/RequestContextBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/RequestContextBooster.cs
new file mode 100644
index 0000000..69dd984
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/RequestContextBooster.cs
@@ -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
+{
+ ///
+ /// Booster that extracts context from HTTP requests.
+ /// Requires IHttpContextAccessor to be registered in DI.
+ ///
+ 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;
+ }
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Configuration/LoggerConfiguration.cs b/EonaCat.LogStack/EonaCatLoggerCore/Configuration/LoggerConfiguration.cs
new file mode 100644
index 0000000..f48efa7
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Configuration/LoggerConfiguration.cs
@@ -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.
+
+///
+/// Advanced logger configuration settings
+///
+public sealed class LoggerConfiguration
+{
+ private readonly Dictionary _config = new();
+
+ ///
+ /// Sets a configuration value
+ ///
+ public LoggerConfiguration Set(string key, object value)
+ {
+ _config[key] = value;
+ return this;
+ }
+
+ ///
+ /// Gets a configuration value
+ ///
+ public T? Get(string key, T? defaultValue = default)
+ {
+ if (_config.TryGetValue(key, out var value))
+ {
+ return (T?)value;
+ }
+ return defaultValue;
+ }
+
+ ///
+ /// Enables buffering with specified size
+ ///
+ public LoggerConfiguration WithBuffering(int bufferSize = 1000)
+ {
+ Set("BufferSize", bufferSize);
+ return this;
+ }
+
+ ///
+ /// Enables batching with specified batch size
+ ///
+ public LoggerConfiguration WithBatching(int batchSize = 100)
+ {
+ Set("BatchSize", batchSize);
+ return this;
+ }
+
+ ///
+ /// Sets the maximum pool size for object reuse
+ ///
+ public LoggerConfiguration WithPooling(int maxPoolSize = 10000)
+ {
+ Set("MaxPoolSize", maxPoolSize);
+ return this;
+ }
+
+ ///
+ /// Enables diagnostics collection
+ ///
+ public LoggerConfiguration WithDiagnostics(bool enabled = true)
+ {
+ Set("DiagnosticsEnabled", enabled);
+ return this;
+ }
+
+ ///
+ /// Enables context propagation using AsyncLocal
+ ///
+ public LoggerConfiguration WithContextPropagation(bool enabled = true)
+ {
+ Set("ContextPropagationEnabled", enabled);
+ return this;
+ }
+
+ ///
+ /// Sets sampling rate (0-1, where 1 = log all events)
+ ///
+ public LoggerConfiguration WithSampling(double rate)
+ {
+ var clampedRate = rate < 0.0 ? 0.0 : (rate > 1.0 ? 1.0 : rate);
+ Set("SamplingRate", clampedRate);
+ return this;
+ }
+
+ ///
+ /// Sets rate limit (events per second)
+ ///
+ public LoggerConfiguration WithRateLimit(int eventsPerSecond)
+ {
+ Set("RateLimit", eventsPerSecond);
+ return this;
+ }
+
+ public Dictionary Build()
+ {
+ return new Dictionary(_config);
+ }
+}
+
+///
+/// Predefined logger presets
+///
+public enum LoggerPreset
+{
+ Development,
+ Production,
+ Diagnostics,
+ Performance
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Destructuring/TypeDestructor.cs b/EonaCat.LogStack/EonaCatLoggerCore/Destructuring/TypeDestructor.cs
new file mode 100644
index 0000000..bd99dc5
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Destructuring/TypeDestructor.cs
@@ -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.
+
+///
+/// Renders complex types to JSON-like format without external dependencies
+///
+public sealed class TypeDestructor
+{
+ private readonly HashSet
LoggerDiagnostics GetDiagnostics();
+
+ ///
+ /// Gets detailed metrics about the logger
+ ///
+ LoggerMetrics GetMetrics();
}
///
@@ -124,6 +129,19 @@ public sealed class LoggerFactory : ILoggerFactory
return _logStack.GetDiagnostics();
}
+ ///
+ /// Gets detailed metrics about the logger
+ ///
+ public LoggerMetrics GetMetrics()
+ {
+ if (_isDisposed)
+ {
+ throw new ObjectDisposedException(nameof(LoggerFactory));
+ }
+
+ return _logStack.GetMetrics();
+ }
+
///
/// Disposes all loggers and the underlying log stack
///
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Memory/MemoryOptimizations.cs b/EonaCat.LogStack/EonaCatLoggerCore/Memory/MemoryOptimizations.cs
new file mode 100644
index 0000000..4f52bfc
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Memory/MemoryOptimizations.cs
@@ -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.
+
+///
+/// Memory optimization utilities for zero-allocation logging paths
+///
+public static class MemoryOptimizations
+{
+ ///
+ /// Rents a scoped array from ArrayPool for using within a scope
+ ///
+ public static ScopedArray RentArray(int minimumLength)
+ {
+ return new ScopedArray(ArrayPool.Shared.Rent(minimumLength));
+ }
+
+ ///
+ /// Gets size estimate for a log event
+ ///
+ 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;
+ }
+
+ ///
+ /// Checks if memory usage is within acceptable range
+ ///
+ 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);
+ }
+
+ ///
+ /// Attempts to reduce memory usage
+ ///
+ public static void TryCompact()
+ {
+ GC.Collect(GC.MaxGeneration, GCCollectionMode.Optimized, false);
+ GC.WaitForPendingFinalizers();
+ }
+
+ ///
+ /// Gets current memory statistics
+ ///
+ public static MemoryStatistics GetStatistics()
+ {
+ return new MemoryStatistics
+ {
+ TotalMemory = GC.GetTotalMemory(false),
+ Gen0Collections = GC.CollectionCount(0),
+ Gen1Collections = GC.CollectionCount(1),
+ Gen2Collections = GC.CollectionCount(2)
+ };
+ }
+}
+
+///
+/// Scoped array rental from ArrayPool
+///
+public struct ScopedArray : IDisposable
+{
+ private T[]? _array;
+ private readonly int _length;
+
+ public ScopedArray(int length)
+ {
+ _array = ArrayPool.Shared.Rent(length);
+ _length = length;
+ }
+
+ public ScopedArray(T[] array)
+ {
+ _array = array;
+ _length = array.Length;
+ }
+
+ public Span AsSpan() => new Span(_array, 0, _length);
+ public Memory AsMemory() => new Memory(_array, 0, _length);
+ public T[] Array => _array!;
+
+ public void Dispose()
+ {
+ if (_array != null)
+ {
+ ArrayPool.Shared.Return(_array, false);
+ _array = null;
+ }
+ }
+}
+
+///
+/// Memory statistics
+///
+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;
+}
+
+///
+/// String interpolation helper for zero-allocation string building
+///
+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();
+ }
+ }
+}
+
+///
+/// Value type cache for frequently-used strings
+///
+public sealed class StringCache
+{
+ private readonly Dictionary _cache;
+ private readonly int _maxSize;
+ private readonly object _lock = new();
+
+ public StringCache(int maxSize = 1000)
+ {
+ _maxSize = maxSize;
+ _cache = new Dictionary(maxSize);
+ }
+
+ ///
+ /// Gets or caches a string
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Clears the cache
+ ///
+ public void Clear()
+ {
+ lock (_lock)
+ {
+ _cache.Clear();
+ }
+ }
+
+ ///
+ /// Gets cache statistics
+ ///
+ public (int count, int maxSize) GetStatistics()
+ {
+ lock (_lock)
+ {
+ return (_cache.Count, _maxSize);
+ }
+ }
+}
+
+///
+/// Memory-efficient concurrent bag alternative
+///
+public sealed class EfficientObjectPool where T : class
+{
+ private readonly Stack _stack;
+ private readonly Func _factory;
+ private int _count;
+ private readonly int _maxSize;
+ private readonly object _lock = new();
+
+ public EfficientObjectPool(Func factory, int maxSize = 100)
+ {
+ _factory = factory;
+ _maxSize = maxSize;
+ _stack = new Stack(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;
+ }
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/MessageTemplate.cs b/EonaCat.LogStack/EonaCatLoggerCore/MessageTemplate.cs
new file mode 100644
index 0000000..8379929
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/MessageTemplate.cs
@@ -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.
+
+///
+/// Hint for how a token's value should be captured.
+///
+public enum TokenDestructureHint : byte
+{
+ /// ToString() / scalar value
+ Default = 0,
+ /// {@Property} — deep object destructuring (JSON-like)
+ Destructure = 1,
+ /// {$Property} — force ToString()
+ Stringify = 2
+}
+
+///
+/// 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'}
+///
+public readonly struct TemplateToken
+{
+ /// True = literal text; False = property hole.
+ public readonly bool IsLiteral;
+ /// Literal text segment, or null for property holes.
+ public readonly string? Text;
+ /// Property name for holes (may be a digit for positional placeholders, supports dot notation).
+ public readonly string? Name;
+ /// Zero-based positional index for positional placeholders; -1 for named.
+ public readonly int Position;
+ /// Destructure / stringify hint.
+ public readonly TokenDestructureHint Hint;
+ /// Optional format string (e.g. "D2").
+ public readonly string? Format;
+ /// Alignment width (positive=right, negative=left).
+ public readonly int Alignment;
+ /// Applied filters (e.g., "uppercase", "truncate:10").
+ public readonly string[]? Filters;
+ /// Fallback value if property is null/missing.
+ public readonly string? Fallback;
+ /// Optional conditional true/false values for {?PropertyName:True|False}.
+ 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 : "")})";
+}
+
+///
+/// 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 { }
+///
+public sealed class MessageTemplate
+{
+ private readonly string _raw;
+ private readonly TemplateToken[] _tokens;
+
+ public string Raw => _raw;
+ public ReadOnlySpan 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(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;
+ }
+
+ ///
+ /// Renders the template to a string, binding positional args and returning
+ /// a dictionary of named properties.
+ ///
+ public string Render(object?[]? args, out Dictionary properties)
+ {
+ properties = new Dictionary(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();
+ }
+
+ ///
+ /// Resolves nested property access (e.g., "Object.Property.SubProperty")
+ ///
+ private static object? ResolveNestedProperty(string? propertyPath, Dictionary 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;
+ }
+
+ ///
+ /// Renders and populates the builder's properties from named holes.
+ ///
+ 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 _cache
+ = new(StringComparer.Ordinal);
+
+ /// Returns a cached parsed template (recommended for hot paths).
+ public static MessageTemplate FromCache(string template) =>
+ _cache.GetOrAdd(template, static t => Parse(t));
+
+ /// Clears the template parse cache.
+ public static void ClearCache() => _cache.Clear();
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Policies/PolicyEngine.cs b/EonaCat.LogStack/EonaCatLoggerCore/Policies/PolicyEngine.cs
new file mode 100644
index 0000000..f4e23f0
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Policies/PolicyEngine.cs
@@ -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.
+
+///
+/// Represents a routing rule that determines which flows should handle an event
+///
+public class RoutingRule
+{
+ public string Name { get; set; } = "";
+ public int Priority { get; set; }
+ public Func Condition { get; set; } = _ => true;
+ public List TargetFlows { get; set; } = new();
+ public bool IsActive { get; set; } = true;
+}
+
+///
+/// Policy engine that manages conditional routing and flow selection
+///
+public sealed class PolicyEngine
+{
+ private readonly List _rules = new();
+ private readonly Dictionary _flowRegistry = new();
+ private readonly object _lock = new object();
+
+ public PolicyEngine() { }
+
+ ///
+ /// Registers a flow by name
+ ///
+ 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));
+ }
+ }
+
+ ///
+ /// Unregisters a flow
+ ///
+ public void UnregisterFlow(string name)
+ {
+ lock (_lock)
+ {
+ _flowRegistry.Remove(name);
+ }
+ }
+
+ ///
+ /// Adds a routing rule
+ ///
+ 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;
+ }
+
+ ///
+ /// Creates and adds a conditional routing rule
+ ///
+ public PolicyEngine AddConditionalRule(
+ string name,
+ Func condition,
+ List targetFlows,
+ int priority = 0)
+ {
+ var rule = new RoutingRule
+ {
+ Name = name,
+ Condition = condition,
+ TargetFlows = targetFlows,
+ Priority = priority,
+ IsActive = true
+ };
+
+ return AddRule(rule);
+ }
+
+ ///
+ /// Creates a rule that routes errors to specific flows
+ ///
+ public PolicyEngine AddErrorRoute(List targetFlows, int priority = 10)
+ {
+ return AddConditionalRule(
+ "ErrorRoute",
+ e => e.Level >= LogLevel.Error,
+ targetFlows,
+ priority);
+ }
+
+ ///
+ /// Creates a rule that routes events from a specific category
+ ///
+ public PolicyEngine AddCategoryRoute(string category, List targetFlows, int priority = 5)
+ {
+ return AddConditionalRule(
+ $"Category:{category}",
+ e => e.Category == category,
+ targetFlows,
+ priority);
+ }
+
+ ///
+ /// Gets the flows that should handle a given event
+ ///
+ public List GetTargetFlows(LogEvent logEvent)
+ {
+ lock (_lock)
+ {
+ var targetFlows = new List();
+
+ 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(_flowRegistry.Values);
+ }
+ }
+
+ ///
+ /// Enables/disables a rule by name
+ ///
+ public void SetRuleActive(string ruleName, bool active)
+ {
+ lock (_lock)
+ {
+ var rule = _rules.FirstOrDefault(r => r.Name == ruleName);
+ if (rule != null)
+ {
+ rule.IsActive = active;
+ }
+ }
+ }
+
+ ///
+ /// Gets all registered flows
+ ///
+ public Dictionary GetAllFlows()
+ {
+ lock (_lock)
+ {
+ return new Dictionary(_flowRegistry);
+ }
+ }
+
+ ///
+ /// Gets all routing rules
+ ///
+ public List GetRules()
+ {
+ lock (_lock)
+ {
+ return new List(_rules);
+ }
+ }
+
+ ///
+ /// Clears all rules
+ ///
+ public void ClearRules()
+ {
+ lock (_lock)
+ {
+ _rules.Clear();
+ }
+ }
+
+ ///
+ /// Gets diagnostics about policies
+ ///
+ public PolicyDiagnostics GetDiagnostics()
+ {
+ lock (_lock)
+ {
+ return new PolicyDiagnostics
+ {
+ TotalRules = _rules.Count,
+ ActiveRules = _rules.Count(r => r.IsActive),
+ RegisteredFlows = _flowRegistry.Count
+ };
+ }
+ }
+}
+
+///
+/// Diagnostics about policy engine state
+///
+public class PolicyDiagnostics
+{
+ public int TotalRules { get; set; }
+ public int ActiveRules { get; set; }
+ public int RegisteredFlows { get; set; }
+}
+
+///
+/// Quota management for rate limiting across categories
+///
+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 _quotas = new();
+ private readonly object _lock = new object();
+
+ ///
+ /// Sets the quota for a category
+ ///
+ public void SetQuota(string category, int eventsPerSecond)
+ {
+ if (eventsPerSecond <= 0)
+ {
+ throw new ArgumentOutOfRangeException(nameof(eventsPerSecond));
+ }
+
+ lock (_lock)
+ {
+ _quotas[category] = new QuotaEntry { AllowedPerSecond = eventsPerSecond };
+ }
+ }
+
+ ///
+ /// Checks if an event should be allowed based on quota
+ ///
+ 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;
+ }
+ }
+
+ ///
+ /// Gets usage statistics for all quotas
+ ///
+ public Dictionary GetStatistics()
+ {
+ lock (_lock)
+ {
+ return _quotas.ToDictionary(
+ kvp => kvp.Key,
+ kvp => (kvp.Value.AllowedPerSecond, kvp.Value.CurrentCount));
+ }
+ }
+
+ ///
+ /// Resets all quotas
+ ///
+ public void Reset()
+ {
+ lock (_lock)
+ {
+ foreach (var quota in _quotas.Values)
+ {
+ quota.CurrentCount = 0;
+ quota.WindowStart = DateTime.UtcNow;
+ }
+ }
+ }
+
+ ///
+ /// Clears all quota entries
+ ///
+ public void Clear()
+ {
+ lock (_lock)
+ {
+ _quotas.Clear();
+ }
+ }
+}
+
+///
+/// Extension methods for policy management
+///
+public static class PolicyExtensions
+{
+ ///
+ /// Adds a default error flow route
+ ///
+ public static PolicyEngine AddDefaultErrorHandling(
+ this PolicyEngine engine,
+ string errorFlowName)
+ {
+ return engine.AddErrorRoute(new List { errorFlowName });
+ }
+
+ ///
+ /// Creates a policy engine with common patterns
+ ///
+ 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 { "error", "console" }, priority: 10);
+ }
+
+ return engine;
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Pooling/BufferPool.cs b/EonaCat.LogStack/EonaCatLoggerCore/Pooling/BufferPool.cs
new file mode 100644
index 0000000..2d79f2a
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Pooling/BufferPool.cs
@@ -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.
+
+///
+/// High-performance buffer pool using ArrayPool for optimal memory efficiency.
+/// Manages buffers across multiple size tiers for flexible allocation patterns.
+///
+public sealed class BufferPool : IDisposable
+{
+ private readonly int[] _standardSizes = { 512, 1024, 4096, 8192, 16384, 65536 };
+ private readonly ConcurrentDictionary> _pools;
+ private bool _isDisposed;
+
+ public BufferPool()
+ {
+ _pools = new ConcurrentDictionary>();
+ foreach (var size in _standardSizes)
+ {
+ _pools[size] = ArrayPool.Shared;
+ }
+ }
+
+ ///
+ /// Rents a buffer of at least the specified length
+ ///
+ 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.Shared.Rent(selectedSize);
+ }
+
+ ///
+ /// Returns a buffer to the pool
+ ///
+ public void Return(byte[] buffer, bool clearBuffer = false)
+ {
+ if (_isDisposed || buffer == null)
+ {
+ return;
+ }
+
+ ArrayPool.Shared.Return(buffer, clearBuffer);
+ }
+
+ ///
+ /// Gets statistics about pool usage
+ ///
+ public ArrayPoolStatistics GetStatistics()
+ {
+ return new ArrayPoolStatistics
+ {
+ TotalAllocations = ArrayPool.Shared.GetTotalAllocations(),
+ BytesAllocated = ArrayPool.Shared.GetTotalBytesAllocated()
+ };
+ }
+
+ public void Dispose()
+ {
+ if (_isDisposed)
+ {
+ return;
+ }
+
+ _isDisposed = true;
+ // ArrayPool.Shared is static and managed by runtime
+ }
+}
+
+///
+/// Statistics about array pool usage
+///
+public class ArrayPoolStatistics
+{
+ public long TotalAllocations { get; set; }
+ public long BytesAllocated { get; set; }
+}
+
+///
+/// Rented buffer that automatically returns itself to the pool when disposed
+///
+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 AsSpan() => new Span(_buffer, 0, _actualLength);
+ public Memory AsMemory() => new Memory(_buffer, 0, _actualLength);
+
+ public void Dispose()
+ {
+ if (_buffer != null)
+ {
+ _pool?.Return(_buffer, clearBuffer: true);
+ _buffer = null!;
+ }
+ }
+}
+
+///
+/// Extension methods for ArrayPool integration
+///
+public static class ArrayPoolExtensions
+{
+ ///
+ /// Gets total allocations from the shared ArrayPool (via reflection/diagnostics)
+ ///
+ public static long GetTotalAllocations(this ArrayPool pool)
+ {
+ // This is a placeholder - actual statistics would require reflection or instrumentation
+ // For now, this provides a hook for future diagnostics integration
+ return 0;
+ }
+
+ ///
+ /// Gets total bytes allocated from the shared ArrayPool
+ ///
+ public static long GetTotalBytesAllocated(this ArrayPool pool)
+ {
+ // This is a placeholder for diagnostics integration
+ return 0;
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Pooling/ObjectPool.cs b/EonaCat.LogStack/EonaCatLoggerCore/Pooling/ObjectPool.cs
new file mode 100644
index 0000000..45a74b0
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Pooling/ObjectPool.cs
@@ -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.
+
+///
+/// High-performance object pool for reusing expensive objects without external dependencies.
+/// Supports automatic cleanup and size limiting.
+///
+public sealed class ObjectPool : IDisposable where T : class
+{
+ private readonly ConcurrentBag _pool;
+ private readonly Func _factory;
+ private readonly Action? _resetAction;
+ private readonly int _maxSize;
+ private int _currentCount;
+ private bool _isDisposed;
+
+ public ObjectPool(Func factory, Action? resetAction = null, int maxSize = 100)
+ {
+ _factory = factory ?? throw new ArgumentNullException(nameof(factory));
+ _resetAction = resetAction;
+ _maxSize = maxSize;
+ _pool = new ConcurrentBag();
+ _currentCount = 0;
+ }
+
+ ///
+ /// Rents an object from the pool, creating a new one if necessary
+ ///
+ public T Rent()
+ {
+ if (_isDisposed)
+ {
+ throw new ObjectDisposedException(nameof(ObjectPool));
+ }
+
+ if (_pool.TryTake(out var item))
+ {
+ Interlocked.Decrement(ref _currentCount);
+ return item;
+ }
+
+ return _factory();
+ }
+
+ ///
+ /// Returns an object to the pool after optional reset
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Gets the current number of pooled objects
+ ///
+ public int PooledCount => _currentCount;
+
+ ///
+ /// Clears all pooled objects
+ ///
+ 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();
+ }
+}
+
+///
+/// Pooled object wrapper that returns itself to the pool when disposed
+///
+public sealed class PooledObject : IDisposable where T : class
+{
+ private readonly ObjectPool _pool;
+ private readonly T _item;
+
+ public PooledObject(ObjectPool pool)
+ {
+ _pool = pool ?? throw new ArgumentNullException(nameof(pool));
+ _item = pool.Rent();
+ }
+
+ public T Item => _item;
+
+ public void Dispose()
+ {
+ _pool.Return(_item);
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Pooling/StringBuilderPool.cs b/EonaCat.LogStack/EonaCatLoggerCore/Pooling/StringBuilderPool.cs
new file mode 100644
index 0000000..cabf8cb
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Pooling/StringBuilderPool.cs
@@ -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.
+
+///
+/// High-performance StringBuilder pool with automatic capacity management.
+/// Reuses StringBuilders to reduce GC pressure in high-throughput scenarios.
+///
+public sealed class StringBuilderPool : IDisposable
+{
+ private readonly ConcurrentBag _smallPool; // <= 1KB
+ private readonly ConcurrentBag _mediumPool; // 1KB - 8KB
+ private readonly ConcurrentBag _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();
+ _mediumPool = new ConcurrentBag();
+ _largePool = new ConcurrentBag();
+ }
+
+ ///
+ /// Rents a StringBuilder from the appropriate pool based on requested capacity
+ ///
+ public StringBuilder Rent(int capacity = 1024)
+ {
+ if (_isDisposed)
+ {
+ throw new ObjectDisposedException(nameof(StringBuilderPool));
+ }
+
+ ConcurrentBag? 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? 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);
+ }
+ }
+ }
+
+ ///
+ /// Gets pooled item counts for diagnostics
+ ///
+ public (int small, int medium, int large) GetPoolCounts()
+ => (_smallCount, _mediumCount, _largeCount);
+
+ ///
+ /// Clears all pools
+ ///
+ 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();
+ }
+}
+
+///
+/// Rented StringBuilder that automatically returns itself to the pool when disposed
+///
+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);
+ }
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Structured/StructuredProperties.cs b/EonaCat.LogStack/EonaCatLoggerCore/Structured/StructuredProperties.cs
new file mode 100644
index 0000000..87caa79
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Structured/StructuredProperties.cs
@@ -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.
+
+///
+/// Represents a structured property with type information
+///
+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;
+ }
+}
+
+///
+/// Builder for structured properties using Serilog-like syntax
+///
+public sealed class StructuredPropertyBuilder
+{
+ private readonly Dictionary _properties = new();
+
+ ///
+ /// Adds a simple property
+ ///
+ public StructuredPropertyBuilder Add(string name, object? value)
+ {
+ _properties[name] = new StructuredProperty(name, value, false);
+ return this;
+ }
+
+ ///
+ /// Adds a property that should be destructured (rendered as JSON)
+ ///
+ public StructuredPropertyBuilder AddDestructured(string name, object? value)
+ {
+ _properties[name] = new StructuredProperty(name, value, true);
+ return this;
+ }
+
+ ///
+ /// Adds multiple properties from an anonymous object
+ /// Example: .AddFromAnonymous(new { UserId = 123, Action = "Login" })
+ ///
+ 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;
+ }
+
+ ///
+ /// Adds multiple properties from a dictionary
+ ///
+ public StructuredPropertyBuilder AddFromDictionary(Dictionary dict)
+ {
+ if (dict == null)
+ {
+ return this;
+ }
+
+ foreach (var kvp in dict)
+ {
+ Add(kvp.Key, kvp.Value);
+ }
+
+ return this;
+ }
+
+ ///
+ /// Removes a property
+ ///
+ public StructuredPropertyBuilder Remove(string name)
+ {
+ _properties.Remove(name);
+ return this;
+ }
+
+ ///
+ /// Clears all properties
+ ///
+ public StructuredPropertyBuilder Clear()
+ {
+ _properties.Clear();
+ return this;
+ }
+
+ ///
+ /// Gets the built dictionary
+ ///
+ public Dictionary Build()
+ {
+ return _properties.ToDictionary(kvp => kvp.Key, kvp => kvp.Value.Value);
+ }
+
+ ///
+ /// Gets the structured properties
+ ///
+ public Dictionary BuildStructured()
+ {
+ return new Dictionary(_properties);
+ }
+
+ ///
+ /// Gets count of properties
+ ///
+ public int Count => _properties.Count;
+}
+
+///
+/// Contextual property scope that inherits to nested calls
+/// Similar to Serilog's LogContext.PushProperty
+///
+public sealed class PropertyScope : IDisposable
+{
+ private readonly Stack> _scopeStack;
+ private readonly Dictionary _currentScope;
+
+ private static readonly AsyncLocal _currentScopeHolder = new();
+
+ public PropertyScope(Dictionary initialProperties)
+ {
+ _scopeStack = new Stack>();
+ _currentScope = new Dictionary(initialProperties);
+ _scopeStack.Push(_currentScope);
+
+ var previous = _currentScopeHolder.Value;
+ _currentScopeHolder.Value = this;
+ }
+
+ public void PushScope(Dictionary properties)
+ {
+ var newScope = new Dictionary(_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 GetCurrentProperties()
+ {
+ if (_scopeStack.Count == 0)
+ {
+ return new Dictionary();
+ }
+
+ return new Dictionary(_scopeStack.Peek());
+ }
+
+ public static Dictionary GetActiveProperties()
+ {
+ var scope = _currentScopeHolder.Value;
+ return scope?.GetCurrentProperties() ?? new Dictionary();
+ }
+
+ public void Dispose()
+ {
+ _scopeStack.Clear();
+ if (_currentScopeHolder.Value == this)
+ {
+ _currentScopeHolder.Value = null;
+ }
+ }
+}
+
+///
+/// Extensions for adding properties to LogEvent
+///
+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.
+}
diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Templates/MessageTemplateEngine.cs b/EonaCat.LogStack/EonaCatLoggerCore/Templates/MessageTemplateEngine.cs
new file mode 100644
index 0000000..4a32514
--- /dev/null
+++ b/EonaCat.LogStack/EonaCatLoggerCore/Templates/MessageTemplateEngine.cs
@@ -0,0 +1,1122 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+
+namespace EonaCat.LogStack.Templates;
+
+// 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.
+
+///
+/// Legacy adapter to support existing MessageTemplateEngine interface
+/// while maintaining compatibility with the enhanced MessageTemplate class.
+///
+/// For new code, use MessageTemplate directly for better performance.
+///
+public abstract class TemplateToken
+{
+ public abstract void Render(StringBuilder sb, Dictionary properties);
+}
+
+///
+/// Literal text token
+///
+public sealed class LiteralToken : TemplateToken
+{
+ private readonly string _text;
+
+ public LiteralToken(string text)
+ {
+ _text = text;
+ }
+
+ public override void Render(StringBuilder sb, Dictionary properties)
+ {
+ sb.Append(_text);
+ }
+}
+
+///
+/// Conditional token for if/then/else constructs
+/// Syntax: {@if:condition:trueOutput|falseOutput}
+/// Example: {@if:IsActive:Active|Inactive}
+///
+public sealed class ConditionalToken : TemplateToken
+{
+ private readonly string _condition;
+ private readonly string _trueOutput;
+ private readonly string _falseOutput;
+
+ public ConditionalToken(string condition, string trueOutput, string falseOutput)
+ {
+ _condition = condition;
+ _trueOutput = trueOutput;
+ _falseOutput = falseOutput;
+ }
+
+ public override void Render(StringBuilder sb, Dictionary properties)
+ {
+ object? value = ResolveCondition(_condition, properties);
+ bool result = IsTruthy(value);
+ sb.Append(result ? _trueOutput : _falseOutput);
+ }
+
+ private static object? ResolveCondition(string condition, Dictionary properties)
+ {
+ // Support simple property names or comparisons like "Count>0" or "Status==Active"
+ condition = condition.Trim();
+
+ // Check for comparison operators
+ string[] operators = new[] { ">=", "<=", "==", "!=", ">", "<" };
+ foreach (var op in operators)
+ {
+ if (condition.Contains(op, StringComparison.Ordinal))
+ {
+ int opIdx = condition.IndexOf(op, StringComparison.Ordinal);
+ string left = condition.Substring(0, opIdx).Trim();
+ string right = condition.Substring(opIdx + op.Length).Trim();
+
+ object? leftVal = GetPropertyValue(left, properties);
+ object? rightVal = GetPropertyValue(right, properties);
+
+ return CompareValues(leftVal, rightVal, op);
+ }
+ }
+
+ // Simple property name
+ return properties.TryGetValue(condition, out var val) ? val : null;
+ }
+
+ private static object? GetPropertyValue(string key, Dictionary properties)
+ {
+ key = key.Trim();
+
+ // Check if it's a property reference
+ if (properties.TryGetValue(key, out var val))
+ {
+ return val;
+ }
+
+ // Try to parse as number
+ if (int.TryParse(key, out int intVal))
+ {
+ return intVal;
+ }
+
+ if (double.TryParse(key, out double dblVal))
+ {
+ return dblVal;
+ }
+
+ // Treat as string literal
+ if ((key.StartsWith("'") && key.EndsWith("'")) || (key.StartsWith("\"") && key.EndsWith("\"")))
+ {
+ return key.Substring(1, key.Length - 2);
+ }
+
+ return key;
+ }
+
+ private static bool CompareValues(object? left, object? right, string op)
+ {
+ return op switch
+ {
+ "==" => ValuesEqual(left, right),
+ "!=" => !ValuesEqual(left, right),
+ ">" => CompareNumeric(left, right) > 0,
+ "<" => CompareNumeric(left, right) < 0,
+ ">=" => CompareNumeric(left, right) >= 0,
+ "<=" => CompareNumeric(left, right) <= 0,
+ _ => false
+ };
+ }
+
+ private static bool ValuesEqual(object? left, object? right)
+ {
+ if (left == null && right == null)
+ {
+ return true;
+ }
+
+ if (left == null || right == null)
+ {
+ return false;
+ }
+
+ return left.ToString().Equals(right.ToString(), StringComparison.OrdinalIgnoreCase);
+ }
+
+ private static int CompareNumeric(object? left, object? right)
+ {
+ double leftNum = ToDouble(left);
+ double rightNum = ToDouble(right);
+ return leftNum.CompareTo(rightNum);
+ }
+
+ private static double ToDouble(object? value)
+ {
+ return value switch
+ {
+ null => 0,
+ double d => d,
+ int i => i,
+ long l => l,
+ float f => f,
+ decimal dec => (double)dec,
+ string s => double.TryParse(s, out double d) ? d : 0,
+ _ => 0
+ };
+ }
+
+ 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 double d)
+ {
+ return d != 0.0;
+ }
+
+ if (value is string s)
+ {
+ return !string.IsNullOrEmpty(s);
+ }
+
+ return true;
+ }
+}
+
+///
+/// Loop token for iterating over arrays/lists
+/// Syntax: {@loop:PropertyName:itemTemplate}
+/// Example: {@loop:Items:Item={Item}}
+///
+public sealed class LoopToken : TemplateToken
+{
+ private readonly string _collectionName;
+ private readonly string _itemTemplate;
+ private readonly string _separator;
+
+ public LoopToken(string collectionName, string itemTemplate, string separator = ", ")
+ {
+ _collectionName = collectionName;
+ _itemTemplate = itemTemplate;
+ _separator = separator;
+ }
+
+ public override void Render(StringBuilder sb, Dictionary properties)
+ {
+ if (!properties.TryGetValue(_collectionName, out var collection))
+ {
+ return;
+ }
+
+ var items = collection as System.Collections.IEnumerable;
+ if (items == null)
+ {
+ return;
+ }
+
+ bool first = true;
+ foreach (var item in items)
+ {
+ if (!first)
+ {
+ sb.Append(_separator);
+ }
+
+ // Create a local scope with the item
+ var itemProps = new Dictionary(properties)
+ {
+ { "Item", item }
+ };
+
+ // Render the item template
+ var renderer = new MessageTemplateRenderer(_itemTemplate);
+ renderer.Render(sb, itemProps);
+
+ first = false;
+ }
+ }
+}
+
+///
+/// Property reference token (e.g., {PropertyName})
+/// Supports nested properties, formatting, filters, conditionals, and fallback values
+///
+public sealed class PropertyToken : TemplateToken
+{
+ private readonly string _propertyName;
+ private readonly string? _format;
+ private readonly bool _destructure;
+ private readonly int _alignment;
+ private readonly string[]? _filters;
+ private readonly string? _fallback;
+ private readonly string? _conditionalTrueValue;
+ private readonly string? _conditionalFalseValue;
+
+ public string PropertyName => _propertyName;
+ public string? Format => _format;
+ public bool Destructure => _destructure;
+ public int Alignment => _alignment;
+ public string[]? Filters => _filters;
+ public string? Fallback => _fallback;
+ public string? ConditionalTrueValue => _conditionalTrueValue;
+ public string? ConditionalFalseValue => _conditionalFalseValue;
+ public bool IsConditional => _conditionalTrueValue != null || _conditionalFalseValue != null;
+
+ public PropertyToken(string propertyName, string? format = null, bool destructure = false, int alignment = 0, string[]? filters = null, string? fallback = null, string? conditionalTrueValue = null, string? conditionalFalseValue = null)
+ {
+ _propertyName = propertyName;
+ _format = format;
+ _destructure = destructure;
+ _alignment = alignment;
+ _filters = filters;
+ _fallback = fallback;
+ _conditionalTrueValue = conditionalTrueValue;
+ _conditionalFalseValue = conditionalFalseValue;
+ }
+
+ public override void Render(StringBuilder sb, Dictionary properties)
+ {
+ object? value = null;
+
+ // Support nested property access (Object.Property.SubProperty)
+ if (_propertyName.Contains("."))
+ {
+ value = ResolveNestedProperty(_propertyName, properties);
+ }
+ else if (!properties.TryGetValue(_propertyName, out value))
+ {
+ value = null;
+ }
+
+ // Handle conditionals: {?PropertyName:TrueValue|FalseValue}
+ if (IsConditional)
+ {
+ bool condition = IsTruthy(value);
+ string result = condition ? _conditionalTrueValue ?? "" : _conditionalFalseValue ?? "";
+ sb.Append(result);
+ return;
+ }
+
+ if (value == null)
+ {
+ sb.Append(_fallback ?? "null");
+ return;
+ }
+
+ // Apply filters
+ if (_filters != null && _filters.Length > 0)
+ {
+ value = ApplyFilters(value, _filters);
+ }
+
+ string formatted = FormatValue(value, _destructure, _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 double d)
+ {
+ return d != 0.0;
+ }
+
+ if (value is string s)
+ {
+ return !string.IsNullOrEmpty(s);
+ }
+
+ return true;
+ }
+
+ private static object? ResolveNestedProperty(string propertyPath, Dictionary properties)
+ {
+ if (propertyPath == null)
+ {
+ return 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;
+ }
+
+ 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? 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
+ {
+ // String filters
+ "uppercase" or "upper" => str.ToUpperInvariant(),
+ "lowercase" or "lower" => str.ToLowerInvariant(),
+ "trim" => str.Trim(),
+ "trimstart" => str.TrimStart(),
+ "trimend" => str.TrimEnd(),
+ "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,
+ "replace" => ApplyReplaceFilter(str, param),
+ "pad" => ApplyPadFilter(str, param),
+ "repeat" => param != null && int.TryParse(param, out int count) ? string.Concat(System.Linq.Enumerable.Repeat(str, count)) : str,
+ "startswith" => param != null ? (str.StartsWith(param, StringComparison.OrdinalIgnoreCase) ? "true" : "false") : str,
+ "endswith" => param != null ? (str.EndsWith(param, StringComparison.OrdinalIgnoreCase) ? "true" : "false") : str,
+ "contains" => param != null ? (str.Contains(param, StringComparison.OrdinalIgnoreCase) ? "true" : "false") : str,
+ "split" => ApplySplitFilter(str, param),
+
+ // Math filters
+ "add" => ApplyMathFilter(value, param, (a, b) => a + b),
+ "subtract" => ApplyMathFilter(value, param, (a, b) => a - b),
+ "multiply" => ApplyMathFilter(value, param, (a, b) => a * b),
+ "divide" => ApplyMathFilter(value, param, (a, b) => b != 0 ? a / b : double.NaN),
+ "modulo" => ApplyMathFilter(value, param, (a, b) => b != 0 ? a % b : double.NaN),
+ "abs" => ApplyMathFilter(value, param, (a, _) => Math.Abs(a)),
+ "floor" => ApplyMathFilter(value, param, (a, _) => Math.Floor(a)),
+ "ceil" => ApplyMathFilter(value, param, (a, _) => Math.Ceiling(a)),
+ "round" => param != null && int.TryParse(param, out int decimals) ? Math.Round(GetDoubleValue(value), decimals).ToString() : Math.Round(GetDoubleValue(value)).ToString(),
+ "min" => ApplyMathFilter(value, param, (a, b) => Math.Min(a, b)),
+ "max" => ApplyMathFilter(value, param, (a, b) => Math.Max(a, b)),
+
+ // Comparison filters
+ "equals" => param != null ? (str.Equals(param, StringComparison.OrdinalIgnoreCase) ? "true" : "false") : str,
+ "lessthan" or "lt" => param != null && double.TryParse(param, out double paramVal) && double.TryParse(str, out double strVal) ? (strVal < paramVal ? "true" : "false") : str,
+ "greaterthan" or "gt" => param != null && double.TryParse(param, out double paramVal) && double.TryParse(str, out double strVal) ? (strVal > paramVal ? "true" : "false") : str,
+ "lessthaneq" or "lte" => param != null && double.TryParse(param, out double paramVal) && double.TryParse(str, out double strVal) ? (strVal <= paramVal ? "true" : "false") : str,
+ "greaterthaneq" or "gte" => param != null && double.TryParse(param, out double paramVal) && double.TryParse(str, out double strVal) ? (strVal >= paramVal ? "true" : "false") : str,
+
+ // Date/Time filters
+ "date" => ApplyDateFilter(value, param),
+ "timespan" => ApplyTimeSpanFilter(value, param),
+
+ // Default
+ _ => str
+ };
+ }
+
+ private static string ApplyReplaceFilter(string str, string? param)
+ {
+ if (string.IsNullOrEmpty(param))
+ {
+ return str;
+ }
+
+ var parts = param.Split(':');
+ if (parts.Length >= 2)
+ {
+ return str.Replace(parts[0], parts[1]);
+ }
+ return str;
+ }
+
+ private static string ApplyPadFilter(string str, string? param)
+ {
+ if (!int.TryParse(param, out int width))
+ {
+ return str;
+ }
+
+ return str.Length < width ? str.PadRight(width) : str;
+ }
+
+ private static string ApplySplitFilter(string str, string? param)
+ {
+ if (string.IsNullOrEmpty(param))
+ {
+ return str;
+ }
+
+ var parts = str.Split(new[] { param }, StringSplitOptions.None);
+ return string.Join(", ", parts);
+ }
+
+ private static string ApplyMathFilter(object? value, string? param, Func operation)
+ {
+ double baseVal = GetDoubleValue(value);
+ if (!double.TryParse(param, out double paramVal))
+ {
+ return baseVal.ToString();
+ }
+
+ double result = operation(baseVal, paramVal);
+ return result.ToString();
+ }
+
+ private static double GetDoubleValue(object? value)
+ {
+ return value switch
+ {
+ double d => d,
+ float f => f,
+ int i => i,
+ long l => l,
+ decimal dec => (double)dec,
+ string s => double.TryParse(s, out double d) ? d : 0,
+ _ => 0
+ };
+ }
+
+ private static string ApplyDateFilter(object? value, string? param)
+ {
+ if (value == null)
+ {
+ return "";
+ }
+
+ DateTime dt = value switch
+ {
+ DateTime d => d,
+ DateTimeOffset dto => dto.DateTime,
+ string s => DateTime.TryParse(s, out DateTime parsed) ? parsed : DateTime.Now,
+ _ => DateTime.Now
+ };
+
+ string format = param ?? "O";
+ try
+ {
+ return dt.ToString(format, System.Globalization.CultureInfo.InvariantCulture);
+ }
+ catch
+ {
+ return dt.ToString();
+ }
+ }
+
+ private static string ApplyTimeSpanFilter(object? value, string? param)
+ {
+ if (value == null)
+ {
+ return "";
+ }
+
+ TimeSpan ts = value switch
+ {
+ TimeSpan t => t,
+ string s => TimeSpan.TryParse(s, out TimeSpan parsed) ? parsed : TimeSpan.Zero,
+ _ => TimeSpan.Zero
+ };
+
+ return param?.ToLowerInvariant() switch
+ {
+ "totalseconds" => ts.TotalSeconds.ToString(),
+ "totalminutes" => ts.TotalMinutes.ToString(),
+ "totalhours" => ts.TotalHours.ToString(),
+ "totaldays" => ts.TotalDays.ToString(),
+ "seconds" => ts.Seconds.ToString(),
+ "minutes" => ts.Minutes.ToString(),
+ "hours" => ts.Hours.ToString(),
+ "days" => ts.Days.ToString(),
+ _ => ts.ToString()
+ };
+ }
+
+ private static string FormatValue(object value, bool destructure, string? format)
+ {
+ if (destructure)
+ {
+ return DestructureObject(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 DestructureObject(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()!;
+ }
+
+ 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(DestructureObject(val));
+ }
+
+ first = false;
+ }
+ catch { }
+ }
+ sb.Append('}');
+ return sb.ToString();
+ }
+}
+
+///
+/// Parses message templates (legacy interface for compatibility)
+/// For new code, use MessageTemplate.Parse() directly
+///
+public sealed class MessageTemplateParser
+{
+ private readonly string _template;
+ private int _position;
+
+ public MessageTemplateParser(string template)
+ {
+ _template = template ?? "";
+ }
+
+ public List Parse()
+ {
+ var tokens = new List();
+ var sb = new StringBuilder();
+
+ while (_position < _template.Length)
+ {
+ char c = _template[_position];
+
+ if (c == '{')
+ {
+ // Flush any accumulated literal text
+ if (sb.Length > 0)
+ {
+ tokens.Add(new LiteralToken(sb.ToString()));
+ sb.Clear();
+ }
+
+ // Try to parse special tokens first (@if, @loop)
+ TemplateToken? token = TryParseSpecialToken();
+ if (token != null)
+ {
+ tokens.Add(token);
+ }
+ else
+ {
+ // Fall back to property token
+ _position--; // Back up to re-parse the opening brace
+ token = ParsePropertyToken();
+ if (token != null)
+ {
+ tokens.Add(token);
+ }
+ }
+ }
+ else if (c == '}')
+ {
+ // Escaped closing brace
+ if (_position + 1 < _template.Length && _template[_position + 1] == '}')
+ {
+ sb.Append('}');
+ _position += 2;
+ }
+ else
+ {
+ sb.Append(c);
+ _position++;
+ }
+ }
+ else
+ {
+ sb.Append(c);
+ _position++;
+ }
+ }
+
+ // Flush remaining literal text
+ if (sb.Length > 0)
+ {
+ tokens.Add(new LiteralToken(sb.ToString()));
+ }
+
+ return tokens;
+ }
+
+ private TemplateToken? TryParseSpecialToken()
+ {
+ if (_template[_position] != '{')
+ {
+ return null;
+ }
+
+ _position++; // Skip {
+
+ // Look ahead for @ symbol
+ if (_position >= _template.Length || _template[_position] != '@')
+ {
+ _position--; // Back up if not @
+ return null;
+ }
+
+ _position++; // Skip @
+
+ // Read the keyword (if, loop, etc.)
+ var keywordSb = new StringBuilder();
+ while (_position < _template.Length && char.IsLetter(_template[_position]))
+ {
+ keywordSb.Append(_template[_position]);
+ _position++;
+ }
+
+ string keyword = keywordSb.ToString().ToLowerInvariant();
+
+ // Read the rest of the token content until }
+ if (_position >= _template.Length || _template[_position] != ':')
+ {
+ _position -= keyword.Length + 2; // Back up
+ return null;
+ }
+
+ _position++; // Skip :
+
+ var contentSb = new StringBuilder();
+ int braceCount = 1;
+ while (_position < _template.Length && braceCount > 0)
+ {
+ char c = _template[_position];
+ if (c == '{')
+ {
+ braceCount++;
+ }
+ else if (c == '}')
+ {
+ braceCount--;
+ }
+
+ if (braceCount > 0)
+ {
+ contentSb.Append(c);
+ }
+
+ _position++;
+ }
+
+ string content = contentSb.ToString();
+
+ return keyword switch
+ {
+ "if" => ParseConditionalToken(content),
+ "loop" => ParseLoopToken(content),
+ _ => null
+ };
+ }
+
+ private TemplateToken? ParseConditionalToken(string content)
+ {
+ // Format: condition:trueOutput|falseOutput
+ int colonIdx = content.IndexOf(':');
+ if (colonIdx == -1)
+ {
+ return null;
+ }
+
+ string condition = content.Substring(0, colonIdx).Trim();
+ string rest = content.Substring(colonIdx + 1).Trim();
+
+ int pipeIdx = rest.IndexOf('|');
+ if (pipeIdx == -1)
+ {
+ return new ConditionalToken(condition, rest, "");
+ }
+
+ string trueOutput = rest.Substring(0, pipeIdx).Trim();
+ string falseOutput = rest.Substring(pipeIdx + 1).Trim();
+
+ return new ConditionalToken(condition, trueOutput, falseOutput);
+ }
+
+ private TemplateToken? ParseLoopToken(string content)
+ {
+ // Format: collectionName:itemTemplate or collectionName:itemTemplate:separator
+ var parts = content.Split(':');
+ if (parts.Length < 2)
+ {
+ return null;
+ }
+
+ string collectionName = parts[0].Trim();
+ string itemTemplate = parts[1].Trim();
+ string separator = parts.Length > 2 ? parts[2].Trim() : ", ";
+
+ return new LoopToken(collectionName, itemTemplate, separator);
+ }
+
+ private PropertyToken? ParsePropertyToken()
+ {
+ _position++; // Skip opening {
+
+ if (_position >= _template.Length)
+ {
+ return null;
+ }
+
+ // Check for conditional prefix ?
+ bool isConditional = false;
+ if (_template[_position] == '?')
+ {
+ isConditional = true;
+ _position++;
+ }
+
+ // Check for destructuring prefix @
+ bool destructure = false;
+ if (_position < _template.Length && _template[_position] == '@')
+ {
+ destructure = true;
+ _position++;
+ }
+
+ // Read property name
+ var nameSb = new StringBuilder();
+ while (_position < _template.Length)
+ {
+ char c = _template[_position];
+ if (c == ':' || c == '}' || c == ',' || c == '|')
+ {
+ break;
+ }
+
+ nameSb.Append(c);
+ _position++;
+ }
+
+ string propertyName = nameSb.ToString().Trim();
+ if (propertyName.Length == 0)
+ {
+ return null;
+ }
+
+ // Read optional alignment (,10 or ,-10)
+ int alignment = 0;
+ if (_position < _template.Length && _template[_position] == ',')
+ {
+ _position++; // Skip comma
+ var alignSb = new StringBuilder();
+ while (_position < _template.Length && (char.IsDigit(_template[_position]) || (_template[_position] == '-' && alignSb.Length == 0)))
+ {
+ alignSb.Append(_template[_position]);
+ _position++;
+ }
+ if (int.TryParse(alignSb.ToString(), out int align))
+ {
+ alignment = align;
+ }
+ }
+
+ // Read optional filters (|uppercase, |truncate:10)
+ string[]? filters = null;
+ if (_position < _template.Length && _template[_position] == '|')
+ {
+ _position++; // Skip pipe
+ var filterSb = new StringBuilder();
+ while (_position < _template.Length && _template[_position] != ':' && _template[_position] != '}')
+ {
+ filterSb.Append(_template[_position]);
+ _position++;
+ }
+ string filterStr = filterSb.ToString().Trim();
+ if (filterStr.Length > 0)
+ {
+ filters = filterStr.Split('|');
+ }
+ }
+
+ // Handle conditional values {?Property:TrueValue|FalseValue}
+ string? conditionalTrueValue = null;
+ string? conditionalFalseValue = null;
+ if (isConditional && _position < _template.Length && _template[_position] == ':')
+ {
+ _position++; // Skip :
+ var trueSb = new StringBuilder();
+ while (_position < _template.Length && _template[_position] != '|' && _template[_position] != '}')
+ {
+ trueSb.Append(_template[_position]);
+ _position++;
+ }
+ conditionalTrueValue = trueSb.ToString().Trim();
+
+ if (_position < _template.Length && _template[_position] == '|')
+ {
+ _position++; // Skip |
+ var falseSb = new StringBuilder();
+ while (_position < _template.Length && _template[_position] != '}')
+ {
+ falseSb.Append(_template[_position]);
+ _position++;
+ }
+ conditionalFalseValue = falseSb.ToString().Trim();
+ }
+ }
+ else if (!isConditional)
+ {
+ // Read optional format specifier (non-conditional path)
+ string? format = null;
+ if (_position < _template.Length && _template[_position] == ':')
+ {
+ _position++; // Skip :
+ var formatSb = new StringBuilder();
+ while (_position < _template.Length && _template[_position] != '}')
+ {
+ formatSb.Append(_template[_position]);
+ _position++;
+ }
+ format = formatSb.ToString().Trim();
+ }
+
+ // Skip closing }
+ if (_position < _template.Length && _template[_position] == '}')
+ {
+ _position++;
+ }
+
+ return new PropertyToken(propertyName, format, destructure, alignment, filters);
+ }
+
+ // Skip closing }
+ if (_position < _template.Length && _template[_position] == '}')
+ {
+ _position++;
+ }
+
+ return new PropertyToken(propertyName, null, destructure, alignment, filters, null, conditionalTrueValue, conditionalFalseValue);
+ }
+}
+
+///
+/// Renders a parsed message template with properties (legacy compatibility layer)
+/// For new code, use MessageTemplate.Render() directly
+///
+public sealed class MessageTemplateRenderer
+{
+ private readonly List _tokens;
+
+ public MessageTemplateRenderer(string template)
+ {
+ var parser = new MessageTemplateParser(template);
+ _tokens = parser.Parse();
+ }
+
+ public MessageTemplateRenderer(List tokens)
+ {
+ _tokens = tokens;
+ }
+
+ public string Render(Dictionary properties)
+ {
+ var sb = new StringBuilder();
+ foreach (var token in _tokens)
+ {
+ token.Render(sb, properties);
+ }
+ return sb.ToString();
+ }
+
+ public void Render(StringBuilder sb, Dictionary properties)
+ {
+ foreach (var token in _tokens)
+ {
+ token.Render(sb, properties);
+ }
+ }
+
+ ///
+ /// Gets all property names referenced in the template
+ ///
+ public IEnumerable GetPropertyNames()
+ {
+ return _tokens
+ .OfType()
+ .Select(t => t.PropertyName)
+ .Distinct();
+ }
+}
+
+///
+/// Template cache for frequently-used templates (legacy compatibility)
+///
+public sealed class MessageTemplateCache
+{
+ private readonly Dictionary _cache = new();
+ private readonly int _maxSize;
+ private readonly object _lock = new object();
+
+ public MessageTemplateCache(int maxSize = 1000)
+ {
+ _maxSize = maxSize;
+ }
+
+ public MessageTemplateRenderer GetOrParse(string template)
+ {
+ lock (_lock)
+ {
+ if (_cache.TryGetValue(template, out var cached))
+ {
+ return cached;
+ }
+
+ if (_cache.Count >= _maxSize)
+ {
+ // Remove oldest entry (simple FIFO)
+ var oldestKey = _cache.Keys.First();
+ _cache.Remove(oldestKey);
+ }
+
+ var renderer = new MessageTemplateRenderer(template);
+ _cache[template] = renderer;
+ return renderer;
+ }
+ }
+
+ public void Clear()
+ {
+ lock (_lock)
+ {
+ _cache.Clear();
+ }
+ }
+
+ public int Count
+ {
+ get
+ {
+ lock (_lock)
+ {
+ return _cache.Count;
+ }
+ }
+ }
+}
diff --git a/EonaCat.LogStack/Extensions/HostApplicationBuilderExtensions.cs b/EonaCat.LogStack/Extensions/HostApplicationBuilderExtensions.cs
index 6efbb9a..01e03b5 100644
--- a/EonaCat.LogStack/Extensions/HostApplicationBuilderExtensions.cs
+++ b/EonaCat.LogStack/Extensions/HostApplicationBuilderExtensions.cs
@@ -1,14 +1,125 @@
+using EonaCat.LogStack.Configuration;
using EonaCat.LogStack.Core;
+using EonaCat.LogStack.Logging;
using Microsoft.Extensions.Hosting;
using System;
namespace EonaCat.LogStack.Extensions;
+///
+/// Extension methods for integrating EonaCat LogStack with HostApplicationBuilder
+///
public static class HostApplicationBuilderExtensions
{
- public static IHostApplicationBuilder AddEonaCatLogging(this IHostApplicationBuilder builder, Action? configure=null)
+ ///
+ /// Adds EonaCat LogStack logging with a configuration callback
+ ///
+ public static IHostApplicationBuilder AddEonaCatLogging(
+ this IHostApplicationBuilder builder,
+ Action? configure = null)
{
- builder.Services.AddEonaCatLogging(configure ?? (_=>{}));
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.AddEonaCatLogging(configure ?? (_ => { }));
+ return builder;
+ }
+
+ ///
+ /// Adds EonaCat LogStack logging with fluent LogBuilder configuration
+ ///
+ public static IHostApplicationBuilder AddEonaCatLogging(
+ this IHostApplicationBuilder builder,
+ Action configure)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ builder.Services.AddEonaCatLogging(configure);
+ return builder;
+ }
+
+ ///
+ /// Adds EonaCat LogStack logging with fluent LogBuilder configuration and a specific category
+ ///
+ public static IHostApplicationBuilder AddEonaCatLogging(
+ this IHostApplicationBuilder builder,
+ string category,
+ Action configure)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ builder.Services.AddEonaCatLogging(category, configure);
+ return builder;
+ }
+
+ ///
+ /// Adds EonaCat LogStack with AdvancedLoggerFactory for enhanced features
+ ///
+ 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;
+ }
+
+ ///
+ /// Adds EonaCat LogStack with AdvancedLoggerFactory and a configuration callback
+ ///
+ public static IHostApplicationBuilder AddAdvancedEonaCatLogging(
+ this IHostApplicationBuilder builder,
+ Action configure)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ builder.Services.AddAdvancedEonaCatLogging(configure);
+ return builder;
+ }
+
+ ///
+ /// Adds the AdvancedMetricsCollector for system-wide analytics
+ ///
+ public static IHostApplicationBuilder AddEonaCatMetricsCollection(
+ this IHostApplicationBuilder builder)
+ {
+ if (builder == null)
+ {
+ throw new ArgumentNullException(nameof(builder));
+ }
+
+ builder.Services.AddEonaCatMetricsCollection();
return builder;
}
}
diff --git a/EonaCat.LogStack/Extensions/ServiceCollectionExtensions.cs b/EonaCat.LogStack/Extensions/ServiceCollectionExtensions.cs
index 5211773..c92b1c5 100644
--- a/EonaCat.LogStack/Extensions/ServiceCollectionExtensions.cs
+++ b/EonaCat.LogStack/Extensions/ServiceCollectionExtensions.cs
@@ -248,4 +248,93 @@ public static class ServiceCollectionExtensions
return services.AddEonaCatLoggingFactory("Application", configure);
}
+
+ ///
+ /// Registers EonaCat LogStack with AdvancedLoggerFactory for enhanced features like category-specific configuration and context propagation
+ ///
+ /// The service collection to register with
+ /// The minimum log level to process
+ /// The timestamp mode to use
+ /// The service collection for chaining
+ 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(advancedFactory);
+ services.AddSingleton(
+ new MicrosoftExtensionsLoggerFactoryAdapter(advancedFactory));
+
+ // Also register as ILogger for constructor injection
+ services.AddSingleton(sp => sp.GetRequiredService().CreateLogger("Default"));
+ services.AddSingleton(sp =>
+ new MicrosoftExtensionsLoggerAdapter(
+ sp.GetRequiredService().CreateLogger("Default")));
+
+ return services;
+ }
+
+ ///
+ /// Registers EonaCat LogStack with AdvancedLoggerFactory and a configuration callback
+ ///
+ /// The service collection to register with
+ /// Callback to configure the AdvancedLoggerFactory
+ /// The service collection for chaining
+ public static IServiceCollection AddAdvancedEonaCatLogging(
+ this IServiceCollection services,
+ Action configure)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ if (configure == null)
+ {
+ throw new ArgumentNullException(nameof(configure));
+ }
+
+ services.AddSingleton(sp =>
+ {
+ var factory = new AdvancedLoggerFactory();
+ configure(factory);
+ return factory;
+ });
+
+ services.AddSingleton(sp => sp.GetRequiredService());
+ services.AddSingleton(sp =>
+ new MicrosoftExtensionsLoggerFactoryAdapter(sp.GetRequiredService()));
+
+ // Also register as ILogger for constructor injection
+ services.AddSingleton(sp => sp.GetRequiredService().CreateLogger("Default"));
+ services.AddSingleton(sp =>
+ new MicrosoftExtensionsLoggerAdapter(
+ sp.GetRequiredService().CreateLogger("Default")));
+
+ return services;
+ }
+
+ ///
+ /// Registers the AdvancedMetricsCollector for system-wide analytics
+ ///
+ /// The service collection to register with
+ /// The service collection for chaining
+ public static IServiceCollection AddEonaCatMetricsCollection(
+ this IServiceCollection services)
+ {
+ if (services == null)
+ {
+ throw new ArgumentNullException(nameof(services));
+ }
+
+ services.AddSingleton();
+ return services;
+ }
}
diff --git a/EonaCat.LogStack/LogBuilder.cs b/EonaCat.LogStack/LogBuilder.cs
index 51b3c4a..b07d8f1 100644
--- a/EonaCat.LogStack/LogBuilder.cs
+++ b/EonaCat.LogStack/LogBuilder.cs
@@ -28,7 +28,10 @@ public sealed class LogBuilder
private TimestampMode _timestampMode = TimestampMode.Utc;
private readonly List _flows = new();
private readonly List _boosters = new();
-
+ private DynamicLevelController? _dynamicLevel;
+ private bool _useAsyncPipeline;
+ private int _asyncPipelineCapacity = 65536;
+
public event EventHandler OnLog;
public LogBuilder(string category = "Application")
@@ -991,6 +994,16 @@ public sealed class LogBuilder
logger.AddBooster(booster);
}
+ if (_dynamicLevel != null)
+ {
+ logger.UseDynamicLevel(_dynamicLevel);
+ }
+
+ if (_useAsyncPipeline)
+ {
+ logger.UseAsyncPipeline(_asyncPipelineCapacity);
+ }
+
return logger;
}
@@ -1080,4 +1093,113 @@ public sealed class LogBuilder
lock (_flows) { _flows.RemoveAll(f => f.Name == name); }
}
+
+ ///
+ /// Enables the Channel-based async dispatch pipeline for zero-blocking logging.
+ /// Events are enqueued to a and
+ /// consumed by a dedicated background Task.
+ ///
+ /// Bounded capacity (0 = unbounded).
+ public LogBuilder UseAsyncPipeline(int capacity = 65536)
+ {
+ _useAsyncPipeline = true;
+ _asyncPipelineCapacity = capacity;
+ return this;
+ }
+
+ ///
+ /// Attaches a so the minimum log level can
+ /// be changed at runtime without restarting the application.
+ ///
+ public LogBuilder WithDynamicLevelController(DynamicLevelController controller)
+ {
+ _dynamicLevel = controller ?? throw new ArgumentNullException(nameof(controller));
+ return this;
+ }
+
+ ///
+ /// Wraps any flow with a Circuit Breaker that opens after repeated failures
+ /// and probes for recovery after a configurable timeout.
+ ///
+ 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;
+ }
+
+ ///
+ /// Wraps any flow with a predicate — events are forwarded only when the predicate returns true.
+ ///
+ public LogBuilder WriteToConditional(
+ IFlow inner,
+ Func 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;
+ }
+
+ ///
+ /// Adds a that fans events out to multiple inner flows in parallel.
+ /// Use to attach targets.
+ ///
+ public LogBuilder WriteToMulticast(
+ MulticastFlow multicast)
+ {
+ if (multicast == null)
+ {
+ throw new ArgumentNullException(nameof(multicast));
+ }
+
+ _flows.Add(multicast);
+ return this;
+ }
+
+ ///
+ /// Pushes log events to a Grafana Loki instance.
+ ///
+ public LogBuilder WriteToLoki(
+ string lokiUrl,
+ Dictionary? 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;
+ }
+
+ ///
+ /// Adds the which captures caller member/file/line.
+ ///
+ public LogBuilder BoostWithCallerInfo()
+ {
+ _boosters.Add(new CallerInfoBooster());
+ return this;
+ }
}
\ No newline at end of file
diff --git a/EonaCat.LogStack/LoggerDiagnostics.cs b/EonaCat.LogStack/LoggerDiagnostics.cs
index 80ed0e6..8dc1740 100644
--- a/EonaCat.LogStack/LoggerDiagnostics.cs
+++ b/EonaCat.LogStack/LoggerDiagnostics.cs
@@ -16,6 +16,7 @@ public class LoggerDiagnostics
public LogLevel MinimumLevel { get; set; }
public long TotalLogged { get; set; }
public long TotalDropped { get; set; }
+ public long TotalExceptions { get; set; }
public int FlowCount { get; set; }
public int BoosterCount { get; set; }
public List Flows { get; set; }
diff --git a/EonaCat.LogStack/LoggerMetrics.cs b/EonaCat.LogStack/LoggerMetrics.cs
new file mode 100644
index 0000000..1011d53
--- /dev/null
+++ b/EonaCat.LogStack/LoggerMetrics.cs
@@ -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.
+
+///
+/// Comprehensive metrics about the logger
+///
+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 FlowMetrics { get; set; } = new();
+
+ ///
+ /// Gets the success rate (logged / (logged + dropped))
+ ///
+ public double SuccessRate
+ {
+ get
+ {
+ var total = TotalLogged + TotalDropped;
+ return total > 0 ? (TotalLogged * 100.0) / total : 100;
+ }
+ }
+
+ ///
+ /// Gets average bytes per event
+ ///
+ public double AverageBytesPerEvent
+ {
+ get => TotalLogged > 0 ? TotalBytes / (double)TotalLogged : 0;
+ }
+
+ ///
+ /// Returns formatted report
+ ///
+ 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();
+ }
+}
diff --git a/README.md b/README.md
index 339b369..d5ffa83 100644
--- a/README.md
+++ b/README.md
@@ -1,59 +1,68 @@
# EonaCat.LogStack
-**EonaCat.LogStack** flow-based logging library for .NET, designed for zero-allocation logging paths and superior memory efficiency.
+**EonaCat.LogStack** is a flow-based, high-performance logging library for .NET, designed for zero-allocation logging paths and superior memory efficiency.
It features a rich fluent API for routing log events to dozens of destinations - from console and file to Slack, Discord, Redis, Elasticsearch, and beyond.
----
-
## Features
-- **Flow-based architecture** - route log events to one or many output destinations simultaneously
-- **Booster system** - enrich every log event with contextual metadata (machine name, process ID, thread info, memory, uptime, correlation IDs, and more)
-- **Pre-build modifiers** - intercept and mutate log events before they are written
-- **Zero-allocation hot path** - `AggressiveInlining` throughout, `StringBuilder` pooling, and `ref`-based builder pattern
-- **Async-first** - all flows implement `IAsyncDisposable` and `FlushAsync`
-- **Resilience built-in** - retry, failover, throttling, and rolling buffer flows
-- **Tamper-evident audit trail** - SHA-256 hash-chained audit files
-- **Encrypted file logging** - AES-encrypted log files with a built-in decrypt utility
-- **Compression** - GZip-compressed rolled log files
-- **Category routing** - split logs into separate files per category or log level
-- **Diagnostics** - live counters (total logged, total dropped, per-flow stats)
+### Core Architecture
+- **Flow-based system** - Route log events to multiple destinations simultaneously. Each flow is independent and can be configured with its own level filters and batching strategy.
+- **Booster system** - Automatically enrich log events with contextual metadata like machine name, process ID, thread info, memory usage, uptime, correlation IDs, and custom properties.
+- **Pre-build modifiers** - Intercept and mutate log events before they reach any flow using a chainable modifier system.
+- **Zero-allocation hot path** - `AggressiveInlining` throughout, `StringBuilder` pooling, and `ref`-based builder pattern for minimal GC pressure during logging.
+- **Async-first design** - All flows implement `IAsyncDisposable` and `FlushAsync()` for proper async resource cleanup and batching.
----
+### Advanced Features
+- **Resilience patterns** - Built-in `RetryFlow` with exponential backoff, `FailoverFlow` for primary/secondary failover, and `ThrottledFlow` for rate limiting with deduplication.
+- **Tamper-evident audit** - SHA-256 hash-chained audit files where deleting or modifying any past entry invalidates all subsequent hashes. Built-in integrity verification.
+- **Encrypted file logging** - AES-encrypted log files with password protection. Includes `DecryptFile()` utility to decrypt files on demand.
+- **Log compression** - Automatic GZip compression of rolled-over log files to conserve disk space.
+- **Category/level-based routing** - Split logs into separate files per category or log level for better organization.
+- **Rolling buffers** - Circular buffer with trigger-based flushing. When a critical event occurs, buffer context (previous N lines) is forwarded to a secondary flow.
+- **Diagnostics & metrics** - Real-time counters for total logged, total dropped, per-flow statistics, and flow-specific performance data.
+- **Live level control** - Dynamically change log level at runtime without recreating the logger.
+- **Structured logging** - First-class support for structured properties (tuples, dictionaries) that are included in JSON output where applicable.
+
+### Performance & Reliability
+- **Configurable backpressure** - Choose between Wait, Drop, or Block strategies when log queues are full.
+- **Batch processing** - Most flows support configurable batch sizes and intervals for efficient I/O.
+- **Rate limiting & deduplication** - Protect high-latency sinks (email, Slack, HTTP) from log storms using token-bucket rate limiting and optional message deduplication.
+- **Memory flow** - Store recent logs in a circular in-memory buffer for quick diagnostics or fallback output.
+- **Lazy initialization** - Flows are only initialized when first used, reducing startup overhead.
## Supported Targets
- .NET Standard 2.1
- .NET 8.0
+- .NET 9.0
+- .NET 10.0
- .NET Framework 4.8
----
-
## Installation
```bash
dotnet add package EonaCat.LogStack
```
----
-
## Quick Start
+### Minimal - Console + File
+
```csharp
await using var logger = LogBuilder.CreateDefault("MyApp");
logger.Information("Application started");
logger.Warning("Low memory warning");
logger.Error(ex, "Unexpected error occurred");
+
+// Automatic cleanup on disposal
```
`CreateDefault` creates a logger writing to both the console and a `./logs` directory, enriched with machine name and process ID.
----
+### Fluent Configuration
-## Fluent Configuration
-
-Build a fully customised logger using `LogBuilder`:
+Build a fully customized logger using `LogBuilder`:
```csharp
await using var logger = new LogBuilder("MyApp")
@@ -66,261 +75,2003 @@ await using var logger = new LogBuilder("MyApp")
.BoostWithProcessId()
.BoostWithCorrelationId()
.Build();
+
+try
+{
+ logger.Information("Application started");
+ // Your application code...
+}
+finally
+{
+ await logger.FlushAsync();
+ await logger.DisposeAsync();
+}
```
----
-
## Logging Methods
+### Basic Logging
+
```csharp
logger.Trace("Verbose trace message");
logger.Debug("Debug detail");
logger.Information("Something happened");
logger.Warning("Potential problem");
-logger.Warning(ex, "Warning with exception");
logger.Error("Something failed");
-logger.Error(ex, "Error with exception");
logger.Critical("System is going down");
-logger.Critical(ex, "Critical failure");
-
-// With structured properties
-logger.Log(LogLevel.Information, "User logged in",
- ("UserId", 42),
- ("IP", "192.168.1.1"));
```
----
-
-## Available Flows
-### Flows can be extended with custom implementations of `IFlow`, but here are the built-in options:
-
-| Flow | Method | Description |
-|------|--------|-------------|
-| Console | `WriteToConsole()` | Colored console output |
-| File | `WriteToFile()` | Batched, rotated, compressed file output |
-| Encrypted File | `WriteToEncryptedFile()` | AES-encrypted log files |
-| Memory | `WriteToMemory()` | In-memory ring buffer |
-| Audit | `WriteToAudit()` | Tamper-evident hash-chained audit trail |
-| Database | `WriteToDatabase()` | ADO.NET database sink |
-| HTTP | `WriteToHttp()` | Generic HTTP endpoint (batched) |
-| Webhook | `WriteToWebhook()` | Generic webhook POST |
-| Email | `WriteToEmail()` | HTML digest emails via SMTP |
-| Slack | `WriteToSlack()` | Slack incoming webhooks |
-| Discord | `WriteToDiscord()` | Discord webhooks |
-| Microsoft Teams | `WriteToMicrosoftTeams()` | Teams incoming webhooks |
-| Telegram | `WriteToTelegram()` | Telegram bot messages |
-| SignalR | `WriteToSignalR()` | Real-time SignalR hub push |
-| Redis | `RedisFlow()` | Redis Pub/Sub + optional List persistence |
-| Elasticsearch | `WriteToElasticSearch()` | Elasticsearch index |
-| Splunk | `WriteToSplunkFlow()` | Splunk HEC |
-| Graylog | `WriteToGraylogFlow()` | GELF over UDP or TCP |
-| Syslog UDP | `WriteToSyslogUdp()` | RFC-5424 Syslog over UDP |
-| Syslog TCP | `WriteToSyslogTcp()` | RFC-5424 Syslog over TCP (with optional TLS) |
-| TCP | `WriteToTcp()` | Raw TCP (with optional TLS) |
-| UDP | `WriteToUdp()` | Raw UDP datagrams |
-| SNMP Trap | `WriteToSnmpTrap()` | SNMP v2c traps |
-| Zabbix | `WriteToZabbixFlow()` | Zabbix trapper protocol |
-| EventLog | `WriteToEventLogFlow()` | Remote event log forwarding |
-| Rolling Buffer | `WriteToRollingBuffer()` | Circular buffer with trigger-based flush |
-| Throttled | `WriteToThrottled()` | Token-bucket rate limiting + deduplication |
-| Retry | `WriteToRetry()` | Automatic retry with exponential back-off |
-| Failover | `WriteToFailover()` | Primary/secondary failover |
-| Diagnostics | `WriteDiagnostics()` | Periodic diagnostic snapshots |
-| Status | `WriteToStatusFlow()` | Service health monitoring |
-
----
-
-## Available Boosters
-
-Boosters enrich every log event with additional properties before it reaches any flow.
+### Logging with Exceptions
```csharp
-new LogBuilder("MyApp")
- .BoostWithMachineName() // host name
- .BoostWithProcessId() // PID
- .BoostWithThreadId() // managed thread ID
- .BoostWithThreadName() // thread name
- .BoostWithUser() // current OS user
- .BoostWithApp() // app name and base directory
- .BoostWithApplication("MyApp", "2.0.0") // explicit name + version
- .BoostWithEnvironment("Production")
- .BoostWithOS() // OS description
- .BoostWithFramework() // .NET runtime description
- .BoostWithMemory() // working set in MB
- .BoostWithUptime() // process uptime in seconds
- .BoostWithProcStart() // process start time
- .BoostWithDate() // current date (yyyy-MM-dd)
- .BoostWithTime() // current time (HH:mm:ss.fff)
- .BoostWithTicks() // current timestamp ticks
- .BoostWithCorrelationId() // Activity.Current correlation ID
- .BoostWithCustomText("env", "prod") // arbitrary key/value
- .Boost("myBooster", () => new Dictionary { ["key"] = "val" })
- ...
+try
+{
+ // risky operation
+}
+catch (Exception ex)
+{
+ logger.Warning(ex, "Operation failed, attempting retry");
+ logger.Error(ex, "Operation failed after retries");
+ logger.Critical(ex, "Critical failure, shutting down");
+}
```
----
+### Structured Properties
-## Pre-Build Modifiers
+```csharp
+// With tuples (preferred for performance)
+logger.Log(LogLevel.Information, "User logged in",
+ ("UserId", 42),
+ ("IP", "192.168.1.1"),
+ ("Session", "abc-123"));
-Modifiers run after boosters and can mutate or cancel a log event before it is dispatched to flows:
+// Properties appear in most flows (database, JSON output, etc.)
+// In file output: `UserId=42, IP=192.168.1.1, Session=abc-123`
+```
+
+### Custom Log Event Modification
```csharp
logger.AddModifier((ref LogEventBuilder builder) =>
{
- builder.WithProperty("RequestId", Guid.NewGuid().ToString());
+ builder.WithProperty("RequestId", HttpContext.TraceIdentifier);
+ builder.WithProperty("UserId", User.Id);
+});
+
+logger.Information("Processing request"); // RequestId and UserId added automatically
+```
+
+## Available Flows
+
+Flows are the destinations where log events are written. Each flow is independent and can have its own level filter and configuration.
+
+### Flows Overview
+
+| Flow | Method | Description |
+|------|--------|-------------|
+| Console | `WriteToConsole()` | Colored console output with customizable templates |
+| File | `WriteToFile()` | Batched, rotated, compressed file output with retention policies |
+| Encrypted File | `WriteToEncryptedFile()` | AES-encrypted log files with password protection |
+| Memory | `WriteToMemory()` | In-memory ring buffer for quick access and diagnostics |
+| Audit | `WriteToAudit()` | Tamper-evident hash-chained audit trail with verification |
+| Database | `WriteToDatabase()` | ADO.NET database sink with custom table support |
+| HTTP | `WriteToHttp()` | Generic HTTP endpoint with custom headers and batching |
+| Webhook | `WriteToWebhook()` | Generic webhook POST endpoint |
+| Email | `WriteToEmail()` | HTML digest emails via SMTP with configurable batching |
+| Slack | `WriteToSlack()` | Slack incoming webhooks with message formatting |
+| Discord | `WriteToDiscord()` | Discord webhooks with embed formatting |
+| Microsoft Teams | `WriteToMicrosoftTeams()` | Teams incoming webhooks with adaptive cards |
+| Telegram | `WriteToTelegram()` | Telegram bot messages |
+| SignalR | `WriteToSignalR()` | Real-time SignalR hub push for live dashboards |
+| Redis | `RedisFlow()` | Redis Pub/Sub + optional List persistence with reconnect |
+| Elasticsearch | `WriteToElasticSearch()` | Elasticsearch index with custom index names |
+| Splunk | `WriteToSplunkFlow()` | Splunk HEC (HTTP Event Collector) |
+| Graylog | `WriteToGraylogFlow()` | GELF over UDP or TCP |
+| Syslog UDP | `WriteToSyslogUdp()` | RFC-5424 Syslog over UDP |
+| Syslog TCP | `WriteToSyslogTcp()` | RFC-5424 Syslog over TCP with optional TLS |
+| TCP | `WriteToTcp()` | Raw TCP with optional TLS support |
+| UDP | `WriteToUdp()` | Raw UDP datagrams |
+| SNMP Trap | `WriteToSnmpTrap()` | SNMP v2c traps for network monitoring |
+| Zabbix | `WriteToZabbixFlow()` | Zabbix trapper protocol |
+| EventLog | `WriteToEventLogFlow()` | Remote Windows event log forwarding |
+| Rolling Buffer | `WriteToRollingBuffer()` | Circular buffer with trigger-based context flushing |
+| Throttled | `WriteToThrottled()` | Token-bucket rate limiting with optional deduplication |
+| Retry | `WriteToRetry()` | Automatic retry with exponential back-off |
+| Failover | `WriteToFailover()` | Primary/secondary failover with recovery detection |
+| Diagnostics | `WriteDiagnostics()` | Periodic diagnostic snapshots and metrics |
+| Status | `WriteToStatusFlow()` | Service health monitoring |
+| Conditional | `WriteToConditional()` | Route logs based on custom predicates |
+| Circuit Breaker | `WriteToCircuitBreaker()` | Protect against cascading failures |
+
+### Flow Examples
+
+#### Console Output
+```csharp
+// Basic colored output
+.WriteToConsole(useColors: true)
+
+// Minimal console (no colors)
+.WriteToConsole(useColors: false)
+
+// Only warnings and above to console
+.WriteToConsole(minimumLevel: LogLevel.Warning, useColors: true)
+```
+
+#### File Output
+```csharp
+// Basic file logging
+.WriteToFile("./logs")
+
+// Custom configuration
+.WriteToFile(
+ directory: "./logs",
+ filePrefix: "myapp",
+ maxFileSize: 100 * 1024 * 1024, // 100 MB
+ maxDirectorySize: 10L * 1024 * 1024 * 1024, // 10 GB total
+ flushIntervalInMilliSeconds: 2000,
+ batchSize: 50,
+ compression: CompressionFormat.GZip,
+ outputFormat: FileOutputFormat.Text)
+
+// Category-based routing (separate files per category)
+.WriteToFile(
+ directory: "./logs",
+ useCategoryRouting: true)
+
+// Level-based routing (separate files per log level)
+.WriteToFile(
+ directory: "./logs",
+ logLevelsForSeparateFiles: new[] { LogLevel.Error, LogLevel.Critical })
+```
+
+#### Encrypted File Output
+```csharp
+// Encrypt logs with password
+.WriteToEncryptedFile(
+ directory: "./secure-logs",
+ password: "MySecurePassword123!")
+
+// Decrypt later when needed
+LogBuilder.DecryptFile(
+ encryptedPath: "./secure-logs/log.enc",
+ outputPath: "./secure-logs/log.txt",
+ password: "MySecurePassword123!");
+```
+
+#### In-Memory Buffer
+```csharp
+// Store last 1000 events in memory
+.WriteToMemory(capacity: 1000, minimumLevel: LogLevel.Information)
+
+// Access stored events
+var memoryFlow = logger.GetFlowOfType();
+var events = memoryFlow.GetEvents();
+```
+
+#### Slack
+```csharp
+.WriteToSlack("https://hooks.slack.com/services/YOUR/WEBHOOK/URL")
+
+// Only errors to Slack
+.WriteToSlack(
+ webhookUrl: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL",
+ minimumLevel: LogLevel.Error)
+```
+
+#### Discord
+```csharp
+.WriteToDiscord(
+ webHookUrl: "https://discordapp.com/api/webhooks/YOUR/WEBHOOK",
+ botName: "ErrorBot")
+```
+
+#### Email Digest
+```csharp
+.WriteToEmail(
+ smtpHost: "smtp.gmail.com",
+ smtpPort: 587,
+ useSsl: true,
+ username: "your-email@gmail.com",
+ password: "app-password",
+ from: "logs@company.com",
+ to: "ops@company.com",
+ subjectPrefix: "[Production Alerts]",
+ digestMinutes: 5, // Send every 5 minutes
+ flushOnCritical: true, // Send immediately on Critical
+ minimumLevel: LogLevel.Error)
+```
+
+#### Database (SQL Server, PostgreSQL, MySQL, etc.)
+```csharp
+.WriteToDatabase(
+ connectionFactory: () => new SqlConnection("connection-string"),
+ tableName: "ApplicationLogs",
+ batchSize: 10)
+```
+
+#### Elasticsearch
+```csharp
+.WriteToElasticSearch(
+ elasticSearchUrl: "https://elasticsearch.company.com:9200",
+ indexName: "myapp-logs",
+ batchSize: 100)
+```
+
+#### Redis (Pub/Sub + List)
+```csharp
+// Pub/Sub only (real-time subscribers)
+.RedisFlow(
+ host: "redis.company.com",
+ port: 6379,
+ channel: "eonacat:logs")
+
+// Pub/Sub + List persistence (subscribers + history)
+.RedisFlow(
+ host: "redis.company.com",
+ port: 6379,
+ password: "redis-password",
+ database: 0,
+ channel: "eonacat:logs",
+ listKey: "eonacat:logs:history",
+ maxListLength: 10000)
+```
+
+#### Syslog
+```csharp
+// RFC-5424 Syslog over UDP
+.WriteToSyslogUdp(
+ host: "syslog.company.com",
+ port: 514)
+
+// RFC-5424 Syslog over TCP with TLS
+.WriteToSyslogTcp(
+ host: "syslog.company.com",
+ port: 514,
+ useTls: true)
+```
+
+#### HTTP Endpoint
+```csharp
+.WriteToHttp(
+ endpoint: "https://logs.company.com/ingest",
+ batchSize: 50,
+ batchInterval: TimeSpan.FromSeconds(2),
+ headers: new Dictionary
+ {
+ ["Authorization"] = "Bearer token123",
+ ["X-API-Key"] = "secret"
+ })
+```
+
+#### Audit Trail (Tamper-Evident)
+```csharp
+// Record warnings and above in hash-chained audit trail
+.WriteToAudit(
+ directory: "./audit",
+ auditLevel: AuditLevel.WarningAndAbove,
+ includeProperties: true)
+
+// Verify audit file integrity
+bool isIntact = AuditFlow.Verify("./audit/audit.audit");
+if (!isIntact)
+ Console.WriteLine("Audit trail has been tampered with!");
+```
+
+#### Rolling Buffer (Context-on-Error)
+```csharp
+// Buffer 500 events; on Error, flush preceding 100 events to file
+.WriteToRollingBuffer(
+ capacity: 500,
+ minimumLevel: LogLevel.Trace,
+ triggerLevel: LogLevel.Error,
+ triggerTarget: new FileFlow("./error-context"),
+ preContextLines: 100)
+```
+
+## Available Boosters
+
+Boosters automatically enrich every log event with additional properties before it reaches any flow. They run once per log event and can add system information, application context, and custom data.
+
+### System Information Boosters
+
+```csharp
+.BoostWithMachineName() // Add computer/host name
+.BoostWithProcessId() // Add current process ID
+.BoostWithThreadId() // Add managed thread ID
+.BoostWithThreadName() // Add thread name (if set)
+.BoostWithUser() // Add current OS user name
+.BoostWithOS() // Add OS description and version
+.BoostWithFramework() // Add .NET runtime description
+.BoostWithMemory() // Add process working set in MB
+.BoostWithUptime() // Add process uptime in seconds
+.BoostWithProcStart() // Add process start time (DateTime)
+```
+
+### Date/Time Boosters
+
+```csharp
+.BoostWithDate() // Add current date (yyyy-MM-dd)
+.BoostWithTime() // Add current time (HH:mm:ss.fff)
+.BoostWithTicks() // Add current timestamp ticks (for precise timing)
+```
+
+### Application Context Boosters
+
+```csharp
+.BoostWithApp() // Add app name and base directory (auto-detected)
+.BoostWithApplication("MyApp", "2.0.0") // Add explicit app name + version
+.BoostWithEnvironment("Production") // Add environment name (Prod/Dev/Test)
+.BoostWithCorrelationId() // Add Activity.Current correlation ID for distributed tracing
+```
+
+### Custom Data Boosters
+
+```csharp
+// Single key/value pair
+.BoostWithCustomText("Environment", "Production")
+.BoostWithCustomText("ServiceVersion", "2.0.1")
+
+// Callback-based booster for dynamic data
+.Boost("RequestInfo", () => new Dictionary
+{
+ ["UserId"] = GetCurrentUserId(),
+ ["TenantId"] = GetCurrentTenantId(),
+ ["ApiVersion"] = GetApiVersion()
+})
+
+// Custom booster implementation
+.Boost(new MyCustomBooster())
+```
+
+### Complete Booster Configuration Example
+
+```csharp
+var logger = new LogBuilder("MyApplication")
+ // System info
+ .BoostWithMachineName()
+ .BoostWithProcessId()
+ .BoostWithThreadId()
+ .BoostWithOS()
+ .BoostWithFramework()
+ .BoostWithMemory()
+
+ // Application context
+ .BoostWithApplication("MyApp", "1.0.0")
+ .BoostWithEnvironment("Production")
+ .BoostWithCorrelationId()
+
+ // Startup info
+ .BoostWithProcStart()
+ .BoostWithUptime()
+
+ // Custom context
+ .BoostWithCustomText("DataCenter", "US-East-1")
+ .BoostWithCustomText("InstanceId", Environment.MachineName)
+ .Boost("Request", () => new Dictionary
+ {
+ ["TraceId"] = Activity.Current?.Id ?? HttpContext?.TraceIdentifier,
+ ["UserId"] = CurrentUser?.Id,
+ })
+
+ .WriteToConsole()
+ .WriteToFile("./logs")
+ .Build();
+```
+
+### What Boosters Add to Logs
+
+When you enable boosters, they add structured properties to each log event. In file output, these appear as:
+
+```
+[2026-03-27 09:15:00.123] [INFO] [Application=MyApp, Version=1.0.0, Environment=Production, Machine=srv-01, PID=1234, ThreadId=5]
+User logged in successfully
+```
+
+In JSON outputs (database, Elasticsearch, etc.), boosters add properties like:
+
+```json
+{
+ "timestamp": "2026-03-27T09:15:00.123Z",
+ "level": "Information",
+ "message": "User logged in successfully",
+ "machine": "srv-01",
+ "processId": 1234,
+ "threadId": 5,
+ "userId": 42,
+ "application": "MyApp",
+ "version": "1.0.0",
+ "environment": "Production"
+}
+```
+
+## Pre-Build Modifiers
+
+Modifiers run after boosters and can mutate or cancel a log event before it is dispatched to flows. Use modifiers to add request-scoped data, redact sensitive info, or filter events.
+
+### Basic Modifier Example
+
+```csharp
+logger.AddModifier((ref LogEventBuilder builder) =>
+{
+ // Add request context
+ builder.WithProperty("RequestId", HttpContext.TraceIdentifier);
+});
+
+// Now every log will include RequestId automatically
+logger.Information("Processing request");
+```
+
+### Multiple Modifiers
+
+```csharp
+// Add request context
+logger.AddModifier((ref LogEventBuilder builder) =>
+{
+ builder.WithProperty("RequestId", HttpContext?.TraceIdentifier);
+ builder.WithProperty("UserId", User?.Id);
+});
+
+// Add custom timing info
+logger.AddModifier((ref LogEventBuilder builder) =>
+{
+ builder.WithProperty("Timestamp", DateTime.UtcNow);
+});
+
+// Redact sensitive data (example)
+logger.AddModifier((ref LogEventBuilder builder) =>
+{
+ if (builder.Message?.Contains("password") ?? false)
+ builder.WithMessage("[REDACTED - contains sensitive data]");
});
```
----
+### Canceling Events with Modifiers
+
+```csharp
+logger.AddModifier((ref LogEventBuilder builder) =>
+{
+ // Example: Skip verbose trace logs in production
+ if (builder.Level == LogLevel.Trace && !IsDebugMode)
+ builder.Cancel(); // Event won't be sent to any flow
+});
+```
+
+## Request Context Integration (ASP.NET Core / Razor Pages)
+
+```csharp
+// In Program.cs, register request context booster
+builder.Services.AddEonaCatLogging("WebApp", logBuilder =>
+{
+ logBuilder
+ .WriteToConsole()
+ .WriteToFile("./logs")
+ .BoostWithCorrelationId(); // Distributed tracing support
+});
+
+// In your page or controller
+public class IndexModel : PageModel
+{
+ private readonly ILogger _logger;
+
+ public IndexModel(ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ public void OnGet()
+ {
+ // Correlation ID is automatically added by booster
+ _logger.Information("Page loaded");
+ }
+}
+```
## Resilience Patterns
-### Retry with exponential back-off
+### Retry with Exponential Back-off
+
+Automatically retry failed writes with exponential delays. Useful for flaky remote endpoints.
+
```csharp
.WriteToRetry(
primaryFlow: new HttpFlow("https://logs.example.com"),
maxRetries: 5,
initialDelay: TimeSpan.FromMilliseconds(200),
exponentialBackoff: true)
+
+// Retry delays: 200ms, 400ms, 800ms, 1.6s, 3.2s
```
-### Primary / secondary failover
+### Primary / Secondary Failover
+
+Automatically fall back to a secondary destination if the primary fails.
+
```csharp
.WriteToFailover(
primaryFlow: new ElasticSearchFlow("https://es-prod:9200"),
- secondaryFlow: new FileFlow("./fallback-logs"))
+ secondaryFlow: new FileFlow("./fallback-logs"),
+ recoveryCheckInterval: TimeSpan.FromSeconds(30),
+ failureThreshold: 5) // Switch after 5 consecutive failures
```
-### Token-bucket throttling with deduplication
+### Token-Bucket Rate Limiting with Deduplication
+
+Protect high-latency sinks (email, Slack, HTTP) from log storms.
+
```csharp
.WriteToThrottled(
inner: new SlackFlow(webhookUrl),
- burstCapacity: 10,
- refillPerSecond: 1.0,
- deduplicate: true,
- dedupWindow: TimeSpan.FromSeconds(60))
+ burstCapacity: 10, // Allow 10 events in a burst
+ refillPerSecond: 1.0, // Refill 1 token per second
+ deduplicate: true, // Collapse identical messages
+ dedupWindow: TimeSpan.FromSeconds(60), // Within 60-second window
+ dedupMaxKeys: 1000) // Track up to 1000 unique messages
```
-### Rolling buffer - flush context on error
+Example behavior:
+- `10` log errors occur simultaneously → first 10 are sent (burst)
+- Next error within 1 second is queued (rate-limited to 1/sec)
+- Identical errors within 60s → counted, then sent as "N duplicate messages"
+
+### Rolling Buffer - Context-on-Error
+
+Buffer recent logs and flush context when an error occurs. Useful for debugging transient issues.
+
```csharp
+.WriteToRollingBuffer(
+ capacity: 500, // Keep last 500 events
+ minimumLevel: LogLevel.Trace, // Buffer everything
+ triggerLevel: LogLevel.Error, // Trigger on errors
+ triggerTarget: new FileFlow("./error-context"), // Write context to file
+ preContextLines: 100) // Include 100 lines of context before the error
+```
+
+Real-world scenario:
+```
+# In-memory buffer contains: [Trace, Debug, Info, Warning, ...100+ events...]
+# Error occurs
+# Rolling buffer flushes: previous 100 logs + the error itself
+# Result: ./error-context/2026-03-27.log contains the full context
+```
+
+### Circuit Breaker
+
+Stop sending to a failing destination and automatically resume when it recovers.
+
+```csharp
+.WriteToCircuitBreaker(
+ inner: new ElasticSearchFlow("https://es-prod:9200"),
+ failureThreshold: 10, // Open after 10 failures
+ successThreshold: 3, // Close after 3 successes
+ timeout: TimeSpan.FromSeconds(30)) // Check recovery every 30s
+```
+
+### Combining Patterns
+
+```csharp
+await using var logger = new LogBuilder("Production")
+ .WriteToConsole()
+
+ // Local file as primary
+ .WriteToFile("./logs")
+
+ // Elasticsearch with resilience
+ .WriteToRetry(
+ primaryFlow: .WriteToFailover(
+ primaryFlow: new ElasticSearchFlow("https://es-prod:9200"),
+ secondaryFlow: new FileFlow("./fallback")),
+ maxRetries: 3)
+
+ // Rate-limited Slack for errors only
+ .WriteToThrottled(
+ inner: new SlackFlow(webhookUrl),
+ burstCapacity: 5,
+ refillPerSecond: 0.5,
+ minimumLevel: LogLevel.Error)
+
+ // Rolling buffer for debugging
+ .WriteToRollingBuffer(
+ capacity: 1000,
+ triggerLevel: LogLevel.Error,
+ triggerTarget: new FileFlow("./error-context"))
+
+ .BoostWithMachineName()
+ .BoostWithCorrelationId()
+ .Build();
+```
+
+## Encrypted File Logging
+
+Encrypt sensitive logs with AES encryption and password protection.
+
+### Writing Encrypted Logs
+
+```csharp
+.WriteToEncryptedFile(
+ directory: "./secure-logs",
+ filePrefix: "encrypted",
+ password: "YourStrongPassword123!",
+ maxFileSize: 50 * 1024 * 1024)
+```
+
+### Decrypting Logs
+
+```csharp
+// Decrypt a specific file when needed
+LogBuilder.DecryptFile(
+ encryptedPath: "./secure-logs/encrypted.enc",
+ outputPath: "./secure-logs/decrypted.txt",
+ password: "YourStrongPassword123!");
+
+// Now you can read the decrypted logs
+string logs = File.ReadAllText("./secure-logs/decrypted.txt");
+```
+
+### Use Cases
+
+- Compliance requirements (HIPAA, PCI-DSS)
+- Storing sensitive user data or API keys in logs
+- Secure log storage on shared infrastructure
+- Audit trails with encryption
+
+## Audit Trail (Tamper-Evident)
+
+The audit flow produces a tamper-evident file where every entry is SHA-256 hash-chained. Deleting or modifying any past entry invalidates all subsequent hashes.
+
+### Writing Audit Logs
+
+```csharp
+.WriteToAudit(
+ directory: "./audit",
+ filePrefix: "audit",
+ auditLevel: AuditLevel.WarningAndAbove, // Warning, Error, Critical
+ includeProperties: true)
+```
+
+### Verification
+
+```csharp
+// Verify file integrity at any time
+bool intact = AuditFlow.Verify("./audit/audit.audit");
+
+if (!intact)
+{
+ Console.WriteLine("ERROR: Audit trail has been tampered with!");
+ // Take appropriate action (alert, disable service, etc.)
+}
+```
+
+### Audit Levels
+
+```csharp
+AuditLevel.All // Every log event
+AuditLevel.WarningAndAbove // Warning, Error, Critical
+AuditLevel.ErrorAndAbove // Error, Critical
+AuditLevel.CriticalOnly // Critical only
+```
+
+### Example Audit File
+
+```
+2026-03-27T09:15:00.123Z|WARNING|Low disk space|[hash: sha256(prev_hash + data)]
+2026-03-27T09:15:05.456Z|ERROR|Database connection failed|[hash: sha256(prev_hash + data)]
+2026-03-27T09:15:10.789Z|CRITICAL|Service shutting down|[hash: sha256(prev_hash + data)]
+```
+
+If someone modifies the second entry, the third entry's hash validation fails, indicating tampering.
+
+## Log Message Template
+
+Both `ConsoleFlow` and `FileFlow` accept a customizable template string for formatting output.
+
+### Default Template
+
+```
+[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}
+```
+
+### Available Tokens
+
+| Token | Description | Example |
+|-------|-------------|---------|
+| `{ts}` | Timestamp (yyyy-MM-dd HH:mm:ss.fff) | 2026-03-27 09:15:00.123 |
+| `{tz}` | Timezone (UTC or local name) | UTC or EST |
+| `{host}` | Machine name | srv-prod-01 |
+| `{category}` | Logger category | MyApp.Services |
+| `{thread}` | Managed thread ID | 5 |
+| `{pid}` | Process ID | 1234 |
+| `{logtype}` | Log level label | INFO, WARN, ERROR |
+| `{message}` | Log message text | User login successful |
+| `{props}` | Structured properties | UserId=42, IP=192.168.1.1 |
+| `{newline}` | Line break | (actual newline) |
+
+### Custom Templates
+
+```csharp
+// Minimal template
+.WriteToFile(
+ directory: "./logs",
+ template: "[{ts}] [{logtype}] {message}")
+// Output: [2026-03-27 09:15:00.123] [INFO] User login successful
+
+// Verbose template with all info
+.WriteToFile(
+ directory: "./logs",
+ template: "[{ts}] [{tz}] [{logtype}] [Thread={thread}] [PID={pid}] [Host={host}] {category}: {message}{props}")
+// Output: [2026-03-27 09:15:00.123] [UTC] [INFO] [Thread=5] [PID=1234] [Host=srv-01] MyApp: User login successful UserId=42, IP=192.168.1.1
+
+// JSON-like format
+.WriteToFile(
+ directory: "./logs",
+ template: "timestamp={ts}|level={logtype}|category={category}|pid={pid}|message={message}{props}")
+```
+
+### Advanced Message Template Features
+
+EonaCat.LogStack supports advanced templating beyond basic property placeholders. These features work with message templates used in logging calls:
+
+```csharp
+logger.Information("User {User} logged in from {IP}", user, ipAddress);
+```
+
+#### Nested Property Access
+
+Access properties of objects using dot notation:
+
+```csharp
+var user = new { Name = "John", Address = new { City = "NYC" } };
+logger.Information("User {User.Name} lives in {User.Address.City}", user, user.Address);
+// Output: User John lives in NYC
+```
+
+#### Array/Collection Indexing
+
+Access specific items in arrays or lists:
+
+```csharp
+var items = new[] { "apple", "banana", "cherry" };
+logger.Information("First item: {Items[0]}, Second: {Items[1]}", items);
+// Output: First item: apple, Second: banana
+```
+
+#### Alignment and Padding
+
+Pad values to a specific width for aligned output:
+
+```csharp
+logger.Information("{Name,20} {Email,-30}", "John", "john@example.com");
+// Output: " John john@example.com "
+// (right-aligned 20 chars) (left-aligned 30 chars)
+```
+
+#### String Filters
+
+Apply transformations to property values:
+
+```csharp
+// Uppercase
+logger.Information("Status: {Status|uppercase}", "pending");
+// Output: Status: PENDING
+
+// Lowercase
+logger.Information("Event: {Event|lowercase}", "UserCreated");
+// Output: Event: usercreated
+
+// Trim whitespace
+logger.Information("Value: '{Value|trim}'", " spaces ");
+// Output: Value: 'spaces'
+
+// Truncate with ellipsis
+logger.Information("Description: {Description|truncate:50}", veryLongText);
+// Output: Description: This is a very long description that …
+
+// Reverse string
+logger.Information("Reversed: {Text|reverse}", "hello");
+// Output: Reversed: olleh
+
+// Multiple filters (chained)
+logger.Information("Result: {Input|trim|uppercase}", " hello world ");
+// Output: Result: HELLO WORLD
+```
+
+#### Fallback Values
+
+Provide default values when properties are null or missing:
+
+```csharp
+// Fallback with ?? operator and quotes
+logger.Information("User: {User??'Anonymous'}", user);
+// Output: User: Anonymous (if user is null)
+
+logger.Information("Email: {Email??'no-email@example.com'}", email);
+// Output: Email: no-email@example.com (if email is null)
+```
+
+#### Conditional Rendering
+
+Display different text based on boolean properties:
+
+```csharp
+logger.Information("Status: {?IsActive:Active|Inactive}", isActive);
+// Output: Status: Active (if isActive is true)
+// Output: Status: Inactive (if isActive is false)
+
+logger.Information("Result: {?Success:✓ Success|✗ Failed}", success);
+// Output: Result: ✓ Success (if success is true)
+```
+
+#### Advanced Conditionals
+
+Use comparison operators in conditional templates:
+
+```csharp
+// Advanced conditional token with if syntax
+// Syntax: {@if:condition:trueOutput|falseOutput}
+
+logger.Information("User role: {@if:RoleId>5:Admin|User}", user);
+// Output: User role: Admin (if RoleId > 5)
+// Output: User role: User (if RoleId <= 5)
+
+logger.Information("Account: {@if:Status==Premium:Premium Member|Standard}", account);
+// Output: Account: Premium Member (if Status equals 'Premium')
+
+// Comparison operators: ==, !=, <, >, <=, >=
+logger.Information("{@if:Count>=100:Large|Small}", data);
+logger.Information("{@if:Price<50:Budget|Premium}", item);
+logger.Information("{@if:IsDeleted!=false:Deleted|Active}", record);
+```
+
+#### Loops and Collections
+
+Iterate over arrays and collections in templates:
+
+```csharp
+// Syntax: {@loop:CollectionName:itemTemplate:separator}
+
+var items = new[] { "apple", "banana", "cherry" };
+logger.Information("Items: {@loop:Items:{Item}|, }", items);
+// Output: Items: apple, banana, cherry
+
+// Custom separator
+var tags = new[] { "urgent", "high-priority", "production" };
+logger.Information("Tags: {@loop:Tags:{Item}| | }", tags);
+// Output: Tags: urgent | high-priority | production
+
+// Complex items
+var users = new[]
+{
+ new { Id = 1, Name = "John" },
+ new { Id = 2, Name = "Jane" }
+};
+logger.Information("Users: {@loop:Users:({Id}:{Name})|, }", users);
+// Output: Users: (1:John), (2:Jane)
+```
+
+#### Math Filters
+
+Perform arithmetic operations on numeric properties:
+
+```csharp
+logger.Information("Total: ${Amount|add:10}", 50);
+// Output: Total: $60
+
+logger.Information("Discount: ${Price|multiply:0.9}", 100);
+// Output: Discount: $90
+
+logger.Information("Half: {Value|divide:2}", 100);
+// Output: Half: 50
+
+logger.Information("Remainder: {Number|modulo:3}", 10);
+// Output: Remainder: 1
+
+logger.Information("Absolute: {Change|abs}", -15);
+// Output: Absolute: 15
+
+logger.Information("Rounded: {Value|round:2}", 19.9999);
+// Output: Rounded: 20
+
+logger.Information("Max: {Value|max:100}", 150);
+// Output: Max: 100
+
+logger.Information("Floor: {Decimal|floor}", 19.9);
+// Output: Floor: 19
+
+logger.Information("Ceil: {Decimal|ceil}", 19.1);
+// Output: Ceil: 20
+```
+
+#### String Manipulation Filters
+
+Transform string values with various filters:
+
+```csharp
+// Case conversion
+logger.Information("Lower: {Text|lowercase}", "HELLO");
+// Output: Lower: hello
+
+logger.Information("Upper: {Text|uppercase}", "world");
+// Output: Upper: WORLD
+
+// Padding
+logger.Information("Padded: |{Name|pad:15}|", "John");
+// Output: Padded: |John |
+
+// Repetition
+logger.Information("Repeated: {Char|repeat:5}", "x");
+// Output: Repeated: xxxxx
+
+// String replacement
+logger.Information("Fixed: {Path|replace:old:new}", "/old/path/old");
+// Output: Fixed: /new/path/new
+
+// Substring operations
+logger.Information("Skip first 3: {Text|substring:3}", "12345");
+// Output: Skip first 3: 45
+
+// Trim variations
+logger.Information("Trimmed: '{Text|trim}'", " spaces ");
+// Output: Trimmed: 'spaces'
+
+logger.Information("Trim start: '{Text|trimstart}'", " spaces ");
+// Output: Trim start: 'spaces '
+
+logger.Information("Trim end: '{Text|trimend}'", " spaces ");
+// Output: Trim end: ' spaces'
+
+// String testing
+logger.Information("Starts with 'user': {Email|startswith:user}", "user@example.com");
+// Output: Starts with 'user': true
+
+logger.Information("Ends with '.org': {Url|endswith:.org}", "website.org");
+// Output: Ends with '.org': true
+
+logger.Information("Contains 'app': {Path|contains:app}", "/app/data");
+// Output: Contains 'app': true
+
+// String reversal
+logger.Information("Reversed: {Text|reverse}", "hello");
+// Output: Reversed: olleh
+
+// Split and join
+logger.Information("Split CSV: {Data|split:,}", "a,b,c");
+// Output: Split CSV: a, b, c
+```
+
+#### Comparison Filters
+
+Compare values and return boolean results:
+
+```csharp
+logger.Information("Equals: {Status|equals:Active}", status);
+// Output: Equals: true (if status == 'Active')
+
+logger.Information("Less than 100: {Value|lessthan:100}", 50);
+// Output: Less than 100: true
+
+logger.Information("Greater than 50: {Value|greaterthan:50}", 100);
+// Output: Greater than 50: true
+
+logger.Information("Greater or equal: {Count|gte:10}", 15);
+// Output: Greater or equal: true
+
+logger.Information("Less or equal: {Count|lte:20}", 15);
+// Output: Less or equal: true
+
+logger.Information("Not equal: {Type|ne:User}", "Admin");
+// Output: Not equal: true
+```
+
+#### Date & Time Filters
+
+Format and manipulate date/time values:
+
+```csharp
+// Date formatting
+logger.Information("Date: {CreatedAt|date:yyyy-MM-dd}", DateTime.Now);
+// Output: Date: 2026-03-27
+
+logger.Information("Full timestamp: {CreatedAt|date:O}", DateTime.Now);
+// Output: Full timestamp: 2026-03-27T09:15:00.1234567Z
+
+logger.Information("Custom format: {CreatedAt|date:dd/MM/yyyy HH:mm:ss}", DateTime.Now);
+// Output: Custom format: 27/03/2026 09:15:00
+
+// TimeSpan operations
+var duration = TimeSpan.FromSeconds(3661);
+logger.Information("Total seconds: {Duration|timespan:totalseconds}", duration);
+// Output: Total seconds: 3661
+
+logger.Information("Total minutes: {Duration|timespan:totalminutes}", duration);
+// Output: Total minutes: 61.0166...
+
+logger.Information("Total hours: {Duration|timespan:totalhours}", duration);
+// Output: Total hours: 1.01388...
+
+logger.Information("Days: {Duration|timespan:days}", duration);
+// Output: Days: 0
+
+logger.Information("Hours: {Duration|timespan:hours}", duration);
+// Output: Hours: 1
+
+logger.Information("Minutes: {Duration|timespan:minutes}", duration);
+// Output: Minutes: 1
+
+logger.Information("Seconds: {Duration|timespan:seconds}", duration);
+// Output: Seconds: 1
+```
+
+#### Chaining Multiple Filters
+
+Combine filters for complex transformations:
+
+```csharp
+// Trim, then uppercase, then truncate
+logger.Information("Processed: {Input|trim|uppercase|truncate:10}", " hello world ");
+// Output: Processed: HELLO WOR…
+
+// Substring, then lowercase
+logger.Information("Modified: {Path|substring:5|lowercase}", "/DATA/MyFile.TXT");
+// Output: Modified: myfile.txt
+
+// Apply multiple math operations
+logger.Information("Calculated: {Value|multiply:2|add:10|divide:3}", 5);
+// Output: Calculated: 6.66... (5 * 2 = 10, 10 + 10 = 20, 20 / 3 = 6.66)
+```
+
+#### Complete Advanced Template Examples
+
+```csharp
+// API request logging with advanced features
+var request = new
+{
+ Method = "POST",
+ Path = "/api/users",
+ UserId = 42,
+ ResponseTime = 150,
+ Success = true,
+ Tags = new[] { "api", "users", "production" }
+};
+
+logger.Information(
+ "[{Timestamp|date:HH:mm:ss}] {Method|uppercase} {Path} - User {UserId} - {ResponseTime|pad:5}ms - {?Success:✓|✗} - Tags: {@loop:Tags:{Item}|, }",
+ DateTime.Now, request.Method, request.Path, request.UserId,
+ request.ResponseTime, request.Success, request.Tags
+);
+// Output: [09:15:00] POST /api/users - User 42 - 150ms - ✓ - Tags: api, users, production
+
+// Conditional status with comparison
+var operation = new
+{
+ Name = "DataSync",
+ Status = "Completed",
+ Duration = 45000,
+ RecordsProcessed = 1500,
+ ErrorCount = 0
+};
+
+logger.Information(
+ "{Name}: {@if:ErrorCount==0:✓ Success|⚠ With Errors} | Duration: {Duration|timespan:totalseconds}s | Records: {RecordsProcessed|add:0} processed",
+ operation.Name, operation.ErrorCount, operation.Duration, operation.RecordsProcessed
+);
+// Output: DataSync: ✓ Success | Duration: 45s | Records: 1500 processed
+
+// Complex nested template
+var batch = new
+{
+ Id = "batch-001",
+ Items = new[] { "item1", "item2", "item3" },
+ Size = 3,
+ Price = 99.99m,
+ Discount = 0.15m
+};
+
+logger.Information(
+ "Batch {Id}: {?Size>5:Large|Small} batch | Items: {@loop:Items:{Item}|, } | Price: ${Price|multiply:Discount|add:0|round:2}",
+ batch.Id, batch.Size, batch.Items, batch.Price, batch.Discount
+);
+// Output: Batch batch-001: Small batch | Items: item1, item2, item3 | Price: $15.00
+```
+
+#### Template Features Summary
+
+The templating engine supports:
+
+| Feature | Syntax | Example |
+|---------|--------|---------|
+| **Basic Property** | `{PropertyName}` | `{UserId}` |
+| **Nested Properties** | `{Object.Property.Sub}` | `{User.Address.City}` |
+| **Array Indexing** | `{Array[Index]}` | `{Items[0]}` |
+| **Alignment** | `{Value,Width}` | `{Name,20}` |
+| **Format Specifiers** | `{Value:Format}` | `{Date:yyyy-MM-dd}` |
+| **String Filters** | `{Value\|Filter}` | `{Text\|uppercase}` |
+| **Math Filters** | `{Value\|add:10}` | `{Price\|multiply:0.9}` |
+| **Comparison** | `{Value\|equals:text}` | `{Status\|equals:Active}` |
+| **Simple Conditionals** | `{?Property:True\|False}` | `{?IsActive:Active\|Inactive}` |
+| **Advanced Conditionals** | `{@if:Condition:T\|F}` | `{@if:Count>10:High\|Low}` |
+| **Loops** | `{@loop:Collection:Template}` | `{@loop:Items:{Item}\|, }` |
+| **Fallback Values** | `{Value??'Default'}` | `{Name??'Unknown'}` |
+| **Chained Filters** | `{Value\|filter1\|filter2}` | `{Text\|trim\|uppercase}` |
+| **Destructuring** | `{@Object}` | `{@User}` |
+
+#### Format Specifiers
+
+Apply .NET format strings to values:
+
+```csharp
+// Date formatting
+logger.Information("Date: {CreatedAt:yyyy-MM-dd}", DateTime.Now);
+// Output: Date: 2026-03-27
+
+// Decimal formatting
+logger.Information("Price: {Price:C}", 19.99m);
+// Output: Price: $19.99
+
+// Numeric formatting
+logger.Information("Count: {Count:D5}", 42);
+// Output: Count: 00042
+```
+
+#### Destructuring
+
+Deep-inspect complex objects to reveal their structure:
+
+```csharp
+var user = new { Id = 1, Name = "John", Email = "john@example.com" };
+
+// Default destructuring with @ prefix
+logger.Information("User: {@User}", user);
+// Output: User: {Id: 1, Name: John, Email: john@example.com}
+
+// Force string conversion with $ prefix
+logger.Information("User: {$User}", user);
+// Output: User: YourNamespace.User
+
+// Nested destructuring
+var order = new
+{
+ Id = 1,
+ Customer = new { Name = "John", City = "NYC" },
+ Items = new[] { "Item1", "Item2" }
+};
+
+logger.Information("Order: {@Order}", order);
+// Output: Order: {Id: 1, Customer: {Name: John, City: NYC}, Items: [...]}
+```
+
+#### Complete Advanced Template Examples
+
+```csharp
+// API request logging
+var request = new
+{
+ Method = "POST",
+ Path = "/api/users",
+ UserId = 42,
+ ResponseTime = 150,
+ Success = true
+};
+
+logger.Information(
+ "[{Time|uppercase}] {Method} {Path} - User {UserId} - {ResponseTime,5}ms - {?Success:✓|✗}",
+ DateTime.Now.ToString("HH:mm:ss"), request.Method, request.Path,
+ request.UserId, request.ResponseTime, request.Success
+);
+// Output: [09:15:00] POST /api/users - User 42 - 150ms - ✓
+
+// Database operation with fallback
+logger.Information(
+ "Database query {QueryName??'Unknown'} by user {UserId??'System'} took {Duration|truncate:10}ms",
+ storedProcName, currentUserId, duration
+);
+// Output: Database query sp_GetUsers by user System took 125ms
+
+// File processing with conditions
+logger.Information(
+ "File {FileName} processed: {?HasErrors:⚠ ERRORS|✓ OK} - {LineCount,6} lines",
+ file.Name, file.HasErrors, file.LineCount
+);
+// Output: File log.txt processed: ✓ OK - 1024 lines
+
+// Nested property access
+var company = new
+{
+ Name = "Acme Corp",
+ HeadOffice = new { City = "New York", Country = "USA" },
+ Employees = new[] { "John", "Jane", "Jack" }
+};
+
+logger.Information(
+ "Company {Company.Name} from {Company.HeadOffice.City}, {Company.HeadOffice.Country} - First employee: {Company.Employees[0]}",
+ company
+);
+// Output: Company Acme Corp from New York, USA - First employee: John
+```
+
+## Real-World Configuration Scenarios
+
+### Scenario 1: Development Environment
+
+Console output with verbose logging, local files, and no remote sends.
+
+```csharp
+await using var logger = new LogBuilder("MyApp")
+ .WithMinimumLevel(LogLevel.Debug)
+ .WithTimestampMode(TimestampMode.Local)
+ .WriteToConsole(useColors: true)
+ .WriteToFile("./logs", filePrefix: "dev")
+
+ .BoostWithThreadId()
+ .BoostWithCorrelationId()
+ .Build();
+```
+
+### Scenario 2: Production - Multi-Destination with Resilience
+
+Files locally, Elasticsearch for search, Slack for alerts, encrypted audit trail.
+
+```csharp
+await using var logger = new LogBuilder("ProductionApp")
+ .WithMinimumLevel(LogLevel.Information)
+ .WithTimestampMode(TimestampMode.Utc)
+
+ // Local backup
+ .WriteToFile(
+ directory: "./logs",
+ maxFileSize: 100 * 1024 * 1024,
+ compression: CompressionFormat.GZip)
+
+ // Primary analytics with fallback
+ .WriteToFailover(
+ primaryFlow: new ElasticSearchFlow("https://elastic.company.com"),
+ secondaryFlow: new FileFlow("./fallback-elastic"))
+
+ // Rate-limited alerts
+ .WriteToThrottled(
+ inner: new SlackFlow(slackWebhookUrl),
+ burstCapacity: 5,
+ refillPerSecond: 1.0,
+ minimumLevel: LogLevel.Error)
+
+ // Compliance audit
+ .WriteToAudit(
+ directory: "./audit",
+ auditLevel: AuditLevel.WarningAndAbove)
+
+ // Encrypted sensitive logs
+ .WriteToEncryptedFile(
+ directory: "./secure-logs",
+ password: Environment.GetEnvironmentVariable("LOG_ENCRYPTION_KEY"))
+
+ // Diagnostics snapshot
+ .WriteDiagnostics(
+ snapshotInterval: TimeSpan.FromMinutes(5))
+
+ // Boosters
+ .BoostWithMachineName()
+ .BoostWithProcessId()
+ .BoostWithApplication("ProductionApp", "1.0.0")
+ .BoostWithEnvironment("Production")
+ .BoostWithCorrelationId()
+ .Build();
+```
+
+### Scenario 3: Microservices / Distributed Tracing
+
+Elasticsearch for centralized logs, Redis for real-time events, correlation IDs.
+
+```csharp
+await using var logger = new LogBuilder("OrderService")
+ .WithMinimumLevel(LogLevel.Information)
+
+ // Centralized log storage
+ .WriteToElasticSearch(
+ elasticSearchUrl: "https://elastic-cluster.company.com",
+ indexName: "orderservice-logs")
+
+ // Real-time event stream
+ .RedisFlow(
+ host: "redis.company.com",
+ channel: "orderservice:logs",
+ listKey: "orderservice:logs:history",
+ maxListLength: 10000)
+
+ // Local file backup
+ .WriteToFile("./logs")
+
+ // Correlation tracking for distributed tracing
+ .BoostWithCorrelationId() // Automatically includes Activity.Current?.Id
+ .BoostWithApplication("OrderService", ServiceVersion)
+ .BoostWithProcessId()
+ .BoostWithMachineName()
+
+ .Build();
+```
+
+### Scenario 4: Real-Time Dashboard
+
+SignalR flow for live log streaming to web dashboard.
+
+```csharp
+// Backend: Log streaming to SignalR hub
+await using var logger = new LogBuilder("DashboardApp")
+ .WriteToFile("./logs")
+ .WriteToSignalR(
+ hubUrl: "https://dashboard.company.com/loghub",
+ hubMethod: "ReceiveLog",
+ batchSize: 20,
+ batchIntervalMs: 500,
+ minimumLevel: LogLevel.Warning) // Only send warnings+ to dashboard
+
+ .Build();
+
+// Frontend: Receive logs in real-time (JavaScript example)
+const connection = new signalR.HubConnectionBuilder()
+ .withUrl("https://dashboard.company.com/loghub")
+ .withAutomaticReconnect()
+ .build();
+
+connection.on("ReceiveLog", (log) => {
+ console.log(`[${log.level}] ${log.message}`);
+ addToLiveLogUI(log);
+});
+
+await connection.start();
+```
+
+### Scenario 5: Compliance & Audit
+
+Separate audit trail, encrypted logs, tamper detection.
+
+```csharp
+await using var logger = new LogBuilder("ComplianceApp")
+
+ // Tamper-evident audit trail
+ .WriteToAudit(
+ directory: "./compliance/audit",
+ auditLevel: AuditLevel.WarningAndAbove,
+ includeProperties: true)
+
+ // Encrypted sensitive data
+ .WriteToEncryptedFile(
+ directory: "./compliance/encrypted",
+ password: GetAuditPassword(),
+ maxFileSize: 50 * 1024 * 1024)
+
+ // Database for detailed analysis
+ .WriteToDatabase(
+ connectionFactory: () => new SqlConnection(connectionString),
+ tableName: "AuditLogs",
+ batchSize: 10)
+
+ // Regular file backup
+ .WriteToFile("./logs")
+
+ // Boost with full context
+ .BoostWithUser()
+ .BoostWithMachineName()
+ .BoostWithProcessId()
+ .BoostWithApplication("ComplianceApp", "1.0.0")
+
+ .Build();
+
+// Periodic verification
+_ = Task.Run(async () =>
+{
+ while (true)
+ {
+ await Task.Delay(TimeSpan.FromHours(1));
+ bool isIntact = AuditFlow.Verify("./compliance/audit/audit.audit");
+ if (!isIntact)
+ {
+ // Alert security team
+ logger.Critical("SECURITY ALERT: Audit trail tampering detected!");
+ }
+ }
+});
+```
+
+### Scenario 6: High-Volume with Batching & Rate Limiting
+
+For services handling high log volume, use batching and throttling.
+
+```csharp
+await using var logger = new LogBuilder("HighVolumeApp")
+ .WriteToThrottled(
+ inner: new HttpFlow("https://logs.company.com/ingest"),
+ burstCapacity: 100,
+ refillPerSecond: 50.0, // 50 logs/sec steady state
+ deduplicate: true,
+ dedupWindow: TimeSpan.FromSeconds(30))
+
+ .WriteToFile(
+ directory: "./logs",
+ batchSize: 100, // Batch 100 logs before writing
+ flushIntervalInMilliSeconds: 1000) // Flush every 1 second
+
+ .RedisFlow(
+ host: "redis.company.com",
+ channel: "app:logs",
+ listKey: "app:logs:history")
+
+ .Build();
+```
+
+## Diagnostics
+
+### Real-Time Metrics
+
+```csharp
+// Get current statistics
+var stats = logger.GetDiagnostics();
+
+Console.WriteLine($"Total Logged: {stats.TotalLogged}");
+Console.WriteLine($"Total Dropped: {stats.TotalDropped}");
+Console.WriteLine($"Drop Rate: {(double)stats.TotalDropped / stats.TotalLogged:P2}");
+
+// Per-flow statistics
+foreach (var flowStat in stats.FlowStats)
+{
+ Console.WriteLine($"Flow: {flowStat.Name}");
+ Console.WriteLine($" Processed: {flowStat.Processed}");
+ Console.WriteLine($" Dropped: {flowStat.Dropped}");
+ Console.WriteLine($" Errors: {flowStat.ErrorCount}");
+}
+```
+
+### Diagnostic Snapshots
+
+Enable periodic diagnostic snapshots to track performance over time.
+
+```csharp
+.WriteDiagnostics(
+ snapshotInterval: TimeSpan.FromMinutes(5),
+ injectIntoEvents: true, // Include metrics in log events
+ writeSnapshotEvents: true, // Write snapshot to logs
+ snapshotCategory: "Diagnostics",
+ forwardTo: new FileFlow("./diagnostics"), // Also forward to file
+ minimumLevel: LogLevel.Information,
+ customMetrics: () => new Dictionary
+ {
+ ["ActiveRequests"] = GetActiveRequestCount(),
+ ["CacheHitRate"] = GetCacheHitRate(),
+ ["DatabasePoolSize"] = GetDbPoolSize()
+ })
+```
+
+### Monitoring Health
+
+```csharp
+// Monitor in a background task
+_ = Task.Run(async () =>
+{
+ while (true)
+ {
+ await Task.Delay(TimeSpan.FromMinutes(1));
+
+ var stats = logger.GetDiagnostics();
+
+ // Alert if drop rate is high
+ if (stats.TotalLogged > 0)
+ {
+ double dropRate = (double)stats.TotalDropped / stats.TotalLogged;
+ if (dropRate > 0.01) // > 1% drop rate
+ {
+ logger.Warning("High log drop rate detected", ("DropRate", dropRate));
+ }
+ }
+
+ // Alert on errors
+ var hasErrors = stats.FlowStats.Any(f => f.ErrorCount > 0);
+ if (hasErrors)
+ {
+ logger.Warning("Some flows are experiencing errors");
+ }
+ }
+});
+```
+
+## Flushing and Disposal
+
+All flows support batching, so events may not be written immediately. Use `FlushAsync()` to ensure all pending events are written before shutdown.
+
+### Explicit Flushing
+
+```csharp
+// Flush all pending events synchronously (up to 5 seconds)
+await logger.FlushAsync();
+
+// After flush, you can safely dispose
+await logger.DisposeAsync();
+```
+
+### Automatic Flushing on Disposal
+
+Using `await using` automatically flushes and disposes:
+
+```csharp
+await using var logger = new LogBuilder("MyApp")
+ .WriteToFile("./logs")
+ .Build();
+
+logger.Information("Application running");
+
+// At scope exit: FlushAsync() called automatically, then DisposeAsync()
+```
+
+### Best Practices
+
+```csharp
+try
+{
+ await using var logger = new LogBuilder("MyApp").Build();
+
+ // Log events here
+ logger.Information("App started");
+
+ // Your application logic
+ await RunApplication();
+}
+catch (Exception ex)
+{
+ // Ensure errors are logged even if something goes wrong
+ logger?.Critical(ex, "Unexpected error during shutdown");
+}
+finally
+{
+ // FlushAsync() is called automatically when leaving the using block
+}
+```
+
+### Custom Disposal Logic
+
+```csharp
+var logger = new LogBuilder("MyApp").Build();
+
+try
+{
+ logger.Information("Processing...");
+}
+finally
+{
+ // Manual control
+ await logger.FlushAsync();
+
+ // Perform custom cleanup
+ CleanupResources();
+
+ await logger.DisposeAsync();
+}
+```
+
+## Events
+
+Subscribe to log events for real-time processing or custom handling.
+
+### Basic Event Handler
+
+```csharp
+logger.OnLog += (sender, message) =>
+{
+ // Fired for every log event that passes filters
+ Console.WriteLine($"[Event] {message.Level}: {message.Message}");
+};
+
+logger.Information("This event will be raised");
+```
+
+### Advanced Event Handling
+
+```csharp
+logger.OnLog += (sender, message) =>
+{
+ // Log level filtering
+ if (message.Level >= LogLevel.Error)
+ {
+ // Send critical errors to external alert system
+ SendAlert($"{message.Level}: {message.Message}");
+ }
+
+ // Category filtering
+ if (message.Category?.Contains("Payment") ?? false)
+ {
+ // Audit sensitive operations
+ LogToAuditSystem(message);
+ }
+
+ // Property-based filtering
+ if (message.Properties != null && message.Properties.ContainsKey("UserId"))
+ {
+ TrackUserActivity(message.Properties["UserId"], message);
+ }
+};
+```
+
+### Multiple Event Handlers
+
+```csharp
+// Handler 1: Alert on errors
+logger.OnLog += (sender, msg) =>
+{
+ if (msg.Level == LogLevel.Error)
+ SendSlackAlert(msg);
+};
+
+// Handler 2: Track metrics
+logger.OnLog += (sender, msg) =>
+{
+ Metrics.Increment($"logs.{msg.Level.ToString().ToLower()}");
+};
+
+// Handler 3: Update dashboard
+logger.OnLog += (sender, msg) =>
+{
+ Dashboard.AddLog(msg);
+};
+```
+
+## Custom Flows
+
+Create custom flows by implementing `IFlow` or extending `FlowBase` for complex scenarios.
+
+### Simple Custom Flow
+
+```csharp
+public class MyCustomFlow : FlowBase
+{
+ public MyCustomFlow() : base("MyCustomFlow", LogLevel.Trace) { }
+
+ public override Task BlastAsync(LogEvent logEvent, CancellationToken ct = default)
+ {
+ try
+ {
+ // Your custom logic here
+ string formatted = $"[{logEvent.Timestamp:O}] {logEvent.Level}: {logEvent.Message}";
+ MyBackendService.SendLog(formatted);
+
+ return Task.FromResult(WriteResult.Success);
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult(WriteResult.Failure(ex));
+ }
+ }
+
+ public override Task FlushAsync(CancellationToken ct = default)
+ {
+ // Optional: implement batching flush logic
+ return Task.CompletedTask;
+ }
+}
+
+// Register with:
+new LogBuilder("App")
+ .WriteTo(new MyCustomFlow())
+ .Build();
+```
+
+### Advanced Custom Flow with Batching
+
+```csharp
+public class BatchedCustomFlow : FlowBase
+{
+ private readonly List _batch = new(100);
+ private readonly object _lock = new();
+
+ public BatchedCustomFlow() : base("BatchedCustom", LogLevel.Trace) { }
+
+ public override Task BlastAsync(LogEvent logEvent, CancellationToken ct = default)
+ {
+ lock (_lock)
+ {
+ _batch.Add(logEvent);
+
+ if (_batch.Count >= 100)
+ {
+ return FlushBatchAsync(ct);
+ }
+ }
+
+ return Task.FromResult(WriteResult.Success);
+ }
+
+ private Task FlushBatchAsync(CancellationToken ct)
+ {
+ try
+ {
+ var toSend = _batch.ToList();
+ _batch.Clear();
+
+ // Send batch to external service
+ MyBackendService.SendLogBatch(toSend);
+ return Task.FromResult(WriteResult.Success);
+ }
+ catch (Exception ex)
+ {
+ return Task.FromResult(WriteResult.Failure(ex));
+ }
+ }
+
+ public override async Task FlushAsync(CancellationToken ct = default)
+ {
+ lock (_lock)
+ {
+ if (_batch.Count > 0)
+ {
+ await FlushBatchAsync(ct);
+ }
+ }
+ }
+
+ public override async ValueTask DisposeAsync()
+ {
+ await FlushAsync();
+ await base.DisposeAsync();
+ }
+}
+```
+
+### Custom Flow with Retry Logic
+
+```csharp
+public class RetryableCustomFlow : FlowBase
+{
+ private const int MaxRetries = 3;
+
+ public RetryableCustomFlow() : base("RetryableCustom", LogLevel.Trace) { }
+
+ public override async Task BlastAsync(LogEvent logEvent, CancellationToken ct = default)
+ {
+ int attempts = 0;
+ Exception lastEx = null;
+
+ while (attempts < MaxRetries)
+ {
+ try
+ {
+ await SendToServiceAsync(logEvent, ct);
+ return WriteResult.Success;
+ }
+ catch (Exception ex)
+ {
+ lastEx = ex;
+ attempts++;
+
+ if (attempts < MaxRetries)
+ await Task.Delay(TimeSpan.FromMilliseconds(Math.Pow(2, attempts) * 100), ct);
+ }
+ }
+
+ return WriteResult.Failure(lastEx);
+ }
+
+ private Task SendToServiceAsync(LogEvent logEvent, CancellationToken ct)
+ {
+ // Your implementation
+ return MyBackendService.SendLogAsync(logEvent, ct);
+ }
+
+ public override Task FlushAsync(CancellationToken ct = default) => Task.CompletedTask;
+}
+```
+
+## Best Practices & Tips
+
+### 1. **Use Appropriate Log Levels**
+
+```csharp
+logger.Trace("Very detailed diagnostic info (most verbose)");
+logger.Debug("Debug-level diagnostic information");
+logger.Information("General informational message (default level)");
+logger.Warning("Warning message (potential issue)");
+logger.Error("Error occurred, operation failed");
+logger.Critical("Critical failure, system may be unstable");
+```
+
+### 2. **Use Structured Properties for Queryability**
+
+```csharp
+// Good: Structured properties
+logger.Information("User logged in",
+ ("UserId", user.Id),
+ ("Email", user.Email),
+ ("IpAddress", request.RemoteIpAddress),
+ ("Timestamp", DateTime.UtcNow));
+
+// Avoid: String interpolation in message
+logger.Information($"User {user.Id} logged in from {request.RemoteIpAddress}");
+```
+
+### 3. **Use Categories for Filtering**
+
+```csharp
+// Create category-specific loggers
+ILogger serviceLogger = loggerFactory.CreateLogger("MyApp.Services.UserService");
+ILogger dataLogger = loggerFactory.CreateLogger("MyApp.Data.Repository");
+
+// Configure file splitting by category
+.WriteToFile("./logs", useCategoryRouting: true)
+// Results in: logs/MyApp.Services.UserService.log, logs/MyApp.Data.Repository.log
+```
+
+### 4. **Always Handle Exceptions**
+
+```csharp
+try
+{
+ // Risky operation
+ await database.ExecuteAsync(query);
+}
+catch (TimeoutException ex)
+{
+ logger.Warning(ex, "Database timeout, retrying");
+}
+catch (Exception ex)
+{
+ logger.Error(ex, "Database error, operation failed");
+ throw; // Re-throw to propagate to caller
+}
+```
+
+### 5. **Use Correlation IDs for Tracing**
+
+```csharp
+builder.Services.AddEonaCatLogging("MyApp", logBuilder =>
+{
+ logBuilder.BoostWithCorrelationId(); // Automatic tracing
+});
+
+// All logs in the same request/operation automatically get the same correlation ID
+```
+
+### 6. **Configure Different Levels per Flow**
+
+```csharp
+.WithMinimumLevel(LogLevel.Information) // Global minimum
+.WriteToConsole(minimumLevel: LogLevel.Debug) // More verbose
+.WriteToFile(minimumLevel: LogLevel.Warning) // Less verbose
+.WriteToSlack(minimumLevel: LogLevel.Error) // Errors only
+```
+
+### 7. **Use Async Flushing Before Shutdown**
+
+```csharp
+var host = builder.Build();
+
+// Graceful shutdown: flush logs before exit
+var lifetime = host.Services.GetRequiredService();
+lifetime.ApplicationStopping.Register(async () =>
+{
+ var logger = host.Services.GetRequiredService();
+ await logger.FlushAsync();
+});
+
+await host.RunAsync();
+```
+
+### 8. **Monitor Drop Rates**
+
+```csharp
+_ = Task.Run(async () =>
+{
+ while (true)
+ {
+ await Task.Delay(TimeSpan.FromMinutes(1));
+
+ var stats = logger.GetDiagnostics();
+ if (stats.TotalLogged + stats.TotalDropped > 0)
+ {
+ double dropRate = (double)stats.TotalDropped / (stats.TotalLogged + stats.TotalDropped);
+ if (dropRate > 0.01) // > 1%
+ logger.Warning("High drop rate detected", ("DropRate", dropRate));
+ }
+ }
+});
+```
+
+### 9. **Use Rolling Buffers for Post-Mortem**
+
+```csharp
+// Before: error happens, context is lost
+// After: rolling buffer captures previous 100 logs
.WriteToRollingBuffer(
capacity: 500,
triggerLevel: LogLevel.Error,
triggerTarget: new FileFlow("./error-context"))
```
----
-
-## Encrypted File Logging
+### 10. **Encrypt Sensitive Data**
```csharp
-.WriteToEncryptedFile("./secure-logs", password: "s3cr3t")
+.WriteToEncryptedFile(
+ directory: "./secure-logs",
+ password: GetEncryptionPasswordFromVault())
```
-To decrypt later:
-```csharp
-LogBuilder.DecryptFile(
- encryptedPath: "./secure-logs/log.enc",
- outputPath: "./secure-logs/log.txt",
- password: "s3cr3t");
-```
+## Troubleshooting
----
+### High CPU Usage
-## Audit Trail
+**Symptoms**: Excessive CPU while logging
-The audit flow produces a tamper-evident file where every entry is SHA-256 hash-chained. Deleting or modifying any past entry invalidates all subsequent hashes.
+**Solutions**:
+- Increase batch size to reduce I/O operations
+- Use `WriteToThrottled()` to rate limit
+- Check if remote endpoints are responding (network latency)
```csharp
-.WriteToAudit(
- directory: "./audit",
- auditLevel: AuditLevel.WarningAndAbove,
- includeProperties: true)
+.WriteToFile("./logs", batchSize: 100, flushIntervalInMilliSeconds: 5000)
+.WriteToThrottled(inner: new HttpFlow(url), burstCapacity: 50)
```
-Verify integrity at any time:
-```csharp
-bool intact = AuditFlow.Verify("./audit/audit.audit");
-```
+### High Memory Usage
----
+**Symptoms**: Memory grows linearly with time
-## Log Message Template
-
-Both `ConsoleFlow` and `FileFlow` accept a customisable template string:
-
-```
-[{ts}] [{tz}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}
-```
-
-| Token | Description |
-|-------|-------------|
-| `{ts}` | Timestamp (yyyy-MM-dd HH:mm:ss.fff) |
-| `{tz}` | Timezone (UTC or local name) |
-| `{host}` | Machine name |
-| `{category}` | Logger category |
-| `{thread}` | Managed thread ID |
-| `{pid}` | Process ID |
-| `{logtype}` | Log level label (INFO, WARN, ERROR, …) |
-| `{message}` | Log message text |
-| `{props}` | Structured properties as key=value pairs |
-| `{newline}` | Line break |
-
----
-
-## Diagnostics
+**Solutions**:
+- Check if logs are being flushed (batches aren't sent)
+- Reduce rolling buffer capacity
+- Enable compression for file logs
```csharp
-var diag = logger.GetDiagnostics();
-Console.WriteLine($"Logged: {diag.TotalLogged}, Dropped: {diag.TotalDropped}");
+.WriteToFile(compression: CompressionFormat.GZip)
+.WriteToRollingBuffer(capacity: 250) // Reduce from 500
```
----
+### Events Being Dropped
-## Flushing and Disposal
+**Symptoms**: `TotalDropped > 0` in diagnostics
+
+**Causes & Solutions**:
+- Backpressure queue full → increase capacity or reduce send rate
+- Flow errors → check flow configuration and connectivity
+- Log level filters → verify minimumLevel settings
```csharp
-// Flush all pending events
-await logger.FlushAsync();
-
-// Dispose (flushes automatically)
-await logger.DisposeAsync();
-```
-
----
-
-## Events
-
-```csharp
-logger.OnLog += (sender, msg) =>
+// Investigate drop reasons
+var stats = logger.GetDiagnostics();
+foreach (var flow in stats.FlowStats.Where(f => f.Dropped > 0))
{
- // Fired for every log event that passes filters
- Console.WriteLine($"[Event] {msg.Level}: {msg.Message}");
-};
-```
-
----
-
-## Custom Flows
-
-Implement `IFlow` (or extend `FlowBase`) to create your own destination:
-
-```csharp
-public class MyFlow : FlowBase
-{
- public MyFlow() : base("MyFlow", LogLevel.Trace) { }
-
- public override Task BlastAsync(LogEvent logEvent, CancellationToken ct = default)
- {
- // Write logEvent somewhere
- return Task.FromResult(WriteResult.Success);
- }
-
- public override Task FlushAsync(CancellationToken ct = default) => Task.CompletedTask;
+ logger.Warning($"Flow {flow.Name} dropped {flow.Dropped} events");
}
+```
-// Register with:
-new LogBuilder("App").WriteTo(new MyFlow()).Build();
+### Logs Not Reaching Remote Destination
+
+**Symptoms**: Local logs exist, but remote endpoint has nothing
+
+**Troubleshooting Steps**:
+1. Verify endpoint connectivity: `telnet host port`
+2. Check authentication (API keys, certificates)
+3. Enable retry and failover patterns
+4. Ensure events pass level filters
+
+```csharp
+.WriteToRetry(
+ primaryFlow: new HttpFlow(endpoint),
+ maxRetries: 5)
+
+// Or use failover:
+.WriteToFailover(
+ primaryFlow: new HttpFlow(endpoint),
+ secondaryFlow: new FileFlow("./fallback"))
+```
+
+### File Size Growing Too Large
+
+**Symptoms**: Log files grow without bound
+
+**Solutions**:
+- Configure `maxFileSize` and `maxDirectorySize`
+- Enable compression
+- Set retention policy
+
+```csharp
+.WriteToFile(
+ directory: "./logs",
+ maxFileSize: 100 * 1024 * 1024, // 100 MB
+ maxDirectorySize: 10L * 1024 * 1024 * 1024, // 10 GB
+ compression: CompressionFormat.GZip,
+ fileRetentionPolicy: new FileRetentionPolicy { RetentionDays = 30 })
```
@@ -795,12 +2546,245 @@ public class DiagnosticsService
public void PrintDiagnostics()
{
var diagnostics = _loggerFactory.GetDiagnostics();
- Console.WriteLine($"Total Logged: {diagnostics.TotalLoggedCount}");
- Console.WriteLine($"Total Dropped: {diagnostics.TotalDroppedCount}");
+ Console.WriteLine($"Total Logged: {diagnostics.TotalLogged}");
+ Console.WriteLine($"Total Dropped: {diagnostics.TotalDropped}");
+ Console.WriteLine($"Total Exceptions: {diagnostics.TotalExceptions}");
}
}
```
+## Statistics & Metrics
+
+EonaCat.LogStack provides comprehensive statistics and metrics tracking for monitoring and optimizing your logging infrastructure.
+
+### Per-Level Metrics
+
+Track events logged at each level:
+
+```csharp
+var metrics = loggerFactory.GetMetrics();
+
+Console.WriteLine($"Trace: {metrics.TraceCount}");
+Console.WriteLine($"Debug: {metrics.DebugCount}");
+Console.WriteLine($"Information: {metrics.InformationCount}");
+Console.WriteLine($"Warning: {metrics.WarningCount}");
+Console.WriteLine($"Error: {metrics.ErrorCount}");
+Console.WriteLine($"Critical: {metrics.CriticalCount}");
+```
+
+### Performance Metrics
+
+Monitor throughput and latency:
+
+```csharp
+var metrics = loggerFactory.GetMetrics();
+
+Console.WriteLine($"Events/Second: {metrics.WritesPerSecond:F2}");
+Console.WriteLine($"Total Bytes: {metrics.TotalBytes:N0}");
+Console.WriteLine($"Avg Bytes/Event: {metrics.AverageBytesPerEvent:F2}");
+Console.WriteLine($"Success Rate: {metrics.SuccessRate:F2}%");
+Console.WriteLine($"Uptime: {TimeSpan.FromMilliseconds(metrics.UptimeMilliseconds):hh\\:mm\\:ss}");
+```
+
+### Exception Tracking
+
+Monitor exceptions logged:
+
+```csharp
+var metrics = loggerFactory.GetMetrics();
+Console.WriteLine($"Total Exceptions: {metrics.TotalExceptions}");
+Console.WriteLine($"Exceptions in Errors: {metrics.ErrorCount} errors logged");
+```
+
+### Flow-Specific Statistics
+
+Each flow tracks its own performance:
+
+```csharp
+var metrics = loggerFactory.GetMetrics();
+
+foreach (var flow in metrics.FlowMetrics)
+{
+ Console.WriteLine($"{flow.FlowName} ({flow.FlowType}):");
+ Console.WriteLine($" Processed: {flow.EventsProcessed}");
+ Console.WriteLine($" Failed: {flow.EventsFailed}");
+ Console.WriteLine($" Success Rate: {flow.SuccessRate:F2}%");
+ Console.WriteLine($" Events/Second: {flow.EventsPerSecond:F2}");
+ Console.WriteLine($" P95 Latency: {flow.P95LatencyMs:F3}ms");
+ Console.WriteLine($" P99 Latency: {flow.P99LatencyMs:F3}ms");
+}
+```
+
+### System-Wide Metrics Collection
+
+Use `AdvancedMetricsCollector` to aggregate metrics across multiple loggers:
+
+```csharp
+var collector = app.Services.GetRequiredService();
+
+// Register loggers for collection
+var logStack = app.Services.GetRequiredService();
+collector.RegisterLogger(logStack);
+
+// Get aggregated metrics
+var aggregated = collector.GetAggregatedMetrics();
+Console.WriteLine($"Total Logged (All): {aggregated.TotalLoggedAcrossAll:N0}");
+Console.WriteLine($"Overall Success Rate: {aggregated.OverallSuccessRate:F2}%");
+
+// Get performance comparison
+var comparison = collector.GetPerformanceComparison();
+Console.WriteLine($"Highest Throughput: {comparison.HighestThroughputLogger?.WritesPerSecond:F2} events/sec");
+Console.WriteLine($"Average Throughput: {comparison.AverageWritesPerSecond:F2} events/sec");
+
+// Get health report
+var health = collector.GetHealthReport();
+Console.WriteLine($"System Status: {health.OverallStatus}");
+foreach (var warning in health.Warnings)
+ Console.WriteLine($" ⚠️ {warning}");
+```
+
+## Dependency Injection Integration
+
+EonaCat.LogStack seamlessly integrates with Microsoft Dependency Injection for ASP.NET Core and other .NET applications.
+
+### Basic Registration
+
+```csharp
+var builder = Host.CreateApplicationBuilder();
+builder.Services.AddEonaCatLogging();
+```
+
+This registers:
+- `ILoggerFactory` (EonaCat interface)
+- `Microsoft.Extensions.Logging.ILoggerFactory` (standard interface)
+- `ILogger` (EonaCat interface)
+- `Microsoft.Extensions.Logging.ILogger` (standard interface)
+
+### With Fluent Configuration
+
+```csharp
+var builder = Host.CreateApplicationBuilder();
+builder.Services.AddEonaCatLogging(b =>
+{
+ b.WithMinimumLevel(LogLevel.Information)
+ .WriteToConsole()
+ .WriteToFile("./logs")
+ .BoostWithCorrelationId();
+});
+```
+
+### With HostApplicationBuilder (Recommended)
+
+```csharp
+var builder = Host.CreateApplicationBuilder();
+
+builder.AddEonaCatLogging(b =>
+{
+ b.WithMinimumLevel(LogLevel.Information)
+ .WriteToConsole()
+ .WriteToFile("./logs");
+});
+
+var app = builder.Build();
+```
+
+### Advanced Features with AdvancedLoggerFactory
+
+For category-specific configuration and context propagation:
+
+```csharp
+var builder = Host.CreateApplicationBuilder();
+
+builder.Services.AddAdvancedEonaCatLogging(factory =>
+{
+ factory.ConfigureCategory("Database", config =>
+ {
+ config.MinimumLevel = LogLevel.Debug;
+ config.Boosters.Add(new CorrelationIdBooster());
+ });
+
+ factory.ConfigureCategory("Security", config =>
+ {
+ config.MinimumLevel = LogLevel.Warning;
+ });
+});
+
+var app = builder.Build();
+
+// Get loggers
+var dbLogger = app.Services.GetRequiredService().CreateLogger("Database");
+var secLogger = app.Services.GetRequiredService().CreateLogger("Security");
+```
+
+### With Metrics Collection
+
+```csharp
+var builder = Host.CreateApplicationBuilder();
+
+builder.AddEonaCatLogging(b =>
+{
+ b.WithMinimumLevel(LogLevel.Information)
+ .WriteToConsole()
+ .WriteToFile("./logs");
+});
+
+builder.AddEonaCatMetricsCollection();
+
+var app = builder.Build();
+
+// Access metrics
+var collector = app.Services.GetRequiredService();
+var metrics = collector.GetAggregatedMetrics();
+Console.WriteLine(metrics);
+```
+
+### Using Microsoft.Extensions.Logging
+
+The registered adapters allow you to use standard Microsoft logging interfaces:
+
+```csharp
+[ApiController]
+[Route("api/[controller]")]
+public class UserController : ControllerBase
+{
+ private readonly Microsoft.Extensions.Logging.ILogger _logger;
+
+ public UserController(Microsoft.Extensions.Logging.ILogger logger)
+ {
+ _logger = logger;
+ }
+
+ [HttpGet("{id}")]
+ public async Task GetUser(int id)
+ {
+ _logger.LogInformation("Fetching user {UserId}", id);
+ // ...
+ }
+}
+```
+
+The request is automatically routed through EonaCat.LogStack flows (file, console, Slack, etc.).
+
+### Context Propagation
+
+With AdvancedLoggerFactory, you can propagate context across async operations:
+
+```csharp
+var factory = app.Services.GetRequiredService();
+
+// Set context in request scope
+factory.SetContextData("RequestId", context.TraceIdentifier);
+factory.SetContextData("UserId", user.Id);
+
+// Logger can access this context
+var logger = factory.CreateLogger("MyCategory");
+logger.Log(LogLevel.Information, "Processing request");
+// Context is included automatically
+
+// Clear context when done
+factory.ClearContextData();
+```
+
## Disposing of the Logger
The logger is registered as a Singleton in the DI container, so it will be automatically disposed when the application shuts down. You can also manually access and dispose it: