Added new flows
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
||||
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
|
||||
<PackageIconUrl />
|
||||
<FileVersion>0.1.2</FileVersion>
|
||||
<FileVersion>0.1.3</FileVersion>
|
||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||
@@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<EVRevisionFormat>0.1.2+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||
<EVRevisionFormat>0.1.3+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||
<EVDefault>true</EVDefault>
|
||||
<EVInfo>true</EVInfo>
|
||||
<EVTagMatch>v[0-9]*</EVTagMatch>
|
||||
@@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<Version>0.1.2</Version>
|
||||
<Version>0.1.3</Version>
|
||||
<PackageId>EonaCat.LogStack</PackageId>
|
||||
<Product>EonaCat.LogStack</Product>
|
||||
<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>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Configuration\" />
|
||||
<Folder Include="Patterns\" />
|
||||
<Folder Include="Utilities\" />
|
||||
</ItemGroup>
|
||||
</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;
|
||||
}
|
||||
}
|
||||
}
|
||||
+384
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,11 @@
|
||||
using EonaCat.LogStack.Boosters;
|
||||
using EonaCat.LogStack.Core;
|
||||
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.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
@@ -62,6 +67,13 @@ namespace EonaCat.LogStack
|
||||
// Dynamic level controller
|
||||
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;
|
||||
|
||||
/// <summary>
|
||||
@@ -69,12 +81,22 @@ namespace EonaCat.LogStack
|
||||
/// </summary>
|
||||
public EonaCatLogStack(string category = "Application",
|
||||
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));
|
||||
_minimumLevel = minimumLevel;
|
||||
_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
|
||||
UseAsyncPipeline();
|
||||
}
|
||||
@@ -119,6 +141,38 @@ namespace EonaCat.LogStack
|
||||
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)]
|
||||
private LogLevel EffectiveMinLevel() =>
|
||||
_dynamicLevel != null ? _dynamicLevel.CurrentLevel : _minimumLevel;
|
||||
@@ -413,6 +467,13 @@ namespace EonaCat.LogStack
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
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
|
||||
lock (_boosters)
|
||||
{
|
||||
@@ -443,6 +504,25 @@ namespace EonaCat.LogStack
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (!_asyncChannel!.Writer.TryWrite(logEvent))
|
||||
@@ -458,6 +538,18 @@ namespace EonaCat.LogStack
|
||||
{
|
||||
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;
|
||||
foreach (var flow in flows)
|
||||
{
|
||||
@@ -467,9 +559,25 @@ namespace EonaCat.LogStack
|
||||
if (result == WriteResult.Dropped)
|
||||
{
|
||||
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>
|
||||
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()
|
||||
{
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
+199
@@ -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.Core;
|
||||
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 System;
|
||||
|
||||
@@ -337,4 +341,98 @@ public static class ServiceCollectionExtensions
|
||||
services.AddSingleton<AdvancedMetricsCollector>();
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,6 +796,73 @@ public sealed class LogBuilder
|
||||
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>
|
||||
/// Boost logs with machine name
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
|
||||
### 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.
|
||||
- **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.
|
||||
- **Tracing support** - Activity/span integration for distributed request correlation.
|
||||
- **Custom exporters** - Build exporters around telemetry snapshots for your monitoring backend.
|
||||
### Enhanced Telemetry System
|
||||
|
||||
#### Metrics Collection & Aggregation
|
||||
|
||||
Track logging operations with built-in metrics:
|
||||
|
||||
Example:
|
||||
```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,
|
||||
IncludeRuntimeMetrics = true,
|
||||
IncludeTraceMetrics = true
|
||||
BytesPerSecondThreshold = 10_000_000,
|
||||
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
|
||||
|
||||
- .NET Standard 2.1
|
||||
|
||||
Reference in New Issue
Block a user