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>
|
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||||
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
|
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
|
||||||
<PackageIconUrl />
|
<PackageIconUrl />
|
||||||
<FileVersion>0.1.2</FileVersion>
|
<FileVersion>0.1.3</FileVersion>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
@@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<EVRevisionFormat>0.1.2+{chash:10}.{c:ymd}</EVRevisionFormat>
|
<EVRevisionFormat>0.1.3+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||||
<EVDefault>true</EVDefault>
|
<EVDefault>true</EVDefault>
|
||||||
<EVInfo>true</EVInfo>
|
<EVInfo>true</EVInfo>
|
||||||
<EVTagMatch>v[0-9]*</EVTagMatch>
|
<EVTagMatch>v[0-9]*</EVTagMatch>
|
||||||
@@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>0.1.2</Version>
|
<Version>0.1.3</Version>
|
||||||
<PackageId>EonaCat.LogStack</PackageId>
|
<PackageId>EonaCat.LogStack</PackageId>
|
||||||
<Product>EonaCat.LogStack</Product>
|
<Product>EonaCat.LogStack</Product>
|
||||||
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl>
|
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl>
|
||||||
@@ -101,4 +101,10 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
<PackagePath>\</PackagePath>
|
<PackagePath>\</PackagePath>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="Configuration\" />
|
||||||
|
<Folder Include="Patterns\" />
|
||||||
|
<Folder Include="Utilities\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,358 @@
|
|||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Flows
|
||||||
|
{
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// CorrelatedEventFlow tracks and correlates related events across flows and time windows.
|
||||||
|
/// Enables request tracing, transaction tracking, and detection of related log patterns.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CorrelatedEventFlow : FlowBase
|
||||||
|
{
|
||||||
|
private readonly int _maxCorrelationGroups;
|
||||||
|
private readonly TimeSpan _correlationWindow;
|
||||||
|
private readonly ConcurrentDictionary<string, CorrelationGroup> _correlationGroups = new();
|
||||||
|
private readonly object _cleanupLock = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new CorrelatedEventFlow
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxCorrelationGroups">Maximum number of correlation groups to track (default 10000)</param>
|
||||||
|
/// <param name="correlationWindow">Time window for correlating events (default 5 minutes)</param>
|
||||||
|
/// <param name="minimumLevel">Minimum log level to process</param>
|
||||||
|
public CorrelatedEventFlow(
|
||||||
|
int maxCorrelationGroups = 10000,
|
||||||
|
TimeSpan? correlationWindow = null,
|
||||||
|
LogLevel minimumLevel = LogLevel.Trace)
|
||||||
|
: base("CorrelatedEvent", minimumLevel)
|
||||||
|
{
|
||||||
|
_maxCorrelationGroups = Math.Max(1000, maxCorrelationGroups);
|
||||||
|
_correlationWindow = correlationWindow ?? TimeSpan.FromMinutes(5);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
|
||||||
|
return WriteResult.Success;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var correlationId = ExtractCorrelationId(logEvent);
|
||||||
|
if (string.IsNullOrEmpty(correlationId))
|
||||||
|
return WriteResult.Success;
|
||||||
|
|
||||||
|
// Get or create correlation group
|
||||||
|
var group = _correlationGroups.AddOrUpdate(
|
||||||
|
correlationId,
|
||||||
|
new CorrelationGroup(correlationId),
|
||||||
|
(key, existing) => existing
|
||||||
|
);
|
||||||
|
|
||||||
|
group.AddEvent(logEvent);
|
||||||
|
|
||||||
|
// Cleanup if needed
|
||||||
|
if (_correlationGroups.Count > _maxCorrelationGroups)
|
||||||
|
{
|
||||||
|
CleanupExpiredGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.FromResult(WriteResult.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return WriteResult.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var logEvent in logEvents.Span)
|
||||||
|
{
|
||||||
|
if (IsLogLevelEnabled(logEvent))
|
||||||
|
{
|
||||||
|
var correlationId = ExtractCorrelationId(logEvent);
|
||||||
|
if (!string.IsNullOrEmpty(correlationId))
|
||||||
|
{
|
||||||
|
var group = _correlationGroups.AddOrUpdate(
|
||||||
|
correlationId,
|
||||||
|
new CorrelationGroup(correlationId),
|
||||||
|
(key, existing) => existing
|
||||||
|
);
|
||||||
|
|
||||||
|
group.AddEvent(logEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_correlationGroups.Count > _maxCorrelationGroups)
|
||||||
|
{
|
||||||
|
CleanupExpiredGroups();
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.FromResult(WriteResult.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return WriteResult.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task FlushAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
CleanupExpiredGroups();
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get all events for a correlation ID
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogEvent> GetCorrelatedEvents(string correlationId)
|
||||||
|
{
|
||||||
|
if (_correlationGroups.TryGetValue(correlationId, out var group))
|
||||||
|
return group.Events.ToList();
|
||||||
|
|
||||||
|
return new List<LogEvent>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get correlation statistics
|
||||||
|
/// </summary>
|
||||||
|
public CorrelationStatistics GetStatistics()
|
||||||
|
{
|
||||||
|
var stats = new CorrelationStatistics
|
||||||
|
{
|
||||||
|
TotalCorrelationIds = _correlationGroups.Count,
|
||||||
|
TotalTrackedEvents = _correlationGroups.Values.Sum(g => g.Events.Count),
|
||||||
|
AverageEventsPerCorrelation = _correlationGroups.Count > 0
|
||||||
|
? _correlationGroups.Values.Average(g => g.Events.Count)
|
||||||
|
: 0
|
||||||
|
};
|
||||||
|
|
||||||
|
// Calculate event distribution
|
||||||
|
if (_correlationGroups.Count > 0)
|
||||||
|
{
|
||||||
|
var eventCounts = _correlationGroups.Values.Select(g => g.Events.Count).ToList();
|
||||||
|
stats.MinEventsPerCorrelation = eventCounts.Min();
|
||||||
|
stats.MaxEventsPerCorrelation = eventCounts.Max();
|
||||||
|
stats.MedianEventsPerCorrelation = GetMedian(eventCounts);
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find correlation groups with errors
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CorrelationGroup> GetErrorCorrelations()
|
||||||
|
{
|
||||||
|
return _correlationGroups.Values
|
||||||
|
.Where(g => g.Events.Any(e => e.Level >= LogLevel.Error))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Find slow correlation groups (based on duration property)
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CorrelationGroup> GetSlowCorrelations(TimeSpan minimumDuration)
|
||||||
|
{
|
||||||
|
return _correlationGroups.Values
|
||||||
|
.Where(g => g.Time().TotalMilliseconds >= minimumDuration.TotalMilliseconds)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get correlation groups by category pattern
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CorrelationGroup> QueryByPattern(string pattern, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
return _correlationGroups.Values
|
||||||
|
.Where(g => g.Events.Any(e =>
|
||||||
|
{
|
||||||
|
var msg = e.Message.ToString();
|
||||||
|
return msg.Contains(pattern, comparison);
|
||||||
|
}))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear all correlation tracking
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
_correlationGroups.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Private helpers
|
||||||
|
|
||||||
|
private string ExtractCorrelationId(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
// Try to extract from common sources
|
||||||
|
if (logEvent.Properties != null)
|
||||||
|
{
|
||||||
|
if (logEvent.Properties.TryGetValue("CorrelationId", out var corrId) && corrId is string str)
|
||||||
|
return str;
|
||||||
|
if (logEvent.Properties.TryGetValue("RequestId", out var reqId) && reqId is string str2)
|
||||||
|
return str2;
|
||||||
|
if (logEvent.Properties.TryGetValue("TraceId", out var traceId) && traceId is string str3)
|
||||||
|
return str3;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try thread-based correlation
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanupExpiredGroups()
|
||||||
|
{
|
||||||
|
lock (_cleanupLock)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var expiredIds = _correlationGroups
|
||||||
|
.Where(kvp => now - kvp.Value.LastEventTime > _correlationWindow)
|
||||||
|
.Select(kvp => kvp.Key)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
foreach (var id in expiredIds)
|
||||||
|
{
|
||||||
|
_correlationGroups.TryRemove(id, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double GetMedian(List<int> values)
|
||||||
|
{
|
||||||
|
if (values.Count == 0) return 0;
|
||||||
|
var sorted = values.OrderBy(x => x).ToList();
|
||||||
|
if (sorted.Count % 2 == 0)
|
||||||
|
return (sorted[sorted.Count / 2 - 1] + sorted[sorted.Count / 2]) / 2.0;
|
||||||
|
return sorted[sorted.Count / 2];
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
await base.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a group of correlated log events
|
||||||
|
/// </summary>
|
||||||
|
public class CorrelationGroup
|
||||||
|
{
|
||||||
|
private readonly List<LogEvent> _events = new();
|
||||||
|
private readonly object _eventLock = new();
|
||||||
|
|
||||||
|
public string CorrelationId { get; }
|
||||||
|
public IReadOnlyList<LogEvent> Events
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_eventLock)
|
||||||
|
return _events.AsReadOnly();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
public DateTime FirstEventTime { get; private set; }
|
||||||
|
public DateTime LastEventTime { get; private set; }
|
||||||
|
|
||||||
|
public CorrelationGroup(string correlationId)
|
||||||
|
{
|
||||||
|
CorrelationId = correlationId ?? throw new ArgumentNullException(nameof(correlationId));
|
||||||
|
FirstEventTime = DateTime.UtcNow;
|
||||||
|
LastEventTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddEvent(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
lock (_eventLock)
|
||||||
|
{
|
||||||
|
_events.Add(logEvent);
|
||||||
|
LastEventTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the total time span of this correlation group
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan Time()
|
||||||
|
{
|
||||||
|
lock (_eventLock)
|
||||||
|
{
|
||||||
|
if (_events.Count == 0) return TimeSpan.Zero;
|
||||||
|
return new TimeSpan(_events[_events.Count - 1].Timestamp - _events[0].Timestamp);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get events at specific log level
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogEvent> GetEventsByLevel(LogLevel level)
|
||||||
|
{
|
||||||
|
lock (_eventLock)
|
||||||
|
return _events.Where(e => e.Level == level).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Check if this group contains any exceptions
|
||||||
|
/// </summary>
|
||||||
|
public bool HasExceptions
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_eventLock)
|
||||||
|
return _events.Any(e => e.Exception != null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get event count by category
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, int> GetEventsByCategory()
|
||||||
|
{
|
||||||
|
lock (_eventLock)
|
||||||
|
{
|
||||||
|
return _events
|
||||||
|
.GroupBy(e => e.Category ?? "Unknown")
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
lock (_eventLock)
|
||||||
|
{
|
||||||
|
var errorCount = _events.Count(e => e.Level >= LogLevel.Error);
|
||||||
|
var duration = Time();
|
||||||
|
return $"CorrelationGroup [ID: {CorrelationId}, Events: {_events.Count}, Errors: {errorCount}, Duration: {duration.TotalMilliseconds:F0}ms]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics about correlation tracking
|
||||||
|
/// </summary>
|
||||||
|
public class CorrelationStatistics
|
||||||
|
{
|
||||||
|
public int TotalCorrelationIds { get; set; }
|
||||||
|
public int TotalTrackedEvents { get; set; }
|
||||||
|
public double AverageEventsPerCorrelation { get; set; }
|
||||||
|
public int MinEventsPerCorrelation { get; set; }
|
||||||
|
public int MaxEventsPerCorrelation { get; set; }
|
||||||
|
public double MedianEventsPerCorrelation { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return $"CorrelationStats: IDs={TotalCorrelationIds}, Events={TotalTrackedEvents}, " +
|
||||||
|
$"Avg={AverageEventsPerCorrelation:F1}, Min={MinEventsPerCorrelation}, Max={MaxEventsPerCorrelation}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,262 @@
|
|||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Flows
|
||||||
|
{
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LocalStorageQueryFlow stores recent log events in memory and provides LINQ-like querying capabilities.
|
||||||
|
/// Useful for diagnostics, testing, and real-time log analysis without external storage.
|
||||||
|
/// Uses a circular buffer with configurable maximum size to prevent unbounded memory growth.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LocalStorageQueryFlow : FlowBase
|
||||||
|
{
|
||||||
|
private readonly int _maxCapacity;
|
||||||
|
private readonly ConcurrentQueue<LogEvent> _logBuffer = new();
|
||||||
|
private int _currentCount = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all stored log events
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyList<LogEvent> AllEvents => _logBuffer.ToList();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the number of events currently stored
|
||||||
|
/// </summary>
|
||||||
|
public int StoredEventCount => _currentCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new LocalStorageQueryFlow with specified capacity
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="maxCapacity">Maximum number of log events to keep in memory (default 10000)</param>
|
||||||
|
/// <param name="minimumLevel">Minimum log level to store</param>
|
||||||
|
public LocalStorageQueryFlow(
|
||||||
|
int maxCapacity = 10000,
|
||||||
|
LogLevel minimumLevel = LogLevel.Trace)
|
||||||
|
: base("LocalStorageQuery", minimumLevel)
|
||||||
|
{
|
||||||
|
if (maxCapacity <= 0)
|
||||||
|
throw new ArgumentException("Capacity must be greater than 0", nameof(maxCapacity));
|
||||||
|
|
||||||
|
_maxCapacity = maxCapacity;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
|
||||||
|
return WriteResult.Success;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logBuffer.Enqueue(logEvent);
|
||||||
|
int newCount = Interlocked.Increment(ref _currentCount);
|
||||||
|
|
||||||
|
// Maintain circular buffer by dropping oldest items when over capacity
|
||||||
|
while (newCount > _maxCapacity)
|
||||||
|
{
|
||||||
|
if (_logBuffer.TryDequeue(out _))
|
||||||
|
{
|
||||||
|
newCount = Interlocked.Decrement(ref _currentCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.FromResult(WriteResult.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return WriteResult.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var logEvent in logEvents.Span)
|
||||||
|
{
|
||||||
|
if (IsLogLevelEnabled(logEvent))
|
||||||
|
{
|
||||||
|
_logBuffer.Enqueue(logEvent);
|
||||||
|
int newCount = Interlocked.Increment(ref _currentCount);
|
||||||
|
|
||||||
|
// Maintain circular buffer
|
||||||
|
while (newCount > _maxCapacity)
|
||||||
|
{
|
||||||
|
if (_logBuffer.TryDequeue(out _))
|
||||||
|
{
|
||||||
|
newCount = Interlocked.Decrement(ref _currentCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return await Task.FromResult(WriteResult.Success);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return WriteResult.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task FlushAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
// No async I/O needed for in-memory storage
|
||||||
|
await Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query stored events by log level
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogEvent> QueryByLevel(LogLevel level)
|
||||||
|
{
|
||||||
|
return _logBuffer.Where(e => e.Level == level);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query stored events by category
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogEvent> QueryByCategory(string category)
|
||||||
|
{
|
||||||
|
return _logBuffer.Where(e => e.Category?.Equals(category, StringComparison.OrdinalIgnoreCase) ?? false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query stored events by message pattern
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogEvent> QueryByMessagePattern(string pattern, StringComparison comparison = StringComparison.OrdinalIgnoreCase)
|
||||||
|
{
|
||||||
|
return _logBuffer.Where(e =>
|
||||||
|
{
|
||||||
|
var msg = e.Message.ToString();
|
||||||
|
return msg.Contains(pattern, comparison);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query stored events by time range
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogEvent> QueryByTimeRange(DateTime from, DateTime to)
|
||||||
|
{
|
||||||
|
var fromTicks = from.Ticks;
|
||||||
|
var toTicks = to.Ticks;
|
||||||
|
return _logBuffer.Where(e => e.Timestamp >= fromTicks && e.Timestamp <= toTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query stored events that contain exceptions
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogEvent> QueryExceptionEvents()
|
||||||
|
{
|
||||||
|
return _logBuffer.Where(e => e.Exception != null);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query stored events by duration (for performance logs)
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogEvent> QueryByMinimumDuration(TimeSpan duration)
|
||||||
|
{
|
||||||
|
return _logBuffer.Where(e =>
|
||||||
|
e.Properties != null &&
|
||||||
|
e.Properties.TryGetValue("duration", out var durValue) &&
|
||||||
|
durValue is TimeSpan ts &&
|
||||||
|
ts >= duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get statistics about stored events
|
||||||
|
/// </summary>
|
||||||
|
public LogStorageStatistics GetStatistics()
|
||||||
|
{
|
||||||
|
var events = _logBuffer.ToList();
|
||||||
|
if (events.Count == 0)
|
||||||
|
return new LogStorageStatistics();
|
||||||
|
|
||||||
|
var stats = new LogStorageStatistics
|
||||||
|
{
|
||||||
|
TotalEvents = events.Count,
|
||||||
|
EventsByLevel = events
|
||||||
|
.GroupBy(e => e.Level)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count()),
|
||||||
|
EventsByCategory = events
|
||||||
|
.GroupBy(e => e.Category ?? "Unknown")
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count()),
|
||||||
|
OldestEventTime = new DateTime(events.Min(e => e.Timestamp)),
|
||||||
|
NewestEventTime = new DateTime(events.Max(e => e.Timestamp)),
|
||||||
|
ExceptionCount = events.Count(e => e.Exception != null),
|
||||||
|
AverageMessageLength = (int)events.Average(e => e.Message.Length)
|
||||||
|
};
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clear all stored events
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
while (_logBuffer.TryDequeue(out _)) { }
|
||||||
|
_currentCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Get the most recent N events
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogEvent> GetLatest(int count)
|
||||||
|
{
|
||||||
|
return _logBuffer.Reverse().Take(count);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Search with custom predicate
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogEvent> Search(Func<LogEvent, bool> predicate)
|
||||||
|
{
|
||||||
|
return _logBuffer.Where(predicate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
Clear();
|
||||||
|
await base.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics about logs stored in LocalStorageQueryFlow
|
||||||
|
/// </summary>
|
||||||
|
public class LogStorageStatistics
|
||||||
|
{
|
||||||
|
public int TotalEvents { get; set; }
|
||||||
|
public Dictionary<LogLevel, int> EventsByLevel { get; set; } = new();
|
||||||
|
public Dictionary<string, int> EventsByCategory { get; set; } = new();
|
||||||
|
public DateTime OldestEventTime { get; set; }
|
||||||
|
public DateTime NewestEventTime { get; set; }
|
||||||
|
public int ExceptionCount { get; set; }
|
||||||
|
public int AverageMessageLength { get; set; }
|
||||||
|
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var levelBreakdown = string.Join(", ", EventsByLevel.Select(kvp => $"{kvp.Key}: {kvp.Value}"));
|
||||||
|
var categoryBreakdown = string.Join(", ", EventsByCategory.Take(3).Select(kvp => $"{kvp.Key}: {kvp.Value}"));
|
||||||
|
|
||||||
|
return $"LogStorageStats: Total={TotalEvents}, Exceptions={ExceptionCount}, " +
|
||||||
|
$"Levels=[{levelBreakdown}], Categories=[{categoryBreakdown}]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,357 @@
|
|||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Channels;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Flows
|
||||||
|
{
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// LogFilterPresetFlow provides commonly-used filter combinations as reusable presets.
|
||||||
|
/// Reduces boilerplate for common scenarios like error tracking, performance monitoring, etc.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LogFilterPresetFlow : FlowBase
|
||||||
|
{
|
||||||
|
private readonly Channel<LogEvent> _channel;
|
||||||
|
private readonly IFlow _targetFlow;
|
||||||
|
private readonly LogFilterPreset _preset;
|
||||||
|
private Task _processingTask;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a LogFilterPresetFlow with a specific preset
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetFlow">The flow to send filtered events to</param>
|
||||||
|
/// <param name="preset">The filter preset to apply</param>
|
||||||
|
/// <param name="minimumLevel">Minimum log level</param>
|
||||||
|
public LogFilterPresetFlow(
|
||||||
|
IFlow targetFlow,
|
||||||
|
LogFilterPreset preset,
|
||||||
|
LogLevel minimumLevel = LogLevel.Trace)
|
||||||
|
: base($"FilterPreset_{preset.Name}", minimumLevel)
|
||||||
|
{
|
||||||
|
_targetFlow = targetFlow ?? throw new ArgumentNullException(nameof(targetFlow));
|
||||||
|
_preset = preset ?? throw new ArgumentNullException(nameof(preset));
|
||||||
|
_channel = Channel.CreateUnbounded<LogEvent>(new UnboundedChannelOptions
|
||||||
|
{
|
||||||
|
SingleWriter = false,
|
||||||
|
SingleReader = true
|
||||||
|
});
|
||||||
|
|
||||||
|
StartProcessing();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void StartProcessing()
|
||||||
|
{
|
||||||
|
_processingTask = ProcessChannelAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ProcessChannelAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await foreach (var logEvent in _channel.Reader.ReadAllAsync())
|
||||||
|
{
|
||||||
|
var filtered = _preset.Filter(logEvent);
|
||||||
|
if (filtered)
|
||||||
|
{
|
||||||
|
await _targetFlow.BlastAsync(logEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Expected when channel is closed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
|
||||||
|
return WriteResult.Success;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _channel.Writer.WriteAsync(logEvent, cancellationToken);
|
||||||
|
return WriteResult.Success;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return WriteResult.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var events = logEvents.ToArray(); // Copy to array to avoid span issues across await
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var logEvent in events)
|
||||||
|
{
|
||||||
|
if (IsLogLevelEnabled(logEvent))
|
||||||
|
{
|
||||||
|
await _channel.Writer.WriteAsync(logEvent, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return WriteResult.Success;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return WriteResult.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async Task FlushAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
await _targetFlow.FlushAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
_channel.Writer.TryComplete();
|
||||||
|
if (_processingTask != null)
|
||||||
|
await _processingTask;
|
||||||
|
await _targetFlow.DisposeAsync();
|
||||||
|
await base.DisposeAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Base class for filter presets
|
||||||
|
/// </summary>
|
||||||
|
public abstract class LogFilterPreset
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
protected LogFilterPreset(string name)
|
||||||
|
{
|
||||||
|
Name = name ?? "Custom";
|
||||||
|
}
|
||||||
|
|
||||||
|
public abstract bool Filter(LogEvent logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: Only errors and exceptions
|
||||||
|
/// </summary>
|
||||||
|
public class ErrorOnlyPreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
public ErrorOnlyPreset() : base("ErrorOnly") { }
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
return logEvent.Level >= LogLevel.Error || logEvent.Exception != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: Errors with full stack traces
|
||||||
|
/// </summary>
|
||||||
|
public class ErrorWithStackTracePreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
public ErrorWithStackTracePreset() : base("ErrorWithStackTrace") { }
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
return (logEvent.Level >= LogLevel.Error || logEvent.Exception != null) &&
|
||||||
|
logEvent.Exception?.StackTrace != null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: Performance-related logs (duration, latency, timeout)
|
||||||
|
/// </summary>
|
||||||
|
public class PerformanceLogsPreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
private readonly string[] _keywords = { "duration", "latency", "timeout", "slow", "performance", "elapsed", "ms" };
|
||||||
|
|
||||||
|
public PerformanceLogsPreset() : base("PerformanceLogs") { }
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
var message = logEvent.Message.ToString();
|
||||||
|
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase)) ||
|
||||||
|
(logEvent.Properties != null && logEvent.Properties.Keys.Any(k => k.Contains("duration", StringComparison.OrdinalIgnoreCase)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: Authentication and security-related logs
|
||||||
|
/// </summary>
|
||||||
|
public class SecurityLogsPreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
private readonly string[] _keywords = { "auth", "security", "permission", "token", "password", "encrypt", "decrypt", "unauthorized", "forbidden" };
|
||||||
|
|
||||||
|
public SecurityLogsPreset() : base("SecurityLogs") { }
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
var message = logEvent.Message.ToString();
|
||||||
|
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: Health check and monitoring logs
|
||||||
|
/// </summary>
|
||||||
|
public class HealthCheckPreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
private readonly string[] _keywords = { "health", "check", "monitor", "alive", "heartbeat", "ping", "status", "ready" };
|
||||||
|
|
||||||
|
public HealthCheckPreset() : base("HealthCheck") { }
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
var message = logEvent.Message.ToString();
|
||||||
|
var category = logEvent.Category ?? string.Empty;
|
||||||
|
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
category.Contains(kw, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: Data access and database-related logs
|
||||||
|
/// </summary>
|
||||||
|
public class DatabaseLogsPreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
private readonly string[] _keywords = { "database", "sql", "query", "transaction", "connection", "db:", "entity", "datacontext" };
|
||||||
|
|
||||||
|
public DatabaseLogsPreset() : base("DatabaseLogs") { }
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
var message = logEvent.Message.ToString();
|
||||||
|
var category = logEvent.Category ?? string.Empty;
|
||||||
|
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
category.Contains(kw, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: Business logic and domain events
|
||||||
|
/// </summary>
|
||||||
|
public class DomainEventsPreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
private readonly string[] _keywords = { "event", "domain", "aggregate", "business", "order", "payment", "transaction" };
|
||||||
|
|
||||||
|
public DomainEventsPreset() : base("DomainEvents") { }
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
var message = logEvent.Message.ToString();
|
||||||
|
var category = logEvent.Category ?? string.Empty;
|
||||||
|
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
category.Contains(kw, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: External service calls and integrations
|
||||||
|
/// </summary>
|
||||||
|
public class ExternalServicePreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
private readonly string[] _keywords = { "http", "api", "external", "service", "remote", "call", "webhook", "webhook", "third-party" };
|
||||||
|
|
||||||
|
public ExternalServicePreset() : base("ExternalService") { }
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
var message = logEvent.Message.ToString();
|
||||||
|
var category = logEvent.Category ?? string.Empty;
|
||||||
|
return _keywords.Any(kw => message.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
category.Contains(kw, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: Critical warnings and errors (anything that might need attention)
|
||||||
|
/// </summary>
|
||||||
|
public class CriticalAlertsPreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
public CriticalAlertsPreset() : base("CriticalAlerts") { }
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
return logEvent.Level >= LogLevel.Warning ||
|
||||||
|
logEvent.Exception != null ||
|
||||||
|
(logEvent.Properties?.ContainsKey("Alert") ?? false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: Development and debugging logs
|
||||||
|
/// </summary>
|
||||||
|
public class DebugLogsPreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
private readonly string[] _keywords = { "debug", "trace", "verbose", "diagnostic", "test", "dev" };
|
||||||
|
|
||||||
|
public DebugLogsPreset() : base("DebugLogs") { }
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
var msg = logEvent.Message.ToString();
|
||||||
|
var cat = logEvent.Category ?? string.Empty;
|
||||||
|
return logEvent.Level <= LogLevel.Debug ||
|
||||||
|
_keywords.Any(kw => msg.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
cat.Contains(kw, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset: Custom filter based on predicate
|
||||||
|
/// </summary>
|
||||||
|
public class CustomPreset : LogFilterPreset
|
||||||
|
{
|
||||||
|
private readonly Func<LogEvent, bool> _predicate;
|
||||||
|
|
||||||
|
public CustomPreset(string name, Func<LogEvent, bool> predicate)
|
||||||
|
: base(name)
|
||||||
|
{
|
||||||
|
_predicate = predicate ?? throw new ArgumentNullException(nameof(predicate));
|
||||||
|
}
|
||||||
|
|
||||||
|
public override bool Filter(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
return _predicate(logEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Preset Registry for managing and creating presets
|
||||||
|
/// </summary>
|
||||||
|
public static class LogFilterPresetRegistry
|
||||||
|
{
|
||||||
|
private static readonly Dictionary<string, LogFilterPreset> _presets = new()
|
||||||
|
{
|
||||||
|
["ErrorOnly"] = new ErrorOnlyPreset(),
|
||||||
|
["ErrorWithStackTrace"] = new ErrorWithStackTracePreset(),
|
||||||
|
["Performance"] = new PerformanceLogsPreset(),
|
||||||
|
["Security"] = new SecurityLogsPreset(),
|
||||||
|
["HealthCheck"] = new HealthCheckPreset(),
|
||||||
|
["Database"] = new DatabaseLogsPreset(),
|
||||||
|
["DomainEvents"] = new DomainEventsPreset(),
|
||||||
|
["ExternalService"] = new ExternalServicePreset(),
|
||||||
|
["CriticalAlerts"] = new CriticalAlertsPreset(),
|
||||||
|
["Debug"] = new DebugLogsPreset()
|
||||||
|
};
|
||||||
|
|
||||||
|
public static LogFilterPreset? Get(string presetName)
|
||||||
|
{
|
||||||
|
return _presets.TryGetValue(presetName, out var preset) ? preset : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static IEnumerable<string> GetAvailablePresets()
|
||||||
|
{
|
||||||
|
return _presets.Keys;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void Register(string name, LogFilterPreset preset)
|
||||||
|
{
|
||||||
|
_presets[name] = preset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.Boosters;
|
||||||
using EonaCat.LogStack.Core;
|
using EonaCat.LogStack.Core;
|
||||||
using EonaCat.LogStack.Flows;
|
using EonaCat.LogStack.Flows;
|
||||||
|
using EonaCat.LogStack.Routing;
|
||||||
|
using EonaCat.LogStack.Sampling;
|
||||||
|
using EonaCat.LogStack.Anomalies;
|
||||||
|
using EonaCat.LogStack.DeadLettering;
|
||||||
|
using EonaCat.LogStack.Diagnostics;
|
||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
@@ -62,6 +67,13 @@ namespace EonaCat.LogStack
|
|||||||
// Dynamic level controller
|
// Dynamic level controller
|
||||||
private volatile DynamicLevelController? _dynamicLevel;
|
private volatile DynamicLevelController? _dynamicLevel;
|
||||||
|
|
||||||
|
// Superior logging features
|
||||||
|
private readonly IntelligentRouter? _intelligentRouter;
|
||||||
|
private readonly AdaptiveSamplingEngine? _adaptiveSampler;
|
||||||
|
private readonly AnomalyDetector? _anomalyDetector;
|
||||||
|
private readonly ContextSnapshotCollector? _contextSnapshots;
|
||||||
|
private DeadLetterQueue? _deadLetterQueue;
|
||||||
|
|
||||||
public event EventHandler<LogMessage> OnLog;
|
public event EventHandler<LogMessage> OnLog;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -69,12 +81,22 @@ namespace EonaCat.LogStack
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public EonaCatLogStack(string category = "Application",
|
public EonaCatLogStack(string category = "Application",
|
||||||
LogLevel minimumLevel = LogLevel.Trace,
|
LogLevel minimumLevel = LogLevel.Trace,
|
||||||
TimestampMode timestampMode = TimestampMode.Utc)
|
TimestampMode timestampMode = TimestampMode.Utc,
|
||||||
|
bool enableIntelligentRouting = false,
|
||||||
|
bool enableAdaptiveSampling = false,
|
||||||
|
bool enableAnomalyDetection = false,
|
||||||
|
bool enableContextSnapshots = false)
|
||||||
{
|
{
|
||||||
_category = category ?? throw new ArgumentNullException(nameof(category));
|
_category = category ?? throw new ArgumentNullException(nameof(category));
|
||||||
_minimumLevel = minimumLevel;
|
_minimumLevel = minimumLevel;
|
||||||
_timestampMode = timestampMode;
|
_timestampMode = timestampMode;
|
||||||
|
|
||||||
|
// Initialize superior features if requested
|
||||||
|
_intelligentRouter = enableIntelligentRouting ? new IntelligentRouter() : null;
|
||||||
|
_adaptiveSampler = enableAdaptiveSampling ? new AdaptiveSamplingEngine() : null;
|
||||||
|
_anomalyDetector = enableAnomalyDetection ? new AnomalyDetector() : null;
|
||||||
|
_contextSnapshots = enableContextSnapshots ? new ContextSnapshotCollector() : null;
|
||||||
|
|
||||||
// Enable async pipeline by default
|
// Enable async pipeline by default
|
||||||
UseAsyncPipeline();
|
UseAsyncPipeline();
|
||||||
}
|
}
|
||||||
@@ -119,6 +141,38 @@ namespace EonaCat.LogStack
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Intelligent Router for pattern-based flow routing (if enabled).
|
||||||
|
/// </summary>
|
||||||
|
public IntelligentRouter GetIntelligentRouter() => _intelligentRouter;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Adaptive Sampling Engine (if enabled).
|
||||||
|
/// </summary>
|
||||||
|
public AdaptiveSamplingEngine GetAdaptiveSampler() => _adaptiveSampler;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Anomaly Detector (if enabled).
|
||||||
|
/// </summary>
|
||||||
|
public AnomalyDetector GetAnomalyDetector() => _anomalyDetector;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the Context Snapshot Collector (if enabled).
|
||||||
|
/// </summary>
|
||||||
|
public ContextSnapshotCollector GetContextSnapshots() => _contextSnapshots;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or creates the Dead Letter Queue for handling failed logs.
|
||||||
|
/// </summary>
|
||||||
|
public DeadLetterQueue GetDeadLetterQueue()
|
||||||
|
{
|
||||||
|
if (_deadLetterQueue == null)
|
||||||
|
{
|
||||||
|
_deadLetterQueue = new DeadLetterQueue();
|
||||||
|
}
|
||||||
|
return _deadLetterQueue;
|
||||||
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private LogLevel EffectiveMinLevel() =>
|
private LogLevel EffectiveMinLevel() =>
|
||||||
_dynamicLevel != null ? _dynamicLevel.CurrentLevel : _minimumLevel;
|
_dynamicLevel != null ? _dynamicLevel.CurrentLevel : _minimumLevel;
|
||||||
@@ -413,6 +467,13 @@ namespace EonaCat.LogStack
|
|||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private void ProcessLogEvent(ref LogEventBuilder builder)
|
private void ProcessLogEvent(ref LogEventBuilder builder)
|
||||||
{
|
{
|
||||||
|
// Check adaptive sampling first - if sampling is enabled and this log is dropped, return early
|
||||||
|
if (_adaptiveSampler != null && !_adaptiveSampler.ShouldSample())
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _totalDroppedCount);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Apply boosters
|
// Apply boosters
|
||||||
lock (_boosters)
|
lock (_boosters)
|
||||||
{
|
{
|
||||||
@@ -443,6 +504,25 @@ namespace EonaCat.LogStack
|
|||||||
}
|
}
|
||||||
|
|
||||||
var logEvent = builder.Build();
|
var logEvent = builder.Build();
|
||||||
|
|
||||||
|
// Record sampling metric
|
||||||
|
if (_adaptiveSampler != null)
|
||||||
|
{
|
||||||
|
_adaptiveSampler.RecordLogEvent();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Detect anomalies
|
||||||
|
if (_anomalyDetector != null)
|
||||||
|
{
|
||||||
|
_anomalyDetector.AnalyzeEvent(logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Capture context snapshots on critical logs
|
||||||
|
if (_contextSnapshots != null)
|
||||||
|
{
|
||||||
|
_contextSnapshots.CaptureIfNeeded(logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
Interlocked.Increment(ref _totalLoggedCount);
|
Interlocked.Increment(ref _totalLoggedCount);
|
||||||
|
|
||||||
if (!_asyncChannel!.Writer.TryWrite(logEvent))
|
if (!_asyncChannel!.Writer.TryWrite(logEvent))
|
||||||
@@ -458,6 +538,18 @@ namespace EonaCat.LogStack
|
|||||||
{
|
{
|
||||||
await foreach (var logEvent in reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
await foreach (var logEvent in reader.ReadAllAsync(cancellationToken).ConfigureAwait(false))
|
||||||
{
|
{
|
||||||
|
// If intelligent router is enabled, use it for routing
|
||||||
|
if (_intelligentRouter != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _intelligentRouter.RouteAsync(logEvent).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch { /* Router should handle its own errors */ }
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Default routing to all flows
|
||||||
var flows = _concurrentFlows;
|
var flows = _concurrentFlows;
|
||||||
foreach (var flow in flows)
|
foreach (var flow in flows)
|
||||||
{
|
{
|
||||||
@@ -467,9 +559,25 @@ namespace EonaCat.LogStack
|
|||||||
if (result == WriteResult.Dropped)
|
if (result == WriteResult.Dropped)
|
||||||
{
|
{
|
||||||
Interlocked.Increment(ref _totalDroppedCount);
|
Interlocked.Increment(ref _totalDroppedCount);
|
||||||
|
|
||||||
|
// Enqueue to DLQ if available
|
||||||
|
if (_deadLetterQueue != null)
|
||||||
|
{
|
||||||
|
_deadLetterQueue.Enqueue(logEvent, $"Dropped by flow '{flow.Name}'");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
// Enqueue to DLQ if available
|
||||||
|
if (_deadLetterQueue != null)
|
||||||
|
{
|
||||||
|
_deadLetterQueue.Enqueue(logEvent,
|
||||||
|
$"Exception in flow '{flow.Name}': {ex.Message}",
|
||||||
|
ex);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch { }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -557,6 +665,48 @@ namespace EonaCat.LogStack
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public LoggingMetrics GetMetricsCollector() => _metrics;
|
public LoggingMetrics GetMetricsCollector() => _metrics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets comprehensive statistics about all superior features.
|
||||||
|
/// </summary>
|
||||||
|
public SuperiorFeaturesStats GetSuperiorFeaturesStats()
|
||||||
|
{
|
||||||
|
var stats = new SuperiorFeaturesStats();
|
||||||
|
|
||||||
|
if (_intelligentRouter != null)
|
||||||
|
{
|
||||||
|
stats.RouterStats = _intelligentRouter.GetStats();
|
||||||
|
stats.IntelligentRoutingEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_adaptiveSampler != null)
|
||||||
|
{
|
||||||
|
stats.SamplingMetrics = _adaptiveSampler.GetMetrics();
|
||||||
|
stats.AdaptiveSamplingEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_anomalyDetector != null)
|
||||||
|
{
|
||||||
|
stats.AnomalyStats = _anomalyDetector.GetStats();
|
||||||
|
stats.RecentAnomalies = _anomalyDetector.GetRecentAnomalies(5);
|
||||||
|
stats.AnomalyDetectionEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_contextSnapshots != null)
|
||||||
|
{
|
||||||
|
stats.SnapshotStats = _contextSnapshots.GetStats();
|
||||||
|
stats.ContextSnapshotsEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_deadLetterQueue != null)
|
||||||
|
{
|
||||||
|
stats.DlqStats = _deadLetterQueue.GetStats();
|
||||||
|
stats.DlqFailureReasons = _deadLetterQueue.GetFailureReasonStats();
|
||||||
|
stats.DeadLetterQueueEnabled = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public async ValueTask DisposeAsync()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,314 @@
|
|||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using System;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Sampling;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adaptive sampling engine that dynamically adjusts sampling rates based on log volume and system resources.
|
||||||
|
/// Superior to static sampling as it prevents log loss during normal operation while reducing overhead during peaks.
|
||||||
|
/// </summary>
|
||||||
|
public class AdaptiveSamplingEngine
|
||||||
|
{
|
||||||
|
private double _samplingRate = 1.0; // Sample 100% of logs by default
|
||||||
|
private double _targetThroughput = 10000; // Target 10k logs/sec
|
||||||
|
private long _logCount;
|
||||||
|
private long _lastAdjustmentTicks;
|
||||||
|
private readonly Stopwatch _perfCounter = Stopwatch.StartNew();
|
||||||
|
private readonly double _adjustmentIntervalMs;
|
||||||
|
private readonly SamplingStrategy _strategy;
|
||||||
|
private readonly object _statsLock = new object();
|
||||||
|
|
||||||
|
// CPU and Memory monitoring
|
||||||
|
private double _currentCpuPercent;
|
||||||
|
private double _currentMemoryPercent;
|
||||||
|
private readonly double _cpuThreshold;
|
||||||
|
private readonly double _memoryThreshold;
|
||||||
|
private readonly Process _currentProcess = Process.GetCurrentProcess();
|
||||||
|
private DateTime _lastCpuCheck;
|
||||||
|
|
||||||
|
public AdaptiveSamplingEngine(
|
||||||
|
SamplingStrategy strategy = SamplingStrategy.Throughput,
|
||||||
|
double targetThroughput = 10000,
|
||||||
|
double adjustmentIntervalMs = 5000,
|
||||||
|
double cpuThreshold = 0.80,
|
||||||
|
double memoryThreshold = 0.85)
|
||||||
|
{
|
||||||
|
_strategy = strategy;
|
||||||
|
_targetThroughput = targetThroughput;
|
||||||
|
_adjustmentIntervalMs = adjustmentIntervalMs;
|
||||||
|
_cpuThreshold = cpuThreshold;
|
||||||
|
_memoryThreshold = memoryThreshold;
|
||||||
|
_lastAdjustmentTicks = _perfCounter.ElapsedTicks;
|
||||||
|
_lastCpuCheck = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines if a log event should be sampled (kept) or dropped.
|
||||||
|
/// Returns true if the event should be logged.
|
||||||
|
/// </summary>
|
||||||
|
public bool ShouldSample()
|
||||||
|
{
|
||||||
|
// Periodically adjust sampling rate
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
if (_perfCounter.ElapsedTicks - _lastAdjustmentTicks >
|
||||||
|
TicksFromMilliseconds(_adjustmentIntervalMs))
|
||||||
|
{
|
||||||
|
AdjustSamplingRate();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use random sampling based on current rate
|
||||||
|
if (_samplingRate >= 1.0)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
if (_samplingRate <= 0.0)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
return ThreadSafeRandom.NextDouble() < _samplingRate;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adjusts the sampling rate based on current metrics and strategy.
|
||||||
|
/// </summary>
|
||||||
|
private void AdjustSamplingRate()
|
||||||
|
{
|
||||||
|
_lastAdjustmentTicks = _perfCounter.ElapsedTicks;
|
||||||
|
var currentLogCount = Interlocked.Exchange(ref _logCount, 0);
|
||||||
|
|
||||||
|
UpdateResourceMetrics();
|
||||||
|
|
||||||
|
switch (_strategy)
|
||||||
|
{
|
||||||
|
case SamplingStrategy.Throughput:
|
||||||
|
AdjustForThroughput();
|
||||||
|
break;
|
||||||
|
case SamplingStrategy.Resources:
|
||||||
|
AdjustForResources();
|
||||||
|
break;
|
||||||
|
case SamplingStrategy.Hybrid:
|
||||||
|
AdjustForThroughput();
|
||||||
|
AdjustForResources();
|
||||||
|
break;
|
||||||
|
case SamplingStrategy.Adaptive:
|
||||||
|
AdjustAdaptive();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clamp between 0 and 1
|
||||||
|
_samplingRate = Math.Max(0.0, Math.Min(1.0, _samplingRate));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AdjustForThroughput()
|
||||||
|
{
|
||||||
|
var elapsedSeconds = _adjustmentIntervalMs / 1000.0;
|
||||||
|
var currentLogCount = 0L;
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
currentLogCount = Interlocked.Read(ref _logCount);
|
||||||
|
}
|
||||||
|
var actualThroughput = currentLogCount / elapsedSeconds;
|
||||||
|
|
||||||
|
if (actualThroughput > _targetThroughput * 1.2) // 20% over target
|
||||||
|
{
|
||||||
|
// Reduce sampling rate
|
||||||
|
_samplingRate *= 0.9;
|
||||||
|
}
|
||||||
|
else if (actualThroughput < _targetThroughput * 0.8) // 20% under target
|
||||||
|
{
|
||||||
|
// Increase sampling rate
|
||||||
|
_samplingRate *= 1.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AdjustForResources()
|
||||||
|
{
|
||||||
|
if (_currentCpuPercent > _cpuThreshold || _currentMemoryPercent > _memoryThreshold)
|
||||||
|
{
|
||||||
|
// High resource usage - reduce sampling rate more aggressively
|
||||||
|
_samplingRate *= 0.7;
|
||||||
|
}
|
||||||
|
else if (_currentCpuPercent < _cpuThreshold * 0.5 && _currentMemoryPercent < _memoryThreshold * 0.5)
|
||||||
|
{
|
||||||
|
// Low resource usage - increase sampling rate
|
||||||
|
_samplingRate *= 1.15;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void AdjustAdaptive()
|
||||||
|
{
|
||||||
|
/* Adaptive strategy: adjust based on both throughput and resources,
|
||||||
|
with memory being more critical than CPU */
|
||||||
|
var cpuFactor = _currentCpuPercent / _cpuThreshold;
|
||||||
|
var memoryFactor = _currentMemoryPercent / _memoryThreshold;
|
||||||
|
|
||||||
|
// Memory pressure has higher weight
|
||||||
|
var resourcePressure = (cpuFactor * 0.4) + (memoryFactor * 0.6);
|
||||||
|
|
||||||
|
if (resourcePressure > 1.0)
|
||||||
|
{
|
||||||
|
_samplingRate *= (1.0 - (resourcePressure - 1.0) * 0.5);
|
||||||
|
}
|
||||||
|
else if (resourcePressure < 0.5)
|
||||||
|
{
|
||||||
|
_samplingRate *= 1.1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void UpdateResourceMetrics()
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if ((now - _lastCpuCheck).TotalMilliseconds < 1000)
|
||||||
|
return; // Update at most once per second to avoid overhead
|
||||||
|
|
||||||
|
_lastCpuCheck = now;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// CPU usage (process)
|
||||||
|
_currentCpuProcess = _currentProcess.TotalProcessorTime.TotalMilliseconds;
|
||||||
|
|
||||||
|
// Simplified: estimate CPU % based on processor count
|
||||||
|
var cpuUsageEstimate = Math.Min(1.0, _currentCpuProcess / (Environment.ProcessorCount * 100));
|
||||||
|
_currentCpuPercent = cpuUsageEstimate;
|
||||||
|
|
||||||
|
// Memory usage
|
||||||
|
var totalMemory = GC.GetTotalMemory(false);
|
||||||
|
var workingSet = _currentProcess.WorkingSet64;
|
||||||
|
|
||||||
|
// Estimate based on available system memory
|
||||||
|
// In production, use MemoryMarshal or other APIs for more precise data
|
||||||
|
_currentMemoryPercent = Math.Min(1.0, workingSet / (8.0 * 1024 * 1024 * 1024)); // Assume 8GB
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If we can't get metrics, don't crash
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private double _currentCpuProcess;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the target throughput for throughput-based sampling.
|
||||||
|
/// </summary>
|
||||||
|
public void SetTargetThroughput(double logsPerSecond)
|
||||||
|
{
|
||||||
|
_targetThroughput = Math.Max(1, logsPerSecond);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets current sampling rate (0.0 to 1.0).
|
||||||
|
/// </summary>
|
||||||
|
public double GetSamplingRate() => _samplingRate;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets current resource utilization metrics.
|
||||||
|
/// </summary>
|
||||||
|
public SamplingMetrics GetMetrics()
|
||||||
|
{
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
return new SamplingMetrics
|
||||||
|
{
|
||||||
|
SamplingRate = _samplingRate,
|
||||||
|
CurrentCpuPercent = _currentCpuPercent,
|
||||||
|
CurrentMemoryPercent = _currentMemoryPercent,
|
||||||
|
Strategy = _strategy,
|
||||||
|
TargetThroughput = _targetThroughput,
|
||||||
|
LogsProcessedInLastInterval = Interlocked.Read(ref _logCount)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records a log event (increments counter for sampling decisions).
|
||||||
|
/// </summary>
|
||||||
|
public void RecordLogEvent()
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref _logCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long TicksFromMilliseconds(double ms)
|
||||||
|
{
|
||||||
|
return (long)(ms * Stopwatch.Frequency / 1000.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sampling strategies for adaptive engine.
|
||||||
|
/// </summary>
|
||||||
|
public enum SamplingStrategy
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adjusts sampling based on throughput to match target logs/sec
|
||||||
|
/// </summary>
|
||||||
|
Throughput,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adjusts sampling based on CPU and memory usage
|
||||||
|
/// </summary>
|
||||||
|
Resources,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Combines throughput and resource monitoring
|
||||||
|
/// </summary>
|
||||||
|
Hybrid,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Advanced adaptive strategy that learns optimal rate
|
||||||
|
/// </summary>
|
||||||
|
Adaptive
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Metrics from the adaptive sampling engine.
|
||||||
|
/// </summary>
|
||||||
|
public class SamplingMetrics
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Current sampling rate (0.0 = drop all, 1.0 = keep all)
|
||||||
|
/// </summary>
|
||||||
|
public double SamplingRate { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current CPU usage percentage (0.0 to 1.0)
|
||||||
|
/// </summary>
|
||||||
|
public double CurrentCpuPercent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Current memory usage percentage (0.0 to 1.0)
|
||||||
|
/// </summary>
|
||||||
|
public double CurrentMemoryPercent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Active sampling strategy
|
||||||
|
/// </summary>
|
||||||
|
public SamplingStrategy Strategy { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Target throughput in logs per second
|
||||||
|
/// </summary>
|
||||||
|
public double TargetThroughput { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Logs processed in last adjustment interval
|
||||||
|
/// </summary>
|
||||||
|
public long LogsProcessedInLastInterval { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thread-safe random number generator for sampling decisions.
|
||||||
|
/// </summary>
|
||||||
|
internal static class ThreadSafeRandom
|
||||||
|
{
|
||||||
|
private static readonly ThreadLocal<Random> _random = new ThreadLocal<Random>(
|
||||||
|
() => new Random(Guid.NewGuid().GetHashCode())
|
||||||
|
);
|
||||||
|
|
||||||
|
public static double NextDouble() => _random.Value.NextDouble();
|
||||||
|
}
|
||||||
@@ -0,0 +1,368 @@
|
|||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Anomalies;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Real-time anomaly detector that identifies unusual logging patterns and escalates them.
|
||||||
|
/// Superior to simple threshold-based alerting as it learns normal behavior over time.
|
||||||
|
/// </summary>
|
||||||
|
public class AnomalyDetector
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, CategoryStatistics> _categoryStats
|
||||||
|
= new ConcurrentDictionary<string, CategoryStatistics>();
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<LogLevel, LevelStatistics> _levelStats
|
||||||
|
= new ConcurrentDictionary<LogLevel, LevelStatistics>();
|
||||||
|
|
||||||
|
private readonly double _standardDeviationThreshold;
|
||||||
|
private readonly int _windowSizeSeconds;
|
||||||
|
private readonly object _statsLock = new object();
|
||||||
|
private long _totalEventsProcessed;
|
||||||
|
|
||||||
|
private readonly List<AnomalyAlert> _recentAlerts = new List<AnomalyAlert>();
|
||||||
|
private readonly Stopwatch _uptime = Stopwatch.StartNew();
|
||||||
|
|
||||||
|
public event EventHandler<AnomalyAlert> AnomalyDetected;
|
||||||
|
|
||||||
|
public AnomalyDetector(
|
||||||
|
double standardDeviationThreshold = 3.0,
|
||||||
|
int windowSizeSeconds = 300)
|
||||||
|
{
|
||||||
|
_standardDeviationThreshold = standardDeviationThreshold;
|
||||||
|
_windowSizeSeconds = windowSizeSeconds;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analyzes a log event for anomalies.
|
||||||
|
/// </summary>
|
||||||
|
public AnomalyAlert AnalyzeEvent(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
|
||||||
|
_totalEventsProcessed++;
|
||||||
|
|
||||||
|
// Track category statistics
|
||||||
|
var categoryStats = _categoryStats.GetOrAdd(logEvent.Category ?? "Unknown",
|
||||||
|
_ => new CategoryStatistics());
|
||||||
|
categoryStats.RecordEvent(logEvent);
|
||||||
|
|
||||||
|
// Track level statistics
|
||||||
|
var levelStats = _levelStats.GetOrAdd(logEvent.Level,
|
||||||
|
_ => new LevelStatistics());
|
||||||
|
levelStats.RecordEvent();
|
||||||
|
|
||||||
|
// Detect anomalies
|
||||||
|
var anomalies = new List<string>();
|
||||||
|
|
||||||
|
// Check for unusual error spike
|
||||||
|
if (logEvent.Level == LogLevel.Error || logEvent.Level == LogLevel.Critical)
|
||||||
|
{
|
||||||
|
var errorRate = levelStats.GetErrorRatePerSecond();
|
||||||
|
if (errorRate > 50) // More than 50 errors per second
|
||||||
|
{
|
||||||
|
anomalies.Add($"High error rate detected: {errorRate:F2} errors/sec");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for category anomalies
|
||||||
|
if (categoryStats.IsAnomalous(_standardDeviationThreshold))
|
||||||
|
{
|
||||||
|
anomalies.Add($"Category '{logEvent.Category}' showing unusual activity");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for exception frequency spike
|
||||||
|
if (logEvent.Exception != null)
|
||||||
|
{
|
||||||
|
var exceptionKey = logEvent.Exception.GetType().Name;
|
||||||
|
if (categoryStats.RecordException(exceptionKey) > 5)
|
||||||
|
{
|
||||||
|
anomalies.Add($"Exception '{exceptionKey}' spike detected");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for message repetition
|
||||||
|
if (logEvent.Message.Length > 0)
|
||||||
|
{
|
||||||
|
if (categoryStats.RecordMessageHash(logEvent.Message.ToString().GetHashCode()) > 10)
|
||||||
|
{
|
||||||
|
anomalies.Add("Repeated message pattern detected (possible infinite loop)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anomalies.Any())
|
||||||
|
{
|
||||||
|
var alert = new AnomalyAlert
|
||||||
|
{
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
Category = logEvent.Category,
|
||||||
|
Level = logEvent.Level,
|
||||||
|
LogEvent = logEvent,
|
||||||
|
Anomalies = anomalies,
|
||||||
|
AnomalySeverity = DetermineAnomalySeverity(anomalies),
|
||||||
|
SystemMetrics = CaptureSystemMetrics()
|
||||||
|
};
|
||||||
|
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
_recentAlerts.Add(alert);
|
||||||
|
if (_recentAlerts.Count > 1000)
|
||||||
|
_recentAlerts.RemoveAt(0); // Keep last 1000 alerts
|
||||||
|
}
|
||||||
|
|
||||||
|
AnomalyDetected?.Invoke(this, alert);
|
||||||
|
return alert;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets statistics summary for analysis.
|
||||||
|
/// </summary>
|
||||||
|
public AnomalyDetectorStats GetStats()
|
||||||
|
{
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
return new AnomalyDetectorStats
|
||||||
|
{
|
||||||
|
TotalEventsProcessed = _totalEventsProcessed,
|
||||||
|
UniqueCategories = _categoryStats.Count,
|
||||||
|
RecentAnomalies = _recentAlerts.Count,
|
||||||
|
AnomalyRate = _recentAlerts.Count / Math.Max(1, _uptime.Elapsed.TotalMinutes),
|
||||||
|
TopCategories = _categoryStats
|
||||||
|
.OrderByDescending(x => x.Value.EventCount)
|
||||||
|
.Take(5)
|
||||||
|
.Select(x => new CategorySummary
|
||||||
|
{
|
||||||
|
Category = x.Key,
|
||||||
|
EventCount = x.Value.EventCount,
|
||||||
|
IsAnomalous = x.Value.IsAnomalous(_standardDeviationThreshold)
|
||||||
|
})
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets recent detected anomalies.
|
||||||
|
/// </summary>
|
||||||
|
public List<AnomalyAlert> GetRecentAnomalies(int count = 10)
|
||||||
|
{
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
return _recentAlerts.Skip(Math.Max(0, _recentAlerts.Count - count))
|
||||||
|
.OrderBy(a => a.Timestamp)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all statistics (useful for resetting baseline after deployment).
|
||||||
|
/// </summary>
|
||||||
|
public void ResetStatistics()
|
||||||
|
{
|
||||||
|
_categoryStats.Clear();
|
||||||
|
_levelStats.Clear();
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
_recentAlerts.Clear();
|
||||||
|
}
|
||||||
|
_totalEventsProcessed = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private AnomalySeverity DetermineAnomalySeverity(List<string> anomalies)
|
||||||
|
{
|
||||||
|
if (anomalies.Any(a => a.Contains("error rate")))
|
||||||
|
return AnomalySeverity.Critical;
|
||||||
|
|
||||||
|
if (anomalies.Any(a => a.Contains("Exception") || a.Contains("infinite loop")))
|
||||||
|
return AnomalySeverity.High;
|
||||||
|
|
||||||
|
return AnomalySeverity.Medium;
|
||||||
|
}
|
||||||
|
|
||||||
|
private SystemMetrics CaptureSystemMetrics()
|
||||||
|
{
|
||||||
|
var process = Process.GetCurrentProcess();
|
||||||
|
return new SystemMetrics
|
||||||
|
{
|
||||||
|
ProcessMemoryMb = process.WorkingSet64 / (1024 * 1024),
|
||||||
|
ProcessorCount = Environment.ProcessorCount,
|
||||||
|
Uptime = _uptime.Elapsed,
|
||||||
|
ThreadCount = process.Threads.Count,
|
||||||
|
TotalEventsProcessed = _totalEventsProcessed
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks statistics for a log category.
|
||||||
|
/// </summary>
|
||||||
|
internal class CategoryStatistics
|
||||||
|
{
|
||||||
|
private readonly Queue<DateTime> _recentEventTimes = new Queue<DateTime>();
|
||||||
|
private readonly ConcurrentDictionary<int, int> _messageHashCounts = new ConcurrentDictionary<int, int>();
|
||||||
|
private readonly ConcurrentDictionary<string, int> _exceptionCounts = new ConcurrentDictionary<string, int>();
|
||||||
|
|
||||||
|
public long EventCount { get; private set; }
|
||||||
|
private long _errorCount;
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
|
||||||
|
public void RecordEvent(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
EventCount++;
|
||||||
|
_recentEventTimes.Enqueue(DateTime.UtcNow);
|
||||||
|
if (_recentEventTimes.Count > 1000)
|
||||||
|
_recentEventTimes.Dequeue();
|
||||||
|
|
||||||
|
if (logEvent.Level == LogLevel.Error || logEvent.Level == LogLevel.Critical)
|
||||||
|
_errorCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RecordException(string exceptionType)
|
||||||
|
{
|
||||||
|
return _exceptionCounts.AddOrUpdate(exceptionType, 1, (k, v) => v + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public int RecordMessageHash(int messageHash)
|
||||||
|
{
|
||||||
|
return _messageHashCounts.AddOrUpdate(messageHash, 1, (k, v) => v + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool IsAnomalous(double stdDevThreshold)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_recentEventTimes.Count < 10)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var times = _recentEventTimes.ToList();
|
||||||
|
var recentIntervals = new List<double>();
|
||||||
|
|
||||||
|
for (int i = 1; i < times.Count; i++)
|
||||||
|
{
|
||||||
|
var interval = (now - times[i]).TotalSeconds;
|
||||||
|
if (interval > 0)
|
||||||
|
recentIntervals.Add(interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recentIntervals.Count < 2)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
var mean = recentIntervals.Average();
|
||||||
|
var variance = recentIntervals.Average(x => Math.Pow(x - mean, 2));
|
||||||
|
var stdDev = Math.Sqrt(variance);
|
||||||
|
|
||||||
|
// Event times are anomalous if they're too frequent (low stdDev) or erratic (high stdDev)
|
||||||
|
return stdDev < 0.1 || stdDev > stdDevThreshold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks statistics for a log level.
|
||||||
|
/// </summary>
|
||||||
|
internal class LevelStatistics
|
||||||
|
{
|
||||||
|
private readonly Queue<DateTime> _eventTimes = new Queue<DateTime>();
|
||||||
|
private readonly object _lock = new object();
|
||||||
|
private long _errorCount;
|
||||||
|
|
||||||
|
public void RecordEvent()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
_errorCount++;
|
||||||
|
_eventTimes.Enqueue(DateTime.UtcNow);
|
||||||
|
if (_eventTimes.Count > 10000)
|
||||||
|
_eventTimes.Dequeue();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public double GetErrorRatePerSecond()
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
|
{
|
||||||
|
if (_eventTimes.Count < 2)
|
||||||
|
return 0;
|
||||||
|
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
var span = (now - _eventTimes.Peek()).TotalSeconds;
|
||||||
|
|
||||||
|
if (span < 1)
|
||||||
|
return _eventTimes.Count;
|
||||||
|
|
||||||
|
return _eventTimes.Count / span;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An alert raised when an anomaly is detected.
|
||||||
|
/// </summary>
|
||||||
|
public class AnomalyAlert
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public string Category { get; set; }
|
||||||
|
public LogLevel Level { get; set; }
|
||||||
|
public LogEvent LogEvent { get; set; }
|
||||||
|
public List<string> Anomalies { get; set; } = new List<string>();
|
||||||
|
public AnomalySeverity AnomalySeverity { get; set; }
|
||||||
|
public SystemMetrics SystemMetrics { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Severity of detected anomaly.
|
||||||
|
/// </summary>
|
||||||
|
public enum AnomalySeverity
|
||||||
|
{
|
||||||
|
Low,
|
||||||
|
Medium,
|
||||||
|
High,
|
||||||
|
Critical
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// System metrics captured when anomaly detected.
|
||||||
|
/// </summary>
|
||||||
|
public class SystemMetrics
|
||||||
|
{
|
||||||
|
public long ProcessMemoryMb { get; set; }
|
||||||
|
public int ProcessorCount { get; set; }
|
||||||
|
public TimeSpan Uptime { get; set; }
|
||||||
|
public int ThreadCount { get; set; }
|
||||||
|
public long TotalEventsProcessed { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics about anomaly detector.
|
||||||
|
/// </summary>
|
||||||
|
public class AnomalyDetectorStats
|
||||||
|
{
|
||||||
|
public long TotalEventsProcessed { get; set; }
|
||||||
|
public int UniqueCategories { get; set; }
|
||||||
|
public int RecentAnomalies { get; set; }
|
||||||
|
public double AnomalyRate { get; set; }
|
||||||
|
public List<CategorySummary> TopCategories { get; set; } = new List<CategorySummary>();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Summary of a category's status.
|
||||||
|
/// </summary>
|
||||||
|
public class CategorySummary
|
||||||
|
{
|
||||||
|
public string Category { get; set; }
|
||||||
|
public long EventCount { get; set; }
|
||||||
|
public bool IsAnomalous { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,392 @@
|
|||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Diagnostics;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Context Snapshot Collector - captures heap snapshots and thread stacks on critical errors.
|
||||||
|
/// Superior to simple error logging as it provides full context for debugging.
|
||||||
|
/// </summary>
|
||||||
|
public class ContextSnapshotCollector
|
||||||
|
{
|
||||||
|
private readonly List<ContextSnapshot> _snapshots = new List<ContextSnapshot>();
|
||||||
|
private readonly int _maxSnapshots;
|
||||||
|
private readonly object _snapshotsLock = new object();
|
||||||
|
private readonly LogLevel _triggerLevel;
|
||||||
|
private long _totalSnapshotsCaptured;
|
||||||
|
|
||||||
|
public event EventHandler<ContextSnapshot> SnapshotCaptured;
|
||||||
|
|
||||||
|
public ContextSnapshotCollector(
|
||||||
|
LogLevel triggerLevel = LogLevel.Critical,
|
||||||
|
int maxSnapshots = 100)
|
||||||
|
{
|
||||||
|
_triggerLevel = triggerLevel;
|
||||||
|
_maxSnapshots = Math.Max(10, maxSnapshots);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Captures a context snapshot if the log event meets trigger conditions.
|
||||||
|
/// </summary>
|
||||||
|
public ContextSnapshot CaptureIfNeeded(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
if (logEvent.Level < _triggerLevel)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
return CaptureSnapshot(logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Captures a full context snapshot for the given log event.
|
||||||
|
/// </summary>
|
||||||
|
public ContextSnapshot CaptureSnapshot(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
|
||||||
|
var snapshot = new ContextSnapshot
|
||||||
|
{
|
||||||
|
CapturedAt = DateTime.UtcNow,
|
||||||
|
LogEvent = logEvent,
|
||||||
|
ProcessInfo = CaptureProcessInfo(),
|
||||||
|
MemoryInfo = CaptureMemoryInfo(),
|
||||||
|
ThreadInfo = CaptureThreadInfo(),
|
||||||
|
StackTraces = CaptureStackTraces(),
|
||||||
|
EnvironmentVars = CaptureEnvironmentSnapshot()
|
||||||
|
};
|
||||||
|
|
||||||
|
lock (_snapshotsLock)
|
||||||
|
{
|
||||||
|
_snapshots.Add(snapshot);
|
||||||
|
if (_snapshots.Count > _maxSnapshots)
|
||||||
|
_snapshots.RemoveAt(0);
|
||||||
|
|
||||||
|
_totalSnapshotsCaptured++;
|
||||||
|
}
|
||||||
|
|
||||||
|
SnapshotCaptured?.Invoke(this, snapshot);
|
||||||
|
return snapshot;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the most recent snapshot.
|
||||||
|
/// </summary>
|
||||||
|
public ContextSnapshot GetLatestSnapshot()
|
||||||
|
{
|
||||||
|
lock (_snapshotsLock)
|
||||||
|
{
|
||||||
|
return _snapshots.LastOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all captured snapshots.
|
||||||
|
/// </summary>
|
||||||
|
public List<ContextSnapshot> GetSnapshots(int skip = 0, int take = 10)
|
||||||
|
{
|
||||||
|
lock (_snapshotsLock)
|
||||||
|
{
|
||||||
|
return _snapshots.Skip(skip).Take(take).ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a snapshot by timestamp window.
|
||||||
|
/// </summary>
|
||||||
|
public ContextSnapshot GetSnapshotNear(DateTime timestamp)
|
||||||
|
{
|
||||||
|
lock (_snapshotsLock)
|
||||||
|
{
|
||||||
|
return _snapshots
|
||||||
|
.OrderBy(s => Math.Abs((s.CapturedAt - timestamp).TotalSeconds))
|
||||||
|
.FirstOrDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets statistics about captured snapshots.
|
||||||
|
/// </summary>
|
||||||
|
public SnapshotCollectorStats GetStats()
|
||||||
|
{
|
||||||
|
lock (_snapshotsLock)
|
||||||
|
{
|
||||||
|
var levelDistribution = _snapshots
|
||||||
|
.GroupBy(s => s.LogEvent.Level)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
|
||||||
|
return new SnapshotCollectorStats
|
||||||
|
{
|
||||||
|
TotalCaptured = _totalSnapshotsCaptured,
|
||||||
|
CurrentSnapshots = _snapshots.Count,
|
||||||
|
MaxCapacity = _maxSnapshots,
|
||||||
|
LevelDistribution = levelDistribution,
|
||||||
|
OldestSnapshot = _snapshots.FirstOrDefault()?.CapturedAt,
|
||||||
|
NewestSnapshot = _snapshots.LastOrDefault()?.CapturedAt
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all snapshots.
|
||||||
|
/// </summary>
|
||||||
|
public int ClearSnapshots()
|
||||||
|
{
|
||||||
|
lock (_snapshotsLock)
|
||||||
|
{
|
||||||
|
var count = _snapshots.Count;
|
||||||
|
_snapshots.Clear();
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ProcessSnapshot CaptureProcessInfo()
|
||||||
|
{
|
||||||
|
var process = Process.GetCurrentProcess();
|
||||||
|
return new ProcessSnapshot
|
||||||
|
{
|
||||||
|
ProcessId = process.Id,
|
||||||
|
ProcessName = process.ProcessName,
|
||||||
|
StartTime = process.StartTime,
|
||||||
|
TotalProcessorTime = process.TotalProcessorTime,
|
||||||
|
UserProcessorTime = process.UserProcessorTime,
|
||||||
|
BasePriority = process.BasePriority,
|
||||||
|
Handles = 0, // Not available in .NET Standard
|
||||||
|
ThreadCount = process.Threads.Count
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private MemorySnapshot CaptureMemoryInfo()
|
||||||
|
{
|
||||||
|
var totalMemory = GC.GetTotalMemory(false);
|
||||||
|
var process = Process.GetCurrentProcess();
|
||||||
|
|
||||||
|
var gen0Collections = GC.CollectionCount(0);
|
||||||
|
var gen1Collections = GC.CollectionCount(1);
|
||||||
|
var gen2Collections = GC.CollectionCount(2);
|
||||||
|
|
||||||
|
return new MemorySnapshot
|
||||||
|
{
|
||||||
|
ManagedHeapBytes = totalMemory,
|
||||||
|
ProcessWorkingSetBytes = process.WorkingSet64,
|
||||||
|
ProcessPrivateMemoryBytes = process.PrivateMemorySize64,
|
||||||
|
Gen0Collections = gen0Collections,
|
||||||
|
Gen1Collections = gen1Collections,
|
||||||
|
Gen2Collections = gen2Collections,
|
||||||
|
TotalAllocatedBytes = totalMemory, // Simplified fallback
|
||||||
|
IsHighMemoryPressure = false // Conservative estimate
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private ThreadSnapshot CaptureThreadInfo()
|
||||||
|
{
|
||||||
|
var process = Process.GetCurrentProcess();
|
||||||
|
var threads = process.Threads;
|
||||||
|
var threadDetails = new List<ThreadDetail>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (ProcessThread t in threads)
|
||||||
|
{
|
||||||
|
threadDetails.Add(new ThreadDetail
|
||||||
|
{
|
||||||
|
ThreadId = t.Id,
|
||||||
|
State = t.ThreadState.ToString(),
|
||||||
|
WaitReason = t.WaitReason.ToString(),
|
||||||
|
Priority = t.CurrentPriority
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Some properties may not be available on all platforms
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ThreadSnapshot
|
||||||
|
{
|
||||||
|
TotalThreadCount = threads.Count,
|
||||||
|
ThreadIds = threadDetails,
|
||||||
|
ManagedThreadCount = 0 // ThreadPool.ThreadCount not available in .NET Standard
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<int, string[]> CaptureStackTraces()
|
||||||
|
{
|
||||||
|
var stacks = new Dictionary<int, string[]>();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Get current thread stack
|
||||||
|
var currentThread = Thread.CurrentThread;
|
||||||
|
var stackFrames = new StackTrace(true).GetFrames();
|
||||||
|
|
||||||
|
if (stackFrames != null)
|
||||||
|
{
|
||||||
|
stacks[currentThread.ManagedThreadId] = stackFrames
|
||||||
|
.Select(f => f.GetMethod()?.Name ?? "Unknown")
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// If we can't get stack traces, just skip
|
||||||
|
}
|
||||||
|
|
||||||
|
return stacks;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Dictionary<string, string> CaptureEnvironmentSnapshot()
|
||||||
|
{
|
||||||
|
var env = new Dictionary<string, string>();
|
||||||
|
|
||||||
|
// Capture selected environment variables (not all for security)
|
||||||
|
var safeVars = new[]
|
||||||
|
{
|
||||||
|
"DOTNET_VERSION",
|
||||||
|
"PROCESSOR_COUNT",
|
||||||
|
"OS",
|
||||||
|
"RUNNING_UNDER_DOCKER",
|
||||||
|
"ASPNETCORE_ENVIRONMENT"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var varName in safeVars)
|
||||||
|
{
|
||||||
|
var varValue = Environment.GetEnvironmentVariable(varName);
|
||||||
|
if (varValue != null)
|
||||||
|
env[varName] = varValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add runtime info
|
||||||
|
env["RUNTIME_VERSION"] = RuntimeInformation.FrameworkDescription;
|
||||||
|
env["PROCESSOR_COUNT"] = Environment.ProcessorCount.ToString();
|
||||||
|
|
||||||
|
return env;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// A captured context snapshot.
|
||||||
|
/// </summary>
|
||||||
|
public class ContextSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// When this snapshot was captured.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime CapturedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The log event that triggered this snapshot.
|
||||||
|
/// </summary>
|
||||||
|
public LogEvent LogEvent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process information at time of capture.
|
||||||
|
/// </summary>
|
||||||
|
public ProcessSnapshot ProcessInfo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory information at time of capture.
|
||||||
|
/// </summary>
|
||||||
|
public MemorySnapshot MemoryInfo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thread information at time of capture.
|
||||||
|
/// </summary>
|
||||||
|
public ThreadSnapshot ThreadInfo { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stack traces for active threads.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<int, string[]> StackTraces { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Environment snapshot.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, string> EnvironmentVars { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Process information snapshot.
|
||||||
|
/// </summary>
|
||||||
|
public class ProcessSnapshot
|
||||||
|
{
|
||||||
|
public int ProcessId { get; set; }
|
||||||
|
public string ProcessName { get; set; }
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public TimeSpan TotalProcessorTime { get; set; }
|
||||||
|
public TimeSpan UserProcessorTime { get; set; }
|
||||||
|
public int BasePriority { get; set; }
|
||||||
|
public int Handles { get; set; }
|
||||||
|
public int ThreadCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Memory information snapshot.
|
||||||
|
/// </summary>
|
||||||
|
public class MemorySnapshot
|
||||||
|
{
|
||||||
|
public long ManagedHeapBytes { get; set; }
|
||||||
|
public long ProcessWorkingSetBytes { get; set; }
|
||||||
|
public long ProcessPrivateMemoryBytes { get; set; }
|
||||||
|
public int Gen0Collections { get; set; }
|
||||||
|
public int Gen1Collections { get; set; }
|
||||||
|
public int Gen2Collections { get; set; }
|
||||||
|
public long TotalAllocatedBytes { get; set; }
|
||||||
|
public bool IsHighMemoryPressure { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Thread information snapshot.
|
||||||
|
/// </summary>
|
||||||
|
public class ThreadSnapshot
|
||||||
|
{
|
||||||
|
public int TotalThreadCount { get; set; }
|
||||||
|
public List<ThreadDetail> ThreadIds { get; set; }
|
||||||
|
public int ManagedThreadCount { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Detail about a single thread.
|
||||||
|
/// </summary>
|
||||||
|
public class ThreadDetail
|
||||||
|
{
|
||||||
|
public int ThreadId { get; set; }
|
||||||
|
public string State { get; set; }
|
||||||
|
public string WaitReason { get; set; }
|
||||||
|
public int Priority { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics about the snapshot collector.
|
||||||
|
/// </summary>
|
||||||
|
public class SnapshotCollectorStats
|
||||||
|
{
|
||||||
|
public long TotalCaptured { get; set; }
|
||||||
|
public int CurrentSnapshots { get; set; }
|
||||||
|
public int MaxCapacity { get; set; }
|
||||||
|
public Dictionary<LogLevel, int> LevelDistribution { get; set; }
|
||||||
|
public DateTime? OldestSnapshot { get; set; }
|
||||||
|
public DateTime? NewestSnapshot { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Runtime information helper.
|
||||||
|
/// </summary>
|
||||||
|
internal static class RuntimeInformation
|
||||||
|
{
|
||||||
|
public static string FrameworkDescription
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
#if NET6_0_OR_GREATER
|
||||||
|
return System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription;
|
||||||
|
#else
|
||||||
|
return ".NET " + Environment.Version;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using EonaCat.LogStack.Flows;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.DeadLettering;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dead Letter Queue (DLQ) for capturing failed log attempts and enabling replay.
|
||||||
|
/// Superior to data loss as it preserves logs that couldn't be delivered to any flow.
|
||||||
|
/// </summary>
|
||||||
|
public class DeadLetterQueue
|
||||||
|
{
|
||||||
|
private readonly ConcurrentQueue<DeadLetterEvent> _queue = new ConcurrentQueue<DeadLetterEvent>();
|
||||||
|
private readonly int _maxCapacity;
|
||||||
|
private int _currentCount;
|
||||||
|
private long _totalDropped;
|
||||||
|
private long _totalEnqueued;
|
||||||
|
private readonly object _statsLock = new object();
|
||||||
|
|
||||||
|
public event EventHandler<DeadLetterEvent> EventEnqueued;
|
||||||
|
public event EventHandler<DeadLetterEventReplayed> EventReplayed;
|
||||||
|
|
||||||
|
public DeadLetterQueue(int maxCapacity = 100000)
|
||||||
|
{
|
||||||
|
_maxCapacity = Math.Max(1000, maxCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enqueues a failed log event with the reason for failure.
|
||||||
|
/// </summary>
|
||||||
|
public bool Enqueue(LogEvent logEvent, string failureReason, Exception failureException = null)
|
||||||
|
{
|
||||||
|
|
||||||
|
if (_currentCount >= _maxCapacity)
|
||||||
|
{
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
_totalDropped++;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dlEvent = new DeadLetterEvent
|
||||||
|
{
|
||||||
|
LogEvent = logEvent,
|
||||||
|
EnqueuedAt = DateTime.UtcNow,
|
||||||
|
FailureReason = failureReason ?? "Unknown",
|
||||||
|
FailureException = failureException?.ToString(),
|
||||||
|
RetryCount = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
_queue.Enqueue(dlEvent);
|
||||||
|
_currentCount++;
|
||||||
|
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
_totalEnqueued++;
|
||||||
|
}
|
||||||
|
|
||||||
|
EventEnqueued?.Invoke(this, dlEvent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the count of events in the DLQ.
|
||||||
|
/// </summary>
|
||||||
|
public int GetCount() => _currentCount;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Peeks at the next event without removing it.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryPeek(out DeadLetterEvent dlEvent)
|
||||||
|
{
|
||||||
|
return _queue.TryPeek(out dlEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dequeues the next event.
|
||||||
|
/// </summary>
|
||||||
|
public bool TryDequeue(out DeadLetterEvent dlEvent)
|
||||||
|
{
|
||||||
|
if (_queue.TryDequeue(out var evt))
|
||||||
|
{
|
||||||
|
_currentCount--;
|
||||||
|
dlEvent = evt;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
dlEvent = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all events in the DLQ without removing them.
|
||||||
|
/// </summary>
|
||||||
|
public List<DeadLetterEvent> GetAll(int skip = 0, int take = 100)
|
||||||
|
{
|
||||||
|
return _queue.Skip(skip).Take(take).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replays a dead letter event to specified flows.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> ReplayAsync(DeadLetterEvent dlEvent, IEnumerable<IFlow> targetFlows)
|
||||||
|
{
|
||||||
|
if (dlEvent == null)
|
||||||
|
throw new ArgumentNullException(nameof(dlEvent));
|
||||||
|
|
||||||
|
dlEvent.RetryCount++;
|
||||||
|
dlEvent.LastRetryAt = DateTime.UtcNow;
|
||||||
|
|
||||||
|
var flowsList = targetFlows.ToList();
|
||||||
|
var tasks = new List<Task<WriteResult>>();
|
||||||
|
|
||||||
|
foreach (var flow in flowsList)
|
||||||
|
{
|
||||||
|
tasks.Add(flow.BlastAsync(dlEvent.LogEvent));
|
||||||
|
}
|
||||||
|
|
||||||
|
var results = await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
var success = results.Any(r => r == WriteResult.Success);
|
||||||
|
|
||||||
|
if (success)
|
||||||
|
{
|
||||||
|
dlEvent.ReplayedAt = DateTime.UtcNow;
|
||||||
|
dlEvent.ReplaySuccessful = true;
|
||||||
|
EventReplayed?.Invoke(this, new DeadLetterEventReplayed
|
||||||
|
{
|
||||||
|
Event = dlEvent,
|
||||||
|
WasSuccessful = true,
|
||||||
|
FlowsTargeted = flowsList.Count
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Bulk replay of oldest events (useful for periodic retry jobs).
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> ReplayOldestAsync(int count, IEnumerable<IFlow> targetFlows)
|
||||||
|
{
|
||||||
|
var toReplay = GetAll(0, count);
|
||||||
|
int successCount = 0;
|
||||||
|
|
||||||
|
foreach (var dlEvent in toReplay)
|
||||||
|
{
|
||||||
|
if (await ReplayAsync(dlEvent, targetFlows))
|
||||||
|
{
|
||||||
|
successCount++;
|
||||||
|
// Remove from queue if successful
|
||||||
|
_currentCount--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return successCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all events from the DLQ.
|
||||||
|
/// </summary>
|
||||||
|
public int Clear()
|
||||||
|
{
|
||||||
|
var count = _currentCount;
|
||||||
|
while (_queue.TryDequeue(out _))
|
||||||
|
{
|
||||||
|
_currentCount--;
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets statistics about the DLQ.
|
||||||
|
/// </summary>
|
||||||
|
public DeadLetterQueueStats GetStats()
|
||||||
|
{
|
||||||
|
lock (_statsLock)
|
||||||
|
{
|
||||||
|
return new DeadLetterQueueStats
|
||||||
|
{
|
||||||
|
CurrentQueueSize = _currentCount,
|
||||||
|
MaxCapacity = _maxCapacity,
|
||||||
|
TotalEnqueued = _totalEnqueued,
|
||||||
|
TotalDropped = _totalDropped,
|
||||||
|
QueueUtilizationPercent = (_currentCount * 100.0) / _maxCapacity,
|
||||||
|
OldestEventAge = _queue.TryPeek(out var oldest)
|
||||||
|
? DateTime.UtcNow - oldest.EnqueuedAt
|
||||||
|
: TimeSpan.Zero
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets events grouped by failure reason.
|
||||||
|
/// </summary>
|
||||||
|
public Dictionary<string, int> GetFailureReasonStats()
|
||||||
|
{
|
||||||
|
var stats = new Dictionary<string, int>();
|
||||||
|
foreach (var evt in _queue)
|
||||||
|
{
|
||||||
|
var reason = evt.FailureReason ?? "Unknown";
|
||||||
|
if (!stats.ContainsKey(reason))
|
||||||
|
stats[reason] = 0;
|
||||||
|
stats[reason]++;
|
||||||
|
}
|
||||||
|
return stats.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a dead letter event.
|
||||||
|
/// </summary>
|
||||||
|
public class DeadLetterEvent
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The original log event that failed to be delivered.
|
||||||
|
/// </summary>
|
||||||
|
public LogEvent LogEvent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When this event was enqueued to the DLQ.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime EnqueuedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reason why the log event failed.
|
||||||
|
/// </summary>
|
||||||
|
public string FailureReason { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exception details if available.
|
||||||
|
/// </summary>
|
||||||
|
public string FailureException { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of times this event has been retried.
|
||||||
|
/// </summary>
|
||||||
|
public int RetryCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When this event was last retried.
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? LastRetryAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// When this event was successfully replayed (if at all).
|
||||||
|
/// </summary>
|
||||||
|
public DateTime? ReplayedAt { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the replay was successful.
|
||||||
|
/// </summary>
|
||||||
|
public bool ReplaySuccessful { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event raised when a dead letter event is replayed.
|
||||||
|
/// </summary>
|
||||||
|
public class DeadLetterEventReplayed
|
||||||
|
{
|
||||||
|
public DeadLetterEvent Event { get; set; }
|
||||||
|
public bool WasSuccessful { get; set; }
|
||||||
|
public int FlowsTargeted { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics about the dead letter queue.
|
||||||
|
/// </summary>
|
||||||
|
public class DeadLetterQueueStats
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Current number of events in the queue.
|
||||||
|
/// </summary>
|
||||||
|
public int CurrentQueueSize { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum capacity of the queue.
|
||||||
|
/// </summary>
|
||||||
|
public int MaxCapacity { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total events ever enqueued.
|
||||||
|
/// </summary>
|
||||||
|
public long TotalEnqueued { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total events dropped due to capacity.
|
||||||
|
/// </summary>
|
||||||
|
public long TotalDropped { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Queue utilization as percentage.
|
||||||
|
/// </summary>
|
||||||
|
public double QueueUtilizationPercent { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Age of the oldest event in queue.
|
||||||
|
/// </summary>
|
||||||
|
public TimeSpan OldestEventAge { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,203 @@
|
|||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using EonaCat.LogStack.DeadLettering;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Flows;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dead Letter Queue Flow - captures log events that failed to be delivered to other flows.
|
||||||
|
/// Enables analysis and replay of failed logs.
|
||||||
|
/// </summary>
|
||||||
|
public class DlqFlow : FlowBase
|
||||||
|
{
|
||||||
|
private readonly DeadLetterQueue _dlq;
|
||||||
|
private readonly List<IFlow> _backupFlows = new List<IFlow>();
|
||||||
|
private readonly object _flowsLock = new object();
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
public DlqFlow(
|
||||||
|
string name = "DLQ",
|
||||||
|
LogLevel minimumLevel = LogLevel.Warning,
|
||||||
|
int maxCapacity = 100000)
|
||||||
|
: base(name, minimumLevel)
|
||||||
|
{
|
||||||
|
_dlq = new DeadLetterQueue(maxCapacity);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the underlying dead letter queue for inspection and replay.
|
||||||
|
/// </summary>
|
||||||
|
public DeadLetterQueue GetDeadLetterQueue() => _dlq;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a backup flow to attempt retrying failed events.
|
||||||
|
/// </summary>
|
||||||
|
public DlqFlow AddBackupFlow(IFlow flow)
|
||||||
|
{
|
||||||
|
if (flow == null) throw new ArgumentNullException(nameof(flow));
|
||||||
|
lock (_flowsLock)
|
||||||
|
{
|
||||||
|
_backupFlows.Add(flow);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blasts a single log event to the DLQ.
|
||||||
|
/// </summary>
|
||||||
|
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_disposed != 0)
|
||||||
|
return WriteResult.Dropped;
|
||||||
|
|
||||||
|
if (!IsLogLevelEnabled(logEvent))
|
||||||
|
return WriteResult.Success;
|
||||||
|
|
||||||
|
// Try backup flows first before enqueueing to DLQ
|
||||||
|
List<IFlow> backupFlows;
|
||||||
|
lock (_flowsLock)
|
||||||
|
{
|
||||||
|
backupFlows = new List<IFlow>(_backupFlows);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool anyBackupSucceeded = false;
|
||||||
|
var failureReasons = new List<string>();
|
||||||
|
|
||||||
|
foreach (var backupFlow in backupFlows)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await backupFlow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (result == WriteResult.Success)
|
||||||
|
{
|
||||||
|
anyBackupSucceeded = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
failureReasons.Add($"Backup flow '{backupFlow.Name}': failed");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
failureReasons.Add($"Backup flow '{backupFlow.Name}' exception: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (anyBackupSucceeded)
|
||||||
|
{
|
||||||
|
BlastedCount++;
|
||||||
|
return WriteResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
// All backups failed, enqueue to DLQ
|
||||||
|
var failureReason = string.Join("; ", failureReasons);
|
||||||
|
if (failureReason.Length > 500)
|
||||||
|
failureReason = failureReason.Substring(0, 500) + "...";
|
||||||
|
|
||||||
|
var enqueued = _dlq.Enqueue(logEvent, failureReason ?? "Unknown failure");
|
||||||
|
|
||||||
|
if (enqueued)
|
||||||
|
{
|
||||||
|
BlastedCount++;
|
||||||
|
return WriteResult.Success;
|
||||||
|
}
|
||||||
|
|
||||||
|
DroppedCount++;
|
||||||
|
return WriteResult.Dropped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blasts a batch of log events to the DLQ.
|
||||||
|
/// </summary>
|
||||||
|
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (_disposed != 0)
|
||||||
|
return WriteResult.Dropped;
|
||||||
|
|
||||||
|
var events = logEvents.ToArray();
|
||||||
|
int successCount = 0;
|
||||||
|
|
||||||
|
foreach (var logEvent in events)
|
||||||
|
{
|
||||||
|
var result = await BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
|
||||||
|
if (result == WriteResult.Success)
|
||||||
|
successCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return successCount > 0 ? WriteResult.Success : WriteResult.Dropped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flushes the DLQ (does nothing, but required by interface).
|
||||||
|
/// </summary>
|
||||||
|
public override Task FlushAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets statistics about this DLQ flow.
|
||||||
|
/// </summary>
|
||||||
|
public DeadLetterQueueStats GetStatistics()
|
||||||
|
{
|
||||||
|
return _dlq.GetStats();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Attempts to replay queued dead letters to backup flows.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<int> ReplayOldestAsync(int count)
|
||||||
|
{
|
||||||
|
List<IFlow> backupFlows;
|
||||||
|
lock (_flowsLock)
|
||||||
|
{
|
||||||
|
backupFlows = new List<IFlow>(_backupFlows);
|
||||||
|
}
|
||||||
|
|
||||||
|
return await _dlq.ReplayOldestAsync(count, (IEnumerable<IFlow>)backupFlows).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all dead letter events.
|
||||||
|
/// </summary>
|
||||||
|
public List<DeadLetterEvent> GetDeadLetters(int skip = 0, int take = 100)
|
||||||
|
{
|
||||||
|
return _dlq.GetAll(skip, take);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all dead letters from the queue.
|
||||||
|
/// </summary>
|
||||||
|
public int ClearDeadLetters()
|
||||||
|
{
|
||||||
|
return _dlq.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
List<IFlow> flowsToDispose;
|
||||||
|
lock (_flowsLock)
|
||||||
|
{
|
||||||
|
flowsToDispose = new List<IFlow>(_backupFlows);
|
||||||
|
_backupFlows.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var flow in flowsToDispose)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await flow.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using EonaCat.LogStack.Flows;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
||||||
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Intelligent message router that directs log events to different flows based on pattern matching and rules.
|
||||||
|
/// This is superior to simple level-based routing as it can apply complex business logic.
|
||||||
|
/// </summary>
|
||||||
|
public class IntelligentRouter : IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly List<RoutingRule> _rules = new List<RoutingRule>();
|
||||||
|
private readonly Dictionary<string, FlowRoute> _flowRoutes = new Dictionary<string, FlowRoute>();
|
||||||
|
private readonly object _rulesLock = new object();
|
||||||
|
|
||||||
|
public IntelligentRouter()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a routing rule that matches messages based on pattern and routes them to specific flows.
|
||||||
|
/// </summary>
|
||||||
|
public IntelligentRouter AddRule(RoutingRule rule)
|
||||||
|
{
|
||||||
|
if (rule == null) throw new ArgumentNullException(nameof(rule));
|
||||||
|
lock (_rulesLock)
|
||||||
|
{
|
||||||
|
_rules.Add(rule);
|
||||||
|
// Sort by priority descending so highest priority rules are checked first
|
||||||
|
_rules.Sort((a, b) => b.Priority.CompareTo(a.Priority));
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a rule with fluent API support.
|
||||||
|
/// </summary>
|
||||||
|
public IntelligentRouter AddRule(
|
||||||
|
string name,
|
||||||
|
Func<LogEvent, bool> matcher,
|
||||||
|
IEnumerable<string> targetFlows,
|
||||||
|
int priority = 0,
|
||||||
|
bool stopIfMatched = false)
|
||||||
|
{
|
||||||
|
var rule = new RoutingRule
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Matcher = matcher,
|
||||||
|
TargetFlows = targetFlows?.ToList() ?? new List<string>(),
|
||||||
|
Priority = priority,
|
||||||
|
StopIfMatched = stopIfMatched
|
||||||
|
};
|
||||||
|
return AddRule(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a regex pattern-based routing rule.
|
||||||
|
/// </summary>
|
||||||
|
public IntelligentRouter AddPatternRule(
|
||||||
|
string name,
|
||||||
|
string messagePattern,
|
||||||
|
IEnumerable<string> targetFlows,
|
||||||
|
int priority = 0,
|
||||||
|
RegexOptions regexOptions = RegexOptions.IgnoreCase)
|
||||||
|
{
|
||||||
|
var regex = new Regex(messagePattern, regexOptions);
|
||||||
|
return AddRule(
|
||||||
|
name,
|
||||||
|
logEvent => regex.IsMatch(logEvent.Message.ToString()),
|
||||||
|
targetFlows,
|
||||||
|
priority
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a category-based routing rule (routes logs from specific categories).
|
||||||
|
/// </summary>
|
||||||
|
public IntelligentRouter AddCategoryRule(
|
||||||
|
string category,
|
||||||
|
IEnumerable<string> targetFlows,
|
||||||
|
LogLevel? minimumLevel = null)
|
||||||
|
{
|
||||||
|
return AddRule(
|
||||||
|
$"Category:{category}",
|
||||||
|
logEvent => logEvent.Category == category && (minimumLevel == null || logEvent.Level >= minimumLevel),
|
||||||
|
targetFlows,
|
||||||
|
priority: 10
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an exception type-based routing rule.
|
||||||
|
/// </summary>
|
||||||
|
public IntelligentRouter AddExceptionRule(
|
||||||
|
string exceptionTypeName,
|
||||||
|
IEnumerable<string> targetFlows)
|
||||||
|
{
|
||||||
|
return AddRule(
|
||||||
|
$"Exception:{exceptionTypeName}",
|
||||||
|
logEvent => logEvent.Exception?.GetType().Name == exceptionTypeName,
|
||||||
|
targetFlows,
|
||||||
|
priority: 20,
|
||||||
|
stopIfMatched: true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers a flow that can be targeted by routing rules.
|
||||||
|
/// </summary>
|
||||||
|
public IntelligentRouter RegisterFlow(string flowName, IFlow flow)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(flowName)) throw new ArgumentNullException(nameof(flowName));
|
||||||
|
if (flow == null) throw new ArgumentNullException(nameof(flow));
|
||||||
|
|
||||||
|
lock (_rulesLock)
|
||||||
|
{
|
||||||
|
_flowRoutes[flowName] = new FlowRoute { Flow = flow, IsEnabled = true };
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables or disables a specific flow by name.
|
||||||
|
/// </summary>
|
||||||
|
public IntelligentRouter SetFlowEnabled(string flowName, bool enabled)
|
||||||
|
{
|
||||||
|
lock (_rulesLock)
|
||||||
|
{
|
||||||
|
if (_flowRoutes.TryGetValue(flowName, out var route))
|
||||||
|
{
|
||||||
|
route.IsEnabled = enabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Routes a log event to appropriate flows based on rules.
|
||||||
|
/// Returns true if at least one flow accepted the message.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<bool> RouteAsync(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
List<RoutingRule> activeRules;
|
||||||
|
Dictionary<string, FlowRoute> activeFlows;
|
||||||
|
|
||||||
|
lock (_rulesLock)
|
||||||
|
{
|
||||||
|
activeRules = new List<RoutingRule>(_rules);
|
||||||
|
activeFlows = new Dictionary<string, FlowRoute>(_flowRoutes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetFlowNames = new HashSet<string>();
|
||||||
|
bool shouldStop = false;
|
||||||
|
|
||||||
|
// Apply rules in priority order
|
||||||
|
foreach (var rule in activeRules)
|
||||||
|
{
|
||||||
|
if (rule.Matcher(logEvent))
|
||||||
|
{
|
||||||
|
foreach (var flowName in rule.TargetFlows)
|
||||||
|
{
|
||||||
|
targetFlowNames.Add(flowName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rule.StopIfMatched)
|
||||||
|
{
|
||||||
|
shouldStop = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route to target flows
|
||||||
|
bool anySuccess = false;
|
||||||
|
var tasks = new List<Task>();
|
||||||
|
|
||||||
|
foreach (var flowName in targetFlowNames)
|
||||||
|
{
|
||||||
|
if (activeFlows.TryGetValue(flowName, out var flowRoute) && flowRoute.IsEnabled)
|
||||||
|
{
|
||||||
|
tasks.Add(flowRoute.Flow.BlastAsync(logEvent)
|
||||||
|
.ContinueWith(t =>
|
||||||
|
{
|
||||||
|
if (t.Status == TaskStatus.RanToCompletion && t.Result == WriteResult.Success)
|
||||||
|
anySuccess = true;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tasks.Any())
|
||||||
|
{
|
||||||
|
await Task.WhenAll(tasks).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
return anySuccess || targetFlowNames.Count == 0; // Return true if no rules matched (default routing)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets statistics about the router.
|
||||||
|
/// </summary>
|
||||||
|
public RouterStats GetStats()
|
||||||
|
{
|
||||||
|
lock (_rulesLock)
|
||||||
|
{
|
||||||
|
return new RouterStats
|
||||||
|
{
|
||||||
|
TotalRules = _rules.Count,
|
||||||
|
RegisteredFlows = _flowRoutes.Count,
|
||||||
|
EnabledFlows = _flowRoutes.Values.Count(f => f.IsEnabled)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
lock (_rulesLock)
|
||||||
|
{
|
||||||
|
_rules.Clear();
|
||||||
|
_flowRoutes.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class FlowRoute
|
||||||
|
{
|
||||||
|
public IFlow Flow { get; set; }
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines a routing rule for intelligent message routing.
|
||||||
|
/// </summary>
|
||||||
|
public class RoutingRule
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Name of this rule for identification/debugging.
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Function that determines if this rule matches a log event.
|
||||||
|
/// </summary>
|
||||||
|
public Func<LogEvent, bool> Matcher { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Names of flows this rule routes to.
|
||||||
|
/// </summary>
|
||||||
|
public List<string> TargetFlows { get; set; } = new List<string>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Priority of this rule (higher priority rules are evaluated first).
|
||||||
|
/// </summary>
|
||||||
|
public int Priority { get; set; } = 0;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If true, stops evaluating further rules after this one matches.
|
||||||
|
/// </summary>
|
||||||
|
public bool StopIfMatched { get; set; } = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics about the intelligent router.
|
||||||
|
/// </summary>
|
||||||
|
public class RouterStats
|
||||||
|
{
|
||||||
|
public int TotalRules { get; set; }
|
||||||
|
public int RegisteredFlows { get; set; }
|
||||||
|
public int EnabledFlows { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,101 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.PerformanceInsights
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Analyzes performance metrics and provides optimization insights.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PerformanceAnalyzer
|
||||||
|
{
|
||||||
|
private readonly PerformanceInsightsCollector _collector;
|
||||||
|
|
||||||
|
public PerformanceAnalyzer(PerformanceInsightsCollector collector = null)
|
||||||
|
{
|
||||||
|
_collector = collector ?? new PerformanceInsightsCollector();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Analyzes current performance state and returns insights
|
||||||
|
/// </summary>
|
||||||
|
public PerformanceAnalysis Analyze()
|
||||||
|
{
|
||||||
|
var snapshot = _collector.GetSnapshot();
|
||||||
|
var recommendations = _collector.GetRecommendations();
|
||||||
|
|
||||||
|
var analysis = new PerformanceAnalysis
|
||||||
|
{
|
||||||
|
AnalyzedAt = DateTime.UtcNow,
|
||||||
|
Uptime = snapshot.Uptime,
|
||||||
|
TotalOperations = snapshot.TotalOperations,
|
||||||
|
AverageErrorRate = snapshot.ErrorRatePercent,
|
||||||
|
Recommendations = recommendations,
|
||||||
|
Summary = GenerateSummary(snapshot, recommendations)
|
||||||
|
};
|
||||||
|
|
||||||
|
return analysis;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a summary string of the analysis
|
||||||
|
/// </summary>
|
||||||
|
private string GenerateSummary(PerformanceInsightsSnapshot snapshot, List<PerformanceRecommendation> recommendations)
|
||||||
|
{
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
$"Performance Analysis - {snapshot.CapturedAt:O}",
|
||||||
|
$"Uptime: {snapshot.Uptime.TotalSeconds:F2}s",
|
||||||
|
$"Total Operations: {snapshot.TotalOperations}",
|
||||||
|
$"Total Errors: {snapshot.TotalErrors}",
|
||||||
|
$"Error Rate: {snapshot.ErrorRatePercent:F2}%",
|
||||||
|
"",
|
||||||
|
"Top Slowest Operations:"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var op in snapshot.SlowestOperations.Take(3))
|
||||||
|
{
|
||||||
|
lines.Add($" - {op.Name}: {op.AverageDurationMs:F2}ms (min: {op.MinDurationMs:F2}ms, max: {op.MaxDurationMs:F2}ms)");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (snapshot.MostErrorsOperations.Any())
|
||||||
|
{
|
||||||
|
lines.Add("");
|
||||||
|
lines.Add("Operations with Most Errors:");
|
||||||
|
foreach (var op in snapshot.MostErrorsOperations.Take(3))
|
||||||
|
{
|
||||||
|
lines.Add($" - {op.Name}: {op.ErrorCount} errors ({op.ErrorRatePercent:F2}% error rate)");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (recommendations.Any())
|
||||||
|
{
|
||||||
|
lines.Add("");
|
||||||
|
lines.Add("Recommendations:");
|
||||||
|
foreach (var rec in recommendations)
|
||||||
|
{
|
||||||
|
lines.Add($" [{rec.Severity}] {rec.Category}: {rec.Message}");
|
||||||
|
foreach (var op in rec.AffectedOperations.Take(3))
|
||||||
|
{
|
||||||
|
lines.Add($" - {op}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(Environment.NewLine, lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performance analysis result
|
||||||
|
/// </summary>
|
||||||
|
public sealed class PerformanceAnalysis
|
||||||
|
{
|
||||||
|
public DateTime AnalyzedAt { get; set; }
|
||||||
|
public TimeSpan Uptime { get; set; }
|
||||||
|
public long TotalOperations { get; set; }
|
||||||
|
public double AverageErrorRate { get; set; }
|
||||||
|
public List<PerformanceRecommendation> Recommendations { get; set; } = new();
|
||||||
|
public string Summary { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
+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.Configuration;
|
||||||
using EonaCat.LogStack.Core;
|
using EonaCat.LogStack.Core;
|
||||||
using EonaCat.LogStack.Logging;
|
using EonaCat.LogStack.Logging;
|
||||||
|
using EonaCat.LogStack.DependencyInjection;
|
||||||
|
using EonaCat.LogStack.Telemetry;
|
||||||
|
using EonaCat.LogStack.Tracing;
|
||||||
|
using EonaCat.LogStack.PerformanceInsights;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
@@ -337,4 +341,98 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddSingleton<AdvancedMetricsCollector>();
|
services.AddSingleton<AdvancedMetricsCollector>();
|
||||||
return services;
|
return services;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers named logger factory for per-category loggers in DI
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddEonaCatNamedLoggers(
|
||||||
|
this IServiceCollection services,
|
||||||
|
LoggerDIOptions? options = null)
|
||||||
|
{
|
||||||
|
if (services == null)
|
||||||
|
throw new ArgumentNullException(nameof(services));
|
||||||
|
|
||||||
|
options ??= new LoggerDIOptions();
|
||||||
|
services.AddSingleton(options);
|
||||||
|
|
||||||
|
// Register telemetry components if enabled
|
||||||
|
if (options.EnableTelemetry)
|
||||||
|
{
|
||||||
|
services.AddSingleton<Telemetry.TelemetryAggregator>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.EnableTracing)
|
||||||
|
{
|
||||||
|
services.AddSingleton<Tracing.SpanFactory>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.EnablePerformanceMonitoring)
|
||||||
|
{
|
||||||
|
services.AddSingleton<PerformanceInsights.PerformanceInsightsCollector>();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.EnableHealthMonitoring)
|
||||||
|
{
|
||||||
|
services.AddSingleton<Telemetry.HealthMonitor>();
|
||||||
|
}
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables distributed tracing support
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddEonaCatTracing(
|
||||||
|
this IServiceCollection services)
|
||||||
|
{
|
||||||
|
if (services == null)
|
||||||
|
throw new ArgumentNullException(nameof(services));
|
||||||
|
|
||||||
|
services.AddSingleton<Tracing.TraceContextManager>();
|
||||||
|
services.AddSingleton<Tracing.SpanFactory>();
|
||||||
|
services.AddSingleton<Telemetry.TelemetryActivitySource>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables performance monitoring and insights
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddEonaCatPerformanceMonitoring(
|
||||||
|
this IServiceCollection services)
|
||||||
|
{
|
||||||
|
if (services == null)
|
||||||
|
throw new ArgumentNullException(nameof(services));
|
||||||
|
|
||||||
|
services.AddSingleton<PerformanceInsights.PerformanceInsightsCollector>();
|
||||||
|
services.AddSingleton<PerformanceInsights.PerformanceAnalyzer>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables health monitoring
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddEonaCatHealthMonitoring(
|
||||||
|
this IServiceCollection services)
|
||||||
|
{
|
||||||
|
if (services == null)
|
||||||
|
throw new ArgumentNullException(nameof(services));
|
||||||
|
|
||||||
|
services.AddSingleton<Telemetry.HealthMonitor>();
|
||||||
|
services.AddSingleton<Telemetry.TelemetryAggregator>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables unified telemetry aggregation
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddEonaCatTelemetryAggregation(
|
||||||
|
this IServiceCollection services)
|
||||||
|
{
|
||||||
|
if (services == null)
|
||||||
|
throw new ArgumentNullException(nameof(services));
|
||||||
|
|
||||||
|
services.AddSingleton<Telemetry.TelemetryAggregator>();
|
||||||
|
services.AddSingleton<Telemetry.HealthMonitor>();
|
||||||
|
return services;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -796,6 +796,73 @@ public sealed class LogBuilder
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stores recent log events in memory for querying and diagnostics
|
||||||
|
/// </summary>
|
||||||
|
public LogBuilder WriteToQueryableStorage(
|
||||||
|
int maxCapacity = 10000,
|
||||||
|
LogLevel minimumLevel = LogLevel.Trace)
|
||||||
|
{
|
||||||
|
_flows.Add(new LocalStorageQueryFlow(maxCapacity, minimumLevel));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tracks and correlates related events across flows using correlation IDs
|
||||||
|
/// </summary>
|
||||||
|
public LogBuilder WriteToCorrelationTracking(
|
||||||
|
TimeSpan? correlationWindow = null,
|
||||||
|
LogLevel minimumLevel = LogLevel.Trace)
|
||||||
|
{
|
||||||
|
_flows.Add(new CorrelatedEventFlow(
|
||||||
|
correlationWindow: correlationWindow,
|
||||||
|
minimumLevel: minimumLevel));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Monitors performance metrics and detects anomalies using statistical analysis
|
||||||
|
/// </summary>
|
||||||
|
public LogBuilder WriteToPerformanceMonitoring(
|
||||||
|
IFlow? alertFlow = null,
|
||||||
|
int windowSize = 100,
|
||||||
|
double standardDeviationThreshold = 2.0)
|
||||||
|
{
|
||||||
|
_flows.Add(new PerformanceAnomalyDetectorFlow(
|
||||||
|
alertFlow,
|
||||||
|
windowSize,
|
||||||
|
standardDeviationThreshold));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a filtered flow using a named preset (ErrorOnly, PerformanceLogs, SecurityLogs, etc.)
|
||||||
|
/// </summary>
|
||||||
|
public LogBuilder WriteToFilteredFlow(
|
||||||
|
IFlow targetFlow,
|
||||||
|
string presetName,
|
||||||
|
LogLevel minimumLevel = LogLevel.Trace)
|
||||||
|
{
|
||||||
|
var preset = LogFilterPresetRegistry.Get(presetName)
|
||||||
|
?? throw new ArgumentException($"Preset '{presetName}' not found. Available presets: {string.Join(", ", LogFilterPresetRegistry.GetAvailablePresets())}", nameof(presetName));
|
||||||
|
_flows.Add(new LogFilterPresetFlow(targetFlow, preset, minimumLevel));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a custom filtered flow using a predicate function
|
||||||
|
/// </summary>
|
||||||
|
public LogBuilder WriteToCustomFilteredFlow(
|
||||||
|
IFlow targetFlow,
|
||||||
|
string name,
|
||||||
|
Func<LogEvent, bool> predicate,
|
||||||
|
LogLevel minimumLevel = LogLevel.Trace)
|
||||||
|
{
|
||||||
|
var preset = new CustomPreset(name, predicate);
|
||||||
|
_flows.Add(new LogFilterPresetFlow(targetFlow, preset, minimumLevel));
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Boost logs with machine name
|
/// Boost logs with machine name
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -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.
|
- **Lazy initialization** - Flows are only initialized when first used, reducing startup overhead.
|
||||||
|
|
||||||
|
|
||||||
### Telemetry & Observability
|
## Telemetry & Observability
|
||||||
|
|
||||||
EonaCat.LogStack includes built-in telemetry primitives for production monitoring:
|
EonaCat.LogStack includes **built-in, zero-dependency telemetry** for comprehensive production monitoring:
|
||||||
|
|
||||||
- **Metrics registry** - Low-overhead counters and snapshots for logging throughput.
|
### Enhanced Telemetry System
|
||||||
- **Telemetry snapshots** - Capture service name, environment, runtime/process metrics, log counters and trace activity.
|
|
||||||
- **Runtime health data** - Export operational status for dashboards and monitoring systems.
|
#### Metrics Collection & Aggregation
|
||||||
- **Tracing support** - Activity/span integration for distributed request correlation.
|
|
||||||
- **Custom exporters** - Build exporters around telemetry snapshots for your monitoring backend.
|
Track logging operations with built-in metrics:
|
||||||
|
|
||||||
Example:
|
|
||||||
```csharp
|
```csharp
|
||||||
var options = new TelemetryOptions
|
var aggregator = new EonaCat.LogStack.Telemetry.TelemetryAggregator();
|
||||||
{
|
|
||||||
Enabled = true,
|
// Record counters
|
||||||
IncludeRuntimeMetrics = true,
|
aggregator.RecordCounter("logs_logged", 1);
|
||||||
IncludeTraceMetrics = true
|
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,
|
||||||
|
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
|
## Supported Targets
|
||||||
|
|
||||||
- .NET Standard 2.1
|
- .NET Standard 2.1
|
||||||
|
|||||||
Reference in New Issue
Block a user