Added new flows

This commit is contained in:
2026-06-24 16:30:22 +02:00
committed by Jeroen Saey
parent 95880f2a0e
commit a0cd906b9f
29 changed files with 6277 additions and 32 deletions
@@ -0,0 +1,45 @@
using System;
namespace EonaCat.LogStack.DependencyInjection
{
/// <summary>
/// Advanced configuration options for DI registration
/// </summary>
public sealed class LoggerDIOptions
{
/// <summary>
/// Gets or sets whether named loggers should be eagerly initialized
/// </summary>
public bool EagerlyInitializeNamedLoggers { get; set; } = false;
/// <summary>
/// Gets or sets whether to register ILogger decorator
/// </summary>
public bool RegisterLoggerDecorator { get; set; } = true;
/// <summary>
/// Gets or sets the default logger name for constructor injection
/// </summary>
public string DefaultLoggerName { get; set; } = "Default";
/// <summary>
/// Gets or sets whether to enable telemetry integration
/// </summary>
public bool EnableTelemetry { get; set; } = true;
/// <summary>
/// Gets or sets whether to enable distributed tracing
/// </summary>
public bool EnableTracing { get; set; } = false;
/// <summary>
/// Gets or sets whether to enable performance monitoring
/// </summary>
public bool EnablePerformanceMonitoring { get; set; } = false;
/// <summary>
/// Gets or sets whether to enable health monitoring
/// </summary>
public bool EnableHealthMonitoring { get; set; } = true;
}
}
+9 -3
View File
@@ -14,7 +14,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
<Copyright>EonaCat (Jeroen Saey)</Copyright> <Copyright>EonaCat (Jeroen Saey)</Copyright>
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags> <PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
<PackageIconUrl /> <PackageIconUrl />
<FileVersion>0.1.2</FileVersion> <FileVersion>0.1.3</FileVersion>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile> <GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
@@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<EVRevisionFormat>0.1.2+{chash:10}.{c:ymd}</EVRevisionFormat> <EVRevisionFormat>0.1.3+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVDefault>true</EVDefault> <EVDefault>true</EVDefault>
<EVInfo>true</EVInfo> <EVInfo>true</EVInfo>
<EVTagMatch>v[0-9]*</EVTagMatch> <EVTagMatch>v[0-9]*</EVTagMatch>
@@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Version>0.1.2</Version> <Version>0.1.3</Version>
<PackageId>EonaCat.LogStack</PackageId> <PackageId>EonaCat.LogStack</PackageId>
<Product>EonaCat.LogStack</Product> <Product>EonaCat.LogStack</Product>
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl> <RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl>
@@ -101,4 +101,10 @@ It features a rich fluent API for routing log events to dozens of destinations f
<PackagePath>\</PackagePath> <PackagePath>\</PackagePath>
</None> </None>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Configuration\" />
<Folder Include="Patterns\" />
<Folder Include="Utilities\" />
</ItemGroup>
</Project> </Project>
@@ -0,0 +1,358 @@
using EonaCat.LogStack.Core;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace EonaCat.LogStack.Flows
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
/// <summary>
/// CorrelatedEventFlow tracks and correlates related events across flows and time windows.
/// Enables request tracing, transaction tracking, and detection of related log patterns.
/// </summary>
public sealed class CorrelatedEventFlow : FlowBase
{
private readonly int _maxCorrelationGroups;
private readonly TimeSpan _correlationWindow;
private readonly ConcurrentDictionary<string, CorrelationGroup> _correlationGroups = new();
private readonly object _cleanupLock = new();
/// <summary>
/// Creates a new CorrelatedEventFlow
/// </summary>
/// <param name="maxCorrelationGroups">Maximum number of correlation groups to track (default 10000)</param>
/// <param name="correlationWindow">Time window for correlating events (default 5 minutes)</param>
/// <param name="minimumLevel">Minimum log level to process</param>
public CorrelatedEventFlow(
int maxCorrelationGroups = 10000,
TimeSpan? correlationWindow = null,
LogLevel minimumLevel = LogLevel.Trace)
: base("CorrelatedEvent", minimumLevel)
{
_maxCorrelationGroups = Math.Max(1000, maxCorrelationGroups);
_correlationWindow = correlationWindow ?? TimeSpan.FromMinutes(5);
}
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
return WriteResult.Success;
try
{
var correlationId = ExtractCorrelationId(logEvent);
if (string.IsNullOrEmpty(correlationId))
return WriteResult.Success;
// Get or create correlation group
var group = _correlationGroups.AddOrUpdate(
correlationId,
new CorrelationGroup(correlationId),
(key, existing) => existing
);
group.AddEvent(logEvent);
// Cleanup if needed
if (_correlationGroups.Count > _maxCorrelationGroups)
{
CleanupExpiredGroups();
}
return await Task.FromResult(WriteResult.Success);
}
catch (Exception ex)
{
return WriteResult.Failed;
}
}
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
{
try
{
foreach (var logEvent in logEvents.Span)
{
if (IsLogLevelEnabled(logEvent))
{
var correlationId = ExtractCorrelationId(logEvent);
if (!string.IsNullOrEmpty(correlationId))
{
var group = _correlationGroups.AddOrUpdate(
correlationId,
new CorrelationGroup(correlationId),
(key, existing) => existing
);
group.AddEvent(logEvent);
}
}
}
if (_correlationGroups.Count > _maxCorrelationGroups)
{
CleanupExpiredGroups();
}
return await Task.FromResult(WriteResult.Success);
}
catch (Exception ex)
{
return WriteResult.Failed;
}
}
public override async Task FlushAsync(CancellationToken cancellationToken = default)
{
CleanupExpiredGroups();
await Task.CompletedTask;
}
/// <summary>
/// Get all events for a correlation ID
/// </summary>
public IEnumerable<LogEvent> GetCorrelatedEvents(string correlationId)
{
if (_correlationGroups.TryGetValue(correlationId, out var group))
return group.Events.ToList();
return new List<LogEvent>();
}
/// <summary>
/// Get correlation statistics
/// </summary>
public CorrelationStatistics GetStatistics()
{
var stats = new CorrelationStatistics
{
TotalCorrelationIds = _correlationGroups.Count,
TotalTrackedEvents = _correlationGroups.Values.Sum(g => g.Events.Count),
AverageEventsPerCorrelation = _correlationGroups.Count > 0
? _correlationGroups.Values.Average(g => g.Events.Count)
: 0
};
// Calculate event distribution
if (_correlationGroups.Count > 0)
{
var eventCounts = _correlationGroups.Values.Select(g => g.Events.Count).ToList();
stats.MinEventsPerCorrelation = eventCounts.Min();
stats.MaxEventsPerCorrelation = eventCounts.Max();
stats.MedianEventsPerCorrelation = GetMedian(eventCounts);
}
return stats;
}
/// <summary>
/// Find correlation groups with errors
/// </summary>
public IEnumerable<CorrelationGroup> GetErrorCorrelations()
{
return _correlationGroups.Values
.Where(g => g.Events.Any(e => e.Level >= LogLevel.Error))
.ToList();
}
/// <summary>
/// Find slow correlation groups (based on duration property)
/// </summary>
public IEnumerable<CorrelationGroup> GetSlowCorrelations(TimeSpan minimumDuration)
{
return _correlationGroups.Values
.Where(g => g.Time().TotalMilliseconds >= minimumDuration.TotalMilliseconds)
.ToList();
}
/// <summary>
/// Get correlation groups by category pattern
/// </summary>
public IEnumerable<CorrelationGroup> QueryByPattern(string pattern, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
{
return _correlationGroups.Values
.Where(g => g.Events.Any(e =>
{
var msg = e.Message.ToString();
return msg.Contains(pattern, comparison);
}))
.ToList();
}
/// <summary>
/// Clear all correlation tracking
/// </summary>
public void Clear()
{
_correlationGroups.Clear();
}
// Private helpers
private string ExtractCorrelationId(LogEvent logEvent)
{
// Try to extract from common sources
if (logEvent.Properties != null)
{
if (logEvent.Properties.TryGetValue("CorrelationId", out var corrId) && corrId is string str)
return str;
if (logEvent.Properties.TryGetValue("RequestId", out var reqId) && reqId is string str2)
return str2;
if (logEvent.Properties.TryGetValue("TraceId", out var traceId) && traceId is string str3)
return str3;
}
// Try thread-based correlation
return null;
}
private void CleanupExpiredGroups()
{
lock (_cleanupLock)
{
var now = DateTime.UtcNow;
var expiredIds = _correlationGroups
.Where(kvp => now - kvp.Value.LastEventTime > _correlationWindow)
.Select(kvp => kvp.Key)
.ToList();
foreach (var id in expiredIds)
{
_correlationGroups.TryRemove(id, out _);
}
}
}
private double GetMedian(List<int> values)
{
if (values.Count == 0) return 0;
var sorted = values.OrderBy(x => x).ToList();
if (sorted.Count % 2 == 0)
return (sorted[sorted.Count / 2 - 1] + sorted[sorted.Count / 2]) / 2.0;
return sorted[sorted.Count / 2];
}
public override async ValueTask DisposeAsync()
{
Clear();
await base.DisposeAsync();
}
}
/// <summary>
/// Represents a group of correlated log events
/// </summary>
public class CorrelationGroup
{
private readonly List<LogEvent> _events = new();
private readonly object _eventLock = new();
public string CorrelationId { get; }
public IReadOnlyList<LogEvent> Events
{
get
{
lock (_eventLock)
return _events.AsReadOnly();
}
}
public DateTime FirstEventTime { get; private set; }
public DateTime LastEventTime { get; private set; }
public CorrelationGroup(string correlationId)
{
CorrelationId = correlationId ?? throw new ArgumentNullException(nameof(correlationId));
FirstEventTime = DateTime.UtcNow;
LastEventTime = DateTime.UtcNow;
}
public void AddEvent(LogEvent logEvent)
{
lock (_eventLock)
{
_events.Add(logEvent);
LastEventTime = DateTime.UtcNow;
}
}
/// <summary>
/// Get the total time span of this correlation group
/// </summary>
public TimeSpan Time()
{
lock (_eventLock)
{
if (_events.Count == 0) return TimeSpan.Zero;
return new TimeSpan(_events[_events.Count - 1].Timestamp - _events[0].Timestamp);
}
}
/// <summary>
/// Get events at specific log level
/// </summary>
public IEnumerable<LogEvent> GetEventsByLevel(LogLevel level)
{
lock (_eventLock)
return _events.Where(e => e.Level == level).ToList();
}
/// <summary>
/// Check if this group contains any exceptions
/// </summary>
public bool HasExceptions
{
get
{
lock (_eventLock)
return _events.Any(e => e.Exception != null);
}
}
/// <summary>
/// Get event count by category
/// </summary>
public Dictionary<string, int> GetEventsByCategory()
{
lock (_eventLock)
{
return _events
.GroupBy(e => e.Category ?? "Unknown")
.ToDictionary(g => g.Key, g => g.Count());
}
}
public override string ToString()
{
lock (_eventLock)
{
var errorCount = _events.Count(e => e.Level >= LogLevel.Error);
var duration = Time();
return $"CorrelationGroup [ID: {CorrelationId}, Events: {_events.Count}, Errors: {errorCount}, Duration: {duration.TotalMilliseconds:F0}ms]";
}
}
}
/// <summary>
/// Statistics about correlation tracking
/// </summary>
public class CorrelationStatistics
{
public int TotalCorrelationIds { get; set; }
public int TotalTrackedEvents { get; set; }
public double AverageEventsPerCorrelation { get; set; }
public int MinEventsPerCorrelation { get; set; }
public int MaxEventsPerCorrelation { get; set; }
public double MedianEventsPerCorrelation { get; set; }
public override string ToString()
{
return $"CorrelationStats: IDs={TotalCorrelationIds}, Events={TotalTrackedEvents}, " +
$"Avg={AverageEventsPerCorrelation:F1}, Min={MinEventsPerCorrelation}, Max={MaxEventsPerCorrelation}";
}
}
}
@@ -0,0 +1,262 @@
using EonaCat.LogStack.Core;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
namespace EonaCat.LogStack.Flows
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
/// <summary>
/// LocalStorageQueryFlow stores recent log events in memory and provides LINQ-like querying capabilities.
/// Useful for diagnostics, testing, and real-time log analysis without external storage.
/// Uses a circular buffer with configurable maximum size to prevent unbounded memory growth.
/// </summary>
public sealed class LocalStorageQueryFlow : FlowBase
{
private readonly int _maxCapacity;
private readonly ConcurrentQueue<LogEvent> _logBuffer = new();
private int _currentCount = 0;
/// <summary>
/// Gets all stored log events
/// </summary>
public IReadOnlyList<LogEvent> AllEvents => _logBuffer.ToList();
/// <summary>
/// Gets the number of events currently stored
/// </summary>
public int StoredEventCount => _currentCount;
/// <summary>
/// Creates a new LocalStorageQueryFlow with specified capacity
/// </summary>
/// <param name="maxCapacity">Maximum number of log events to keep in memory (default 10000)</param>
/// <param name="minimumLevel">Minimum log level to store</param>
public LocalStorageQueryFlow(
int maxCapacity = 10000,
LogLevel minimumLevel = LogLevel.Trace)
: base("LocalStorageQuery", minimumLevel)
{
if (maxCapacity <= 0)
throw new ArgumentException("Capacity must be greater than 0", nameof(maxCapacity));
_maxCapacity = maxCapacity;
}
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
return WriteResult.Success;
try
{
_logBuffer.Enqueue(logEvent);
int newCount = Interlocked.Increment(ref _currentCount);
// Maintain circular buffer by dropping oldest items when over capacity
while (newCount > _maxCapacity)
{
if (_logBuffer.TryDequeue(out _))
{
newCount = Interlocked.Decrement(ref _currentCount);
}
else
{
break;
}
}
return await Task.FromResult(WriteResult.Success);
}
catch (Exception ex)
{
return WriteResult.Failed;
}
}
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
{
try
{
foreach (var logEvent in logEvents.Span)
{
if (IsLogLevelEnabled(logEvent))
{
_logBuffer.Enqueue(logEvent);
int newCount = Interlocked.Increment(ref _currentCount);
// Maintain circular buffer
while (newCount > _maxCapacity)
{
if (_logBuffer.TryDequeue(out _))
{
newCount = Interlocked.Decrement(ref _currentCount);
}
else
{
break;
}
}
}
}
return await Task.FromResult(WriteResult.Success);
}
catch (Exception ex)
{
return WriteResult.Failed;
}
}
public override async Task FlushAsync(CancellationToken cancellationToken = default)
{
// No async I/O needed for in-memory storage
await Task.CompletedTask;
}
/// <summary>
/// Query stored events by log level
/// </summary>
public IEnumerable<LogEvent> QueryByLevel(LogLevel level)
{
return _logBuffer.Where(e => e.Level == level);
}
/// <summary>
/// Query stored events by category
/// </summary>
public IEnumerable<LogEvent> QueryByCategory(string category)
{
return _logBuffer.Where(e => e.Category?.Equals(category, StringComparison.OrdinalIgnoreCase) ?? false);
}
/// <summary>
/// Query stored events by message pattern
/// </summary>
public IEnumerable<LogEvent> QueryByMessagePattern(string pattern, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
{
return _logBuffer.Where(e =>
{
var msg = e.Message.ToString();
return msg.Contains(pattern, comparison);
});
}
/// <summary>
/// Query stored events by time range
/// </summary>
public IEnumerable<LogEvent> QueryByTimeRange(DateTime from, DateTime to)
{
var fromTicks = from.Ticks;
var toTicks = to.Ticks;
return _logBuffer.Where(e => e.Timestamp >= fromTicks && e.Timestamp <= toTicks);
}
/// <summary>
/// Query stored events that contain exceptions
/// </summary>
public IEnumerable<LogEvent> QueryExceptionEvents()
{
return _logBuffer.Where(e => e.Exception != null);
}
/// <summary>
/// Query stored events by duration (for performance logs)
/// </summary>
public IEnumerable<LogEvent> QueryByMinimumDuration(TimeSpan duration)
{
return _logBuffer.Where(e =>
e.Properties != null &&
e.Properties.TryGetValue("duration", out var durValue) &&
durValue is TimeSpan ts &&
ts >= duration);
}
/// <summary>
/// Get statistics about stored events
/// </summary>
public LogStorageStatistics GetStatistics()
{
var events = _logBuffer.ToList();
if (events.Count == 0)
return new LogStorageStatistics();
var stats = new LogStorageStatistics
{
TotalEvents = events.Count,
EventsByLevel = events
.GroupBy(e => e.Level)
.ToDictionary(g => g.Key, g => g.Count()),
EventsByCategory = events
.GroupBy(e => e.Category ?? "Unknown")
.ToDictionary(g => g.Key, g => g.Count()),
OldestEventTime = new DateTime(events.Min(e => e.Timestamp)),
NewestEventTime = new DateTime(events.Max(e => e.Timestamp)),
ExceptionCount = events.Count(e => e.Exception != null),
AverageMessageLength = (int)events.Average(e => e.Message.Length)
};
return stats;
}
/// <summary>
/// Clear all stored events
/// </summary>
public void Clear()
{
while (_logBuffer.TryDequeue(out _)) { }
_currentCount = 0;
}
/// <summary>
/// Get the most recent N events
/// </summary>
public IEnumerable<LogEvent> GetLatest(int count)
{
return _logBuffer.Reverse().Take(count);
}
/// <summary>
/// Search with custom predicate
/// </summary>
public IEnumerable<LogEvent> Search(Func<LogEvent, bool> predicate)
{
return _logBuffer.Where(predicate);
}
public override async ValueTask DisposeAsync()
{
Clear();
await base.DisposeAsync();
}
}
/// <summary>
/// Statistics about logs stored in LocalStorageQueryFlow
/// </summary>
public class LogStorageStatistics
{
public int TotalEvents { get; set; }
public Dictionary<LogLevel, int> EventsByLevel { get; set; } = new();
public Dictionary<string, int> EventsByCategory { get; set; } = new();
public DateTime OldestEventTime { get; set; }
public DateTime NewestEventTime { get; set; }
public int ExceptionCount { get; set; }
public int AverageMessageLength { get; set; }
public override string ToString()
{
var levelBreakdown = string.Join(", ", EventsByLevel.Select(kvp => $"{kvp.Key}: {kvp.Value}"));
var categoryBreakdown = string.Join(", ", EventsByCategory.Take(3).Select(kvp => $"{kvp.Key}: {kvp.Value}"));
return $"LogStorageStats: Total={TotalEvents}, Exceptions={ExceptionCount}, " +
$"Levels=[{levelBreakdown}], Categories=[{categoryBreakdown}]";
}
}
}
@@ -0,0 +1,357 @@
using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Channels;
using System.Threading.Tasks;
namespace EonaCat.LogStack.Flows
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
/// <summary>
/// LogFilterPresetFlow provides commonly-used filter combinations as reusable presets.
/// Reduces boilerplate for common scenarios like error tracking, performance monitoring, etc.
/// </summary>
public sealed class LogFilterPresetFlow : FlowBase
{
private readonly Channel<LogEvent> _channel;
private readonly IFlow _targetFlow;
private readonly LogFilterPreset _preset;
private Task _processingTask;
/// <summary>
/// Creates a LogFilterPresetFlow with a specific preset
/// </summary>
/// <param name="targetFlow">The flow to send filtered events to</param>
/// <param name="preset">The filter preset to apply</param>
/// <param name="minimumLevel">Minimum log level</param>
public LogFilterPresetFlow(
IFlow targetFlow,
LogFilterPreset preset,
LogLevel minimumLevel = LogLevel.Trace)
: base($"FilterPreset_{preset.Name}", minimumLevel)
{
_targetFlow = targetFlow ?? throw new ArgumentNullException(nameof(targetFlow));
_preset = preset ?? throw new ArgumentNullException(nameof(preset));
_channel = Channel.CreateUnbounded<LogEvent>(new UnboundedChannelOptions
{
SingleWriter = false,
SingleReader = true
});
StartProcessing();
}
private void StartProcessing()
{
_processingTask = ProcessChannelAsync();
}
private async Task ProcessChannelAsync()
{
try
{
await foreach (var logEvent in _channel.Reader.ReadAllAsync())
{
var filtered = _preset.Filter(logEvent);
if (filtered)
{
await _targetFlow.BlastAsync(logEvent);
}
}
}
catch (OperationCanceledException)
{
// Expected when channel is closed
}
}
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
return WriteResult.Success;
try
{
await _channel.Writer.WriteAsync(logEvent, cancellationToken);
return WriteResult.Success;
}
catch (Exception ex)
{
return WriteResult.Failed;
}
}
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
{
var events = logEvents.ToArray(); // Copy to array to avoid span issues across await
try
{
foreach (var logEvent in events)
{
if (IsLogLevelEnabled(logEvent))
{
await _channel.Writer.WriteAsync(logEvent, cancellationToken);
}
}
return WriteResult.Success;
}
catch (Exception ex)
{
return WriteResult.Failed;
}
}
public override async Task FlushAsync(CancellationToken cancellationToken = default)
{
await _targetFlow.FlushAsync(cancellationToken);
}
public override async ValueTask DisposeAsync()
{
_channel.Writer.TryComplete();
if (_processingTask != null)
await _processingTask;
await _targetFlow.DisposeAsync();
await base.DisposeAsync();
}
}
/// <summary>
/// Base class for filter presets
/// </summary>
public abstract class LogFilterPreset
{
public string Name { get; set; }
protected LogFilterPreset(string name)
{
Name = name ?? "Custom";
}
public abstract bool Filter(LogEvent logEvent);
}
/// <summary>
/// Preset: Only errors and exceptions
/// </summary>
public class ErrorOnlyPreset : LogFilterPreset
{
public ErrorOnlyPreset() : base("ErrorOnly") { }
public override bool Filter(LogEvent logEvent)
{
return logEvent.Level >= LogLevel.Error || logEvent.Exception != null;
}
}
/// <summary>
/// Preset: Errors with full stack traces
/// </summary>
public class ErrorWithStackTracePreset : LogFilterPreset
{
public ErrorWithStackTracePreset() : base("ErrorWithStackTrace") { }
public override bool Filter(LogEvent logEvent)
{
return (logEvent.Level >= LogLevel.Error || logEvent.Exception != null) &&
logEvent.Exception?.StackTrace != null;
}
}
/// <summary>
/// Preset: Performance-related logs (duration, latency, timeout)
/// </summary>
public class PerformanceLogsPreset : LogFilterPreset
{
private readonly string[] _keywords = { "duration", "latency", "timeout", "slow", "performance", "elapsed", "ms" };
public PerformanceLogsPreset() : base("PerformanceLogs") { }
public override bool Filter(LogEvent logEvent)
{
var message = logEvent.Message.ToString();
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase)) ||
(logEvent.Properties != null && logEvent.Properties.Keys.Any(k => k.Contains("duration", StringComparison.OrdinalIgnoreCase)));
}
}
/// <summary>
/// Preset: Authentication and security-related logs
/// </summary>
public class SecurityLogsPreset : LogFilterPreset
{
private readonly string[] _keywords = { "auth", "security", "permission", "token", "password", "encrypt", "decrypt", "unauthorized", "forbidden" };
public SecurityLogsPreset() : base("SecurityLogs") { }
public override bool Filter(LogEvent logEvent)
{
var message = logEvent.Message.ToString();
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary>
/// Preset: Health check and monitoring logs
/// </summary>
public class HealthCheckPreset : LogFilterPreset
{
private readonly string[] _keywords = { "health", "check", "monitor", "alive", "heartbeat", "ping", "status", "ready" };
public HealthCheckPreset() : base("HealthCheck") { }
public override bool Filter(LogEvent logEvent)
{
var message = logEvent.Message.ToString();
var category = logEvent.Category ?? string.Empty;
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
category.Contains(kw, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary>
/// Preset: Data access and database-related logs
/// </summary>
public class DatabaseLogsPreset : LogFilterPreset
{
private readonly string[] _keywords = { "database", "sql", "query", "transaction", "connection", "db:", "entity", "datacontext" };
public DatabaseLogsPreset() : base("DatabaseLogs") { }
public override bool Filter(LogEvent logEvent)
{
var message = logEvent.Message.ToString();
var category = logEvent.Category ?? string.Empty;
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
category.Contains(kw, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary>
/// Preset: Business logic and domain events
/// </summary>
public class DomainEventsPreset : LogFilterPreset
{
private readonly string[] _keywords = { "event", "domain", "aggregate", "business", "order", "payment", "transaction" };
public DomainEventsPreset() : base("DomainEvents") { }
public override bool Filter(LogEvent logEvent)
{
var message = logEvent.Message.ToString();
var category = logEvent.Category ?? string.Empty;
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
category.Contains(kw, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary>
/// Preset: External service calls and integrations
/// </summary>
public class ExternalServicePreset : LogFilterPreset
{
private readonly string[] _keywords = { "http", "api", "external", "service", "remote", "call", "webhook", "webhook", "third-party" };
public ExternalServicePreset() : base("ExternalService") { }
public override bool Filter(LogEvent logEvent)
{
var message = logEvent.Message.ToString();
var category = logEvent.Category ?? string.Empty;
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
category.Contains(kw, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary>
/// Preset: Critical warnings and errors (anything that might need attention)
/// </summary>
public class CriticalAlertsPreset : LogFilterPreset
{
public CriticalAlertsPreset() : base("CriticalAlerts") { }
public override bool Filter(LogEvent logEvent)
{
return logEvent.Level >= LogLevel.Warning ||
logEvent.Exception != null ||
(logEvent.Properties?.ContainsKey("Alert") ?? false);
}
}
/// <summary>
/// Preset: Development and debugging logs
/// </summary>
public class DebugLogsPreset : LogFilterPreset
{
private readonly string[] _keywords = { "debug", "trace", "verbose", "diagnostic", "test", "dev" };
public DebugLogsPreset() : base("DebugLogs") { }
public override bool Filter(LogEvent logEvent)
{
var msg = logEvent.Message.ToString();
var cat = logEvent.Category ?? string.Empty;
return logEvent.Level <= LogLevel.Debug ||
_keywords.Any(kw => msg.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
cat.Contains(kw, StringComparison.OrdinalIgnoreCase));
}
}
/// <summary>
/// Preset: Custom filter based on predicate
/// </summary>
public class CustomPreset : LogFilterPreset
{
private readonly Func<LogEvent, bool> _predicate;
public CustomPreset(string name, Func<LogEvent, bool> predicate)
: base(name)
{
_predicate = predicate ?? throw new ArgumentNullException(nameof(predicate));
}
public override bool Filter(LogEvent logEvent)
{
return _predicate(logEvent);
}
}
/// <summary>
/// Preset Registry for managing and creating presets
/// </summary>
public static class LogFilterPresetRegistry
{
private static readonly Dictionary<string, LogFilterPreset> _presets = new()
{
["ErrorOnly"] = new ErrorOnlyPreset(),
["ErrorWithStackTrace"] = new ErrorWithStackTracePreset(),
["Performance"] = new PerformanceLogsPreset(),
["Security"] = new SecurityLogsPreset(),
["HealthCheck"] = new HealthCheckPreset(),
["Database"] = new DatabaseLogsPreset(),
["DomainEvents"] = new DomainEventsPreset(),
["ExternalService"] = new ExternalServicePreset(),
["CriticalAlerts"] = new CriticalAlertsPreset(),
["Debug"] = new DebugLogsPreset()
};
public static LogFilterPreset? Get(string presetName)
{
return _presets.TryGetValue(presetName, out var preset) ? preset : null;
}
public static IEnumerable<string> GetAvailablePresets()
{
return _presets.Keys;
}
public static void Register(string name, LogFilterPreset preset)
{
_presets[name] = preset;
}
}
}
@@ -0,0 +1,384 @@
using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace EonaCat.LogStack.Flows
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
/// <summary>
/// PerformanceAnomalyDetectorFlow detects anomalies in performance metrics and generates alerts.
/// Uses statistical analysis to identify deviations from baseline performance.
/// </summary>
public sealed class PerformanceAnomalyDetectorFlow : FlowBase
{
private readonly int _windowSize;
private readonly double _standardDeviationThreshold;
private readonly IFlow _alertTarget;
private readonly Dictionary<string, PerformanceMetricTracker> _trackers = new();
private readonly object _trackersLock = new();
/// <summary>
/// Creates a new PerformanceAnomalyDetectorFlow
/// </summary>
/// <param name="alertTarget">Flow to send anomaly alerts to</param>
/// <param name="windowSize">Size of the sliding window for statistical analysis (default 100)</param>
/// <param name="standardDeviationThreshold">Alert threshold in standard deviations (default 2.0)</param>
/// <param name="minimumLevel">Minimum log level to process</param>
public PerformanceAnomalyDetectorFlow(
IFlow? alertTarget = null,
int windowSize = 100,
double standardDeviationThreshold = 2.0,
LogLevel minimumLevel = LogLevel.Information)
: base("PerformanceAnomalyDetector", minimumLevel)
{
_windowSize = Math.Max(10, windowSize);
_standardDeviationThreshold = Math.Max(1.0, standardDeviationThreshold);
_alertTarget = alertTarget;
}
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
return WriteResult.Success;
try
{
var metrics = ExtractPerformanceMetrics(logEvent);
if (metrics.Count == 0)
return WriteResult.Success;
lock (_trackersLock)
{
foreach (var metric in metrics)
{
var key = metric.Key;
if (!_trackers.TryGetValue(key, out var tracker))
{
tracker = new PerformanceMetricTracker(key, _windowSize, _standardDeviationThreshold);
_trackers[key] = tracker;
}
var anomaly = tracker.AddMeasurement(metric.Value, logEvent);
if (anomaly != null && _alertTarget != null)
{
// Send anomaly alert
var alertEvent = new LogEvent
{
Timestamp = DateTime.UtcNow.Ticks,
Level = LogLevel.Warning,
Category = "PerformanceAnomaly",
Message = anomaly.Description.AsMemory(),
Properties = new Dictionary<string, object?>
{
["MetricName"] = anomaly.MetricName,
["CurrentValue"] = anomaly.CurrentValue,
["AverageValue"] = anomaly.AverageValue,
["Deviation"] = anomaly.DeviationMultiplier,
["IsSlowdown"] = anomaly.IsSlowdown
}
};
_ = _alertTarget.BlastAsync(alertEvent);
}
}
}
return await Task.FromResult(WriteResult.Success);
}
catch (Exception ex)
{
return WriteResult.Failed;
}
}
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
{
foreach (var logEvent in logEvents.ToArray())
{
var result = await BlastAsync(logEvent, cancellationToken);
if (result != WriteResult.Success)
return result;
}
return WriteResult.Success;
}
public override async Task FlushAsync(CancellationToken cancellationToken = default)
{
if (_alertTarget != null)
await _alertTarget.FlushAsync(cancellationToken);
await Task.CompletedTask;
}
/// <summary>
/// Get anomaly detection statistics
/// </summary>
public AnomalyDetectionStatistics GetStatistics()
{
lock (_trackersLock)
{
var stats = new AnomalyDetectionStatistics
{
MonitoredMetrics = _trackers.Count,
TotalMeasurements = _trackers.Values.Sum(t => t.MeasurementCount),
TotalAnomaliesDetected = _trackers.Values.Sum(t => t.AnomalyCount),
MetricStatistics = _trackers.ToDictionary(
kvp => kvp.Key,
kvp => new MetricStatistic
{
AverageValue = kvp.Value.Average,
StandardDeviation = kvp.Value.StandardDeviation,
MinValue = kvp.Value.MinValue,
MaxValue = kvp.Value.MaxValue,
AnomalyCount = kvp.Value.AnomalyCount
})
};
return stats;
}
}
/// <summary>
/// Get recent anomalies
/// </summary>
public IEnumerable<PerformanceAnomaly> GetRecentAnomalies(int count = 10)
{
lock (_trackersLock)
{
return _trackers.Values
.SelectMany(t => t.RecentAnomalies)
.OrderByDescending(a => a.DetectedAt)
.Take(count)
.ToList();
}
}
/// <summary>
/// Get anomalies for a specific metric
/// </summary>
public IEnumerable<PerformanceAnomaly> GetAnomaliesForMetric(string metricName)
{
lock (_trackersLock)
{
if (_trackers.TryGetValue(metricName, out var tracker))
return tracker.RecentAnomalies.ToList();
return new List<PerformanceAnomaly>();
}
}
/// <summary>
/// Reset anomaly tracking for a metric
/// </summary>
public void ResetMetric(string metricName)
{
lock (_trackersLock)
{
_trackers.Remove(metricName);
}
}
/// <summary>
/// Clear all tracking data
/// </summary>
public void Clear()
{
lock (_trackersLock)
{
_trackers.Clear();
}
}
// Private helpers
private Dictionary<string, double> ExtractPerformanceMetrics(LogEvent logEvent)
{
var metrics = new Dictionary<string, double>();
if (logEvent.Properties == null)
return metrics;
// Look for common performance metric property names
var metricNames = new[] { "duration", "latency", "elapsed", "time_ms", "response_time", "memory", "cpu" };
foreach (var propKey in logEvent.Properties.Keys)
{
var keyStr = propKey.ToString();
if (metricNames.Any(mn => keyStr.Contains(mn, StringComparison.OrdinalIgnoreCase)))
{
if (logEvent.Properties[propKey] is double dVal)
{
metrics[keyStr] = dVal;
}
else if (logEvent.Properties[propKey] is int iVal)
{
metrics[keyStr] = iVal;
}
else if (logEvent.Properties[propKey] is long lVal)
{
metrics[keyStr] = lVal;
}
else if (double.TryParse(logEvent.Properties[propKey]?.ToString(), out var parsed))
{
metrics[keyStr] = parsed;
}
}
}
return metrics;
}
public override async ValueTask DisposeAsync()
{
Clear();
if (_alertTarget != null)
await _alertTarget.DisposeAsync();
await base.DisposeAsync();
}
}
/// <summary>
/// Tracks performance metrics and detects anomalies
/// </summary>
public class PerformanceMetricTracker
{
private readonly string _metricName;
private readonly int _windowSize;
private readonly double _threshold;
private readonly List<double> _measurements = new();
private readonly List<PerformanceAnomaly> _recentAnomalies = new();
private double _sum = 0;
public string MetricName => _metricName;
public int MeasurementCount => _measurements.Count;
public int AnomalyCount { get; private set; } = 0;
public IReadOnlyList<PerformanceAnomaly> RecentAnomalies => _recentAnomalies.AsReadOnly();
public double Average => _measurements.Count == 0 ? 0 : _sum / _measurements.Count;
public double StandardDeviation => CalculateStandardDeviation();
public double MinValue => _measurements.Count == 0 ? 0 : _measurements.Min();
public double MaxValue => _measurements.Count == 0 ? 0 : _measurements.Max();
public PerformanceMetricTracker(string metricName, int windowSize, double threshold)
{
_metricName = metricName;
_windowSize = windowSize;
_threshold = threshold;
}
public PerformanceAnomaly? AddMeasurement(double value, LogEvent sourceEvent)
{
// Add measurement
_measurements.Add(value);
_sum += value;
// Keep only recent measurements
if (_measurements.Count > _windowSize)
{
_sum -= _measurements[0];
_measurements.RemoveAt(0);
}
// Check for anomaly
if (_measurements.Count >= 10) // Need enough data for statistical analysis
{
var avg = Average;
var stdDev = StandardDeviation;
if (stdDev > 0)
{
var zScore = Math.Abs((value - avg) / stdDev);
if (zScore > _threshold)
{
var isSlowdown = value > avg;
var anomaly = new PerformanceAnomaly
{
MetricName = _metricName,
CurrentValue = value,
AverageValue = avg,
StandardDeviation = stdDev,
DeviationMultiplier = zScore,
IsSlowdown = isSlowdown,
DetectedAt = DateTime.UtcNow,
Description = $"Performance anomaly detected in '{_metricName}': " +
$"value {value:F2} is {(isSlowdown ? "higher" : "lower")} " +
$"than average {avg:F2} by {zScore:F2} standard deviations"
};
_recentAnomalies.Add(anomaly);
if (_recentAnomalies.Count > 100)
_recentAnomalies.RemoveAt(0);
AnomalyCount++;
return anomaly;
}
}
}
return null;
}
private double CalculateStandardDeviation()
{
if (_measurements.Count < 2)
return 0;
var avg = Average;
var sumSquaredDiff = _measurements.Sum(x => Math.Pow(x - avg, 2));
return Math.Sqrt(sumSquaredDiff / _measurements.Count);
}
}
/// <summary>
/// Represents a detected performance anomaly
/// </summary>
public class PerformanceAnomaly
{
public string MetricName { get; set; }
public double CurrentValue { get; set; }
public double AverageValue { get; set; }
public double StandardDeviation { get; set; }
public double DeviationMultiplier { get; set; }
public bool IsSlowdown { get; set; }
public DateTime DetectedAt { get; set; }
public string Description { get; set; }
public override string ToString()
{
return $"Anomaly[{MetricName}]: {CurrentValue:F2} (avg: {AverageValue:F2}, " +
$"deviation: {DeviationMultiplier:F2}σ) - {(IsSlowdown ? "SLOWDOWN" : "SPEEDUP")}";
}
}
/// <summary>
/// Statistics about anomaly detection
/// </summary>
public class AnomalyDetectionStatistics
{
public int MonitoredMetrics { get; set; }
public int TotalMeasurements { get; set; }
public int TotalAnomaliesDetected { get; set; }
public Dictionary<string, MetricStatistic> MetricStatistics { get; set; } = new();
public override string ToString()
{
return $"AnomalyStats: Metrics={MonitoredMetrics}, Measurements={TotalMeasurements}, " +
$"Anomalies={TotalAnomaliesDetected}";
}
}
/// <summary>
/// Statistics for a single metric
/// </summary>
public class MetricStatistic
{
public double AverageValue { get; set; }
public double StandardDeviation { get; set; }
public double MinValue { get; set; }
public double MaxValue { get; set; }
public int AnomalyCount { get; set; }
}
}
@@ -0,0 +1,119 @@
using EonaCat.LogStack.Core;
using EonaCat.LogStack.Flows;
using System;
using System.Collections.Generic;
using System.Linq;
namespace EonaCat.LogStack.Extensions
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
/// <summary>
/// Extension methods for the new cool logging features
/// </summary>
public static class CoolFeaturesExtensions
{
/// <summary>
/// Add a filter preset flow to route specific log types
/// </summary>
public static LogFilterPresetFlow AddFilteredFlow(
this IFlow targetFlow,
string presetName)
{
var preset = LogFilterPresetRegistry.Get(presetName)
?? throw new ArgumentException($"Preset '{presetName}' not found", nameof(presetName));
return new LogFilterPresetFlow(targetFlow, preset);
}
/// <summary>
/// Add a custom filter preset flow
/// </summary>
public static LogFilterPresetFlow AddCustomFilterFlow(
this IFlow targetFlow,
string name,
Func<LogEvent, bool> predicate)
{
var preset = new CustomPreset(name, predicate);
return new LogFilterPresetFlow(targetFlow, preset);
}
/// <summary>
/// Generate a heatmap visualization of stored logs
/// </summary>
public static string GenerateVisualization(
this LocalStorageQueryFlow queryFlow,
VisualizationType type = VisualizationType.Timeline,
int? width = null,
int? height = null)
{
var events = queryFlow.AllEvents;
return type switch
{
VisualizationType.Timeline => LogHeatmapAnalyzer.GenerateTimelineHeatmap(
events, width: width ?? 60, height: height ?? 10),
VisualizationType.LevelDistribution => LogHeatmapAnalyzer.GenerateLevelDistributionHeatmap(
events, width: width ?? 50),
VisualizationType.CategoryDistribution => LogHeatmapAnalyzer.GenerateCategoryHeatmap(
events, width: width ?? 50, topN: height ?? 10),
VisualizationType.ActivityMatrix => LogHeatmapAnalyzer.GenerateActivityMatrixHeatmap(events),
VisualizationType.Compact => LogHeatmapAnalyzer.GenerateCompactHeatmap(
events, buckets: width ?? 40),
_ => "Unknown visualization type"
};
}
/// <summary>
/// Print correlation group summary
/// </summary>
public static string GetCorrelationSummary(this CorrelationGroup group)
{
return $"CorrelationGroup [ID: {group.CorrelationId}, Events: {group.Events.Count}, Duration: {group.Time().TotalMilliseconds:F0}ms]";
}
/// <summary>
/// Print performance anomaly details
/// </summary>
public static string GetAnomalySummary(this PerformanceAnomaly anomaly)
{
return anomaly.ToString();
}
/// <summary>
/// Get available filter presets
/// </summary>
public static IEnumerable<string> GetAvailableFilterPresets()
{
return LogFilterPresetRegistry.GetAvailablePresets();
}
/// <summary>
/// Print log storage statistics
/// </summary>
public static string GetStorageStatsSummary(this LogStorageStatistics stats)
{
return stats.ToString();
}
/// <summary>
/// Print anomaly detection statistics
/// </summary>
public static string GetAnomalyStatsSummary(this AnomalyDetectionStatistics stats)
{
return stats.ToString();
}
}
/// <summary>
/// Types of visualizations available
/// </summary>
public enum VisualizationType
{
Timeline,
LevelDistribution,
CategoryDistribution,
ActivityMatrix,
Compact
}
}
@@ -0,0 +1,285 @@
using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace EonaCat.LogStack
{
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
/// <summary>
/// LogHeatmapAnalyzer generates ASCII heatmaps to visualize logging frequency patterns over time.
/// Useful for identifying traffic patterns, spikes, and patterns in log generation.
/// </summary>
public sealed class LogHeatmapAnalyzer
{
private const char HotCharacter = '█'; // Full block
private const char WarmCharacter = '▓'; // Dark shade
private const char CoolCharacter = '▒'; // Medium shade
private const char ColdCharacter = '░'; // Light shade
private const char EmptyCharacter = '·'; // Dot for empty
/// <summary>
/// Generate a heatmap showing logging frequency over time
/// </summary>
/// <param name="events">Log events to analyze</param>
/// <param name="timeResolutionMinutes">Time bucket size in minutes (e.g., 5 = 5-minute buckets)</param>
/// <param name="width">Width of the heatmap (default 60)</param>
/// <param name="height">Height of the heatmap (default 10)</param>
/// <returns>ASCII art heatmap string</returns>
public static string GenerateTimelineHeatmap(
IEnumerable<LogEvent> events,
int timeResolutionMinutes = 5,
int width = 60,
int height = 10)
{
if (timeResolutionMinutes <= 0)
throw new ArgumentException("Time resolution must be positive", nameof(timeResolutionMinutes));
var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0)
return "No events to visualize";
var timeBuckets = CreateTimeBuckets(eventList, timeResolutionMinutes);
if (timeBuckets.Count == 0)
return "No valid time data";
var maxCount = timeBuckets.Values.Max();
if (maxCount == 0)
return "All buckets empty";
return RenderHeatmap(timeBuckets, width, height, maxCount);
}
/// <summary>
/// Generate a heatmap by log level distribution
/// </summary>
public static string GenerateLevelDistributionHeatmap(IEnumerable<LogEvent> events, int width = 50)
{
var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0)
return "No events to visualize";
var levels = new[] { LogLevel.Trace, LogLevel.Debug, LogLevel.Information, LogLevel.Warning, LogLevel.Error, LogLevel.Critical };
var levelCounts = levels.ToDictionary(l => l, l => eventList.Count(e => e.Level == l));
var sb = new StringBuilder();
sb.AppendLine("Log Level Distribution:");
sb.AppendLine(new string('=', width));
var maxCount = levelCounts.Values.Max();
if (maxCount == 0)
return "No events";
foreach (var level in levels)
{
var count = levelCounts[level];
if (count == 0 && level != LogLevel.Information) continue;
var percentage = (double)count / eventList.Count * 100;
var barLength = (int)((double)count / maxCount * (width - 20));
var bar = new string(GetHeatCharacter(percentage), Math.Max(1, barLength));
sb.AppendLine($"{level,-12} | {bar} {count,5} ({percentage,5:F1}%)");
}
return sb.ToString();
}
/// <summary>
/// Generate a heatmap for category distribution
/// </summary>
public static string GenerateCategoryHeatmap(IEnumerable<LogEvent> events, int width = 50, int topN = 10)
{
var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0)
return "No events to visualize";
var categoryCounts = eventList
.GroupBy(e => e.Category ?? "Unknown")
.OrderByDescending(g => g.Count())
.Take(topN)
.ToDictionary(g => g.Key, g => g.Count());
var sb = new StringBuilder();
sb.AppendLine($"Top {topN} Categories:");
sb.AppendLine(new string('=', width));
var maxCount = categoryCounts.Values.Max();
if (maxCount == 0)
return "No categories";
foreach (var kvp in categoryCounts)
{
var category = kvp.Key.Length > 15 ? kvp.Key.Substring(0, 12) + "..." : kvp.Key;
var count = kvp.Value;
var percentage = (double)count / eventList.Count * 100;
var barLength = (int)((double)count / maxCount * (width - 25));
var bar = new string(GetHeatCharacter(percentage), Math.Max(1, barLength));
sb.AppendLine($"{category,-15} | {bar} {count,5} ({percentage,5:F1}%)");
}
return sb.ToString();
}
/// <summary>
/// Generate a compact inline heatmap for quick visualization
/// </summary>
public static string GenerateCompactHeatmap(IEnumerable<LogEvent> events, int buckets = 40)
{
var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0)
return string.Empty;
var timeBuckets = CreateTimeBuckets(eventList, (int)Math.Ceiling((double)GetTimeSpanMinutes(eventList) / buckets));
var maxCount = timeBuckets.Values.Max();
if (maxCount == 0)
return string.Empty;
var sb = new StringBuilder();
foreach (var bucket in timeBuckets.OrderBy(kvp => kvp.Key))
{
sb.Append(GetHeatCharacter((double)bucket.Value / maxCount));
}
return sb.ToString();
}
/// <summary>
/// Generate a 2D heatmap showing activity by hour and day
/// </summary>
public static string GenerateActivityMatrixHeatmap(IEnumerable<LogEvent> events)
{
var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0)
return "No events";
// Create a 24x7 matrix (hours x days)
var hourlyMatrix = new int[24, 7];
var minDate = new DateTime(eventList.Min(e => e.Timestamp));
var maxDate = new DateTime(eventList.Max(e => e.Timestamp));
var daySpan = (int)Math.Ceiling((maxDate - minDate).TotalDays);
foreach (var evt in eventList)
{
var timestamp = new DateTime(evt.Timestamp);
var hour = timestamp.Hour;
var dayOfWeek = (int)timestamp.DayOfWeek;
hourlyMatrix[hour, dayOfWeek]++;
}
var sb = new StringBuilder();
sb.AppendLine("Activity by Hour (rows) and Day of Week (cols):");
sb.AppendLine(" Sun Mon Tue Wed Thu Fri Sat");
sb.AppendLine(new string('-', 37));
var maxValue = 0;
for (int h = 0; h < 24; h++)
for (int d = 0; d < 7; d++)
maxValue = Math.Max(maxValue, hourlyMatrix[h, d]);
if (maxValue == 0)
return "No hourly data";
for (int h = 0; h < 24; h++)
{
sb.Append($"{h:D2}h: ");
for (int d = 0; d < 7; d++)
{
var value = hourlyMatrix[h, d];
var intensity = value == 0 ? 0 : (double)value / maxValue;
sb.Append(GetHeatCharacter(intensity));
sb.Append(" ");
}
sb.AppendLine();
}
return sb.ToString();
}
// Private helpers
private static Dictionary<DateTime, int> CreateTimeBuckets(List<LogEvent> events, int bucketMinutes)
{
var buckets = new Dictionary<DateTime, int>();
foreach (var evt in events)
{
var timestamp = new DateTime(evt.Timestamp);
var bucketTime = timestamp.AddSeconds(-timestamp.Second)
.AddMilliseconds(-timestamp.Millisecond)
.AddMinutes(-(timestamp.Minute % bucketMinutes));
if (buckets.ContainsKey(bucketTime))
buckets[bucketTime]++;
else
buckets[bucketTime] = 1;
}
return buckets;
}
private static string RenderHeatmap(Dictionary<DateTime, int> timeBuckets, int width, int height, int maxCount)
{
var sortedBuckets = timeBuckets.OrderBy(kvp => kvp.Key).ToList();
var step = Math.Max(1, sortedBuckets.Count / width);
var sb = new StringBuilder();
sb.AppendLine($"Timeline Heatmap ({sortedBuckets.Count} time periods):");
sb.AppendLine(new string('=', width + 4));
// Render vertical bars
for (int row = height; row > 0; row--)
{
var threshold = (double)maxCount / height * row;
sb.Append("| ");
for (int col = 0; col < width; col++)
{
var idx = col * step;
if (idx < sortedBuckets.Count)
{
var count = sortedBuckets[idx].Value;
var intensity = (double)count / maxCount;
var displayThreshold = (double)(row - 1) / height;
sb.Append(intensity > displayThreshold ? HotCharacter : ' ');
}
else
{
sb.Append(' ');
}
}
sb.AppendLine(" |");
}
sb.AppendLine(new string('=', width + 4));
if (sortedBuckets.Count > 0)
sb.AppendLine($"From: {sortedBuckets.First().Key:yyyy-MM-dd HH:mm}, To: {sortedBuckets.Last().Key:yyyy-MM-dd HH:mm}");
return sb.ToString();
}
private static char GetHeatCharacter(double intensity)
{
if (intensity < 0.01) return EmptyCharacter;
if (intensity < 0.35) return ColdCharacter;
if (intensity < 0.65) return CoolCharacter;
if (intensity < 0.85) return WarmCharacter;
return HotCharacter;
}
private static int GetTimeSpanMinutes(List<LogEvent> events)
{
if (events.Count == 0) return 1;
var maxTicks = events.Max(e => e.Timestamp);
var minTicks = events.Min(e => e.Timestamp);
var span = new TimeSpan(maxTicks - minTicks);
return Math.Max(1, (int)span.TotalMinutes);
}
}
}
+152 -2
View File
@@ -2,6 +2,11 @@
using EonaCat.LogStack.Boosters; using EonaCat.LogStack.Boosters;
using EonaCat.LogStack.Core; using EonaCat.LogStack.Core;
using EonaCat.LogStack.Flows; using EonaCat.LogStack.Flows;
using EonaCat.LogStack.Routing;
using EonaCat.LogStack.Sampling;
using EonaCat.LogStack.Anomalies;
using EonaCat.LogStack.DeadLettering;
using EonaCat.LogStack.Diagnostics;
using System; using System;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
@@ -62,6 +67,13 @@ namespace EonaCat.LogStack
// Dynamic level controller // Dynamic level controller
private volatile DynamicLevelController? _dynamicLevel; private volatile DynamicLevelController? _dynamicLevel;
// Superior logging features
private readonly IntelligentRouter? _intelligentRouter;
private readonly AdaptiveSamplingEngine? _adaptiveSampler;
private readonly AnomalyDetector? _anomalyDetector;
private readonly ContextSnapshotCollector? _contextSnapshots;
private DeadLetterQueue? _deadLetterQueue;
public event EventHandler<LogMessage> OnLog; public event EventHandler<LogMessage> OnLog;
/// <summary> /// <summary>
@@ -69,12 +81,22 @@ namespace EonaCat.LogStack
/// </summary> /// </summary>
public EonaCatLogStack(string category = "Application", public EonaCatLogStack(string category = "Application",
LogLevel minimumLevel = LogLevel.Trace, LogLevel minimumLevel = LogLevel.Trace,
TimestampMode timestampMode = TimestampMode.Utc) TimestampMode timestampMode = TimestampMode.Utc,
bool enableIntelligentRouting = false,
bool enableAdaptiveSampling = false,
bool enableAnomalyDetection = false,
bool enableContextSnapshots = false)
{ {
_category = category ?? throw new ArgumentNullException(nameof(category)); _category = category ?? throw new ArgumentNullException(nameof(category));
_minimumLevel = minimumLevel; _minimumLevel = minimumLevel;
_timestampMode = timestampMode; _timestampMode = timestampMode;
// Initialize superior features if requested
_intelligentRouter = enableIntelligentRouting ? new IntelligentRouter() : null;
_adaptiveSampler = enableAdaptiveSampling ? new AdaptiveSamplingEngine() : null;
_anomalyDetector = enableAnomalyDetection ? new AnomalyDetector() : null;
_contextSnapshots = enableContextSnapshots ? new ContextSnapshotCollector() : null;
// Enable async pipeline by default // Enable async pipeline by default
UseAsyncPipeline(); UseAsyncPipeline();
} }
@@ -119,6 +141,38 @@ namespace EonaCat.LogStack
return this; return this;
} }
/// <summary>
/// Gets the Intelligent Router for pattern-based flow routing (if enabled).
/// </summary>
public IntelligentRouter GetIntelligentRouter() => _intelligentRouter;
/// <summary>
/// Gets the Adaptive Sampling Engine (if enabled).
/// </summary>
public AdaptiveSamplingEngine GetAdaptiveSampler() => _adaptiveSampler;
/// <summary>
/// Gets the Anomaly Detector (if enabled).
/// </summary>
public AnomalyDetector GetAnomalyDetector() => _anomalyDetector;
/// <summary>
/// Gets the Context Snapshot Collector (if enabled).
/// </summary>
public ContextSnapshotCollector GetContextSnapshots() => _contextSnapshots;
/// <summary>
/// Gets or creates the Dead Letter Queue for handling failed logs.
/// </summary>
public DeadLetterQueue GetDeadLetterQueue()
{
if (_deadLetterQueue == null)
{
_deadLetterQueue = new DeadLetterQueue();
}
return _deadLetterQueue;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private LogLevel EffectiveMinLevel() => private LogLevel EffectiveMinLevel() =>
_dynamicLevel != null ? _dynamicLevel.CurrentLevel : _minimumLevel; _dynamicLevel != null ? _dynamicLevel.CurrentLevel : _minimumLevel;
@@ -413,6 +467,13 @@ namespace EonaCat.LogStack
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ProcessLogEvent(ref LogEventBuilder builder) private void ProcessLogEvent(ref LogEventBuilder builder)
{ {
// Check adaptive sampling first - if sampling is enabled and this log is dropped, return early
if (_adaptiveSampler != null && !_adaptiveSampler.ShouldSample())
{
Interlocked.Increment(ref _totalDroppedCount);
return;
}
// Apply boosters // Apply boosters
lock (_boosters) lock (_boosters)
{ {
@@ -443,6 +504,25 @@ namespace EonaCat.LogStack
} }
var logEvent = builder.Build(); var logEvent = builder.Build();
// Record sampling metric
if (_adaptiveSampler != null)
{
_adaptiveSampler.RecordLogEvent();
}
// Detect anomalies
if (_anomalyDetector != null)
{
_anomalyDetector.AnalyzeEvent(logEvent);
}
// Capture context snapshots on critical logs
if (_contextSnapshots != null)
{
_contextSnapshots.CaptureIfNeeded(logEvent);
}
Interlocked.Increment(ref _totalLoggedCount); Interlocked.Increment(ref _totalLoggedCount);
if (!_asyncChannel!.Writer.TryWrite(logEvent)) if (!_asyncChannel!.Writer.TryWrite(logEvent))
@@ -458,6 +538,18 @@ namespace EonaCat.LogStack
{ {
await foreach (var logEvent in reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) await foreach (var logEvent in reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
{ {
// If intelligent router is enabled, use it for routing
if (_intelligentRouter != null)
{
try
{
await _intelligentRouter.RouteAsync(logEvent).ConfigureAwait(false);
}
catch { /* Router should handle its own errors */ }
}
else
{
// Default routing to all flows
var flows = _concurrentFlows; var flows = _concurrentFlows;
foreach (var flow in flows) foreach (var flow in flows)
{ {
@@ -467,9 +559,25 @@ namespace EonaCat.LogStack
if (result == WriteResult.Dropped) if (result == WriteResult.Dropped)
{ {
Interlocked.Increment(ref _totalDroppedCount); Interlocked.Increment(ref _totalDroppedCount);
// Enqueue to DLQ if available
if (_deadLetterQueue != null)
{
_deadLetterQueue.Enqueue(logEvent, $"Dropped by flow '{flow.Name}'");
}
}
}
catch (Exception ex)
{
// Enqueue to DLQ if available
if (_deadLetterQueue != null)
{
_deadLetterQueue.Enqueue(logEvent,
$"Exception in flow '{flow.Name}': {ex.Message}",
ex);
}
} }
} }
catch { }
} }
} }
} }
@@ -557,6 +665,48 @@ namespace EonaCat.LogStack
/// </summary> /// </summary>
public LoggingMetrics GetMetricsCollector() => _metrics; public LoggingMetrics GetMetricsCollector() => _metrics;
/// <summary>
/// Gets comprehensive statistics about all superior features.
/// </summary>
public SuperiorFeaturesStats GetSuperiorFeaturesStats()
{
var stats = new SuperiorFeaturesStats();
if (_intelligentRouter != null)
{
stats.RouterStats = _intelligentRouter.GetStats();
stats.IntelligentRoutingEnabled = true;
}
if (_adaptiveSampler != null)
{
stats.SamplingMetrics = _adaptiveSampler.GetMetrics();
stats.AdaptiveSamplingEnabled = true;
}
if (_anomalyDetector != null)
{
stats.AnomalyStats = _anomalyDetector.GetStats();
stats.RecentAnomalies = _anomalyDetector.GetRecentAnomalies(5);
stats.AnomalyDetectionEnabled = true;
}
if (_contextSnapshots != null)
{
stats.SnapshotStats = _contextSnapshots.GetStats();
stats.ContextSnapshotsEnabled = true;
}
if (_deadLetterQueue != null)
{
stats.DlqStats = _deadLetterQueue.GetStats();
stats.DlqFailureReasons = _deadLetterQueue.GetFailureReasonStats();
stats.DeadLetterQueueEnabled = true;
}
return stats;
}
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
@@ -0,0 +1,314 @@
using EonaCat.LogStack.Core;
using System;
using System.Diagnostics;
using System.Threading;
// 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.Sampling;
/// <summary>
/// Adaptive sampling engine that dynamically adjusts sampling rates based on log volume and system resources.
/// Superior to static sampling as it prevents log loss during normal operation while reducing overhead during peaks.
/// </summary>
public class AdaptiveSamplingEngine
{
private double _samplingRate = 1.0; // Sample 100% of logs by default
private double _targetThroughput = 10000; // Target 10k logs/sec
private long _logCount;
private long _lastAdjustmentTicks;
private readonly Stopwatch _perfCounter = Stopwatch.StartNew();
private readonly double _adjustmentIntervalMs;
private readonly SamplingStrategy _strategy;
private readonly object _statsLock = new object();
// CPU and Memory monitoring
private double _currentCpuPercent;
private double _currentMemoryPercent;
private readonly double _cpuThreshold;
private readonly double _memoryThreshold;
private readonly Process _currentProcess = Process.GetCurrentProcess();
private DateTime _lastCpuCheck;
public AdaptiveSamplingEngine(
SamplingStrategy strategy = SamplingStrategy.Throughput,
double targetThroughput = 10000,
double adjustmentIntervalMs = 5000,
double cpuThreshold = 0.80,
double memoryThreshold = 0.85)
{
_strategy = strategy;
_targetThroughput = targetThroughput;
_adjustmentIntervalMs = adjustmentIntervalMs;
_cpuThreshold = cpuThreshold;
_memoryThreshold = memoryThreshold;
_lastAdjustmentTicks = _perfCounter.ElapsedTicks;
_lastCpuCheck = DateTime.UtcNow;
}
/// <summary>
/// Determines if a log event should be sampled (kept) or dropped.
/// Returns true if the event should be logged.
/// </summary>
public bool ShouldSample()
{
// Periodically adjust sampling rate
lock (_statsLock)
{
if (_perfCounter.ElapsedTicks - _lastAdjustmentTicks >
TicksFromMilliseconds(_adjustmentIntervalMs))
{
AdjustSamplingRate();
}
}
// Use random sampling based on current rate
if (_samplingRate >= 1.0)
return true;
if (_samplingRate <= 0.0)
return false;
return ThreadSafeRandom.NextDouble() < _samplingRate;
}
/// <summary>
/// Adjusts the sampling rate based on current metrics and strategy.
/// </summary>
private void AdjustSamplingRate()
{
_lastAdjustmentTicks = _perfCounter.ElapsedTicks;
var currentLogCount = Interlocked.Exchange(ref _logCount, 0);
UpdateResourceMetrics();
switch (_strategy)
{
case SamplingStrategy.Throughput:
AdjustForThroughput();
break;
case SamplingStrategy.Resources:
AdjustForResources();
break;
case SamplingStrategy.Hybrid:
AdjustForThroughput();
AdjustForResources();
break;
case SamplingStrategy.Adaptive:
AdjustAdaptive();
break;
}
// Clamp between 0 and 1
_samplingRate = Math.Max(0.0, Math.Min(1.0, _samplingRate));
}
private void AdjustForThroughput()
{
var elapsedSeconds = _adjustmentIntervalMs / 1000.0;
var currentLogCount = 0L;
lock (_statsLock)
{
currentLogCount = Interlocked.Read(ref _logCount);
}
var actualThroughput = currentLogCount / elapsedSeconds;
if (actualThroughput > _targetThroughput * 1.2) // 20% over target
{
// Reduce sampling rate
_samplingRate *= 0.9;
}
else if (actualThroughput < _targetThroughput * 0.8) // 20% under target
{
// Increase sampling rate
_samplingRate *= 1.1;
}
}
private void AdjustForResources()
{
if (_currentCpuPercent > _cpuThreshold || _currentMemoryPercent > _memoryThreshold)
{
// High resource usage - reduce sampling rate more aggressively
_samplingRate *= 0.7;
}
else if (_currentCpuPercent < _cpuThreshold * 0.5 && _currentMemoryPercent < _memoryThreshold * 0.5)
{
// Low resource usage - increase sampling rate
_samplingRate *= 1.15;
}
}
private void AdjustAdaptive()
{
/* Adaptive strategy: adjust based on both throughput and resources,
with memory being more critical than CPU */
var cpuFactor = _currentCpuPercent / _cpuThreshold;
var memoryFactor = _currentMemoryPercent / _memoryThreshold;
// Memory pressure has higher weight
var resourcePressure = (cpuFactor * 0.4) + (memoryFactor * 0.6);
if (resourcePressure > 1.0)
{
_samplingRate *= (1.0 - (resourcePressure - 1.0) * 0.5);
}
else if (resourcePressure < 0.5)
{
_samplingRate *= 1.1;
}
}
private void UpdateResourceMetrics()
{
var now = DateTime.UtcNow;
if ((now - _lastCpuCheck).TotalMilliseconds < 1000)
return; // Update at most once per second to avoid overhead
_lastCpuCheck = now;
try
{
// CPU usage (process)
_currentCpuProcess = _currentProcess.TotalProcessorTime.TotalMilliseconds;
// Simplified: estimate CPU % based on processor count
var cpuUsageEstimate = Math.Min(1.0, _currentCpuProcess / (Environment.ProcessorCount * 100));
_currentCpuPercent = cpuUsageEstimate;
// Memory usage
var totalMemory = GC.GetTotalMemory(false);
var workingSet = _currentProcess.WorkingSet64;
// Estimate based on available system memory
// In production, use MemoryMarshal or other APIs for more precise data
_currentMemoryPercent = Math.Min(1.0, workingSet / (8.0 * 1024 * 1024 * 1024)); // Assume 8GB
}
catch
{
// If we can't get metrics, don't crash
}
}
private double _currentCpuProcess;
/// <summary>
/// Sets the target throughput for throughput-based sampling.
/// </summary>
public void SetTargetThroughput(double logsPerSecond)
{
_targetThroughput = Math.Max(1, logsPerSecond);
}
/// <summary>
/// Gets current sampling rate (0.0 to 1.0).
/// </summary>
public double GetSamplingRate() => _samplingRate;
/// <summary>
/// Gets current resource utilization metrics.
/// </summary>
public SamplingMetrics GetMetrics()
{
lock (_statsLock)
{
return new SamplingMetrics
{
SamplingRate = _samplingRate,
CurrentCpuPercent = _currentCpuPercent,
CurrentMemoryPercent = _currentMemoryPercent,
Strategy = _strategy,
TargetThroughput = _targetThroughput,
LogsProcessedInLastInterval = Interlocked.Read(ref _logCount)
};
}
}
/// <summary>
/// Records a log event (increments counter for sampling decisions).
/// </summary>
public void RecordLogEvent()
{
Interlocked.Increment(ref _logCount);
}
private static long TicksFromMilliseconds(double ms)
{
return (long)(ms * Stopwatch.Frequency / 1000.0);
}
}
/// <summary>
/// Sampling strategies for adaptive engine.
/// </summary>
public enum SamplingStrategy
{
/// <summary>
/// Adjusts sampling based on throughput to match target logs/sec
/// </summary>
Throughput,
/// <summary>
/// Adjusts sampling based on CPU and memory usage
/// </summary>
Resources,
/// <summary>
/// Combines throughput and resource monitoring
/// </summary>
Hybrid,
/// <summary>
/// Advanced adaptive strategy that learns optimal rate
/// </summary>
Adaptive
}
/// <summary>
/// Metrics from the adaptive sampling engine.
/// </summary>
public class SamplingMetrics
{
/// <summary>
/// Current sampling rate (0.0 = drop all, 1.0 = keep all)
/// </summary>
public double SamplingRate { get; set; }
/// <summary>
/// Current CPU usage percentage (0.0 to 1.0)
/// </summary>
public double CurrentCpuPercent { get; set; }
/// <summary>
/// Current memory usage percentage (0.0 to 1.0)
/// </summary>
public double CurrentMemoryPercent { get; set; }
/// <summary>
/// Active sampling strategy
/// </summary>
public SamplingStrategy Strategy { get; set; }
/// <summary>
/// Target throughput in logs per second
/// </summary>
public double TargetThroughput { get; set; }
/// <summary>
/// Logs processed in last adjustment interval
/// </summary>
public long LogsProcessedInLastInterval { get; set; }
}
/// <summary>
/// Thread-safe random number generator for sampling decisions.
/// </summary>
internal static class ThreadSafeRandom
{
private static readonly ThreadLocal<Random> _random = new ThreadLocal<Random>(
() => new Random(Guid.NewGuid().GetHashCode())
);
public static double NextDouble() => _random.Value.NextDouble();
}
@@ -0,0 +1,368 @@
using EonaCat.LogStack.Core;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Diagnostics;
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
namespace EonaCat.LogStack.Anomalies;
/// <summary>
/// Real-time anomaly detector that identifies unusual logging patterns and escalates them.
/// Superior to simple threshold-based alerting as it learns normal behavior over time.
/// </summary>
public class AnomalyDetector
{
private readonly ConcurrentDictionary<string, CategoryStatistics> _categoryStats
= new ConcurrentDictionary<string, CategoryStatistics>();
private readonly ConcurrentDictionary<LogLevel, LevelStatistics> _levelStats
= new ConcurrentDictionary<LogLevel, LevelStatistics>();
private readonly double _standardDeviationThreshold;
private readonly int _windowSizeSeconds;
private readonly object _statsLock = new object();
private long _totalEventsProcessed;
private readonly List<AnomalyAlert> _recentAlerts = new List<AnomalyAlert>();
private readonly Stopwatch _uptime = Stopwatch.StartNew();
public event EventHandler<AnomalyAlert> AnomalyDetected;
public AnomalyDetector(
double standardDeviationThreshold = 3.0,
int windowSizeSeconds = 300)
{
_standardDeviationThreshold = standardDeviationThreshold;
_windowSizeSeconds = windowSizeSeconds;
}
/// <summary>
/// Analyzes a log event for anomalies.
/// </summary>
public AnomalyAlert AnalyzeEvent(LogEvent logEvent)
{
_totalEventsProcessed++;
// Track category statistics
var categoryStats = _categoryStats.GetOrAdd(logEvent.Category ?? "Unknown",
_ => new CategoryStatistics());
categoryStats.RecordEvent(logEvent);
// Track level statistics
var levelStats = _levelStats.GetOrAdd(logEvent.Level,
_ => new LevelStatistics());
levelStats.RecordEvent();
// Detect anomalies
var anomalies = new List<string>();
// Check for unusual error spike
if (logEvent.Level == LogLevel.Error || logEvent.Level == LogLevel.Critical)
{
var errorRate = levelStats.GetErrorRatePerSecond();
if (errorRate > 50) // More than 50 errors per second
{
anomalies.Add($"High error rate detected: {errorRate:F2} errors/sec");
}
}
// Check for category anomalies
if (categoryStats.IsAnomalous(_standardDeviationThreshold))
{
anomalies.Add($"Category '{logEvent.Category}' showing unusual activity");
}
// Check for exception frequency spike
if (logEvent.Exception != null)
{
var exceptionKey = logEvent.Exception.GetType().Name;
if (categoryStats.RecordException(exceptionKey) > 5)
{
anomalies.Add($"Exception '{exceptionKey}' spike detected");
}
}
// Check for message repetition
if (logEvent.Message.Length > 0)
{
if (categoryStats.RecordMessageHash(logEvent.Message.ToString().GetHashCode()) > 10)
{
anomalies.Add("Repeated message pattern detected (possible infinite loop)");
}
}
if (anomalies.Any())
{
var alert = new AnomalyAlert
{
Timestamp = DateTime.UtcNow,
Category = logEvent.Category,
Level = logEvent.Level,
LogEvent = logEvent,
Anomalies = anomalies,
AnomalySeverity = DetermineAnomalySeverity(anomalies),
SystemMetrics = CaptureSystemMetrics()
};
lock (_statsLock)
{
_recentAlerts.Add(alert);
if (_recentAlerts.Count > 1000)
_recentAlerts.RemoveAt(0); // Keep last 1000 alerts
}
AnomalyDetected?.Invoke(this, alert);
return alert;
}
return null;
}
/// <summary>
/// Gets statistics summary for analysis.
/// </summary>
public AnomalyDetectorStats GetStats()
{
lock (_statsLock)
{
return new AnomalyDetectorStats
{
TotalEventsProcessed = _totalEventsProcessed,
UniqueCategories = _categoryStats.Count,
RecentAnomalies = _recentAlerts.Count,
AnomalyRate = _recentAlerts.Count / Math.Max(1, _uptime.Elapsed.TotalMinutes),
TopCategories = _categoryStats
.OrderByDescending(x => x.Value.EventCount)
.Take(5)
.Select(x => new CategorySummary
{
Category = x.Key,
EventCount = x.Value.EventCount,
IsAnomalous = x.Value.IsAnomalous(_standardDeviationThreshold)
})
.ToList()
};
}
}
/// <summary>
/// Gets recent detected anomalies.
/// </summary>
public List<AnomalyAlert> GetRecentAnomalies(int count = 10)
{
lock (_statsLock)
{
return _recentAlerts.Skip(Math.Max(0, _recentAlerts.Count - count))
.OrderBy(a => a.Timestamp)
.ToList();
}
}
/// <summary>
/// Clears all statistics (useful for resetting baseline after deployment).
/// </summary>
public void ResetStatistics()
{
_categoryStats.Clear();
_levelStats.Clear();
lock (_statsLock)
{
_recentAlerts.Clear();
}
_totalEventsProcessed = 0;
}
private AnomalySeverity DetermineAnomalySeverity(List<string> anomalies)
{
if (anomalies.Any(a => a.Contains("error rate")))
return AnomalySeverity.Critical;
if (anomalies.Any(a => a.Contains("Exception") || a.Contains("infinite loop")))
return AnomalySeverity.High;
return AnomalySeverity.Medium;
}
private SystemMetrics CaptureSystemMetrics()
{
var process = Process.GetCurrentProcess();
return new SystemMetrics
{
ProcessMemoryMb = process.WorkingSet64 / (1024 * 1024),
ProcessorCount = Environment.ProcessorCount,
Uptime = _uptime.Elapsed,
ThreadCount = process.Threads.Count,
TotalEventsProcessed = _totalEventsProcessed
};
}
}
/// <summary>
/// Tracks statistics for a log category.
/// </summary>
internal class CategoryStatistics
{
private readonly Queue<DateTime> _recentEventTimes = new Queue<DateTime>();
private readonly ConcurrentDictionary<int, int> _messageHashCounts = new ConcurrentDictionary<int, int>();
private readonly ConcurrentDictionary<string, int> _exceptionCounts = new ConcurrentDictionary<string, int>();
public long EventCount { get; private set; }
private long _errorCount;
private readonly object _lock = new object();
public void RecordEvent(LogEvent logEvent)
{
lock (_lock)
{
EventCount++;
_recentEventTimes.Enqueue(DateTime.UtcNow);
if (_recentEventTimes.Count > 1000)
_recentEventTimes.Dequeue();
if (logEvent.Level == LogLevel.Error || logEvent.Level == LogLevel.Critical)
_errorCount++;
}
}
public int RecordException(string exceptionType)
{
return _exceptionCounts.AddOrUpdate(exceptionType, 1, (k, v) => v + 1);
}
public int RecordMessageHash(int messageHash)
{
return _messageHashCounts.AddOrUpdate(messageHash, 1, (k, v) => v + 1);
}
public bool IsAnomalous(double stdDevThreshold)
{
lock (_lock)
{
if (_recentEventTimes.Count < 10)
return false;
var now = DateTime.UtcNow;
var times = _recentEventTimes.ToList();
var recentIntervals = new List<double>();
for (int i = 1; i < times.Count; i++)
{
var interval = (now - times[i]).TotalSeconds;
if (interval > 0)
recentIntervals.Add(interval);
}
if (recentIntervals.Count < 2)
return false;
var mean = recentIntervals.Average();
var variance = recentIntervals.Average(x => Math.Pow(x - mean, 2));
var stdDev = Math.Sqrt(variance);
// Event times are anomalous if they're too frequent (low stdDev) or erratic (high stdDev)
return stdDev < 0.1 || stdDev > stdDevThreshold;
}
}
}
/// <summary>
/// Tracks statistics for a log level.
/// </summary>
internal class LevelStatistics
{
private readonly Queue<DateTime> _eventTimes = new Queue<DateTime>();
private readonly object _lock = new object();
private long _errorCount;
public void RecordEvent()
{
lock (_lock)
{
_errorCount++;
_eventTimes.Enqueue(DateTime.UtcNow);
if (_eventTimes.Count > 10000)
_eventTimes.Dequeue();
}
}
public double GetErrorRatePerSecond()
{
lock (_lock)
{
if (_eventTimes.Count < 2)
return 0;
var now = DateTime.UtcNow;
var span = (now - _eventTimes.Peek()).TotalSeconds;
if (span < 1)
return _eventTimes.Count;
return _eventTimes.Count / span;
}
}
}
/// <summary>
/// An alert raised when an anomaly is detected.
/// </summary>
public class AnomalyAlert
{
public DateTime Timestamp { get; set; }
public string Category { get; set; }
public LogLevel Level { get; set; }
public LogEvent LogEvent { get; set; }
public List<string> Anomalies { get; set; } = new List<string>();
public AnomalySeverity AnomalySeverity { get; set; }
public SystemMetrics SystemMetrics { get; set; }
}
/// <summary>
/// Severity of detected anomaly.
/// </summary>
public enum AnomalySeverity
{
Low,
Medium,
High,
Critical
}
/// <summary>
/// System metrics captured when anomaly detected.
/// </summary>
public class SystemMetrics
{
public long ProcessMemoryMb { get; set; }
public int ProcessorCount { get; set; }
public TimeSpan Uptime { get; set; }
public int ThreadCount { get; set; }
public long TotalEventsProcessed { get; set; }
}
/// <summary>
/// Statistics about anomaly detector.
/// </summary>
public class AnomalyDetectorStats
{
public long TotalEventsProcessed { get; set; }
public int UniqueCategories { get; set; }
public int RecentAnomalies { get; set; }
public double AnomalyRate { get; set; }
public List<CategorySummary> TopCategories { get; set; } = new List<CategorySummary>();
}
/// <summary>
/// Summary of a category's status.
/// </summary>
public class CategorySummary
{
public string Category { get; set; }
public long EventCount { get; set; }
public bool IsAnomalous { get; set; }
}
@@ -0,0 +1,392 @@
using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
// 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.Diagnostics;
/// <summary>
/// Context Snapshot Collector - captures heap snapshots and thread stacks on critical errors.
/// Superior to simple error logging as it provides full context for debugging.
/// </summary>
public class ContextSnapshotCollector
{
private readonly List<ContextSnapshot> _snapshots = new List<ContextSnapshot>();
private readonly int _maxSnapshots;
private readonly object _snapshotsLock = new object();
private readonly LogLevel _triggerLevel;
private long _totalSnapshotsCaptured;
public event EventHandler<ContextSnapshot> SnapshotCaptured;
public ContextSnapshotCollector(
LogLevel triggerLevel = LogLevel.Critical,
int maxSnapshots = 100)
{
_triggerLevel = triggerLevel;
_maxSnapshots = Math.Max(10, maxSnapshots);
}
/// <summary>
/// Captures a context snapshot if the log event meets trigger conditions.
/// </summary>
public ContextSnapshot CaptureIfNeeded(LogEvent logEvent)
{
if (logEvent.Level < _triggerLevel)
return null;
return CaptureSnapshot(logEvent);
}
/// <summary>
/// Captures a full context snapshot for the given log event.
/// </summary>
public ContextSnapshot CaptureSnapshot(LogEvent logEvent)
{
var snapshot = new ContextSnapshot
{
CapturedAt = DateTime.UtcNow,
LogEvent = logEvent,
ProcessInfo = CaptureProcessInfo(),
MemoryInfo = CaptureMemoryInfo(),
ThreadInfo = CaptureThreadInfo(),
StackTraces = CaptureStackTraces(),
EnvironmentVars = CaptureEnvironmentSnapshot()
};
lock (_snapshotsLock)
{
_snapshots.Add(snapshot);
if (_snapshots.Count > _maxSnapshots)
_snapshots.RemoveAt(0);
_totalSnapshotsCaptured++;
}
SnapshotCaptured?.Invoke(this, snapshot);
return snapshot;
}
/// <summary>
/// Gets the most recent snapshot.
/// </summary>
public ContextSnapshot GetLatestSnapshot()
{
lock (_snapshotsLock)
{
return _snapshots.LastOrDefault();
}
}
/// <summary>
/// Gets all captured snapshots.
/// </summary>
public List<ContextSnapshot> GetSnapshots(int skip = 0, int take = 10)
{
lock (_snapshotsLock)
{
return _snapshots.Skip(skip).Take(take).ToList();
}
}
/// <summary>
/// Gets a snapshot by timestamp window.
/// </summary>
public ContextSnapshot GetSnapshotNear(DateTime timestamp)
{
lock (_snapshotsLock)
{
return _snapshots
.OrderBy(s => Math.Abs((s.CapturedAt - timestamp).TotalSeconds))
.FirstOrDefault();
}
}
/// <summary>
/// Gets statistics about captured snapshots.
/// </summary>
public SnapshotCollectorStats GetStats()
{
lock (_snapshotsLock)
{
var levelDistribution = _snapshots
.GroupBy(s => s.LogEvent.Level)
.ToDictionary(g => g.Key, g => g.Count());
return new SnapshotCollectorStats
{
TotalCaptured = _totalSnapshotsCaptured,
CurrentSnapshots = _snapshots.Count,
MaxCapacity = _maxSnapshots,
LevelDistribution = levelDistribution,
OldestSnapshot = _snapshots.FirstOrDefault()?.CapturedAt,
NewestSnapshot = _snapshots.LastOrDefault()?.CapturedAt
};
}
}
/// <summary>
/// Clears all snapshots.
/// </summary>
public int ClearSnapshots()
{
lock (_snapshotsLock)
{
var count = _snapshots.Count;
_snapshots.Clear();
return count;
}
}
private ProcessSnapshot CaptureProcessInfo()
{
var process = Process.GetCurrentProcess();
return new ProcessSnapshot
{
ProcessId = process.Id,
ProcessName = process.ProcessName,
StartTime = process.StartTime,
TotalProcessorTime = process.TotalProcessorTime,
UserProcessorTime = process.UserProcessorTime,
BasePriority = process.BasePriority,
Handles = 0, // Not available in .NET Standard
ThreadCount = process.Threads.Count
};
}
private MemorySnapshot CaptureMemoryInfo()
{
var totalMemory = GC.GetTotalMemory(false);
var process = Process.GetCurrentProcess();
var gen0Collections = GC.CollectionCount(0);
var gen1Collections = GC.CollectionCount(1);
var gen2Collections = GC.CollectionCount(2);
return new MemorySnapshot
{
ManagedHeapBytes = totalMemory,
ProcessWorkingSetBytes = process.WorkingSet64,
ProcessPrivateMemoryBytes = process.PrivateMemorySize64,
Gen0Collections = gen0Collections,
Gen1Collections = gen1Collections,
Gen2Collections = gen2Collections,
TotalAllocatedBytes = totalMemory, // Simplified fallback
IsHighMemoryPressure = false // Conservative estimate
};
}
private ThreadSnapshot CaptureThreadInfo()
{
var process = Process.GetCurrentProcess();
var threads = process.Threads;
var threadDetails = new List<ThreadDetail>();
try
{
foreach (ProcessThread t in threads)
{
threadDetails.Add(new ThreadDetail
{
ThreadId = t.Id,
State = t.ThreadState.ToString(),
WaitReason = t.WaitReason.ToString(),
Priority = t.CurrentPriority
});
}
}
catch
{
// Some properties may not be available on all platforms
}
return new ThreadSnapshot
{
TotalThreadCount = threads.Count,
ThreadIds = threadDetails,
ManagedThreadCount = 0 // ThreadPool.ThreadCount not available in .NET Standard
};
}
private Dictionary<int, string[]> CaptureStackTraces()
{
var stacks = new Dictionary<int, string[]>();
try
{
// Get current thread stack
var currentThread = Thread.CurrentThread;
var stackFrames = new StackTrace(true).GetFrames();
if (stackFrames != null)
{
stacks[currentThread.ManagedThreadId] = stackFrames
.Select(f => f.GetMethod()?.Name ?? "Unknown")
.ToArray();
}
}
catch
{
// If we can't get stack traces, just skip
}
return stacks;
}
private Dictionary<string, string> CaptureEnvironmentSnapshot()
{
var env = new Dictionary<string, string>();
// Capture selected environment variables (not all for security)
var safeVars = new[]
{
"DOTNET_VERSION",
"PROCESSOR_COUNT",
"OS",
"RUNNING_UNDER_DOCKER",
"ASPNETCORE_ENVIRONMENT"
};
foreach (var varName in safeVars)
{
var varValue = Environment.GetEnvironmentVariable(varName);
if (varValue != null)
env[varName] = varValue;
}
// Add runtime info
env["RUNTIME_VERSION"] = RuntimeInformation.FrameworkDescription;
env["PROCESSOR_COUNT"] = Environment.ProcessorCount.ToString();
return env;
}
}
/// <summary>
/// A captured context snapshot.
/// </summary>
public class ContextSnapshot
{
/// <summary>
/// When this snapshot was captured.
/// </summary>
public DateTime CapturedAt { get; set; }
/// <summary>
/// The log event that triggered this snapshot.
/// </summary>
public LogEvent LogEvent { get; set; }
/// <summary>
/// Process information at time of capture.
/// </summary>
public ProcessSnapshot ProcessInfo { get; set; }
/// <summary>
/// Memory information at time of capture.
/// </summary>
public MemorySnapshot MemoryInfo { get; set; }
/// <summary>
/// Thread information at time of capture.
/// </summary>
public ThreadSnapshot ThreadInfo { get; set; }
/// <summary>
/// Stack traces for active threads.
/// </summary>
public Dictionary<int, string[]> StackTraces { get; set; }
/// <summary>
/// Environment snapshot.
/// </summary>
public Dictionary<string, string> EnvironmentVars { get; set; }
}
/// <summary>
/// Process information snapshot.
/// </summary>
public class ProcessSnapshot
{
public int ProcessId { get; set; }
public string ProcessName { get; set; }
public DateTime StartTime { get; set; }
public TimeSpan TotalProcessorTime { get; set; }
public TimeSpan UserProcessorTime { get; set; }
public int BasePriority { get; set; }
public int Handles { get; set; }
public int ThreadCount { get; set; }
}
/// <summary>
/// Memory information snapshot.
/// </summary>
public class MemorySnapshot
{
public long ManagedHeapBytes { get; set; }
public long ProcessWorkingSetBytes { get; set; }
public long ProcessPrivateMemoryBytes { get; set; }
public int Gen0Collections { get; set; }
public int Gen1Collections { get; set; }
public int Gen2Collections { get; set; }
public long TotalAllocatedBytes { get; set; }
public bool IsHighMemoryPressure { get; set; }
}
/// <summary>
/// Thread information snapshot.
/// </summary>
public class ThreadSnapshot
{
public int TotalThreadCount { get; set; }
public List<ThreadDetail> ThreadIds { get; set; }
public int ManagedThreadCount { get; set; }
}
/// <summary>
/// Detail about a single thread.
/// </summary>
public class ThreadDetail
{
public int ThreadId { get; set; }
public string State { get; set; }
public string WaitReason { get; set; }
public int Priority { get; set; }
}
/// <summary>
/// Statistics about the snapshot collector.
/// </summary>
public class SnapshotCollectorStats
{
public long TotalCaptured { get; set; }
public int CurrentSnapshots { get; set; }
public int MaxCapacity { get; set; }
public Dictionary<LogLevel, int> LevelDistribution { get; set; }
public DateTime? OldestSnapshot { get; set; }
public DateTime? NewestSnapshot { get; set; }
}
/// <summary>
/// Runtime information helper.
/// </summary>
internal static class RuntimeInformation
{
public static string FrameworkDescription
{
get
{
#if NET6_0_OR_GREATER
return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;
#else
return ".NET " + Environment.Version;
#endif
}
}
}
@@ -0,0 +1,307 @@
using EonaCat.LogStack.Core;
using EonaCat.LogStack.Flows;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
namespace EonaCat.LogStack.DeadLettering;
/// <summary>
/// Dead Letter Queue (DLQ) for capturing failed log attempts and enabling replay.
/// Superior to data loss as it preserves logs that couldn't be delivered to any flow.
/// </summary>
public class DeadLetterQueue
{
private readonly ConcurrentQueue<DeadLetterEvent> _queue = new ConcurrentQueue<DeadLetterEvent>();
private readonly int _maxCapacity;
private int _currentCount;
private long _totalDropped;
private long _totalEnqueued;
private readonly object _statsLock = new object();
public event EventHandler<DeadLetterEvent> EventEnqueued;
public event EventHandler<DeadLetterEventReplayed> EventReplayed;
public DeadLetterQueue(int maxCapacity = 100000)
{
_maxCapacity = Math.Max(1000, maxCapacity);
}
/// <summary>
/// Enqueues a failed log event with the reason for failure.
/// </summary>
public bool Enqueue(LogEvent logEvent, string failureReason, Exception failureException = null)
{
if (_currentCount >= _maxCapacity)
{
lock (_statsLock)
{
_totalDropped++;
}
return false;
}
var dlEvent = new DeadLetterEvent
{
LogEvent = logEvent,
EnqueuedAt = DateTime.UtcNow,
FailureReason = failureReason ?? "Unknown",
FailureException = failureException?.ToString(),
RetryCount = 0
};
_queue.Enqueue(dlEvent);
_currentCount++;
lock (_statsLock)
{
_totalEnqueued++;
}
EventEnqueued?.Invoke(this, dlEvent);
return true;
}
/// <summary>
/// Gets the count of events in the DLQ.
/// </summary>
public int GetCount() => _currentCount;
/// <summary>
/// Peeks at the next event without removing it.
/// </summary>
public bool TryPeek(out DeadLetterEvent dlEvent)
{
return _queue.TryPeek(out dlEvent);
}
/// <summary>
/// Dequeues the next event.
/// </summary>
public bool TryDequeue(out DeadLetterEvent dlEvent)
{
if (_queue.TryDequeue(out var evt))
{
_currentCount--;
dlEvent = evt;
return true;
}
dlEvent = null;
return false;
}
/// <summary>
/// Gets all events in the DLQ without removing them.
/// </summary>
public List<DeadLetterEvent> GetAll(int skip = 0, int take = 100)
{
return _queue.Skip(skip).Take(take).ToList();
}
/// <summary>
/// Replays a dead letter event to specified flows.
/// </summary>
public async Task<bool> ReplayAsync(DeadLetterEvent dlEvent, IEnumerable<IFlow> targetFlows)
{
if (dlEvent == null)
throw new ArgumentNullException(nameof(dlEvent));
dlEvent.RetryCount++;
dlEvent.LastRetryAt = DateTime.UtcNow;
var flowsList = targetFlows.ToList();
var tasks = new List<Task<WriteResult>>();
foreach (var flow in flowsList)
{
tasks.Add(flow.BlastAsync(dlEvent.LogEvent));
}
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
var success = results.Any(r => r == WriteResult.Success);
if (success)
{
dlEvent.ReplayedAt = DateTime.UtcNow;
dlEvent.ReplaySuccessful = true;
EventReplayed?.Invoke(this, new DeadLetterEventReplayed
{
Event = dlEvent,
WasSuccessful = true,
FlowsTargeted = flowsList.Count
});
}
return success;
}
/// <summary>
/// Bulk replay of oldest events (useful for periodic retry jobs).
/// </summary>
public async Task<int> ReplayOldestAsync(int count, IEnumerable<IFlow> targetFlows)
{
var toReplay = GetAll(0, count);
int successCount = 0;
foreach (var dlEvent in toReplay)
{
if (await ReplayAsync(dlEvent, targetFlows))
{
successCount++;
// Remove from queue if successful
_currentCount--;
}
}
return successCount;
}
/// <summary>
/// Clears all events from the DLQ.
/// </summary>
public int Clear()
{
var count = _currentCount;
while (_queue.TryDequeue(out _))
{
_currentCount--;
}
return count;
}
/// <summary>
/// Gets statistics about the DLQ.
/// </summary>
public DeadLetterQueueStats GetStats()
{
lock (_statsLock)
{
return new DeadLetterQueueStats
{
CurrentQueueSize = _currentCount,
MaxCapacity = _maxCapacity,
TotalEnqueued = _totalEnqueued,
TotalDropped = _totalDropped,
QueueUtilizationPercent = (_currentCount * 100.0) / _maxCapacity,
OldestEventAge = _queue.TryPeek(out var oldest)
? DateTime.UtcNow - oldest.EnqueuedAt
: TimeSpan.Zero
};
}
}
/// <summary>
/// Gets events grouped by failure reason.
/// </summary>
public Dictionary<string, int> GetFailureReasonStats()
{
var stats = new Dictionary<string, int>();
foreach (var evt in _queue)
{
var reason = evt.FailureReason ?? "Unknown";
if (!stats.ContainsKey(reason))
stats[reason] = 0;
stats[reason]++;
}
return stats.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value);
}
}
/// <summary>
/// Represents a dead letter event.
/// </summary>
public class DeadLetterEvent
{
/// <summary>
/// The original log event that failed to be delivered.
/// </summary>
public LogEvent LogEvent { get; set; }
/// <summary>
/// When this event was enqueued to the DLQ.
/// </summary>
public DateTime EnqueuedAt { get; set; }
/// <summary>
/// Reason why the log event failed.
/// </summary>
public string FailureReason { get; set; }
/// <summary>
/// Exception details if available.
/// </summary>
public string FailureException { get; set; }
/// <summary>
/// Number of times this event has been retried.
/// </summary>
public int RetryCount { get; set; }
/// <summary>
/// When this event was last retried.
/// </summary>
public DateTime? LastRetryAt { get; set; }
/// <summary>
/// When this event was successfully replayed (if at all).
/// </summary>
public DateTime? ReplayedAt { get; set; }
/// <summary>
/// Whether the replay was successful.
/// </summary>
public bool ReplaySuccessful { get; set; }
}
/// <summary>
/// Event raised when a dead letter event is replayed.
/// </summary>
public class DeadLetterEventReplayed
{
public DeadLetterEvent Event { get; set; }
public bool WasSuccessful { get; set; }
public int FlowsTargeted { get; set; }
}
/// <summary>
/// Statistics about the dead letter queue.
/// </summary>
public class DeadLetterQueueStats
{
/// <summary>
/// Current number of events in the queue.
/// </summary>
public int CurrentQueueSize { get; set; }
/// <summary>
/// Maximum capacity of the queue.
/// </summary>
public int MaxCapacity { get; set; }
/// <summary>
/// Total events ever enqueued.
/// </summary>
public long TotalEnqueued { get; set; }
/// <summary>
/// Total events dropped due to capacity.
/// </summary>
public long TotalDropped { get; set; }
/// <summary>
/// Queue utilization as percentage.
/// </summary>
public double QueueUtilizationPercent { get; set; }
/// <summary>
/// Age of the oldest event in queue.
/// </summary>
public TimeSpan OldestEventAge { get; set; }
}
@@ -0,0 +1,203 @@
using EonaCat.LogStack.Core;
using EonaCat.LogStack.DeadLettering;
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
namespace EonaCat.LogStack.Flows;
/// <summary>
/// Dead Letter Queue Flow - captures log events that failed to be delivered to other flows.
/// Enables analysis and replay of failed logs.
/// </summary>
public class DlqFlow : FlowBase
{
private readonly DeadLetterQueue _dlq;
private readonly List<IFlow> _backupFlows = new List<IFlow>();
private readonly object _flowsLock = new object();
private int _disposed;
public DlqFlow(
string name = "DLQ",
LogLevel minimumLevel = LogLevel.Warning,
int maxCapacity = 100000)
: base(name, minimumLevel)
{
_dlq = new DeadLetterQueue(maxCapacity);
}
/// <summary>
/// Gets the underlying dead letter queue for inspection and replay.
/// </summary>
public DeadLetterQueue GetDeadLetterQueue() => _dlq;
/// <summary>
/// Adds a backup flow to attempt retrying failed events.
/// </summary>
public DlqFlow AddBackupFlow(IFlow flow)
{
if (flow == null) throw new ArgumentNullException(nameof(flow));
lock (_flowsLock)
{
_backupFlows.Add(flow);
}
return this;
}
/// <summary>
/// Blasts a single log event to the DLQ.
/// </summary>
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (_disposed != 0)
return WriteResult.Dropped;
if (!IsLogLevelEnabled(logEvent))
return WriteResult.Success;
// Try backup flows first before enqueueing to DLQ
List<IFlow> backupFlows;
lock (_flowsLock)
{
backupFlows = new List<IFlow>(_backupFlows);
}
bool anyBackupSucceeded = false;
var failureReasons = new List<string>();
foreach (var backupFlow in backupFlows)
{
try
{
var result = await backupFlow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
if (result == WriteResult.Success)
{
anyBackupSucceeded = true;
break;
}
failureReasons.Add($"Backup flow '{backupFlow.Name}': failed");
}
catch (Exception ex)
{
failureReasons.Add($"Backup flow '{backupFlow.Name}' exception: {ex.Message}");
}
}
if (anyBackupSucceeded)
{
BlastedCount++;
return WriteResult.Success;
}
// All backups failed, enqueue to DLQ
var failureReason = string.Join("; ", failureReasons);
if (failureReason.Length > 500)
failureReason = failureReason.Substring(0, 500) + "...";
var enqueued = _dlq.Enqueue(logEvent, failureReason ?? "Unknown failure");
if (enqueued)
{
BlastedCount++;
return WriteResult.Success;
}
DroppedCount++;
return WriteResult.Dropped;
}
/// <summary>
/// Blasts a batch of log events to the DLQ.
/// </summary>
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
{
if (_disposed != 0)
return WriteResult.Dropped;
var events = logEvents.ToArray();
int successCount = 0;
foreach (var logEvent in events)
{
var result = await BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
if (result == WriteResult.Success)
successCount++;
}
return successCount > 0 ? WriteResult.Success : WriteResult.Dropped;
}
/// <summary>
/// Flushes the DLQ (does nothing, but required by interface).
/// </summary>
public override Task FlushAsync(CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
/// <summary>
/// Gets statistics about this DLQ flow.
/// </summary>
public DeadLetterQueueStats GetStatistics()
{
return _dlq.GetStats();
}
/// <summary>
/// Attempts to replay queued dead letters to backup flows.
/// </summary>
public async Task<int> ReplayOldestAsync(int count)
{
List<IFlow> backupFlows;
lock (_flowsLock)
{
backupFlows = new List<IFlow>(_backupFlows);
}
return await _dlq.ReplayOldestAsync(count, (IEnumerable<IFlow>)backupFlows).ConfigureAwait(false);
}
/// <summary>
/// Gets all dead letter events.
/// </summary>
public List<DeadLetterEvent> GetDeadLetters(int skip = 0, int take = 100)
{
return _dlq.GetAll(skip, take);
}
/// <summary>
/// Clears all dead letters from the queue.
/// </summary>
public int ClearDeadLetters()
{
return _dlq.Clear();
}
public override async ValueTask DisposeAsync()
{
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
return;
List<IFlow> flowsToDispose;
lock (_flowsLock)
{
flowsToDispose = new List<IFlow>(_backupFlows);
_backupFlows.Clear();
}
foreach (var flow in flowsToDispose)
{
try
{
await flow.DisposeAsync().ConfigureAwait(false);
}
catch { }
}
await base.DisposeAsync().ConfigureAwait(false);
}
}
@@ -0,0 +1,277 @@
using EonaCat.LogStack.Core;
using EonaCat.LogStack.Flows;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
namespace EonaCat.LogStack.Routing;
/// <summary>
/// Intelligent message router that directs log events to different flows based on pattern matching and rules.
/// This is superior to simple level-based routing as it can apply complex business logic.
/// </summary>
public class IntelligentRouter : IAsyncDisposable
{
private readonly List<RoutingRule> _rules = new List<RoutingRule>();
private readonly Dictionary<string, FlowRoute> _flowRoutes = new Dictionary<string, FlowRoute>();
private readonly object _rulesLock = new object();
public IntelligentRouter()
{
}
/// <summary>
/// Adds a routing rule that matches messages based on pattern and routes them to specific flows.
/// </summary>
public IntelligentRouter AddRule(RoutingRule rule)
{
if (rule == null) throw new ArgumentNullException(nameof(rule));
lock (_rulesLock)
{
_rules.Add(rule);
// Sort by priority descending so highest priority rules are checked first
_rules.Sort((a, b) => b.Priority.CompareTo(a.Priority));
}
return this;
}
/// <summary>
/// Adds a rule with fluent API support.
/// </summary>
public IntelligentRouter AddRule(
string name,
Func<LogEvent, bool> matcher,
IEnumerable<string> targetFlows,
int priority = 0,
bool stopIfMatched = false)
{
var rule = new RoutingRule
{
Name = name,
Matcher = matcher,
TargetFlows = targetFlows?.ToList() ?? new List<string>(),
Priority = priority,
StopIfMatched = stopIfMatched
};
return AddRule(rule);
}
/// <summary>
/// Adds a regex pattern-based routing rule.
/// </summary>
public IntelligentRouter AddPatternRule(
string name,
string messagePattern,
IEnumerable<string> targetFlows,
int priority = 0,
RegexOptions regexOptions = RegexOptions.IgnoreCase)
{
var regex = new Regex(messagePattern, regexOptions);
return AddRule(
name,
logEvent => regex.IsMatch(logEvent.Message.ToString()),
targetFlows,
priority
);
}
/// <summary>
/// Adds a category-based routing rule (routes logs from specific categories).
/// </summary>
public IntelligentRouter AddCategoryRule(
string category,
IEnumerable<string> targetFlows,
LogLevel? minimumLevel = null)
{
return AddRule(
$"Category:{category}",
logEvent => logEvent.Category == category && (minimumLevel == null || logEvent.Level >= minimumLevel),
targetFlows,
priority: 10
);
}
/// <summary>
/// Adds an exception type-based routing rule.
/// </summary>
public IntelligentRouter AddExceptionRule(
string exceptionTypeName,
IEnumerable<string> targetFlows)
{
return AddRule(
$"Exception:{exceptionTypeName}",
logEvent => logEvent.Exception?.GetType().Name == exceptionTypeName,
targetFlows,
priority: 20,
stopIfMatched: true
);
}
/// <summary>
/// Registers a flow that can be targeted by routing rules.
/// </summary>
public IntelligentRouter RegisterFlow(string flowName, IFlow flow)
{
if (string.IsNullOrEmpty(flowName)) throw new ArgumentNullException(nameof(flowName));
if (flow == null) throw new ArgumentNullException(nameof(flow));
lock (_rulesLock)
{
_flowRoutes[flowName] = new FlowRoute { Flow = flow, IsEnabled = true };
}
return this;
}
/// <summary>
/// Enables or disables a specific flow by name.
/// </summary>
public IntelligentRouter SetFlowEnabled(string flowName, bool enabled)
{
lock (_rulesLock)
{
if (_flowRoutes.TryGetValue(flowName, out var route))
{
route.IsEnabled = enabled;
}
}
return this;
}
/// <summary>
/// Routes a log event to appropriate flows based on rules.
/// Returns true if at least one flow accepted the message.
/// </summary>
public async Task<bool> RouteAsync(LogEvent logEvent)
{
List<RoutingRule> activeRules;
Dictionary<string, FlowRoute> activeFlows;
lock (_rulesLock)
{
activeRules = new List<RoutingRule>(_rules);
activeFlows = new Dictionary<string, FlowRoute>(_flowRoutes);
}
var targetFlowNames = new HashSet<string>();
bool shouldStop = false;
// Apply rules in priority order
foreach (var rule in activeRules)
{
if (rule.Matcher(logEvent))
{
foreach (var flowName in rule.TargetFlows)
{
targetFlowNames.Add(flowName);
}
if (rule.StopIfMatched)
{
shouldStop = true;
break;
}
}
}
// Route to target flows
bool anySuccess = false;
var tasks = new List<Task>();
foreach (var flowName in targetFlowNames)
{
if (activeFlows.TryGetValue(flowName, out var flowRoute) && flowRoute.IsEnabled)
{
tasks.Add(flowRoute.Flow.BlastAsync(logEvent)
.ContinueWith(t =>
{
if (t.Status == TaskStatus.RanToCompletion && t.Result == WriteResult.Success)
anySuccess = true;
}));
}
}
if (tasks.Any())
{
await Task.WhenAll(tasks).ConfigureAwait(false);
}
return anySuccess || targetFlowNames.Count == 0; // Return true if no rules matched (default routing)
}
/// <summary>
/// Gets statistics about the router.
/// </summary>
public RouterStats GetStats()
{
lock (_rulesLock)
{
return new RouterStats
{
TotalRules = _rules.Count,
RegisteredFlows = _flowRoutes.Count,
EnabledFlows = _flowRoutes.Values.Count(f => f.IsEnabled)
};
}
}
public async ValueTask DisposeAsync()
{
lock (_rulesLock)
{
_rules.Clear();
_flowRoutes.Clear();
}
}
private class FlowRoute
{
public IFlow Flow { get; set; }
public bool IsEnabled { get; set; }
}
}
/// <summary>
/// Defines a routing rule for intelligent message routing.
/// </summary>
public class RoutingRule
{
/// <summary>
/// Name of this rule for identification/debugging.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Function that determines if this rule matches a log event.
/// </summary>
public Func<LogEvent, bool> Matcher { get; set; }
/// <summary>
/// Names of flows this rule routes to.
/// </summary>
public List<string> TargetFlows { get; set; } = new List<string>();
/// <summary>
/// Priority of this rule (higher priority rules are evaluated first).
/// </summary>
public int Priority { get; set; } = 0;
/// <summary>
/// If true, stops evaluating further rules after this one matches.
/// </summary>
public bool StopIfMatched { get; set; } = false;
}
/// <summary>
/// Statistics about the intelligent router.
/// </summary>
public class RouterStats
{
public int TotalRules { get; set; }
public int RegisteredFlows { get; set; }
public int EnabledFlows { get; set; }
}
@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace EonaCat.LogStack.PerformanceInsights
{
/// <summary>
/// Analyzes performance metrics and provides optimization insights.
/// </summary>
public sealed class PerformanceAnalyzer
{
private readonly PerformanceInsightsCollector _collector;
public PerformanceAnalyzer(PerformanceInsightsCollector collector = null)
{
_collector = collector ?? new PerformanceInsightsCollector();
}
/// <summary>
/// Analyzes current performance state and returns insights
/// </summary>
public PerformanceAnalysis Analyze()
{
var snapshot = _collector.GetSnapshot();
var recommendations = _collector.GetRecommendations();
var analysis = new PerformanceAnalysis
{
AnalyzedAt = DateTime.UtcNow,
Uptime = snapshot.Uptime,
TotalOperations = snapshot.TotalOperations,
AverageErrorRate = snapshot.ErrorRatePercent,
Recommendations = recommendations,
Summary = GenerateSummary(snapshot, recommendations)
};
return analysis;
}
/// <summary>
/// Generates a summary string of the analysis
/// </summary>
private string GenerateSummary(PerformanceInsightsSnapshot snapshot, List<PerformanceRecommendation> recommendations)
{
var lines = new List<string>
{
$"Performance Analysis - {snapshot.CapturedAt:O}",
$"Uptime: {snapshot.Uptime.TotalSeconds:F2}s",
$"Total Operations: {snapshot.TotalOperations}",
$"Total Errors: {snapshot.TotalErrors}",
$"Error Rate: {snapshot.ErrorRatePercent:F2}%",
"",
"Top Slowest Operations:"
};
foreach (var op in snapshot.SlowestOperations.Take(3))
{
lines.Add($" - {op.Name}: {op.AverageDurationMs:F2}ms (min: {op.MinDurationMs:F2}ms, max: {op.MaxDurationMs:F2}ms)");
}
if (snapshot.MostErrorsOperations.Any())
{
lines.Add("");
lines.Add("Operations with Most Errors:");
foreach (var op in snapshot.MostErrorsOperations.Take(3))
{
lines.Add($" - {op.Name}: {op.ErrorCount} errors ({op.ErrorRatePercent:F2}% error rate)");
}
}
if (recommendations.Any())
{
lines.Add("");
lines.Add("Recommendations:");
foreach (var rec in recommendations)
{
lines.Add($" [{rec.Severity}] {rec.Category}: {rec.Message}");
foreach (var op in rec.AffectedOperations.Take(3))
{
lines.Add($" - {op}");
}
}
}
return string.Join(Environment.NewLine, lines);
}
}
/// <summary>
/// Performance analysis result
/// </summary>
public sealed class PerformanceAnalysis
{
public DateTime AnalyzedAt { get; set; }
public TimeSpan Uptime { get; set; }
public long TotalOperations { get; set; }
public double AverageErrorRate { get; set; }
public List<PerformanceRecommendation> Recommendations { get; set; } = new();
public string Summary { get; set; }
}
}
@@ -0,0 +1,199 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace EonaCat.LogStack.PerformanceInsights
{
/// <summary>
/// Collects performance insights from multiple flows and operations.
/// Detects bottlenecks and provides recommendations.
/// </summary>
public sealed class PerformanceInsightsCollector
{
private readonly ConcurrentDictionary<string, PerformanceTracker> _trackers = new();
private long _totalOperations;
private long _totalErrors;
private DateTime _startTime = DateTime.UtcNow;
/// <summary>
/// Gets or creates a tracker for the given name
/// </summary>
public PerformanceTracker GetOrCreateTracker(string name)
{
return _trackers.GetOrAdd(name, _ => new PerformanceTracker(name));
}
/// <summary>
/// Gets a tracker if it exists
/// </summary>
public PerformanceTracker? GetTracker(string name)
{
return _trackers.TryGetValue(name, out var tracker) ? tracker : null;
}
/// <summary>
/// Records an operation
/// </summary>
public void RecordOperation(string name, long durationTicks, long byteCount = 0, bool isError = false)
{
var tracker = GetOrCreateTracker(name);
tracker.RecordOperation(durationTicks, byteCount, isError);
_totalOperations++;
if (isError) _totalErrors++;
}
/// <summary>
/// Gets all current trackers
/// </summary>
public IReadOnlyCollection<PerformanceTracker> GetAllTrackers()
{
return _trackers.Values.ToList().AsReadOnly();
}
/// <summary>
/// Gets current insights snapshot
/// </summary>
public PerformanceInsightsSnapshot GetSnapshot()
{
var uptime = DateTime.UtcNow - _startTime;
var metrics = _trackers.Values.Select(t => t.GetMetrics()).ToList();
var slowestOps = metrics
.OrderByDescending(m => m.AverageDurationMs)
.Take(5)
.ToList();
var mostErrors = metrics
.OrderByDescending(m => m.ErrorCount)
.Take(5)
.ToList();
var topThroughput = metrics
.OrderByDescending(m => m.ThroughputOpsPerSec)
.Take(5)
.ToList();
return new PerformanceInsightsSnapshot
{
Uptime = uptime,
TotalOperations = _totalOperations,
TotalErrors = _totalErrors,
ErrorRatePercent = _totalOperations > 0 ? (_totalErrors * 100.0) / _totalOperations : 0,
AllMetrics = metrics,
SlowestOperations = slowestOps,
MostErrorsOperations = mostErrors,
HighestThroughputOperations = topThroughput,
CapturedAt = DateTime.UtcNow
};
}
/// <summary>
/// Gets performance recommendations based on current metrics
/// </summary>
public List<PerformanceRecommendation> GetRecommendations()
{
var recommendations = new List<PerformanceRecommendation>();
var snapshot = GetSnapshot();
// Check for high error rates
if (snapshot.ErrorRatePercent > 5)
{
recommendations.Add(new PerformanceRecommendation
{
Category = "ErrorRate",
Severity = Severity.High,
Message = $"Error rate is {snapshot.ErrorRatePercent:F2}%. Consider investigating error causes.",
AffectedOperations = snapshot.MostErrorsOperations.Select(m => m.Name).ToList()
});
}
// Check for slow operations
var verySlowOps = snapshot.AllMetrics
.Where(m => m.AverageDurationMs > 1000)
.ToList();
if (verySlowOps.Any())
{
recommendations.Add(new PerformanceRecommendation
{
Category = "SlowOperations",
Severity = Severity.Medium,
Message = "Some operations are very slow (>1000ms avg). Consider optimization or async batching.",
AffectedOperations = verySlowOps.Select(m => m.Name).ToList()
});
}
// Check for high variance
var highVarianceOps = snapshot.AllMetrics
.Where(m => m.MaxDurationMs > m.AverageDurationMs * 10)
.ToList();
if (highVarianceOps.Any())
{
recommendations.Add(new PerformanceRecommendation
{
Category = "HighVariance",
Severity = Severity.Low,
Message = "High variance detected in operation times. May indicate GC pauses or resource contention.",
AffectedOperations = highVarianceOps.Select(m => m.Name).ToList()
});
}
return recommendations;
}
/// <summary>
/// Resets all trackers
/// </summary>
public void Reset()
{
foreach (var tracker in _trackers.Values)
{
tracker.Reset();
}
_totalOperations = 0;
_totalErrors = 0;
_startTime = DateTime.UtcNow;
}
}
/// <summary>
/// Snapshot of performance insights
/// </summary>
public sealed class PerformanceInsightsSnapshot
{
public TimeSpan Uptime { get; set; }
public long TotalOperations { get; set; }
public long TotalErrors { get; set; }
public double ErrorRatePercent { get; set; }
public List<PerformanceMetrics> AllMetrics { get; set; } = new();
public List<PerformanceMetrics> SlowestOperations { get; set; } = new();
public List<PerformanceMetrics> MostErrorsOperations { get; set; } = new();
public List<PerformanceMetrics> HighestThroughputOperations { get; set; } = new();
public DateTime CapturedAt { get; set; }
}
/// <summary>
/// Performance recommendation
/// </summary>
public sealed class PerformanceRecommendation
{
public string Category { get; set; }
public Severity Severity { get; set; }
public string Message { get; set; }
public List<string> AffectedOperations { get; set; } = new();
}
/// <summary>
/// Severity level
/// </summary>
public enum Severity
{
Low,
Medium,
High,
Critical
}
}
@@ -0,0 +1,160 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace EonaCat.LogStack.PerformanceInsights
{
/// <summary>
/// Tracks performance metrics for flows and operations.
/// Measures latency, throughput, and allocations with minimal overhead.
/// </summary>
public sealed class PerformanceTracker
{
private long _operationCount;
private long _totalDurationTicks;
private long _minDurationTicks = long.MaxValue;
private long _maxDurationTicks;
private long _errorCount;
private long _byteCount;
private readonly object _lock = new();
/// <summary>
/// Gets the operation name
/// </summary>
public string Name { get; }
/// <summary>
/// Gets total operations recorded
/// </summary>
public long OperationCount => _operationCount;
/// <summary>
/// Gets total duration in milliseconds
/// </summary>
public double TotalDurationMs => _totalDurationTicks * 1000.0 / Stopwatch.Frequency;
/// <summary>
/// Gets average duration in milliseconds
/// </summary>
public double AverageDurationMs => _operationCount > 0 ? TotalDurationMs / _operationCount : 0;
/// <summary>
/// Gets minimum duration in milliseconds
/// </summary>
public double MinDurationMs => _minDurationTicks == long.MaxValue ? 0 : _minDurationTicks * 1000.0 / Stopwatch.Frequency;
/// <summary>
/// Gets maximum duration in milliseconds
/// </summary>
public double MaxDurationMs => _maxDurationTicks * 1000.0 / Stopwatch.Frequency;
/// <summary>
/// Gets throughput in operations per second
/// </summary>
public double ThroughputOpsPerSec => TotalDurationMs > 0 ? (_operationCount / TotalDurationMs) * 1000 : 0;
/// <summary>
/// Gets total bytes processed
/// </summary>
public long ByteCount => _byteCount;
/// <summary>
/// Gets error count
/// </summary>
public long ErrorCount => _errorCount;
/// <summary>
/// Gets error rate percentage
/// </summary>
public double ErrorRatePercent => _operationCount > 0 ? (_errorCount * 100.0) / _operationCount : 0;
public PerformanceTracker(string name)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
}
/// <summary>
/// Records an operation with its duration
/// </summary>
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void RecordOperation(long durationTicks, long byteCount = 0, bool isError = false)
{
lock (_lock)
{
_operationCount++;
_totalDurationTicks += durationTicks;
if (durationTicks < _minDurationTicks) _minDurationTicks = durationTicks;
if (durationTicks > _maxDurationTicks) _maxDurationTicks = durationTicks;
if (byteCount > 0) _byteCount += byteCount;
if (isError) _errorCount++;
}
}
/// <summary>
/// Resets all metrics
/// </summary>
public void Reset()
{
lock (_lock)
{
_operationCount = 0;
_totalDurationTicks = 0;
_minDurationTicks = long.MaxValue;
_maxDurationTicks = 0;
_errorCount = 0;
_byteCount = 0;
}
}
/// <summary>
/// Gets a snapshot of current metrics
/// </summary>
public PerformanceMetrics GetMetrics()
{
lock (_lock)
{
return new PerformanceMetrics
{
Name = Name,
OperationCount = _operationCount,
TotalDurationMs = TotalDurationMs,
AverageDurationMs = AverageDurationMs,
MinDurationMs = MinDurationMs,
MaxDurationMs = MaxDurationMs,
ThroughputOpsPerSec = ThroughputOpsPerSec,
ByteCount = _byteCount,
ErrorCount = _errorCount,
ErrorRatePercent = ErrorRatePercent,
CapturedAt = DateTime.UtcNow
};
}
}
}
/// <summary>
/// Snapshot of performance metrics at a point in time
/// </summary>
public sealed class PerformanceMetrics
{
public string Name { get; set; }
public long OperationCount { get; set; }
public double TotalDurationMs { get; set; }
public double AverageDurationMs { get; set; }
public double MinDurationMs { get; set; }
public double MaxDurationMs { get; set; }
public double ThroughputOpsPerSec { get; set; }
public long ByteCount { get; set; }
public long ErrorCount { get; set; }
public double ErrorRatePercent { get; set; }
public DateTime CapturedAt { get; set; }
/// <summary>
/// Gets a formatted string representation
/// </summary>
public override string ToString()
{
return $"{Name} | Ops: {OperationCount} | Avg: {AverageDurationMs:F2}ms | Thrput: {ThroughputOpsPerSec:F2} ops/s | Errors: {ErrorCount} ({ErrorRatePercent:F2}%)";
}
}
}
@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
namespace EonaCat.LogStack.Tracing
{
/// <summary>
/// Factory for creating and managing TracingSpans with automatic context management.
/// Supports parent-child relationships and automatic scope management.
/// </summary>
public sealed class SpanFactory
{
private readonly List<TracingSpan> _activeSpans = new();
private readonly object _activeLock = new();
/// <summary>
/// Creates a new span with auto-context inheritance
/// </summary>
public TracingSpan CreateSpan(string name, string kind = "internal")
{
var context = TraceContextManager.Current;
var span = new TracingSpan(name, context) { Kind = kind };
lock (_activeLock)
{
_activeSpans.Add(span);
}
return span;
}
/// <summary>
/// Creates a span and returns a scope that manages its lifetime
/// </summary>
public IDisposable CreateSpanScope(string name, string kind = "internal")
{
var span = CreateSpan(name, kind);
return new SpanScope(span, this);
}
/// <summary>
/// Creates a span with a child trace context
/// </summary>
public TracingSpan CreateChildSpan(string name, string kind = "internal")
{
using (TraceContextManager.CreateChildScope())
{
return CreateSpan(name, kind);
}
}
/// <summary>
/// Gets all active spans
/// </summary>
public IReadOnlyList<TracingSpan> GetActiveSpans()
{
lock (_activeLock)
{
return _activeSpans.AsReadOnly();
}
}
/// <summary>
/// Executes action within a span
/// </summary>
public void ExecuteInSpan(string name, Action action, string kind = "internal")
{
using (CreateSpanScope(name, kind))
{
action();
}
}
/// <summary>
/// Executes function within a span, returns result and span
/// </summary>
public (T Result, TracingSpan Span) ExecuteInSpan<T>(string name, Func<T> func, string kind = "internal")
{
var span = CreateSpan(name, kind);
try
{
var result = func();
span.End();
return (result, span);
}
catch (Exception ex)
{
span.RecordException(ex);
span.End(SpanStatus.Error, ex.Message);
throw;
}
}
/// <summary>
/// Removes a span from active list
/// </summary>
internal void RemoveSpan(TracingSpan span)
{
lock (_activeLock)
{
_activeSpans.Remove(span);
}
}
/// <summary>
/// Scope that manages span lifetime
/// </summary>
private sealed class SpanScope : IDisposable
{
private readonly TracingSpan _span;
private readonly SpanFactory _factory;
private bool _disposed;
public SpanScope(TracingSpan span, SpanFactory factory)
{
_span = span;
_factory = factory;
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
if (_span.Status == SpanStatus.Unset)
{
_span.End(SpanStatus.Ok);
}
_factory.RemoveSpan(_span);
}
}
}
}
@@ -0,0 +1,101 @@
using System;
using System.Collections.Generic;
namespace EonaCat.LogStack.Tracing
{
/// <summary>
/// Represents a trace context for distributed tracing across async boundaries.
/// Contains trace ID, span ID, parent span ID, and baggage for propagation.
/// </summary>
public sealed class TraceContext
{
/// <summary>
/// Gets or sets the trace ID (root identifier for entire trace)
/// </summary>
public string TraceId { get; set; }
/// <summary>
/// Gets or sets the span ID (current operation identifier)
/// </summary>
public string SpanId { get; set; }
/// <summary>
/// Gets or sets the parent span ID (parent operation identifier, if any)
/// </summary>
public string? ParentSpanId { get; set; }
/// <summary>
/// Gets or sets the correlation ID for request tracking
/// </summary>
public string? CorrelationId { get; set; }
/// <summary>
/// Gets or sets baggage (key-value pairs to propagate across spans)
/// </summary>
public Dictionary<string, string> Baggage { get; set; } = new();
/// <summary>
/// Gets or sets the trace flags (e.g., sampling decision)
/// </summary>
public byte TraceFlags { get; set; } = 0x01;
/// <summary>
/// Gets the creation timestamp
/// </summary>
public DateTime CreatedAt { get; } = DateTime.UtcNow;
/// <summary>
/// Gets whether this trace is sampled for export
/// </summary>
public bool IsSampled => (TraceFlags & 0x01) != 0;
/// <summary>
/// Creates a new TraceContext with a generated trace ID
/// </summary>
public TraceContext()
{
TraceId = GenerateId();
SpanId = GenerateId();
}
/// <summary>
/// Creates a child TraceContext from this context
/// </summary>
public TraceContext CreateChild()
{
return new TraceContext
{
TraceId = TraceId,
ParentSpanId = SpanId,
SpanId = GenerateId(),
CorrelationId = CorrelationId,
TraceFlags = TraceFlags,
Baggage = new Dictionary<string, string>(Baggage)
};
}
/// <summary>
/// Generates a random ID for trace/span
/// </summary>
private static string GenerateId()
{
return Guid.NewGuid().ToString("N").Substring(0, 16);
}
/// <summary>
/// Creates a copy of this TraceContext for propagation
/// </summary>
public TraceContext Clone()
{
return new TraceContext
{
TraceId = TraceId,
SpanId = SpanId,
ParentSpanId = ParentSpanId,
CorrelationId = CorrelationId,
TraceFlags = TraceFlags,
Baggage = new Dictionary<string, string>(Baggage)
};
}
}
}
@@ -0,0 +1,162 @@
using System;
using System.Threading;
namespace EonaCat.LogStack.Tracing
{
/// <summary>
/// Manages trace context across async boundaries using AsyncLocal.
/// Enables automatic trace/span propagation without explicit passing.
/// </summary>
public sealed class TraceContextManager
{
private static readonly AsyncLocal<TraceContext?> _current = new();
private static readonly TraceContextManager _instance = new();
/// <summary>
/// Gets the singleton instance
/// </summary>
public static TraceContextManager Instance => _instance;
/// <summary>
/// Gets the current trace context (creates new if not set)
/// </summary>
public static TraceContext Current
{
get => _current.Value ??= new TraceContext();
set => _current.Value = value;
}
/// <summary>
/// Gets the current trace context without creating
/// </summary>
public static TraceContext? TryGetCurrent() => _current.Value;
/// <summary>
/// Sets the trace context
/// </summary>
public static void SetCurrent(TraceContext context)
{
if (context == null) throw new ArgumentNullException(nameof(context));
_current.Value = context;
}
/// <summary>
/// Clears the current trace context
/// </summary>
public static void Clear()
{
_current.Value = null;
}
/// <summary>
/// Creates a new child context and sets it as current
/// </summary>
public static IDisposable CreateChildScope()
{
var parent = Current;
var child = parent.CreateChild();
_current.Value = child;
return new TraceScope(parent);
}
/// <summary>
/// Executes action with a new child trace context
/// </summary>
public static void WithChild(Action action)
{
using (CreateChildScope())
{
action();
}
}
/// <summary>
/// Executes function with a new child trace context
/// </summary>
public static T WithChild<T>(Func<T> func)
{
using (CreateChildScope())
{
return func();
}
}
/// <summary>
/// Gets the current trace ID
/// </summary>
public static string GetTraceId() => Current.TraceId;
/// <summary>
/// Gets the current span ID
/// </summary>
public static string GetSpanId() => Current.SpanId;
/// <summary>
/// Gets the current correlation ID
/// </summary>
public static string? GetCorrelationId() => Current.CorrelationId;
/// <summary>
/// Sets the correlation ID
/// </summary>
public static void SetCorrelationId(string correlationId)
{
Current.CorrelationId = correlationId;
}
/// <summary>
/// Adds baggage data
/// </summary>
public static void AddBaggage(string key, string value)
{
Current.Baggage[key] = value;
}
/// <summary>
/// Gets baggage value
/// </summary>
public static string? GetBaggage(string key)
{
return Current.Baggage.TryGetValue(key, out var value) ? value : null;
}
/// <summary>
/// Removes baggage value
/// </summary>
public static bool RemoveBaggage(string key)
{
return Current.Baggage.Remove(key);
}
/// <summary>
/// Sets sampling decision
/// </summary>
public static void SetSampled(bool sampled)
{
Current.TraceFlags = sampled ? (byte)0x01 : (byte)0x00;
}
/// <summary>
/// Gets sampling decision
/// </summary>
public static bool IsSampled() => Current.IsSampled;
/// <summary>
/// Scope disposed when exiting
/// </summary>
private sealed class TraceScope : IDisposable
{
private readonly TraceContext _parent;
public TraceScope(TraceContext parent)
{
_parent = parent;
}
public void Dispose()
{
_current.Value = _parent;
}
}
}
}
@@ -0,0 +1,211 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace EonaCat.LogStack.Tracing
{
/// <summary>
/// Enhanced span representation for distributed tracing.
/// Tracks operation timing, status, and linked operations.
/// </summary>
public sealed class TracingSpan : IDisposable
{
private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
private readonly List<Exception> _exceptions = new();
private bool _disposed;
/// <summary>
/// Gets the span name
/// </summary>
public string Name { get; }
/// <summary>
/// Gets the span kind (internal, server, client, producer, consumer)
/// </summary>
public string Kind { get; set; } = "internal";
/// <summary>
/// Gets the trace context
/// </summary>
public TraceContext Context { get; }
/// <summary>
/// Gets or sets the span status
/// </summary>
public SpanStatus Status { get; set; } = SpanStatus.Unset;
/// <summary>
/// Gets or sets the status description
/// </summary>
public string? StatusDescription { get; set; }
/// <summary>
/// Gets the start time
/// </summary>
public DateTime StartTime { get; }
/// <summary>
/// Gets the end time (if ended)
/// </summary>
public DateTime? EndTime { get; private set; }
/// <summary>
/// Gets the duration in milliseconds
/// </summary>
public double DurationMs { get; private set; }
/// <summary>
/// Gets attributes attached to span
/// </summary>
public Dictionary<string, object> Attributes { get; } = new();
/// <summary>
/// Gets events recorded on span
/// </summary>
public List<SpanEvent> Events { get; } = new();
/// <summary>
/// Gets linked spans
/// </summary>
public List<SpanLink> Links { get; } = new();
/// <summary>
/// Gets recorded exceptions
/// </summary>
public IReadOnlyList<Exception> Exceptions => _exceptions.AsReadOnly();
/// <summary>
/// Creates a new tracing span
/// </summary>
public TracingSpan(string name, TraceContext? context = null)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Context = context ?? TraceContextManager.Current;
StartTime = DateTime.UtcNow;
}
/// <summary>
/// Records an event on the span
/// </summary>
public void AddEvent(string eventName, Dictionary<string, object>? attributes = null)
{
Events.Add(new SpanEvent
{
Name = eventName,
Timestamp = DateTime.UtcNow,
Attributes = attributes ?? new Dictionary<string, object>()
});
}
/// <summary>
/// Records an exception on the span
/// </summary>
public void RecordException(Exception exception, Dictionary<string, object>? attributes = null)
{
if (exception == null) return;
_exceptions.Add(exception);
Status = SpanStatus.Error;
StatusDescription = exception.Message;
var eventAttrs = attributes ?? new Dictionary<string, object>();
eventAttrs["exception.type"] = exception.GetType().FullName;
eventAttrs["exception.message"] = exception.Message;
eventAttrs["exception.stacktrace"] = exception.StackTrace ?? "";
AddEvent("exception", eventAttrs);
}
/// <summary>
/// Sets a span attribute
/// </summary>
public void SetAttribute(string key, object value)
{
Attributes[key] = value;
}
/// <summary>
/// Sets multiple attributes
/// </summary>
public void SetAttributes(Dictionary<string, object> attributes)
{
if (attributes == null) return;
foreach (var kvp in attributes)
{
Attributes[kvp.Key] = kvp.Value;
}
}
/// <summary>
/// Adds a linked span
/// </summary>
public void AddLink(string linkedTraceId, string linkedSpanId, Dictionary<string, object>? attributes = null)
{
Links.Add(new SpanLink
{
TraceId = linkedTraceId,
SpanId = linkedSpanId,
Attributes = attributes ?? new Dictionary<string, object>()
});
}
/// <summary>
/// Marks span as ended with success
/// </summary>
public void End(SpanStatus status = SpanStatus.Ok, string? description = null)
{
if (_disposed) return;
_stopwatch.Stop();
EndTime = DateTime.UtcNow;
DurationMs = _stopwatch.Elapsed.TotalMilliseconds;
Status = status;
StatusDescription = description;
}
/// <summary>
/// Gets span as W3C trace context header value
/// </summary>
public string ToW3CTraceContext()
{
return $"{Context.TraceId}-{Context.SpanId}-{(byte)Context.TraceFlags:x2}";
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
End();
}
}
/// <summary>
/// Span status enumeration
/// </summary>
public enum SpanStatus
{
Unset = 0,
Ok = 1,
Error = 2
}
/// <summary>
/// Represents an event recorded on a span
/// </summary>
public sealed class SpanEvent
{
public string Name { get; set; }
public DateTime Timestamp { get; set; }
public Dictionary<string, object> Attributes { get; set; } = new();
}
/// <summary>
/// Represents a link to another span
/// </summary>
public sealed class SpanLink
{
public string TraceId { get; set; }
public string SpanId { get; set; }
public Dictionary<string, object> Attributes { get; set; } = new();
}
}
@@ -1,6 +1,10 @@
using EonaCat.LogStack.Configuration; using EonaCat.LogStack.Configuration;
using EonaCat.LogStack.Core; using EonaCat.LogStack.Core;
using EonaCat.LogStack.Logging; using EonaCat.LogStack.Logging;
using EonaCat.LogStack.DependencyInjection;
using EonaCat.LogStack.Telemetry;
using EonaCat.LogStack.Tracing;
using EonaCat.LogStack.PerformanceInsights;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using System; using System;
@@ -337,4 +341,98 @@ public static class ServiceCollectionExtensions
services.AddSingleton<AdvancedMetricsCollector>(); services.AddSingleton<AdvancedMetricsCollector>();
return services; return services;
} }
/// <summary>
/// Registers named logger factory for per-category loggers in DI
/// </summary>
public static IServiceCollection AddEonaCatNamedLoggers(
this IServiceCollection services,
LoggerDIOptions? options = null)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
options ??= new LoggerDIOptions();
services.AddSingleton(options);
// Register telemetry components if enabled
if (options.EnableTelemetry)
{
services.AddSingleton<Telemetry.TelemetryAggregator>();
}
if (options.EnableTracing)
{
services.AddSingleton<Tracing.SpanFactory>();
}
if (options.EnablePerformanceMonitoring)
{
services.AddSingleton<PerformanceInsights.PerformanceInsightsCollector>();
}
if (options.EnableHealthMonitoring)
{
services.AddSingleton<Telemetry.HealthMonitor>();
}
return services;
}
/// <summary>
/// Enables distributed tracing support
/// </summary>
public static IServiceCollection AddEonaCatTracing(
this IServiceCollection services)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
services.AddSingleton<Tracing.TraceContextManager>();
services.AddSingleton<Tracing.SpanFactory>();
services.AddSingleton<Telemetry.TelemetryActivitySource>();
return services;
}
/// <summary>
/// Enables performance monitoring and insights
/// </summary>
public static IServiceCollection AddEonaCatPerformanceMonitoring(
this IServiceCollection services)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
services.AddSingleton<PerformanceInsights.PerformanceInsightsCollector>();
services.AddSingleton<PerformanceInsights.PerformanceAnalyzer>();
return services;
}
/// <summary>
/// Enables health monitoring
/// </summary>
public static IServiceCollection AddEonaCatHealthMonitoring(
this IServiceCollection services)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
services.AddSingleton<Telemetry.HealthMonitor>();
services.AddSingleton<Telemetry.TelemetryAggregator>();
return services;
}
/// <summary>
/// Enables unified telemetry aggregation
/// </summary>
public static IServiceCollection AddEonaCatTelemetryAggregation(
this IServiceCollection services)
{
if (services == null)
throw new ArgumentNullException(nameof(services));
services.AddSingleton<Telemetry.TelemetryAggregator>();
services.AddSingleton<Telemetry.HealthMonitor>();
return services;
}
} }
+67
View File
@@ -796,6 +796,73 @@ public sealed class LogBuilder
return this; return this;
} }
/// <summary>
/// Stores recent log events in memory for querying and diagnostics
/// </summary>
public LogBuilder WriteToQueryableStorage(
int maxCapacity = 10000,
LogLevel minimumLevel = LogLevel.Trace)
{
_flows.Add(new LocalStorageQueryFlow(maxCapacity, minimumLevel));
return this;
}
/// <summary>
/// Tracks and correlates related events across flows using correlation IDs
/// </summary>
public LogBuilder WriteToCorrelationTracking(
TimeSpan? correlationWindow = null,
LogLevel minimumLevel = LogLevel.Trace)
{
_flows.Add(new CorrelatedEventFlow(
correlationWindow: correlationWindow,
minimumLevel: minimumLevel));
return this;
}
/// <summary>
/// Monitors performance metrics and detects anomalies using statistical analysis
/// </summary>
public LogBuilder WriteToPerformanceMonitoring(
IFlow? alertFlow = null,
int windowSize = 100,
double standardDeviationThreshold = 2.0)
{
_flows.Add(new PerformanceAnomalyDetectorFlow(
alertFlow,
windowSize,
standardDeviationThreshold));
return this;
}
/// <summary>
/// Adds a filtered flow using a named preset (ErrorOnly, PerformanceLogs, SecurityLogs, etc.)
/// </summary>
public LogBuilder WriteToFilteredFlow(
IFlow targetFlow,
string presetName,
LogLevel minimumLevel = LogLevel.Trace)
{
var preset = LogFilterPresetRegistry.Get(presetName)
?? throw new ArgumentException($"Preset '{presetName}' not found. Available presets: {string.Join(", ", LogFilterPresetRegistry.GetAvailablePresets())}", nameof(presetName));
_flows.Add(new LogFilterPresetFlow(targetFlow, preset, minimumLevel));
return this;
}
/// <summary>
/// Adds a custom filtered flow using a predicate function
/// </summary>
public LogBuilder WriteToCustomFilteredFlow(
IFlow targetFlow,
string name,
Func<LogEvent, bool> predicate,
LogLevel minimumLevel = LogLevel.Trace)
{
var preset = new CustomPreset(name, predicate);
_flows.Add(new LogFilterPresetFlow(targetFlow, preset, minimumLevel));
return this;
}
/// <summary> /// <summary>
/// Boost logs with machine name /// Boost logs with machine name
/// </summary> /// </summary>
+177
View File
@@ -0,0 +1,177 @@
using System;
using System.Collections.Generic;
namespace EonaCat.LogStack.Policies
{
/// <summary>
/// Automatic batching policy for flows
/// </summary>
public sealed class AutoBatchingPolicy
{
public int MinimumBatchSize { get; set; } = 10;
public int MaximumBatchSize { get; set; } = 100;
public TimeSpan FlushInterval { get; set; } = TimeSpan.FromSeconds(5);
public bool AdaptBatchSize { get; set; } = true;
public double ScalingFactor { get; set; } = 1.5;
}
/// <summary>
/// Automatic flow scaling policy
/// </summary>
public sealed class AutoScalingPolicy
{
public bool Enabled { get; set; } = true;
public long BytesPerSecondThreshold { get; set; } = 10_000_000; // 10MB/s
public double ErrorRateThreshold { get; set; } = 0.05; // 5%
public TimeSpan EvaluationInterval { get; set; } = TimeSpan.FromSeconds(10);
public int MaxBufferMultiplier { get; set; } = 5;
}
/// <summary>
/// Automatic retention policy for log files
/// </summary>
public sealed class AutoRetentionPolicy
{
public bool Enabled { get; set; } = true;
public TimeSpan MaxAge { get; set; } = TimeSpan.FromDays(30);
public long MaxSize { get; set; } = 10_737_418_240; // 10GB
public int MaxFileCount { get; set; } = 1000;
public TimeSpan CleanupInterval { get; set; } = TimeSpan.FromHours(1);
public bool CompressArchives { get; set; } = true;
}
/// <summary>
/// Automatic optimization policy for performance
/// </summary>
public sealed class AutoOptimizationPolicy
{
public bool Enabled { get; set; } = true;
public TimeSpan AnalysisInterval { get; set; } = TimeSpan.FromMinutes(5);
public double SlowOperationThresholdPercentile { get; set; } = 0.95;
public bool AutoAdjustBufferSizes { get; set; } = true;
public bool AutoAdjustBatchSizes { get; set; } = true;
}
/// <summary>
/// Collection of auto-policies for logger
/// </summary>
public sealed class AutoPolicies
{
public AutoBatchingPolicy Batching { get; } = new();
public AutoScalingPolicy Scaling { get; } = new();
public AutoRetentionPolicy Retention { get; } = new();
public AutoOptimizationPolicy Optimization { get; } = new();
}
/// <summary>
/// Policy engine for automatic optimization
/// </summary>
public sealed class AutoPolicyEngine
{
private readonly AutoPolicies _policies;
private DateTime _lastAnalysis = DateTime.UtcNow;
private DateTime _lastCleanup = DateTime.UtcNow;
public AutoPolicyEngine(AutoPolicies policies = null)
{
_policies = policies ?? new AutoPolicies();
}
/// <summary>
/// Evaluates policies and returns recommended actions
/// </summary>
public List<PolicyAction> EvaluatePolicies()
{
var actions = new List<PolicyAction>();
var now = DateTime.UtcNow;
// Check optimization policy
if (_policies.Optimization.Enabled &&
now - _lastAnalysis > _policies.Optimization.AnalysisInterval)
{
actions.Add(new PolicyAction
{
Type = PolicyActionType.AnalyzePerformance,
Reason = "Scheduled performance analysis"
});
_lastAnalysis = now;
}
// Check retention policy
if (_policies.Retention.Enabled &&
now - _lastCleanup > _policies.Retention.CleanupInterval)
{
actions.Add(new PolicyAction
{
Type = PolicyActionType.CleanupRetentionPolicy,
Reason = "Scheduled retention cleanup"
});
_lastCleanup = now;
}
return actions;
}
/// <summary>
/// Gets all active policies as string
/// </summary>
public string GetPoliciesSummary()
{
var lines = new List<string>
{
"=== Auto-Policies Configuration ===",
"",
"Batching:",
$" Min Batch: {_policies.Batching.MinimumBatchSize}",
$" Max Batch: {_policies.Batching.MaximumBatchSize}",
$" Flush Interval: {_policies.Batching.FlushInterval.TotalSeconds:F1}s",
$" Adaptive: {_policies.Batching.AdaptBatchSize}",
"",
"Scaling:",
$" Enabled: {_policies.Scaling.Enabled}",
$" Throughput Threshold: {_policies.Scaling.BytesPerSecondThreshold / 1_000_000}MB/s",
$" Error Rate Threshold: {_policies.Scaling.ErrorRateThreshold:P}",
"",
"Retention:",
$" Enabled: {_policies.Retention.Enabled}",
$" Max Age: {_policies.Retention.MaxAge.TotalDays} days",
$" Max Size: {_policies.Retention.MaxSize / 1_073_741_824}GB",
$" Max Files: {_policies.Retention.MaxFileCount}",
"",
"Optimization:",
$" Enabled: {_policies.Optimization.Enabled}",
$" Analysis Interval: {_policies.Optimization.AnalysisInterval.TotalMinutes:F1}m",
$" Auto-Adjust Buffer: {_policies.Optimization.AutoAdjustBufferSizes}",
$" Auto-Adjust Batch: {_policies.Optimization.AutoAdjustBatchSizes}"
};
return string.Join(Environment.NewLine, lines);
}
}
/// <summary>
/// Policy action types
/// </summary>
public enum PolicyActionType
{
None = 0,
AnalyzePerformance = 1,
CleanupRetentionPolicy = 2,
AdjustBatchSize = 3,
AdjustBufferSize = 4,
AlertHighErrorRate = 5,
AlertHighThroughput = 6,
CompressArchives = 7
}
/// <summary>
/// Represents a policy action to execute
/// </summary>
public sealed class PolicyAction
{
public PolicyActionType Type { get; set; }
public string? Reason { get; set; }
public Dictionary<string, object> Parameters { get; set; } = new();
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
}
}
+105
View File
@@ -0,0 +1,105 @@
using EonaCat.LogStack.Anomalies;
using EonaCat.LogStack.DeadLettering;
using EonaCat.LogStack.Diagnostics;
using EonaCat.LogStack.Routing;
using EonaCat.LogStack.Sampling;
using System;
using System.Collections.Generic;
// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
namespace EonaCat.LogStack;
/// <summary>
/// Aggregated statistics from all superior features.
/// </summary>
public class SuperiorFeaturesStats
{
/// <summary>
/// Whether intelligent routing is enabled.
/// </summary>
public bool IntelligentRoutingEnabled { get; set; }
/// <summary>
/// Statistics from the intelligent router.
/// </summary>
public RouterStats RouterStats { get; set; }
/// <summary>
/// Whether adaptive sampling is enabled.
/// </summary>
public bool AdaptiveSamplingEnabled { get; set; }
/// <summary>
/// Metrics from the adaptive sampler.
/// </summary>
public SamplingMetrics SamplingMetrics { get; set; }
/// <summary>
/// Whether anomaly detection is enabled.
/// </summary>
public bool AnomalyDetectionEnabled { get; set; }
/// <summary>
/// Statistics from the anomaly detector.
/// </summary>
public AnomalyDetectorStats AnomalyStats { get; set; }
/// <summary>
/// Recent anomalies that were detected.
/// </summary>
public List<AnomalyAlert> RecentAnomalies { get; set; }
/// <summary>
/// Whether context snapshots are enabled.
/// </summary>
public bool ContextSnapshotsEnabled { get; set; }
/// <summary>
/// Statistics from the context snapshot collector.
/// </summary>
public SnapshotCollectorStats SnapshotStats { get; set; }
/// <summary>
/// Whether the dead letter queue is enabled.
/// </summary>
public bool DeadLetterQueueEnabled { get; set; }
/// <summary>
/// Statistics from the dead letter queue.
/// </summary>
public DeadLetterQueueStats DlqStats { get; set; }
/// <summary>
/// Failure reasons grouped by count.
/// </summary>
public Dictionary<string, int> DlqFailureReasons { get; set; }
/// <summary>
/// Gets a summary string of all enabled features.
/// </summary>
public string GetEnabledFeaturesSummary()
{
var features = new List<string>();
if (IntelligentRoutingEnabled)
features.Add($"IntelligentRouting({RouterStats?.TotalRules ?? 0} rules)");
if (AdaptiveSamplingEnabled)
features.Add($"AdaptiveSampling({SamplingMetrics?.SamplingRate:P1} rate)");
if (AnomalyDetectionEnabled)
features.Add($"AnomalyDetection({AnomalyStats?.RecentAnomalies ?? 0} recent)");
if (ContextSnapshotsEnabled)
features.Add($"ContextSnapshots({SnapshotStats?.CurrentSnapshots ?? 0} snapshots)");
if (DeadLetterQueueEnabled)
features.Add($"DeadLetterQueue({DlqStats?.CurrentQueueSize ?? 0}/{DlqStats?.MaxCapacity ?? 0})");
return "Superior Features: " + (features.Count > 0
? string.Join(", ", features)
: "None enabled");
}
}
+239
View File
@@ -0,0 +1,239 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace EonaCat.LogStack.Telemetry
{
/// <summary>
/// Monitors the health status of the logger and flows.
/// Tracks degradation, failures, and recovery events.
/// </summary>
public sealed class HealthMonitor
{
private Dictionary<string, ComponentHealth> _componentHealth = new();
private OverallHealth _overallHealth = new();
private readonly object _lock = new();
/// <summary>
/// Gets overall health status
/// </summary>
public HealthStatus OverallStatus
{
get
{
lock (_lock)
{
return _overallHealth.Status;
}
}
}
/// <summary>
/// Gets health status of a component
/// </summary>
public ComponentHealth? GetComponentHealth(string name)
{
lock (_lock)
{
return _componentHealth.TryGetValue(name, out var health) ? health : null;
}
}
/// <summary>
/// Updates health of a component
/// </summary>
public void UpdateComponentHealth(string name, HealthStatus status, string? message = null)
{
lock (_lock)
{
var now = DateTime.UtcNow;
if (!_componentHealth.TryGetValue(name, out var health))
{
health = new ComponentHealth { Name = name };
_componentHealth[name] = health;
}
var previousStatus = health.Status;
health.Status = status;
health.Message = message;
health.LastUpdated = now;
if (status != previousStatus)
{
health.StatusChangedAt = now;
health.StatusChangeCount++;
}
RecalculateOverallHealth();
}
}
/// <summary>
/// Records an error event
/// </summary>
public void RecordError(string componentName, Exception exception)
{
lock (_lock)
{
if (!_componentHealth.TryGetValue(componentName, out var health))
{
health = new ComponentHealth { Name = componentName };
_componentHealth[componentName] = health;
}
health.ErrorCount++;
health.LastErrorAt = DateTime.UtcNow;
health.LastError = exception;
if (health.ErrorCount > 10)
{
health.Status = HealthStatus.Degraded;
}
if (health.ErrorCount > 50)
{
health.Status = HealthStatus.Unhealthy;
}
RecalculateOverallHealth();
}
}
/// <summary>
/// Recovers a component
/// </summary>
public void RecoverComponent(string componentName)
{
lock (_lock)
{
if (_componentHealth.TryGetValue(componentName, out var health))
{
health.Status = HealthStatus.Healthy;
health.ErrorCount = 0;
health.Message = null;
RecalculateOverallHealth();
}
}
}
/// <summary>
/// Gets all component health statuses
/// </summary>
public IReadOnlyDictionary<string, ComponentHealth> GetAllComponentHealth()
{
lock (_lock)
{
return new Dictionary<string, ComponentHealth>(_componentHealth);
}
}
/// <summary>
/// Gets health snapshot
/// </summary>
public HealthSnapshot GetSnapshot()
{
lock (_lock)
{
return new HealthSnapshot
{
OverallStatus = _overallHealth.Status,
OverallMessage = _overallHealth.Message,
CapturedAt = DateTime.UtcNow,
Components = new Dictionary<string, ComponentHealth>(_componentHealth),
TotalComponents = _componentHealth.Count,
HealthyComponents = _componentHealth.Count(kvp => kvp.Value.Status == HealthStatus.Healthy),
DegradedComponents = _componentHealth.Count(kvp => kvp.Value.Status == HealthStatus.Degraded),
UnhealthyComponents = _componentHealth.Count(kvp => kvp.Value.Status == HealthStatus.Unhealthy)
};
}
}
private void RecalculateOverallHealth()
{
if (_componentHealth.Count == 0)
{
_overallHealth.Status = HealthStatus.Unknown;
return;
}
// If any component is unhealthy, overall is unhealthy
if (_componentHealth.Values.Any(h => h.Status == HealthStatus.Unhealthy))
{
_overallHealth.Status = HealthStatus.Unhealthy;
_overallHealth.Message = "One or more components are unhealthy";
}
// If any component is degraded, overall is degraded
else if (_componentHealth.Values.Any(h => h.Status == HealthStatus.Degraded))
{
_overallHealth.Status = HealthStatus.Degraded;
_overallHealth.Message = "One or more components are degraded";
}
// If all are healthy, overall is healthy
else if (_componentHealth.Values.All(h => h.Status == HealthStatus.Healthy))
{
_overallHealth.Status = HealthStatus.Healthy;
_overallHealth.Message = "All components are healthy";
}
else
{
_overallHealth.Status = HealthStatus.Unknown;
}
}
}
/// <summary>
/// Health status enumeration
/// </summary>
public enum HealthStatus
{
Unknown = 0,
Healthy = 1,
Degraded = 2,
Unhealthy = 3
}
/// <summary>
/// Health status of a single component
/// </summary>
public sealed class ComponentHealth
{
public string Name { get; set; }
public HealthStatus Status { get; set; } = HealthStatus.Unknown;
public string? Message { get; set; }
public DateTime LastUpdated { get; set; } = DateTime.UtcNow;
public DateTime? StatusChangedAt { get; set; }
public int StatusChangeCount { get; set; }
public long ErrorCount { get; set; }
public DateTime? LastErrorAt { get; set; }
public Exception? LastError { get; set; }
}
/// <summary>
/// Overall health information
/// </summary>
public sealed class OverallHealth
{
public HealthStatus Status { get; set; } = HealthStatus.Unknown;
public string? Message { get; set; }
}
/// <summary>
/// Snapshot of health status
/// </summary>
public sealed class HealthSnapshot
{
public HealthStatus OverallStatus { get; set; }
public string? OverallMessage { get; set; }
public DateTime CapturedAt { get; set; }
public Dictionary<string, ComponentHealth> Components { get; set; } = new();
public int TotalComponents { get; set; }
public int HealthyComponents { get; set; }
public int DegradedComponents { get; set; }
public int UnhealthyComponents { get; set; }
public override string ToString()
{
return $"{OverallStatus} | Total: {TotalComponents} | Healthy: {HealthyComponents} | Degraded: {DegradedComponents} | Unhealthy: {UnhealthyComponents}";
}
}
}
@@ -0,0 +1,293 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
namespace EonaCat.LogStack.Telemetry
{
/// <summary>
/// Aggregates telemetry data from multiple sources and flows.
/// Correlates metrics and provides unified telemetry snapshots.
/// </summary>
public sealed class TelemetryAggregator
{
private readonly ConcurrentDictionary<string, MetricValue> _metrics = new();
private readonly ConcurrentDictionary<string, GaugeValue> _gauges = new();
private readonly List<TelemetryEvent> _events = new();
private readonly HealthMonitor _healthMonitor = new();
private readonly object _eventsLock = new();
private long _eventCount;
private DateTime _startTime = DateTime.UtcNow;
/// <summary>
/// Gets the health monitor
/// </summary>
public HealthMonitor HealthMonitor => _healthMonitor;
/// <summary>
/// Records a counter metric
/// </summary>
public void RecordCounter(string name, long value = 1, Dictionary<string, string>? tags = null)
{
var key = $"{name}:{TagsKey(tags)}";
_metrics.AddOrUpdate(key,
new MetricValue { Name = name, Value = value, Tags = tags ?? new() },
(_, existing) => new MetricValue
{
Name = name,
Value = existing.Value + value,
Tags = tags ?? new()
});
_eventCount++;
}
/// <summary>
/// Records a gauge metric
/// </summary>
public void RecordGauge(string name, double value, Dictionary<string, string>? tags = null)
{
var key = $"{name}:{TagsKey(tags)}";
_gauges[key] = new GaugeValue { Name = name, Value = value, Tags = tags ?? new(), RecordedAt = DateTime.UtcNow };
}
/// <summary>
/// Records a histogram metric
/// </summary>
public void RecordHistogram(string name, double value, Dictionary<string, string>? tags = null)
{
var key = $"{name}_histogram:{TagsKey(tags)}";
var metric = _metrics.GetOrAdd(key, new HistogramValue { Name = name, Tags = tags ?? new() });
if (metric is HistogramValue histogram)
{
histogram.Record(value);
}
}
/// <summary>
/// Records a telemetry event
/// </summary>
public void RecordEvent(TelemetryEvent evt)
{
lock (_eventsLock)
{
_events.Add(evt);
if (_events.Count > 10000) // Keep last 10k events
{
_events.RemoveRange(0, 1000);
}
}
}
/// <summary>
/// Gets metric value
/// </summary>
public MetricValue? GetMetric(string name)
{
foreach (var kvp in _metrics.Where(kvp => kvp.Value.Name == name))
{
return kvp.Value;
}
return null;
}
/// <summary>
/// Gets gauge value
/// </summary>
public GaugeValue? GetGauge(string name)
{
foreach (var kvp in _gauges.Where(kvp => kvp.Value.Name == name))
{
return kvp.Value;
}
return null;
}
/// <summary>
/// Gets all current metrics
/// </summary>
public IReadOnlyCollection<MetricValue> GetAllMetrics()
{
return _metrics.Values.ToList().AsReadOnly();
}
/// <summary>
/// Gets all current gauges
/// </summary>
public IReadOnlyCollection<GaugeValue> GetAllGauges()
{
return _gauges.Values.ToList().AsReadOnly();
}
/// <summary>
/// Gets recent events
/// </summary>
public IReadOnlyList<TelemetryEvent> GetRecentEvents(int count = 100)
{
lock (_eventsLock)
{
var start = Math.Max(0, _events.Count - count);
return _events.Skip(start).ToList().AsReadOnly();
}
}
/// <summary>
/// Gets aggregated telemetry snapshot
/// </summary>
public AggregatedTelemetrySnapshot GetSnapshot()
{
var uptime = DateTime.UtcNow - _startTime;
var healthSnapshot = _healthMonitor.GetSnapshot();
// Calculate derived metrics
var loggedCounter = GetMetric("logs_logged");
var droppedCounter = GetMetric("logs_dropped");
return new AggregatedTelemetrySnapshot
{
Uptime = uptime,
CapturedAt = DateTime.UtcNow,
TotalEvents = _eventCount,
MetricCount = _metrics.Count,
GaugeCount = _gauges.Count,
RecordedMetrics = _metrics.Values.ToList(),
RecordedGauges = _gauges.Values.ToList(),
HealthStatus = healthSnapshot,
LogsLogged = loggedCounter?.Value ?? 0,
LogsDropped = droppedCounter?.Value ?? 0,
RecentEventsCount = _events.Count
};
}
/// <summary>
/// Exports metrics in prometheus-like format
/// </summary>
public string ExportPrometheusFormat()
{
var lines = new List<string> { "# HELP EonaCat.LogStack telemetry metrics" };
foreach (var metric in _metrics.Values.GroupBy(m => m.Name))
{
lines.Add($"# TYPE {metric.Key} gauge");
foreach (var m in metric)
{
var tagsStr = m.Tags.Any() ? "{" + string.Join(",", m.Tags.Select(t => $"{t.Key}=\"{t.Value}\"")) + "}" : "";
lines.Add($"{metric.Key}{tagsStr} {m.Value}");
}
}
foreach (var gauge in _gauges.Values.GroupBy(g => g.Name))
{
lines.Add($"# TYPE {gauge.Key} gauge");
foreach (var g in gauge)
{
var tagsStr = g.Tags.Any() ? "{" + string.Join(",", g.Tags.Select(t => $"{t.Key}=\"{t.Value}\"")) + "}" : "";
lines.Add($"{gauge.Key}{tagsStr} {g.Value}");
}
}
return string.Join("\n", lines);
}
/// <summary>
/// Resets all metrics
/// </summary>
public void Reset()
{
_metrics.Clear();
_gauges.Clear();
lock (_eventsLock)
{
_events.Clear();
}
_eventCount = 0;
_startTime = DateTime.UtcNow;
}
private static string TagsKey(Dictionary<string, string>? tags)
{
if (tags == null || tags.Count == 0) return "";
return string.Join(",", tags.OrderBy(t => t.Key).Select(t => $"{t.Key}={t.Value}"));
}
}
/// <summary>
/// Represents a metric value
/// </summary>
public class MetricValue
{
public string Name { get; set; }
public long Value { get; set; }
public Dictionary<string, string> Tags { get; set; } = new();
}
/// <summary>
/// Represents a gauge value
/// </summary>
public sealed class GaugeValue
{
public string Name { get; set; }
public double Value { get; set; }
public Dictionary<string, string> Tags { get; set; } = new();
public DateTime RecordedAt { get; set; }
}
/// <summary>
/// Histogram metric value
/// </summary>
public sealed class HistogramValue : MetricValue
{
private readonly List<double> _values = new();
public IReadOnlyList<double> Values => _values.AsReadOnly();
public double Min { get; private set; } = double.MaxValue;
public double Max { get; private set; } = double.MinValue;
public double Sum { get; private set; }
public double Mean => Values.Count > 0 ? Sum / Values.Count : 0;
public double P95 => CalculatePercentile(0.95);
public double P99 => CalculatePercentile(0.99);
public void Record(double value)
{
_values.Add(value);
Value++;
Sum += value;
if (value < Min) Min = value;
if (value > Max) Max = value;
if (_values.Count > 1000) // Keep reasonable history
{
_values.RemoveRange(0, 100);
}
}
private double CalculatePercentile(double percentile)
{
if (_values.Count == 0) return 0;
var sorted = _values.OrderBy(v => v).ToList();
var index = (int)((percentile / 100.0) * sorted.Count);
index = Math.Min(index, sorted.Count - 1);
return sorted[index];
}
}
/// <summary>
/// Aggregated telemetry snapshot
/// </summary>
public sealed class AggregatedTelemetrySnapshot
{
public TimeSpan Uptime { get; set; }
public DateTime CapturedAt { get; set; }
public long TotalEvents { get; set; }
public int MetricCount { get; set; }
public int GaugeCount { get; set; }
public List<MetricValue> RecordedMetrics { get; set; } = new();
public List<GaugeValue> RecordedGauges { get; set; } = new();
public HealthSnapshot HealthStatus { get; set; }
public long LogsLogged { get; set; }
public long LogsDropped { get; set; }
public int RecentEventsCount { get; set; }
public override string ToString()
{
return $"Uptime: {Uptime.TotalSeconds:F2}s | Events: {TotalEvents} | Health: {HealthStatus.OverallStatus} | Logged: {LogsLogged} | Dropped: {LogsDropped}";
}
}
}
+383 -11
View File
@@ -31,26 +31,398 @@ It features a rich fluent API for routing log events to dozens of destinations -
- **Lazy initialization** - Flows are only initialized when first used, reducing startup overhead. - **Lazy initialization** - Flows are only initialized when first used, reducing startup overhead.
### Telemetry & Observability ## Telemetry & Observability
EonaCat.LogStack includes built-in telemetry primitives for production monitoring: EonaCat.LogStack includes **built-in, zero-dependency telemetry** for comprehensive production monitoring:
- **Metrics registry** - Low-overhead counters and snapshots for logging throughput. ### Enhanced Telemetry System
- **Telemetry snapshots** - Capture service name, environment, runtime/process metrics, log counters and trace activity.
- **Runtime health data** - Export operational status for dashboards and monitoring systems. #### Metrics Collection & Aggregation
- **Tracing support** - Activity/span integration for distributed request correlation.
- **Custom exporters** - Build exporters around telemetry snapshots for your monitoring backend. Track logging operations with built-in metrics:
Example:
```csharp ```csharp
var options = new TelemetryOptions var aggregator = new EonaCat.LogStack.Telemetry.TelemetryAggregator();
// Record counters
aggregator.RecordCounter("logs_logged", 1);
aggregator.RecordCounter("logs_dropped", 1);
// Record gauges (point-in-time measurements)
aggregator.RecordGauge("memory_usage_mb", GC.GetTotalMemory(false) / 1024 / 1024);
// Record histograms (with percentiles)
aggregator.RecordHistogram("request_duration_ms", 42);
// Get snapshot
var snapshot = aggregator.GetSnapshot();
Console.WriteLine($"Uptime: {snapshot.Uptime.TotalSeconds}s");
Console.WriteLine($"Total Events: {snapshot.TotalEvents}");
Console.WriteLine($"Logs Logged: {snapshot.LogsLogged}");
Console.WriteLine($"Logs Dropped: {snapshot.LogsDropped}");
```
#### Health Monitoring
Track component health and detect degradation:
```csharp
var health = new EonaCat.LogStack.Telemetry.HealthMonitor();
// Update component health
health.UpdateComponentHealth("EmailFlow", HealthStatus.Healthy);
health.UpdateComponentHealth("ElasticsearchFlow", HealthStatus.Degraded, "High latency detected");
// Record errors automatically impact health
health.RecordError("DatabaseFlow", new TimeoutException("Connection timeout"));
// Get health snapshot
var snapshot = health.GetSnapshot();
Console.WriteLine($"Overall Status: {snapshot.OverallStatus}");
Console.WriteLine($"Healthy: {snapshot.HealthyComponents}");
Console.WriteLine($"Degraded: {snapshot.DegradedComponents}");
console.WriteLine($"Unhealthy: {snapshot.UnhealthyComponents}");
```
#### Distributed Tracing
Enable end-to-end request tracing across async boundaries:
```csharp
// Enable tracing in DI
services.AddEonaCatTracing();
// Use trace context manager
var traceId = TraceContextManager.GetTraceId();
TraceContextManager.SetCorrelationId("order-123");
TraceContextManager.AddBaggage("UserId", "user-456");
// Create spans for operations
var spanFactory = sp.GetRequiredService<SpanFactory>();
using (spanFactory.CreateSpanScope("ProcessOrder"))
{
// Operation code here
// Automatically tracked with context propagation
}
// Access current trace context
var context = TraceContextManager.Current;
Console.WriteLine($"Trace: {context.TraceId}");
Console.WriteLine($"Span: {context.SpanId}");
Console.WriteLine($"Parent: {context.ParentSpanId}");
```
#### Performance Monitoring & Insights
Automatic performance analysis with bottleneck detection:
```csharp
var insights = new EonaCat.LogStack.PerformanceInsights.PerformanceInsightsCollector();
// Track operations
var sw = System.Diagnostics.Stopwatch.StartNew();
// ... operation ...
sw.Stop();
insights.RecordOperation("DatabaseQuery", sw.ElapsedTicks, byteCount: 1024, isError: false);
// Get performance analysis
var analysis = new EonaCat.LogStack.PerformanceInsights.PerformanceAnalyzer(insights).Analyze();
Console.WriteLine(analysis.Summary);
/* Output:
Performance Analysis - 2026-03-27T09:15:00Z
Uptime: 300.45s
Total Operations: 15234
Total Errors: 2
Top Slowest Operations:
- DatabaseQuery: 245.32ms (min: 10.12ms, max: 1523.45ms)
- HttpRequest: 156.78ms (min: 45.23ms, max: 892.34ms)
Operations with Most Errors:
- ExternalAPI: 2 errors (0.13% error rate)
Recommendations:
[High] ErrorRate: Error rate is 0.01%. Consider investigating error causes.
[Medium] SlowOperations: Some operations are very slow (>1000ms avg).
*/
```
### Advanced DI Integration
#### Named Loggers per Category
Use different logger configurations per component:
```csharp
// Register named loggers
services.AddEonaCatNamedLoggers(new LoggerDIOptions
{
EnableTelemetry = true,
EnableTracing = true,
EnablePerformanceMonitoring = true,
EagerlyInitializeNamedLoggers = true
});
// Later, get loggers by name
var namedLoggerFactory = sp.GetRequiredService<INamedLoggerFactory>();
var apiLogger = namedLoggerFactory.GetLogger("API", builder =>
builder.WithMinimumLevel(LogLevel.Debug));
var dbLogger = namedLoggerFactory.GetLogger("Database", builder =>
builder.WithMinimumLevel(LogLevel.Information));
```
#### Composite Loggers
Combine multiple loggers into one:
```csharp
var logger1 = new LogBuilder("Console").WriteToConsole().Build();
var logger2 = new LogBuilder("File").WriteToFile("./logs").Build();
var composite = new EonaCat.LogStack.DependencyInjection.CompositeLogger(logger1, logger2);
composite.Information("This goes to both console and file");
```
#### Logger Decorators
Add cross-cutting concerns to loggers:
```csharp
var chain = new EonaCat.LogStack.DependencyInjection.DecoratorChain()
.Add(new PerformanceDecorator())
.Add(new SecurityDecorator());
var decoratedLogger = chain.Apply(baseLogger);
```
### Premium Features
#### Automatic Policy Engine
Enable automatic optimization:
```csharp
var policies = new EonaCat.LogStack.Policies.AutoPolicies
{
Batching = new AutoBatchingPolicy
{
MinimumBatchSize = 10,
MaximumBatchSize = 100,
AdaptBatchSize = true
},
Scaling = new AutoScalingPolicy
{ {
Enabled = true, Enabled = true,
IncludeRuntimeMetrics = true, BytesPerSecondThreshold = 10_000_000,
IncludeTraceMetrics = true ErrorRateThreshold = 0.05
},
Retention = new AutoRetentionPolicy
{
MaxAge = TimeSpan.FromDays(30),
MaxSize = 10_737_418_240,
CompressArchives = true
}
}; };
var engine = new EonaCat.LogStack.Policies.AutoPolicyEngine(policies);
var actions = engine.EvaluatePolicies();
foreach (var action in actions)
{
Console.WriteLine($"Action: {action.Type} - {action.Reason}");
}
Console.WriteLine(engine.GetPoliciesSummary());
``` ```
#### Common Logging Patterns
Pre-built patterns for common scenarios:
```csharp
using EonaCat.LogStack.Patterns;
// HTTP Request logging
var reqLogger = new RequestResponseLogger(logger);
reqLogger.LogRequest("GET", "/api/users", "req-123");
reqLogger.LogResponse(200, 42, "req-123");
// Database operation logging
var dbLogger = new DatabaseOperationLogger(logger);
dbLogger.LogQuery("SELECT", "Users");
dbLogger.LogQueryTiming("SELECT", 156, rowsAffected: 500);
dbLogger.LogConnection("opened");
// Service initialization logging
var initLogger = new ServiceInitializationLogger(logger, "OrderService");
initLogger.LogInitializationStart();
initLogger.LogComponentInit("DatabaseConnection", success: true);
initLogger.LogComponentInit("CacheConnection", success: true);
initLogger.LogInitializationComplete(TimeSpan.FromMilliseconds(450));
// Performance measurement
var perfLogger = new PerformanceLogger(logger, thresholdMs: 1000);
perfLogger.LogTiming("UserQuery", 245);
perfLogger.LogMemoryUsage(52_428_800, 104_857_600);
// Timed scope for automatic duration logging
using (var scope = perfLogger.StartTimedScope("OrderProcessing"))
{
// ... processing code ...
} // Automatically logs duration
```
#### Auto-Initialization
Automatic logger initialization by type:
```csharp
var provider = new EonaCat.LogStack.Utilities.AutoInitializingLoggerProvider(loggerFactory);
// Get logger for type (auto-creates if needed)
var logger = provider.GetLoggerForType<UserService>();
// Or generic
var logger = provider.GetLoggerForType(typeof(PaymentProcessor));
// Static provider for global access
EonaCat.LogStack.Utilities.StaticLoggerProvider.Initialize(loggerFactory);
var logger = EonaCat.LogStack.Utilities.StaticLoggerProvider.GetLoggerFor<OrderService>();
```
#### Log Processing Utilities
Advanced log event processing:
```csharp
// Batch processing
var processor = new EonaCat.LogStack.Utilities.LogBatchProcessor(
batchSize: 50,
flushInterval: TimeSpan.FromSeconds(5),
processor: batch => Console.WriteLine($"Processing {batch.Count} events"));
// Intelligent routing
var router = new EonaCat.LogStack.Utilities.LogRouter();
router.RegisterLevelRoute(LogLevel.Error, evt => SendAlert(evt));
router.RegisterCategoryRoute("Security", evt => AuditLog(evt));
router.RegisterDefaultRoute(evt => Console.WriteLine(evt.Message));
// Event filtering
var filter = new EonaCat.LogStack.Utilities.LogEventFilter()
.AddLevelFilter(LogLevel.Warning)
.AddCategoryFilter("API")
.AddMessageFilter("timeout");
var filtered = filter.Filter(allEvents);
```
### DI Configuration Examples
#### Complete Setup
```csharp
services.AddEonaCatLogging("MyApp", builder =>
builder
.WithMinimumLevel(LogLevel.Information)
.WriteToConsole()
.WriteToFile("./logs")
.BoostWithCorrelationId());
// Enable all advanced features
services.AddEonaCatNamedLoggers(new LoggerDIOptions
{
EnableTelemetry = true,
EnableTracing = true,
EnablePerformanceMonitoring = true,
EnableHealthMonitoring = true
});
services.AddEonaCatTracing();
services.AddEonaCatPerformanceMonitoring();
services.AddEonaCatHealthMonitoring();
services.AddEonaCatTelemetryAggregation();
```
#### Usage in Application
```csharp
public class OrderService
{
private readonly ILogger _logger;
private readonly TelemetryAggregator _telemetry;
private readonly SpanFactory _spans;
private readonly PerformanceAnalyzer _perf;
public OrderService(
ILogger logger,
TelemetryAggregator telemetry,
SpanFactory spans,
PerformanceAnalyzer perf)
{
_logger = logger;
_telemetry = telemetry;
_spans = spans;
_perf = perf;
}
public async Task<Order> ProcessOrderAsync(string orderId)
{
using var span = _spans.CreateSpanScope("ProcessOrder");
span.SetAttribute("orderId", orderId);
try
{
_logger.Information("Processing order",
("OrderId", orderId),
("Timestamp", DateTime.UtcNow));
_telemetry.RecordCounter("orders.processed", 1);
// ... processing logic ...
_telemetry.RecordGauge("order.revenue", 99.99);
return order;
}
catch (Exception ex)
{
_telemetry.RecordCounter("orders.failed", 1);
span.RecordException(ex);
_logger.Error(ex, "Order processing failed", ("OrderId", orderId));
throw;
}
}
}
```
### Telemetry Export
Export metrics to external systems:
```csharp
// Prometheus-format export
var prometheus = aggregator.ExportPrometheusFormat();
File.WriteAllText("metrics.txt", prometheus);
// Custom export
var snapshot = aggregator.GetSnapshot();
await httpClient.PostAsJsonAsync(
"https://monitoring.example.com/metrics",
new
{
snapshot.Uptime,
snapshot.TotalEvents,
snapshot.HealthStatus,
Metrics = snapshot.RecordedMetrics
});
```
## Supported Targets ## Supported Targets
- .NET Standard 2.1 - .NET Standard 2.1