From a0cd906b9fd36be7b8ac09fa06791345d3131494 Mon Sep 17 00:00:00 2001 From: EonaCat Date: Wed, 24 Jun 2026 16:30:22 +0200 Subject: [PATCH] Added new flows --- .../DependencyInjection/LoggerDIOptions.cs | 45 ++ EonaCat.LogStack/EonaCat.LogStack.csproj | 12 +- .../Flows/CorrelatedEventFlow.cs | 358 ++++++++++++++++ .../Flows/LocalStorageQueryFlow.cs | 262 ++++++++++++ .../Flows/LogFilterPresetFlow.cs | 357 ++++++++++++++++ .../Flows/PerformanceAnomalyDetectorFlow.cs | 384 +++++++++++++++++ .../Extensions/CoolFeaturesExtensions.cs | 119 ++++++ .../EonaCat.LogStack/LogHeatmapAnalyzer.cs | 285 +++++++++++++ EonaCat.LogStack/EonaCatLogger.cs | 180 +++++++- .../AdaptiveSamplingEngine.cs | 314 ++++++++++++++ .../EonaCatLoggerCore/AnomalyDetector.cs | 368 ++++++++++++++++ .../ContextSnapshotCollector.cs | 392 +++++++++++++++++ .../EonaCatLoggerCore/DeadLetterQueue.cs | 307 ++++++++++++++ .../EonaCatLoggerCore/Flows/DlqFlow.cs | 203 +++++++++ .../EonaCatLoggerCore/IntelligentRouter.cs | 277 ++++++++++++ .../PerformanceAnalyzer.cs | 101 +++++ .../PerformanceInsightsCollector.cs | 199 +++++++++ .../PerformanceInsights/PerformanceTracker.cs | 160 +++++++ .../EonaCatLoggerCore/Tracing/SpanFactory.cs | 133 ++++++ .../EonaCatLoggerCore/Tracing/TraceContext.cs | 101 +++++ .../Tracing/TraceContextManager.cs | 162 +++++++ .../EonaCatLoggerCore/Tracing/TracingSpan.cs | 211 +++++++++ .../Extensions/ServiceCollectionExtensions.cs | 98 +++++ EonaCat.LogStack/LogBuilder.cs | 67 +++ EonaCat.LogStack/Policies/AutoPolicies.cs | 177 ++++++++ EonaCat.LogStack/SuperiorFeaturesStats.cs | 105 +++++ EonaCat.LogStack/Telemetry/HealthMonitor.cs | 239 +++++++++++ .../Telemetry/TelemetryAggregator.cs | 293 +++++++++++++ README.md | 400 +++++++++++++++++- 29 files changed, 6277 insertions(+), 32 deletions(-) create mode 100644 EonaCat.LogStack/DependencyInjection/LoggerDIOptions.cs create mode 100644 EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/CorrelatedEventFlow.cs create mode 100644 EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/LocalStorageQueryFlow.cs create mode 100644 EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/LogFilterPresetFlow.cs create mode 100644 EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/PerformanceAnomalyDetectorFlow.cs create mode 100644 EonaCat.LogStack/EonaCat.LogStack/Extensions/CoolFeaturesExtensions.cs create mode 100644 EonaCat.LogStack/EonaCat.LogStack/LogHeatmapAnalyzer.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/AdaptiveSamplingEngine.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/AnomalyDetector.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/ContextSnapshotCollector.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/DeadLetterQueue.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Flows/DlqFlow.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/IntelligentRouter.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceAnalyzer.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceInsightsCollector.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceTracker.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Tracing/SpanFactory.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Tracing/TraceContext.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Tracing/TraceContextManager.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Tracing/TracingSpan.cs create mode 100644 EonaCat.LogStack/Policies/AutoPolicies.cs create mode 100644 EonaCat.LogStack/SuperiorFeaturesStats.cs create mode 100644 EonaCat.LogStack/Telemetry/HealthMonitor.cs create mode 100644 EonaCat.LogStack/Telemetry/TelemetryAggregator.cs diff --git a/EonaCat.LogStack/DependencyInjection/LoggerDIOptions.cs b/EonaCat.LogStack/DependencyInjection/LoggerDIOptions.cs new file mode 100644 index 0000000..2422b19 --- /dev/null +++ b/EonaCat.LogStack/DependencyInjection/LoggerDIOptions.cs @@ -0,0 +1,45 @@ +using System; + +namespace EonaCat.LogStack.DependencyInjection +{ + /// + /// Advanced configuration options for DI registration + /// + public sealed class LoggerDIOptions + { + /// + /// Gets or sets whether named loggers should be eagerly initialized + /// + public bool EagerlyInitializeNamedLoggers { get; set; } = false; + + /// + /// Gets or sets whether to register ILogger decorator + /// + public bool RegisterLoggerDecorator { get; set; } = true; + + /// + /// Gets or sets the default logger name for constructor injection + /// + public string DefaultLoggerName { get; set; } = "Default"; + + /// + /// Gets or sets whether to enable telemetry integration + /// + public bool EnableTelemetry { get; set; } = true; + + /// + /// Gets or sets whether to enable distributed tracing + /// + public bool EnableTracing { get; set; } = false; + + /// + /// Gets or sets whether to enable performance monitoring + /// + public bool EnablePerformanceMonitoring { get; set; } = false; + + /// + /// Gets or sets whether to enable health monitoring + /// + public bool EnableHealthMonitoring { get; set; } = true; + } +} diff --git a/EonaCat.LogStack/EonaCat.LogStack.csproj b/EonaCat.LogStack/EonaCat.LogStack.csproj index 7d5acb3..7486ce5 100644 --- a/EonaCat.LogStack/EonaCat.LogStack.csproj +++ b/EonaCat.LogStack/EonaCat.LogStack.csproj @@ -14,7 +14,7 @@ It features a rich fluent API for routing log events to dozens of destinations f EonaCat (Jeroen Saey) EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey - 0.1.2 + 0.1.3 README.md True LICENSE @@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f - 0.1.2+{chash:10}.{c:ymd} + 0.1.3+{chash:10}.{c:ymd} true true v[0-9]* @@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f - 0.1.2 + 0.1.3 EonaCat.LogStack EonaCat.LogStack https://git.saey.me/EonaCat/EonaCat.LogStack @@ -101,4 +101,10 @@ It features a rich fluent API for routing log events to dozens of destinations f \ + + + + + + \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/CorrelatedEventFlow.cs b/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/CorrelatedEventFlow.cs new file mode 100644 index 0000000..505e730 --- /dev/null +++ b/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/CorrelatedEventFlow.cs @@ -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. + + /// + /// CorrelatedEventFlow tracks and correlates related events across flows and time windows. + /// Enables request tracing, transaction tracking, and detection of related log patterns. + /// + public sealed class CorrelatedEventFlow : FlowBase + { + private readonly int _maxCorrelationGroups; + private readonly TimeSpan _correlationWindow; + private readonly ConcurrentDictionary _correlationGroups = new(); + private readonly object _cleanupLock = new(); + + /// + /// Creates a new CorrelatedEventFlow + /// + /// Maximum number of correlation groups to track (default 10000) + /// Time window for correlating events (default 5 minutes) + /// Minimum log level to process + 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 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 BlastBatchAsync(ReadOnlyMemory 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; + } + + /// + /// Get all events for a correlation ID + /// + public IEnumerable GetCorrelatedEvents(string correlationId) + { + if (_correlationGroups.TryGetValue(correlationId, out var group)) + return group.Events.ToList(); + + return new List(); + } + + /// + /// Get correlation statistics + /// + 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; + } + + /// + /// Find correlation groups with errors + /// + public IEnumerable GetErrorCorrelations() + { + return _correlationGroups.Values + .Where(g => g.Events.Any(e => e.Level >= LogLevel.Error)) + .ToList(); + } + + /// + /// Find slow correlation groups (based on duration property) + /// + public IEnumerable GetSlowCorrelations(TimeSpan minimumDuration) + { + return _correlationGroups.Values + .Where(g => g.Time().TotalMilliseconds >= minimumDuration.TotalMilliseconds) + .ToList(); + } + + /// + /// Get correlation groups by category pattern + /// + public IEnumerable 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(); + } + + /// + /// Clear all correlation tracking + /// + 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 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(); + } + } + + /// + /// Represents a group of correlated log events + /// + public class CorrelationGroup + { + private readonly List _events = new(); + private readonly object _eventLock = new(); + + public string CorrelationId { get; } + public IReadOnlyList 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; + } + } + + /// + /// Get the total time span of this correlation group + /// + public TimeSpan Time() + { + lock (_eventLock) + { + if (_events.Count == 0) return TimeSpan.Zero; + return new TimeSpan(_events[_events.Count - 1].Timestamp - _events[0].Timestamp); + } + } + + /// + /// Get events at specific log level + /// + public IEnumerable GetEventsByLevel(LogLevel level) + { + lock (_eventLock) + return _events.Where(e => e.Level == level).ToList(); + } + + /// + /// Check if this group contains any exceptions + /// + public bool HasExceptions + { + get + { + lock (_eventLock) + return _events.Any(e => e.Exception != null); + } + } + + /// + /// Get event count by category + /// + public Dictionary 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]"; + } + } + } + + /// + /// Statistics about correlation tracking + /// + 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}"; + } + } +} diff --git a/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/LocalStorageQueryFlow.cs b/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/LocalStorageQueryFlow.cs new file mode 100644 index 0000000..2031036 --- /dev/null +++ b/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/LocalStorageQueryFlow.cs @@ -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. + + /// + /// 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. + /// + public sealed class LocalStorageQueryFlow : FlowBase + { + private readonly int _maxCapacity; + private readonly ConcurrentQueue _logBuffer = new(); + private int _currentCount = 0; + + /// + /// Gets all stored log events + /// + public IReadOnlyList AllEvents => _logBuffer.ToList(); + + /// + /// Gets the number of events currently stored + /// + public int StoredEventCount => _currentCount; + + /// + /// Creates a new LocalStorageQueryFlow with specified capacity + /// + /// Maximum number of log events to keep in memory (default 10000) + /// Minimum log level to store + 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 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 BlastBatchAsync(ReadOnlyMemory 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; + } + + /// + /// Query stored events by log level + /// + public IEnumerable QueryByLevel(LogLevel level) + { + return _logBuffer.Where(e => e.Level == level); + } + + /// + /// Query stored events by category + /// + public IEnumerable QueryByCategory(string category) + { + return _logBuffer.Where(e => e.Category?.Equals(category, StringComparison.OrdinalIgnoreCase) ?? false); + } + + /// + /// Query stored events by message pattern + /// + public IEnumerable QueryByMessagePattern(string pattern, StringComparison comparison = StringComparison.OrdinalIgnoreCase) + { + return _logBuffer.Where(e => + { + var msg = e.Message.ToString(); + return msg.Contains(pattern, comparison); + }); + } + + /// + /// Query stored events by time range + /// + public IEnumerable QueryByTimeRange(DateTime from, DateTime to) + { + var fromTicks = from.Ticks; + var toTicks = to.Ticks; + return _logBuffer.Where(e => e.Timestamp >= fromTicks && e.Timestamp <= toTicks); + } + + /// + /// Query stored events that contain exceptions + /// + public IEnumerable QueryExceptionEvents() + { + return _logBuffer.Where(e => e.Exception != null); + } + + /// + /// Query stored events by duration (for performance logs) + /// + public IEnumerable QueryByMinimumDuration(TimeSpan duration) + { + return _logBuffer.Where(e => + e.Properties != null && + e.Properties.TryGetValue("duration", out var durValue) && + durValue is TimeSpan ts && + ts >= duration); + } + + /// + /// Get statistics about stored events + /// + 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; + } + + /// + /// Clear all stored events + /// + public void Clear() + { + while (_logBuffer.TryDequeue(out _)) { } + _currentCount = 0; + } + + /// + /// Get the most recent N events + /// + public IEnumerable GetLatest(int count) + { + return _logBuffer.Reverse().Take(count); + } + + /// + /// Search with custom predicate + /// + public IEnumerable Search(Func predicate) + { + return _logBuffer.Where(predicate); + } + + public override async ValueTask DisposeAsync() + { + Clear(); + await base.DisposeAsync(); + } + } + + /// + /// Statistics about logs stored in LocalStorageQueryFlow + /// + public class LogStorageStatistics + { + public int TotalEvents { get; set; } + public Dictionary EventsByLevel { get; set; } = new(); + public Dictionary 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}]"; + } + } +} diff --git a/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/LogFilterPresetFlow.cs b/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/LogFilterPresetFlow.cs new file mode 100644 index 0000000..7d65b76 --- /dev/null +++ b/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/LogFilterPresetFlow.cs @@ -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. + + /// + /// LogFilterPresetFlow provides commonly-used filter combinations as reusable presets. + /// Reduces boilerplate for common scenarios like error tracking, performance monitoring, etc. + /// + public sealed class LogFilterPresetFlow : FlowBase + { + private readonly Channel _channel; + private readonly IFlow _targetFlow; + private readonly LogFilterPreset _preset; + private Task _processingTask; + + /// + /// Creates a LogFilterPresetFlow with a specific preset + /// + /// The flow to send filtered events to + /// The filter preset to apply + /// Minimum log level + 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(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 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 BlastBatchAsync(ReadOnlyMemory 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(); + } + } + + /// + /// Base class for filter presets + /// + public abstract class LogFilterPreset + { + public string Name { get; set; } + + protected LogFilterPreset(string name) + { + Name = name ?? "Custom"; + } + + public abstract bool Filter(LogEvent logEvent); + } + + /// + /// Preset: Only errors and exceptions + /// + public class ErrorOnlyPreset : LogFilterPreset + { + public ErrorOnlyPreset() : base("ErrorOnly") { } + + public override bool Filter(LogEvent logEvent) + { + return logEvent.Level >= LogLevel.Error || logEvent.Exception != null; + } + } + + /// + /// Preset: Errors with full stack traces + /// + 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; + } + } + + /// + /// Preset: Performance-related logs (duration, latency, timeout) + /// + 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))); + } + } + + /// + /// Preset: Authentication and security-related logs + /// + 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)); + } + } + + /// + /// Preset: Health check and monitoring logs + /// + 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)); + } + } + + /// + /// Preset: Data access and database-related logs + /// + 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)); + } + } + + /// + /// Preset: Business logic and domain events + /// + 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)); + } + } + + /// + /// Preset: External service calls and integrations + /// + 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)); + } + } + + /// + /// Preset: Critical warnings and errors (anything that might need attention) + /// + 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); + } + } + + /// + /// Preset: Development and debugging logs + /// + 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)); + } + } + + /// + /// Preset: Custom filter based on predicate + /// + public class CustomPreset : LogFilterPreset + { + private readonly Func _predicate; + + public CustomPreset(string name, Func predicate) + : base(name) + { + _predicate = predicate ?? throw new ArgumentNullException(nameof(predicate)); + } + + public override bool Filter(LogEvent logEvent) + { + return _predicate(logEvent); + } + } + + /// + /// Preset Registry for managing and creating presets + /// + public static class LogFilterPresetRegistry + { + private static readonly Dictionary _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 GetAvailablePresets() + { + return _presets.Keys; + } + + public static void Register(string name, LogFilterPreset preset) + { + _presets[name] = preset; + } + } +} diff --git a/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/PerformanceAnomalyDetectorFlow.cs b/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/PerformanceAnomalyDetectorFlow.cs new file mode 100644 index 0000000..e59804b --- /dev/null +++ b/EonaCat.LogStack/EonaCat.LogStack/EonaCatLoggerCore/Flows/PerformanceAnomalyDetectorFlow.cs @@ -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. + + /// + /// PerformanceAnomalyDetectorFlow detects anomalies in performance metrics and generates alerts. + /// Uses statistical analysis to identify deviations from baseline performance. + /// + public sealed class PerformanceAnomalyDetectorFlow : FlowBase + { + private readonly int _windowSize; + private readonly double _standardDeviationThreshold; + private readonly IFlow _alertTarget; + private readonly Dictionary _trackers = new(); + private readonly object _trackersLock = new(); + + /// + /// Creates a new PerformanceAnomalyDetectorFlow + /// + /// Flow to send anomaly alerts to + /// Size of the sliding window for statistical analysis (default 100) + /// Alert threshold in standard deviations (default 2.0) + /// Minimum log level to process + 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 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 + { + ["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 BlastBatchAsync(ReadOnlyMemory 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; + } + + /// + /// Get anomaly detection statistics + /// + 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; + } + } + + /// + /// Get recent anomalies + /// + public IEnumerable GetRecentAnomalies(int count = 10) + { + lock (_trackersLock) + { + return _trackers.Values + .SelectMany(t => t.RecentAnomalies) + .OrderByDescending(a => a.DetectedAt) + .Take(count) + .ToList(); + } + } + + /// + /// Get anomalies for a specific metric + /// + public IEnumerable GetAnomaliesForMetric(string metricName) + { + lock (_trackersLock) + { + if (_trackers.TryGetValue(metricName, out var tracker)) + return tracker.RecentAnomalies.ToList(); + return new List(); + } + } + + /// + /// Reset anomaly tracking for a metric + /// + public void ResetMetric(string metricName) + { + lock (_trackersLock) + { + _trackers.Remove(metricName); + } + } + + /// + /// Clear all tracking data + /// + public void Clear() + { + lock (_trackersLock) + { + _trackers.Clear(); + } + } + + // Private helpers + + private Dictionary ExtractPerformanceMetrics(LogEvent logEvent) + { + var metrics = new Dictionary(); + + 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(); + } + } + + /// + /// Tracks performance metrics and detects anomalies + /// + public class PerformanceMetricTracker + { + private readonly string _metricName; + private readonly int _windowSize; + private readonly double _threshold; + private readonly List _measurements = new(); + private readonly List _recentAnomalies = new(); + private double _sum = 0; + + public string MetricName => _metricName; + public int MeasurementCount => _measurements.Count; + public int AnomalyCount { get; private set; } = 0; + public IReadOnlyList 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); + } + } + + /// + /// Represents a detected performance anomaly + /// + 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")}"; + } + } + + /// + /// Statistics about anomaly detection + /// + public class AnomalyDetectionStatistics + { + public int MonitoredMetrics { get; set; } + public int TotalMeasurements { get; set; } + public int TotalAnomaliesDetected { get; set; } + public Dictionary MetricStatistics { get; set; } = new(); + + public override string ToString() + { + return $"AnomalyStats: Metrics={MonitoredMetrics}, Measurements={TotalMeasurements}, " + + $"Anomalies={TotalAnomaliesDetected}"; + } + } + + /// + /// Statistics for a single metric + /// + 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; } + } +} diff --git a/EonaCat.LogStack/EonaCat.LogStack/Extensions/CoolFeaturesExtensions.cs b/EonaCat.LogStack/EonaCat.LogStack/Extensions/CoolFeaturesExtensions.cs new file mode 100644 index 0000000..c2c3c01 --- /dev/null +++ b/EonaCat.LogStack/EonaCat.LogStack/Extensions/CoolFeaturesExtensions.cs @@ -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. + + /// + /// Extension methods for the new cool logging features + /// + public static class CoolFeaturesExtensions + { + /// + /// Add a filter preset flow to route specific log types + /// + 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); + } + + /// + /// Add a custom filter preset flow + /// + public static LogFilterPresetFlow AddCustomFilterFlow( + this IFlow targetFlow, + string name, + Func predicate) + { + var preset = new CustomPreset(name, predicate); + return new LogFilterPresetFlow(targetFlow, preset); + } + + /// + /// Generate a heatmap visualization of stored logs + /// + 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" + }; + } + + /// + /// Print correlation group summary + /// + public static string GetCorrelationSummary(this CorrelationGroup group) + { + return $"CorrelationGroup [ID: {group.CorrelationId}, Events: {group.Events.Count}, Duration: {group.Time().TotalMilliseconds:F0}ms]"; + } + + /// + /// Print performance anomaly details + /// + public static string GetAnomalySummary(this PerformanceAnomaly anomaly) + { + return anomaly.ToString(); + } + + /// + /// Get available filter presets + /// + public static IEnumerable GetAvailableFilterPresets() + { + return LogFilterPresetRegistry.GetAvailablePresets(); + } + + /// + /// Print log storage statistics + /// + public static string GetStorageStatsSummary(this LogStorageStatistics stats) + { + return stats.ToString(); + } + + /// + /// Print anomaly detection statistics + /// + public static string GetAnomalyStatsSummary(this AnomalyDetectionStatistics stats) + { + return stats.ToString(); + } + } + + /// + /// Types of visualizations available + /// + public enum VisualizationType + { + Timeline, + LevelDistribution, + CategoryDistribution, + ActivityMatrix, + Compact + } +} diff --git a/EonaCat.LogStack/EonaCat.LogStack/LogHeatmapAnalyzer.cs b/EonaCat.LogStack/EonaCat.LogStack/LogHeatmapAnalyzer.cs new file mode 100644 index 0000000..444e46a --- /dev/null +++ b/EonaCat.LogStack/EonaCat.LogStack/LogHeatmapAnalyzer.cs @@ -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. + + /// + /// LogHeatmapAnalyzer generates ASCII heatmaps to visualize logging frequency patterns over time. + /// Useful for identifying traffic patterns, spikes, and patterns in log generation. + /// + 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 + + /// + /// Generate a heatmap showing logging frequency over time + /// + /// Log events to analyze + /// Time bucket size in minutes (e.g., 5 = 5-minute buckets) + /// Width of the heatmap (default 60) + /// Height of the heatmap (default 10) + /// ASCII art heatmap string + public static string GenerateTimelineHeatmap( + IEnumerable 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(); + 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); + } + + /// + /// Generate a heatmap by log level distribution + /// + public static string GenerateLevelDistributionHeatmap(IEnumerable events, int width = 50) + { + var eventList = events?.ToList() ?? new List(); + 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(); + } + + /// + /// Generate a heatmap for category distribution + /// + public static string GenerateCategoryHeatmap(IEnumerable events, int width = 50, int topN = 10) + { + var eventList = events?.ToList() ?? new List(); + 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(); + } + + /// + /// Generate a compact inline heatmap for quick visualization + /// + public static string GenerateCompactHeatmap(IEnumerable events, int buckets = 40) + { + var eventList = events?.ToList() ?? new List(); + 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(); + } + + /// + /// Generate a 2D heatmap showing activity by hour and day + /// + public static string GenerateActivityMatrixHeatmap(IEnumerable events) + { + var eventList = events?.ToList() ?? new List(); + 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 CreateTimeBuckets(List events, int bucketMinutes) + { + var buckets = new Dictionary(); + + 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 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 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); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLogger.cs b/EonaCat.LogStack/EonaCatLogger.cs index 9e25c1b..011a5c9 100644 --- a/EonaCat.LogStack/EonaCatLogger.cs +++ b/EonaCat.LogStack/EonaCatLogger.cs @@ -2,6 +2,11 @@ using EonaCat.LogStack.Boosters; using EonaCat.LogStack.Core; using EonaCat.LogStack.Flows; +using EonaCat.LogStack.Routing; +using EonaCat.LogStack.Sampling; +using EonaCat.LogStack.Anomalies; +using EonaCat.LogStack.DeadLettering; +using EonaCat.LogStack.Diagnostics; using System; using System.Collections.Concurrent; using System.Collections.Generic; @@ -62,6 +67,13 @@ namespace EonaCat.LogStack // Dynamic level controller private volatile DynamicLevelController? _dynamicLevel; + // Superior logging features + private readonly IntelligentRouter? _intelligentRouter; + private readonly AdaptiveSamplingEngine? _adaptiveSampler; + private readonly AnomalyDetector? _anomalyDetector; + private readonly ContextSnapshotCollector? _contextSnapshots; + private DeadLetterQueue? _deadLetterQueue; + public event EventHandler OnLog; /// @@ -69,13 +81,23 @@ namespace EonaCat.LogStack /// public EonaCatLogStack(string category = "Application", LogLevel minimumLevel = LogLevel.Trace, - TimestampMode timestampMode = TimestampMode.Utc) + TimestampMode timestampMode = TimestampMode.Utc, + bool enableIntelligentRouting = false, + bool enableAdaptiveSampling = false, + bool enableAnomalyDetection = false, + bool enableContextSnapshots = false) { _category = category ?? throw new ArgumentNullException(nameof(category)); _minimumLevel = minimumLevel; - _timestampMode = timestampMode; - - // Enable async pipeline by default + _timestampMode = timestampMode; + + // Initialize superior features if requested + _intelligentRouter = enableIntelligentRouting ? new IntelligentRouter() : null; + _adaptiveSampler = enableAdaptiveSampling ? new AdaptiveSamplingEngine() : null; + _anomalyDetector = enableAnomalyDetection ? new AnomalyDetector() : null; + _contextSnapshots = enableContextSnapshots ? new ContextSnapshotCollector() : null; + + // Enable async pipeline by default UseAsyncPipeline(); } @@ -119,6 +141,38 @@ namespace EonaCat.LogStack return this; } + /// + /// Gets the Intelligent Router for pattern-based flow routing (if enabled). + /// + public IntelligentRouter GetIntelligentRouter() => _intelligentRouter; + + /// + /// Gets the Adaptive Sampling Engine (if enabled). + /// + public AdaptiveSamplingEngine GetAdaptiveSampler() => _adaptiveSampler; + + /// + /// Gets the Anomaly Detector (if enabled). + /// + public AnomalyDetector GetAnomalyDetector() => _anomalyDetector; + + /// + /// Gets the Context Snapshot Collector (if enabled). + /// + public ContextSnapshotCollector GetContextSnapshots() => _contextSnapshots; + + /// + /// Gets or creates the Dead Letter Queue for handling failed logs. + /// + public DeadLetterQueue GetDeadLetterQueue() + { + if (_deadLetterQueue == null) + { + _deadLetterQueue = new DeadLetterQueue(); + } + return _deadLetterQueue; + } + [MethodImpl(MethodImplOptions.AggressiveInlining)] private LogLevel EffectiveMinLevel() => _dynamicLevel != null ? _dynamicLevel.CurrentLevel : _minimumLevel; @@ -413,6 +467,13 @@ namespace EonaCat.LogStack [MethodImpl(MethodImplOptions.AggressiveInlining)] private void ProcessLogEvent(ref LogEventBuilder builder) { + // Check adaptive sampling first - if sampling is enabled and this log is dropped, return early + if (_adaptiveSampler != null && !_adaptiveSampler.ShouldSample()) + { + Interlocked.Increment(ref _totalDroppedCount); + return; + } + // Apply boosters lock (_boosters) { @@ -443,11 +504,30 @@ namespace EonaCat.LogStack } var logEvent = builder.Build(); - Interlocked.Increment(ref _totalLoggedCount); - - if (!_asyncChannel!.Writer.TryWrite(logEvent)) - { - Interlocked.Increment(ref _totalDroppedCount); + + // Record sampling metric + if (_adaptiveSampler != null) + { + _adaptiveSampler.RecordLogEvent(); + } + + // Detect anomalies + if (_anomalyDetector != null) + { + _anomalyDetector.AnalyzeEvent(logEvent); + } + + // Capture context snapshots on critical logs + if (_contextSnapshots != null) + { + _contextSnapshots.CaptureIfNeeded(logEvent); + } + + Interlocked.Increment(ref _totalLoggedCount); + + if (!_asyncChannel!.Writer.TryWrite(logEvent)) + { + Interlocked.Increment(ref _totalDroppedCount); } } @@ -458,18 +538,46 @@ namespace EonaCat.LogStack { await foreach (var logEvent in reader.ReadAllAsync(cancellationToken).ConfigureAwait(false)) { - var flows = _concurrentFlows; - foreach (var flow in flows) + // If intelligent router is enabled, use it for routing + if (_intelligentRouter != null) { try { - var result = await flow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); - if (result == WriteResult.Dropped) + await _intelligentRouter.RouteAsync(logEvent).ConfigureAwait(false); + } + catch { /* Router should handle its own errors */ } + } + else + { + // Default routing to all flows + var flows = _concurrentFlows; + foreach (var flow in flows) + { + try { - Interlocked.Increment(ref _totalDroppedCount); + var result = await flow.BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); + if (result == WriteResult.Dropped) + { + Interlocked.Increment(ref _totalDroppedCount); + + // Enqueue to DLQ if available + if (_deadLetterQueue != null) + { + _deadLetterQueue.Enqueue(logEvent, $"Dropped by flow '{flow.Name}'"); + } + } + } + catch (Exception ex) + { + // Enqueue to DLQ if available + if (_deadLetterQueue != null) + { + _deadLetterQueue.Enqueue(logEvent, + $"Exception in flow '{flow.Name}': {ex.Message}", + ex); + } } } - catch { } } } } @@ -557,6 +665,48 @@ namespace EonaCat.LogStack /// public LoggingMetrics GetMetricsCollector() => _metrics; + /// + /// Gets comprehensive statistics about all superior features. + /// + 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() { diff --git a/EonaCat.LogStack/EonaCatLoggerCore/AdaptiveSamplingEngine.cs b/EonaCat.LogStack/EonaCatLoggerCore/AdaptiveSamplingEngine.cs new file mode 100644 index 0000000..6e7dc99 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/AdaptiveSamplingEngine.cs @@ -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; + +/// +/// 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. +/// +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; + } + + /// + /// Determines if a log event should be sampled (kept) or dropped. + /// Returns true if the event should be logged. + /// + 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; + } + + /// + /// Adjusts the sampling rate based on current metrics and strategy. + /// + 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; + + /// + /// Sets the target throughput for throughput-based sampling. + /// + public void SetTargetThroughput(double logsPerSecond) + { + _targetThroughput = Math.Max(1, logsPerSecond); + } + + /// + /// Gets current sampling rate (0.0 to 1.0). + /// + public double GetSamplingRate() => _samplingRate; + + /// + /// Gets current resource utilization metrics. + /// + public SamplingMetrics GetMetrics() + { + lock (_statsLock) + { + return new SamplingMetrics + { + SamplingRate = _samplingRate, + CurrentCpuPercent = _currentCpuPercent, + CurrentMemoryPercent = _currentMemoryPercent, + Strategy = _strategy, + TargetThroughput = _targetThroughput, + LogsProcessedInLastInterval = Interlocked.Read(ref _logCount) + }; + } + } + + /// + /// Records a log event (increments counter for sampling decisions). + /// + public void RecordLogEvent() + { + Interlocked.Increment(ref _logCount); + } + + private static long TicksFromMilliseconds(double ms) + { + return (long)(ms * Stopwatch.Frequency / 1000.0); + } +} + +/// +/// Sampling strategies for adaptive engine. +/// +public enum SamplingStrategy +{ + /// + /// Adjusts sampling based on throughput to match target logs/sec + /// + Throughput, + + /// + /// Adjusts sampling based on CPU and memory usage + /// + Resources, + + /// + /// Combines throughput and resource monitoring + /// + Hybrid, + + /// + /// Advanced adaptive strategy that learns optimal rate + /// + Adaptive +} + +/// +/// Metrics from the adaptive sampling engine. +/// +public class SamplingMetrics +{ + /// + /// Current sampling rate (0.0 = drop all, 1.0 = keep all) + /// + public double SamplingRate { get; set; } + + /// + /// Current CPU usage percentage (0.0 to 1.0) + /// + public double CurrentCpuPercent { get; set; } + + /// + /// Current memory usage percentage (0.0 to 1.0) + /// + public double CurrentMemoryPercent { get; set; } + + /// + /// Active sampling strategy + /// + public SamplingStrategy Strategy { get; set; } + + /// + /// Target throughput in logs per second + /// + public double TargetThroughput { get; set; } + + /// + /// Logs processed in last adjustment interval + /// + public long LogsProcessedInLastInterval { get; set; } +} + +/// +/// Thread-safe random number generator for sampling decisions. +/// +internal static class ThreadSafeRandom +{ + private static readonly ThreadLocal _random = new ThreadLocal( + () => new Random(Guid.NewGuid().GetHashCode()) + ); + + public static double NextDouble() => _random.Value.NextDouble(); +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/AnomalyDetector.cs b/EonaCat.LogStack/EonaCatLoggerCore/AnomalyDetector.cs new file mode 100644 index 0000000..83ed16c --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/AnomalyDetector.cs @@ -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; + +/// +/// 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. +/// +public class AnomalyDetector +{ + private readonly ConcurrentDictionary _categoryStats + = new ConcurrentDictionary(); + + private readonly ConcurrentDictionary _levelStats + = new ConcurrentDictionary(); + + private readonly double _standardDeviationThreshold; + private readonly int _windowSizeSeconds; + private readonly object _statsLock = new object(); + private long _totalEventsProcessed; + + private readonly List _recentAlerts = new List(); + private readonly Stopwatch _uptime = Stopwatch.StartNew(); + + public event EventHandler AnomalyDetected; + + public AnomalyDetector( + double standardDeviationThreshold = 3.0, + int windowSizeSeconds = 300) + { + _standardDeviationThreshold = standardDeviationThreshold; + _windowSizeSeconds = windowSizeSeconds; + } + + /// + /// Analyzes a log event for anomalies. + /// + 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(); + + // 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; + } + + /// + /// Gets statistics summary for analysis. + /// + 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() + }; + } + } + + /// + /// Gets recent detected anomalies. + /// + public List GetRecentAnomalies(int count = 10) + { + lock (_statsLock) + { + return _recentAlerts.Skip(Math.Max(0, _recentAlerts.Count - count)) + .OrderBy(a => a.Timestamp) + .ToList(); + } + } + + /// + /// Clears all statistics (useful for resetting baseline after deployment). + /// + public void ResetStatistics() + { + _categoryStats.Clear(); + _levelStats.Clear(); + lock (_statsLock) + { + _recentAlerts.Clear(); + } + _totalEventsProcessed = 0; + } + + private AnomalySeverity DetermineAnomalySeverity(List 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 + }; + } +} + +/// +/// Tracks statistics for a log category. +/// +internal class CategoryStatistics +{ + private readonly Queue _recentEventTimes = new Queue(); + private readonly ConcurrentDictionary _messageHashCounts = new ConcurrentDictionary(); + private readonly ConcurrentDictionary _exceptionCounts = new ConcurrentDictionary(); + + 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(); + + 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; + } + } +} + +/// +/// Tracks statistics for a log level. +/// +internal class LevelStatistics +{ + private readonly Queue _eventTimes = new Queue(); + 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; + } + } +} + +/// +/// An alert raised when an anomaly is detected. +/// +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 Anomalies { get; set; } = new List(); + public AnomalySeverity AnomalySeverity { get; set; } + public SystemMetrics SystemMetrics { get; set; } +} + +/// +/// Severity of detected anomaly. +/// +public enum AnomalySeverity +{ + Low, + Medium, + High, + Critical +} + +/// +/// System metrics captured when anomaly detected. +/// +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; } +} + +/// +/// Statistics about anomaly detector. +/// +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 TopCategories { get; set; } = new List(); +} + +/// +/// Summary of a category's status. +/// +public class CategorySummary +{ + public string Category { get; set; } + public long EventCount { get; set; } + public bool IsAnomalous { get; set; } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/ContextSnapshotCollector.cs b/EonaCat.LogStack/EonaCatLoggerCore/ContextSnapshotCollector.cs new file mode 100644 index 0000000..9ae3765 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/ContextSnapshotCollector.cs @@ -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; + +/// +/// Context Snapshot Collector - captures heap snapshots and thread stacks on critical errors. +/// Superior to simple error logging as it provides full context for debugging. +/// +public class ContextSnapshotCollector +{ + private readonly List _snapshots = new List(); + private readonly int _maxSnapshots; + private readonly object _snapshotsLock = new object(); + private readonly LogLevel _triggerLevel; + private long _totalSnapshotsCaptured; + + public event EventHandler SnapshotCaptured; + + public ContextSnapshotCollector( + LogLevel triggerLevel = LogLevel.Critical, + int maxSnapshots = 100) + { + _triggerLevel = triggerLevel; + _maxSnapshots = Math.Max(10, maxSnapshots); + } + + /// + /// Captures a context snapshot if the log event meets trigger conditions. + /// + public ContextSnapshot CaptureIfNeeded(LogEvent logEvent) + { + if (logEvent.Level < _triggerLevel) + return null; + + return CaptureSnapshot(logEvent); + } + + /// + /// Captures a full context snapshot for the given log event. + /// + 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; + } + + /// + /// Gets the most recent snapshot. + /// + public ContextSnapshot GetLatestSnapshot() + { + lock (_snapshotsLock) + { + return _snapshots.LastOrDefault(); + } + } + + /// + /// Gets all captured snapshots. + /// + public List GetSnapshots(int skip = 0, int take = 10) + { + lock (_snapshotsLock) + { + return _snapshots.Skip(skip).Take(take).ToList(); + } + } + + /// + /// Gets a snapshot by timestamp window. + /// + public ContextSnapshot GetSnapshotNear(DateTime timestamp) + { + lock (_snapshotsLock) + { + return _snapshots + .OrderBy(s => Math.Abs((s.CapturedAt - timestamp).TotalSeconds)) + .FirstOrDefault(); + } + } + + /// + /// Gets statistics about captured snapshots. + /// + 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 + }; + } + } + + /// + /// Clears all snapshots. + /// + 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(); + + 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 CaptureStackTraces() + { + var stacks = new Dictionary(); + + 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 CaptureEnvironmentSnapshot() + { + var env = new Dictionary(); + + // 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; + } +} + +/// +/// A captured context snapshot. +/// +public class ContextSnapshot +{ + /// + /// When this snapshot was captured. + /// + public DateTime CapturedAt { get; set; } + + /// + /// The log event that triggered this snapshot. + /// + public LogEvent LogEvent { get; set; } + + /// + /// Process information at time of capture. + /// + public ProcessSnapshot ProcessInfo { get; set; } + + /// + /// Memory information at time of capture. + /// + public MemorySnapshot MemoryInfo { get; set; } + + /// + /// Thread information at time of capture. + /// + public ThreadSnapshot ThreadInfo { get; set; } + + /// + /// Stack traces for active threads. + /// + public Dictionary StackTraces { get; set; } + + /// + /// Environment snapshot. + /// + public Dictionary EnvironmentVars { get; set; } +} + +/// +/// Process information snapshot. +/// +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; } +} + +/// +/// Memory information snapshot. +/// +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; } +} + +/// +/// Thread information snapshot. +/// +public class ThreadSnapshot +{ + public int TotalThreadCount { get; set; } + public List ThreadIds { get; set; } + public int ManagedThreadCount { get; set; } +} + +/// +/// Detail about a single thread. +/// +public class ThreadDetail +{ + public int ThreadId { get; set; } + public string State { get; set; } + public string WaitReason { get; set; } + public int Priority { get; set; } +} + +/// +/// Statistics about the snapshot collector. +/// +public class SnapshotCollectorStats +{ + public long TotalCaptured { get; set; } + public int CurrentSnapshots { get; set; } + public int MaxCapacity { get; set; } + public Dictionary LevelDistribution { get; set; } + public DateTime? OldestSnapshot { get; set; } + public DateTime? NewestSnapshot { get; set; } +} + +/// +/// Runtime information helper. +/// +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 + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/DeadLetterQueue.cs b/EonaCat.LogStack/EonaCatLoggerCore/DeadLetterQueue.cs new file mode 100644 index 0000000..b46f5b7 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/DeadLetterQueue.cs @@ -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; + +/// +/// 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. +/// +public class DeadLetterQueue +{ + private readonly ConcurrentQueue _queue = new ConcurrentQueue(); + private readonly int _maxCapacity; + private int _currentCount; + private long _totalDropped; + private long _totalEnqueued; + private readonly object _statsLock = new object(); + + public event EventHandler EventEnqueued; + public event EventHandler EventReplayed; + + public DeadLetterQueue(int maxCapacity = 100000) + { + _maxCapacity = Math.Max(1000, maxCapacity); + } + + /// + /// Enqueues a failed log event with the reason for failure. + /// + 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; + } + + /// + /// Gets the count of events in the DLQ. + /// + public int GetCount() => _currentCount; + + /// + /// Peeks at the next event without removing it. + /// + public bool TryPeek(out DeadLetterEvent dlEvent) + { + return _queue.TryPeek(out dlEvent); + } + + /// + /// Dequeues the next event. + /// + public bool TryDequeue(out DeadLetterEvent dlEvent) + { + if (_queue.TryDequeue(out var evt)) + { + _currentCount--; + dlEvent = evt; + return true; + } + + dlEvent = null; + return false; + } + + /// + /// Gets all events in the DLQ without removing them. + /// + public List GetAll(int skip = 0, int take = 100) + { + return _queue.Skip(skip).Take(take).ToList(); + } + + /// + /// Replays a dead letter event to specified flows. + /// + public async Task ReplayAsync(DeadLetterEvent dlEvent, IEnumerable targetFlows) + { + if (dlEvent == null) + throw new ArgumentNullException(nameof(dlEvent)); + + dlEvent.RetryCount++; + dlEvent.LastRetryAt = DateTime.UtcNow; + + var flowsList = targetFlows.ToList(); + var tasks = new List>(); + + 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; + } + + /// + /// Bulk replay of oldest events (useful for periodic retry jobs). + /// + public async Task ReplayOldestAsync(int count, IEnumerable 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; + } + + /// + /// Clears all events from the DLQ. + /// + public int Clear() + { + var count = _currentCount; + while (_queue.TryDequeue(out _)) + { + _currentCount--; + } + return count; + } + + /// + /// Gets statistics about the DLQ. + /// + 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 + }; + } + } + + /// + /// Gets events grouped by failure reason. + /// + public Dictionary GetFailureReasonStats() + { + var stats = new Dictionary(); + 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); + } +} + +/// +/// Represents a dead letter event. +/// +public class DeadLetterEvent +{ + /// + /// The original log event that failed to be delivered. + /// + public LogEvent LogEvent { get; set; } + + /// + /// When this event was enqueued to the DLQ. + /// + public DateTime EnqueuedAt { get; set; } + + /// + /// Reason why the log event failed. + /// + public string FailureReason { get; set; } + + /// + /// Exception details if available. + /// + public string FailureException { get; set; } + + /// + /// Number of times this event has been retried. + /// + public int RetryCount { get; set; } + + /// + /// When this event was last retried. + /// + public DateTime? LastRetryAt { get; set; } + + /// + /// When this event was successfully replayed (if at all). + /// + public DateTime? ReplayedAt { get; set; } + + /// + /// Whether the replay was successful. + /// + public bool ReplaySuccessful { get; set; } +} + +/// +/// Event raised when a dead letter event is replayed. +/// +public class DeadLetterEventReplayed +{ + public DeadLetterEvent Event { get; set; } + public bool WasSuccessful { get; set; } + public int FlowsTargeted { get; set; } +} + +/// +/// Statistics about the dead letter queue. +/// +public class DeadLetterQueueStats +{ + /// + /// Current number of events in the queue. + /// + public int CurrentQueueSize { get; set; } + + /// + /// Maximum capacity of the queue. + /// + public int MaxCapacity { get; set; } + + /// + /// Total events ever enqueued. + /// + public long TotalEnqueued { get; set; } + + /// + /// Total events dropped due to capacity. + /// + public long TotalDropped { get; set; } + + /// + /// Queue utilization as percentage. + /// + public double QueueUtilizationPercent { get; set; } + + /// + /// Age of the oldest event in queue. + /// + public TimeSpan OldestEventAge { get; set; } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/DlqFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DlqFlow.cs new file mode 100644 index 0000000..24d50e3 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/DlqFlow.cs @@ -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; + +/// +/// Dead Letter Queue Flow - captures log events that failed to be delivered to other flows. +/// Enables analysis and replay of failed logs. +/// +public class DlqFlow : FlowBase +{ + private readonly DeadLetterQueue _dlq; + private readonly List _backupFlows = new List(); + 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); + } + + /// + /// Gets the underlying dead letter queue for inspection and replay. + /// + public DeadLetterQueue GetDeadLetterQueue() => _dlq; + + /// + /// Adds a backup flow to attempt retrying failed events. + /// + public DlqFlow AddBackupFlow(IFlow flow) + { + if (flow == null) throw new ArgumentNullException(nameof(flow)); + lock (_flowsLock) + { + _backupFlows.Add(flow); + } + return this; + } + + /// + /// Blasts a single log event to the DLQ. + /// + public override async Task 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 backupFlows; + lock (_flowsLock) + { + backupFlows = new List(_backupFlows); + } + + bool anyBackupSucceeded = false; + var failureReasons = new List(); + + 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; + } + + /// + /// Blasts a batch of log events to the DLQ. + /// + public override async Task BlastBatchAsync(ReadOnlyMemory 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; + } + + /// + /// Flushes the DLQ (does nothing, but required by interface). + /// + public override Task FlushAsync(CancellationToken cancellationToken = default) + { + return Task.CompletedTask; + } + + /// + /// Gets statistics about this DLQ flow. + /// + public DeadLetterQueueStats GetStatistics() + { + return _dlq.GetStats(); + } + + /// + /// Attempts to replay queued dead letters to backup flows. + /// + public async Task ReplayOldestAsync(int count) + { + List backupFlows; + lock (_flowsLock) + { + backupFlows = new List(_backupFlows); + } + + return await _dlq.ReplayOldestAsync(count, (IEnumerable)backupFlows).ConfigureAwait(false); + } + + /// + /// Gets all dead letter events. + /// + public List GetDeadLetters(int skip = 0, int take = 100) + { + return _dlq.GetAll(skip, take); + } + + /// + /// Clears all dead letters from the queue. + /// + public int ClearDeadLetters() + { + return _dlq.Clear(); + } + + public override async ValueTask DisposeAsync() + { + if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) + return; + + List flowsToDispose; + lock (_flowsLock) + { + flowsToDispose = new List(_backupFlows); + _backupFlows.Clear(); + } + + foreach (var flow in flowsToDispose) + { + try + { + await flow.DisposeAsync().ConfigureAwait(false); + } + catch { } + } + + await base.DisposeAsync().ConfigureAwait(false); + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/IntelligentRouter.cs b/EonaCat.LogStack/EonaCatLoggerCore/IntelligentRouter.cs new file mode 100644 index 0000000..e5a07f4 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/IntelligentRouter.cs @@ -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; + +/// +/// 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. +/// +public class IntelligentRouter : IAsyncDisposable +{ + private readonly List _rules = new List(); + private readonly Dictionary _flowRoutes = new Dictionary(); + private readonly object _rulesLock = new object(); + + public IntelligentRouter() + { + } + + /// + /// Adds a routing rule that matches messages based on pattern and routes them to specific flows. + /// + 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; + } + + /// + /// Adds a rule with fluent API support. + /// + public IntelligentRouter AddRule( + string name, + Func matcher, + IEnumerable targetFlows, + int priority = 0, + bool stopIfMatched = false) + { + var rule = new RoutingRule + { + Name = name, + Matcher = matcher, + TargetFlows = targetFlows?.ToList() ?? new List(), + Priority = priority, + StopIfMatched = stopIfMatched + }; + return AddRule(rule); + } + + /// + /// Adds a regex pattern-based routing rule. + /// + public IntelligentRouter AddPatternRule( + string name, + string messagePattern, + IEnumerable 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 + ); + } + + /// + /// Adds a category-based routing rule (routes logs from specific categories). + /// + public IntelligentRouter AddCategoryRule( + string category, + IEnumerable targetFlows, + LogLevel? minimumLevel = null) + { + return AddRule( + $"Category:{category}", + logEvent => logEvent.Category == category && (minimumLevel == null || logEvent.Level >= minimumLevel), + targetFlows, + priority: 10 + ); + } + + /// + /// Adds an exception type-based routing rule. + /// + public IntelligentRouter AddExceptionRule( + string exceptionTypeName, + IEnumerable targetFlows) + { + return AddRule( + $"Exception:{exceptionTypeName}", + logEvent => logEvent.Exception?.GetType().Name == exceptionTypeName, + targetFlows, + priority: 20, + stopIfMatched: true + ); + } + + /// + /// Registers a flow that can be targeted by routing rules. + /// + 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; + } + + /// + /// Enables or disables a specific flow by name. + /// + public IntelligentRouter SetFlowEnabled(string flowName, bool enabled) + { + lock (_rulesLock) + { + if (_flowRoutes.TryGetValue(flowName, out var route)) + { + route.IsEnabled = enabled; + } + } + return this; + } + + /// + /// Routes a log event to appropriate flows based on rules. + /// Returns true if at least one flow accepted the message. + /// + public async Task RouteAsync(LogEvent logEvent) + { + List activeRules; + Dictionary activeFlows; + + lock (_rulesLock) + { + activeRules = new List(_rules); + activeFlows = new Dictionary(_flowRoutes); + } + + var targetFlowNames = new HashSet(); + 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(); + + 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) + } + + /// + /// Gets statistics about the router. + /// + 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; } + } +} + +/// +/// Defines a routing rule for intelligent message routing. +/// +public class RoutingRule +{ + /// + /// Name of this rule for identification/debugging. + /// + public string Name { get; set; } + + /// + /// Function that determines if this rule matches a log event. + /// + public Func Matcher { get; set; } + + /// + /// Names of flows this rule routes to. + /// + public List TargetFlows { get; set; } = new List(); + + /// + /// Priority of this rule (higher priority rules are evaluated first). + /// + public int Priority { get; set; } = 0; + + /// + /// If true, stops evaluating further rules after this one matches. + /// + public bool StopIfMatched { get; set; } = false; +} + +/// +/// Statistics about the intelligent router. +/// +public class RouterStats +{ + public int TotalRules { get; set; } + public int RegisteredFlows { get; set; } + public int EnabledFlows { get; set; } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceAnalyzer.cs b/EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceAnalyzer.cs new file mode 100644 index 0000000..6f495a4 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceAnalyzer.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EonaCat.LogStack.PerformanceInsights +{ + /// + /// Analyzes performance metrics and provides optimization insights. + /// + public sealed class PerformanceAnalyzer + { + private readonly PerformanceInsightsCollector _collector; + + public PerformanceAnalyzer(PerformanceInsightsCollector collector = null) + { + _collector = collector ?? new PerformanceInsightsCollector(); + } + + /// + /// Analyzes current performance state and returns insights + /// + 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; + } + + /// + /// Generates a summary string of the analysis + /// + private string GenerateSummary(PerformanceInsightsSnapshot snapshot, List recommendations) + { + var lines = new List + { + $"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); + } + } + + /// + /// Performance analysis result + /// + 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 Recommendations { get; set; } = new(); + public string Summary { get; set; } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceInsightsCollector.cs b/EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceInsightsCollector.cs new file mode 100644 index 0000000..7d512e1 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceInsightsCollector.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace EonaCat.LogStack.PerformanceInsights +{ + /// + /// Collects performance insights from multiple flows and operations. + /// Detects bottlenecks and provides recommendations. + /// + public sealed class PerformanceInsightsCollector + { + private readonly ConcurrentDictionary _trackers = new(); + private long _totalOperations; + private long _totalErrors; + private DateTime _startTime = DateTime.UtcNow; + + /// + /// Gets or creates a tracker for the given name + /// + public PerformanceTracker GetOrCreateTracker(string name) + { + return _trackers.GetOrAdd(name, _ => new PerformanceTracker(name)); + } + + /// + /// Gets a tracker if it exists + /// + public PerformanceTracker? GetTracker(string name) + { + return _trackers.TryGetValue(name, out var tracker) ? tracker : null; + } + + /// + /// Records an operation + /// + 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++; + } + + /// + /// Gets all current trackers + /// + public IReadOnlyCollection GetAllTrackers() + { + return _trackers.Values.ToList().AsReadOnly(); + } + + /// + /// Gets current insights snapshot + /// + 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 + }; + } + + /// + /// Gets performance recommendations based on current metrics + /// + public List GetRecommendations() + { + var recommendations = new List(); + 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; + } + + /// + /// Resets all trackers + /// + public void Reset() + { + foreach (var tracker in _trackers.Values) + { + tracker.Reset(); + } + _totalOperations = 0; + _totalErrors = 0; + _startTime = DateTime.UtcNow; + } + } + + /// + /// Snapshot of performance insights + /// + 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 AllMetrics { get; set; } = new(); + public List SlowestOperations { get; set; } = new(); + public List MostErrorsOperations { get; set; } = new(); + public List HighestThroughputOperations { get; set; } = new(); + public DateTime CapturedAt { get; set; } + } + + /// + /// Performance recommendation + /// + public sealed class PerformanceRecommendation + { + public string Category { get; set; } + public Severity Severity { get; set; } + public string Message { get; set; } + public List AffectedOperations { get; set; } = new(); + } + + /// + /// Severity level + /// + public enum Severity + { + Low, + Medium, + High, + Critical + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceTracker.cs b/EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceTracker.cs new file mode 100644 index 0000000..e09a58d --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/PerformanceInsights/PerformanceTracker.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Runtime.CompilerServices; + +namespace EonaCat.LogStack.PerformanceInsights +{ + /// + /// Tracks performance metrics for flows and operations. + /// Measures latency, throughput, and allocations with minimal overhead. + /// + 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(); + + /// + /// Gets the operation name + /// + public string Name { get; } + + /// + /// Gets total operations recorded + /// + public long OperationCount => _operationCount; + + /// + /// Gets total duration in milliseconds + /// + public double TotalDurationMs => _totalDurationTicks * 1000.0 / Stopwatch.Frequency; + + /// + /// Gets average duration in milliseconds + /// + public double AverageDurationMs => _operationCount > 0 ? TotalDurationMs / _operationCount : 0; + + /// + /// Gets minimum duration in milliseconds + /// + public double MinDurationMs => _minDurationTicks == long.MaxValue ? 0 : _minDurationTicks * 1000.0 / Stopwatch.Frequency; + + /// + /// Gets maximum duration in milliseconds + /// + public double MaxDurationMs => _maxDurationTicks * 1000.0 / Stopwatch.Frequency; + + /// + /// Gets throughput in operations per second + /// + public double ThroughputOpsPerSec => TotalDurationMs > 0 ? (_operationCount / TotalDurationMs) * 1000 : 0; + + /// + /// Gets total bytes processed + /// + public long ByteCount => _byteCount; + + /// + /// Gets error count + /// + public long ErrorCount => _errorCount; + + /// + /// Gets error rate percentage + /// + public double ErrorRatePercent => _operationCount > 0 ? (_errorCount * 100.0) / _operationCount : 0; + + public PerformanceTracker(string name) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + } + + /// + /// Records an operation with its duration + /// + [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++; + } + } + + /// + /// Resets all metrics + /// + public void Reset() + { + lock (_lock) + { + _operationCount = 0; + _totalDurationTicks = 0; + _minDurationTicks = long.MaxValue; + _maxDurationTicks = 0; + _errorCount = 0; + _byteCount = 0; + } + } + + /// + /// Gets a snapshot of current metrics + /// + 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 + }; + } + } + } + + /// + /// Snapshot of performance metrics at a point in time + /// + 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; } + + /// + /// Gets a formatted string representation + /// + public override string ToString() + { + return $"{Name} | Ops: {OperationCount} | Avg: {AverageDurationMs:F2}ms | Thrput: {ThroughputOpsPerSec:F2} ops/s | Errors: {ErrorCount} ({ErrorRatePercent:F2}%)"; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Tracing/SpanFactory.cs b/EonaCat.LogStack/EonaCatLoggerCore/Tracing/SpanFactory.cs new file mode 100644 index 0000000..cbed4cb --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Tracing/SpanFactory.cs @@ -0,0 +1,133 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.LogStack.Tracing +{ + /// + /// Factory for creating and managing TracingSpans with automatic context management. + /// Supports parent-child relationships and automatic scope management. + /// + public sealed class SpanFactory + { + private readonly List _activeSpans = new(); + private readonly object _activeLock = new(); + + /// + /// Creates a new span with auto-context inheritance + /// + 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; + } + + /// + /// Creates a span and returns a scope that manages its lifetime + /// + public IDisposable CreateSpanScope(string name, string kind = "internal") + { + var span = CreateSpan(name, kind); + return new SpanScope(span, this); + } + + /// + /// Creates a span with a child trace context + /// + public TracingSpan CreateChildSpan(string name, string kind = "internal") + { + using (TraceContextManager.CreateChildScope()) + { + return CreateSpan(name, kind); + } + } + + /// + /// Gets all active spans + /// + public IReadOnlyList GetActiveSpans() + { + lock (_activeLock) + { + return _activeSpans.AsReadOnly(); + } + } + + /// + /// Executes action within a span + /// + public void ExecuteInSpan(string name, Action action, string kind = "internal") + { + using (CreateSpanScope(name, kind)) + { + action(); + } + } + + /// + /// Executes function within a span, returns result and span + /// + public (T Result, TracingSpan Span) ExecuteInSpan(string name, Func 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; + } + } + + /// + /// Removes a span from active list + /// + internal void RemoveSpan(TracingSpan span) + { + lock (_activeLock) + { + _activeSpans.Remove(span); + } + } + + /// + /// Scope that manages span lifetime + /// + 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); + } + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Tracing/TraceContext.cs b/EonaCat.LogStack/EonaCatLoggerCore/Tracing/TraceContext.cs new file mode 100644 index 0000000..176c223 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Tracing/TraceContext.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.LogStack.Tracing +{ + /// + /// Represents a trace context for distributed tracing across async boundaries. + /// Contains trace ID, span ID, parent span ID, and baggage for propagation. + /// + public sealed class TraceContext + { + /// + /// Gets or sets the trace ID (root identifier for entire trace) + /// + public string TraceId { get; set; } + + /// + /// Gets or sets the span ID (current operation identifier) + /// + public string SpanId { get; set; } + + /// + /// Gets or sets the parent span ID (parent operation identifier, if any) + /// + public string? ParentSpanId { get; set; } + + /// + /// Gets or sets the correlation ID for request tracking + /// + public string? CorrelationId { get; set; } + + /// + /// Gets or sets baggage (key-value pairs to propagate across spans) + /// + public Dictionary Baggage { get; set; } = new(); + + /// + /// Gets or sets the trace flags (e.g., sampling decision) + /// + public byte TraceFlags { get; set; } = 0x01; + + /// + /// Gets the creation timestamp + /// + public DateTime CreatedAt { get; } = DateTime.UtcNow; + + /// + /// Gets whether this trace is sampled for export + /// + public bool IsSampled => (TraceFlags & 0x01) != 0; + + /// + /// Creates a new TraceContext with a generated trace ID + /// + public TraceContext() + { + TraceId = GenerateId(); + SpanId = GenerateId(); + } + + /// + /// Creates a child TraceContext from this context + /// + public TraceContext CreateChild() + { + return new TraceContext + { + TraceId = TraceId, + ParentSpanId = SpanId, + SpanId = GenerateId(), + CorrelationId = CorrelationId, + TraceFlags = TraceFlags, + Baggage = new Dictionary(Baggage) + }; + } + + /// + /// Generates a random ID for trace/span + /// + private static string GenerateId() + { + return Guid.NewGuid().ToString("N").Substring(0, 16); + } + + /// + /// Creates a copy of this TraceContext for propagation + /// + public TraceContext Clone() + { + return new TraceContext + { + TraceId = TraceId, + SpanId = SpanId, + ParentSpanId = ParentSpanId, + CorrelationId = CorrelationId, + TraceFlags = TraceFlags, + Baggage = new Dictionary(Baggage) + }; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Tracing/TraceContextManager.cs b/EonaCat.LogStack/EonaCatLoggerCore/Tracing/TraceContextManager.cs new file mode 100644 index 0000000..78559f5 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Tracing/TraceContextManager.cs @@ -0,0 +1,162 @@ +using System; +using System.Threading; + +namespace EonaCat.LogStack.Tracing +{ + /// + /// Manages trace context across async boundaries using AsyncLocal. + /// Enables automatic trace/span propagation without explicit passing. + /// + public sealed class TraceContextManager + { + private static readonly AsyncLocal _current = new(); + private static readonly TraceContextManager _instance = new(); + + /// + /// Gets the singleton instance + /// + public static TraceContextManager Instance => _instance; + + /// + /// Gets the current trace context (creates new if not set) + /// + public static TraceContext Current + { + get => _current.Value ??= new TraceContext(); + set => _current.Value = value; + } + + /// + /// Gets the current trace context without creating + /// + public static TraceContext? TryGetCurrent() => _current.Value; + + /// + /// Sets the trace context + /// + public static void SetCurrent(TraceContext context) + { + if (context == null) throw new ArgumentNullException(nameof(context)); + _current.Value = context; + } + + /// + /// Clears the current trace context + /// + public static void Clear() + { + _current.Value = null; + } + + /// + /// Creates a new child context and sets it as current + /// + public static IDisposable CreateChildScope() + { + var parent = Current; + var child = parent.CreateChild(); + _current.Value = child; + return new TraceScope(parent); + } + + /// + /// Executes action with a new child trace context + /// + public static void WithChild(Action action) + { + using (CreateChildScope()) + { + action(); + } + } + + /// + /// Executes function with a new child trace context + /// + public static T WithChild(Func func) + { + using (CreateChildScope()) + { + return func(); + } + } + + /// + /// Gets the current trace ID + /// + public static string GetTraceId() => Current.TraceId; + + /// + /// Gets the current span ID + /// + public static string GetSpanId() => Current.SpanId; + + /// + /// Gets the current correlation ID + /// + public static string? GetCorrelationId() => Current.CorrelationId; + + /// + /// Sets the correlation ID + /// + public static void SetCorrelationId(string correlationId) + { + Current.CorrelationId = correlationId; + } + + /// + /// Adds baggage data + /// + public static void AddBaggage(string key, string value) + { + Current.Baggage[key] = value; + } + + /// + /// Gets baggage value + /// + public static string? GetBaggage(string key) + { + return Current.Baggage.TryGetValue(key, out var value) ? value : null; + } + + /// + /// Removes baggage value + /// + public static bool RemoveBaggage(string key) + { + return Current.Baggage.Remove(key); + } + + /// + /// Sets sampling decision + /// + public static void SetSampled(bool sampled) + { + Current.TraceFlags = sampled ? (byte)0x01 : (byte)0x00; + } + + /// + /// Gets sampling decision + /// + public static bool IsSampled() => Current.IsSampled; + + /// + /// Scope disposed when exiting + /// + private sealed class TraceScope : IDisposable + { + private readonly TraceContext _parent; + + public TraceScope(TraceContext parent) + { + _parent = parent; + } + + public void Dispose() + { + _current.Value = _parent; + } + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Tracing/TracingSpan.cs b/EonaCat.LogStack/EonaCatLoggerCore/Tracing/TracingSpan.cs new file mode 100644 index 0000000..43921c6 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Tracing/TracingSpan.cs @@ -0,0 +1,211 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace EonaCat.LogStack.Tracing +{ + /// + /// Enhanced span representation for distributed tracing. + /// Tracks operation timing, status, and linked operations. + /// + public sealed class TracingSpan : IDisposable + { + private readonly Stopwatch _stopwatch = Stopwatch.StartNew(); + private readonly List _exceptions = new(); + private bool _disposed; + + /// + /// Gets the span name + /// + public string Name { get; } + + /// + /// Gets the span kind (internal, server, client, producer, consumer) + /// + public string Kind { get; set; } = "internal"; + + /// + /// Gets the trace context + /// + public TraceContext Context { get; } + + /// + /// Gets or sets the span status + /// + public SpanStatus Status { get; set; } = SpanStatus.Unset; + + /// + /// Gets or sets the status description + /// + public string? StatusDescription { get; set; } + + /// + /// Gets the start time + /// + public DateTime StartTime { get; } + + /// + /// Gets the end time (if ended) + /// + public DateTime? EndTime { get; private set; } + + /// + /// Gets the duration in milliseconds + /// + public double DurationMs { get; private set; } + + /// + /// Gets attributes attached to span + /// + public Dictionary Attributes { get; } = new(); + + /// + /// Gets events recorded on span + /// + public List Events { get; } = new(); + + /// + /// Gets linked spans + /// + public List Links { get; } = new(); + + /// + /// Gets recorded exceptions + /// + public IReadOnlyList Exceptions => _exceptions.AsReadOnly(); + + /// + /// Creates a new tracing span + /// + public TracingSpan(string name, TraceContext? context = null) + { + Name = name ?? throw new ArgumentNullException(nameof(name)); + Context = context ?? TraceContextManager.Current; + StartTime = DateTime.UtcNow; + } + + /// + /// Records an event on the span + /// + public void AddEvent(string eventName, Dictionary? attributes = null) + { + Events.Add(new SpanEvent + { + Name = eventName, + Timestamp = DateTime.UtcNow, + Attributes = attributes ?? new Dictionary() + }); + } + + /// + /// Records an exception on the span + /// + public void RecordException(Exception exception, Dictionary? attributes = null) + { + if (exception == null) return; + + _exceptions.Add(exception); + Status = SpanStatus.Error; + StatusDescription = exception.Message; + + var eventAttrs = attributes ?? new Dictionary(); + eventAttrs["exception.type"] = exception.GetType().FullName; + eventAttrs["exception.message"] = exception.Message; + eventAttrs["exception.stacktrace"] = exception.StackTrace ?? ""; + + AddEvent("exception", eventAttrs); + } + + /// + /// Sets a span attribute + /// + public void SetAttribute(string key, object value) + { + Attributes[key] = value; + } + + /// + /// Sets multiple attributes + /// + public void SetAttributes(Dictionary attributes) + { + if (attributes == null) return; + foreach (var kvp in attributes) + { + Attributes[kvp.Key] = kvp.Value; + } + } + + /// + /// Adds a linked span + /// + public void AddLink(string linkedTraceId, string linkedSpanId, Dictionary? attributes = null) + { + Links.Add(new SpanLink + { + TraceId = linkedTraceId, + SpanId = linkedSpanId, + Attributes = attributes ?? new Dictionary() + }); + } + + /// + /// Marks span as ended with success + /// + 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; + } + + /// + /// Gets span as W3C trace context header value + /// + public string ToW3CTraceContext() + { + return $"{Context.TraceId}-{Context.SpanId}-{(byte)Context.TraceFlags:x2}"; + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + End(); + } + } + + /// + /// Span status enumeration + /// + public enum SpanStatus + { + Unset = 0, + Ok = 1, + Error = 2 + } + + /// + /// Represents an event recorded on a span + /// + public sealed class SpanEvent + { + public string Name { get; set; } + public DateTime Timestamp { get; set; } + public Dictionary Attributes { get; set; } = new(); + } + + /// + /// Represents a link to another span + /// + public sealed class SpanLink + { + public string TraceId { get; set; } + public string SpanId { get; set; } + public Dictionary Attributes { get; set; } = new(); + } +} diff --git a/EonaCat.LogStack/Extensions/ServiceCollectionExtensions.cs b/EonaCat.LogStack/Extensions/ServiceCollectionExtensions.cs index c92b1c5..de5fea3 100644 --- a/EonaCat.LogStack/Extensions/ServiceCollectionExtensions.cs +++ b/EonaCat.LogStack/Extensions/ServiceCollectionExtensions.cs @@ -1,6 +1,10 @@ using EonaCat.LogStack.Configuration; using EonaCat.LogStack.Core; using EonaCat.LogStack.Logging; +using EonaCat.LogStack.DependencyInjection; +using EonaCat.LogStack.Telemetry; +using EonaCat.LogStack.Tracing; +using EonaCat.LogStack.PerformanceInsights; using Microsoft.Extensions.DependencyInjection; using System; @@ -337,4 +341,98 @@ public static class ServiceCollectionExtensions services.AddSingleton(); return services; } + + /// + /// Registers named logger factory for per-category loggers in DI + /// + 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(); + } + + if (options.EnableTracing) + { + services.AddSingleton(); + } + + if (options.EnablePerformanceMonitoring) + { + services.AddSingleton(); + } + + if (options.EnableHealthMonitoring) + { + services.AddSingleton(); + } + + return services; + } + + /// + /// Enables distributed tracing support + /// + public static IServiceCollection AddEonaCatTracing( + this IServiceCollection services) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + + services.AddSingleton(); + services.AddSingleton(); + services.AddSingleton(); + return services; + } + + /// + /// Enables performance monitoring and insights + /// + public static IServiceCollection AddEonaCatPerformanceMonitoring( + this IServiceCollection services) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + + services.AddSingleton(); + services.AddSingleton(); + return services; + } + + /// + /// Enables health monitoring + /// + public static IServiceCollection AddEonaCatHealthMonitoring( + this IServiceCollection services) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + + services.AddSingleton(); + services.AddSingleton(); + return services; + } + + /// + /// Enables unified telemetry aggregation + /// + public static IServiceCollection AddEonaCatTelemetryAggregation( + this IServiceCollection services) + { + if (services == null) + throw new ArgumentNullException(nameof(services)); + + services.AddSingleton(); + services.AddSingleton(); + return services; + } } diff --git a/EonaCat.LogStack/LogBuilder.cs b/EonaCat.LogStack/LogBuilder.cs index 915a506..eb6ac33 100644 --- a/EonaCat.LogStack/LogBuilder.cs +++ b/EonaCat.LogStack/LogBuilder.cs @@ -796,6 +796,73 @@ public sealed class LogBuilder return this; } + /// + /// Stores recent log events in memory for querying and diagnostics + /// + public LogBuilder WriteToQueryableStorage( + int maxCapacity = 10000, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new LocalStorageQueryFlow(maxCapacity, minimumLevel)); + return this; + } + + /// + /// Tracks and correlates related events across flows using correlation IDs + /// + public LogBuilder WriteToCorrelationTracking( + TimeSpan? correlationWindow = null, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new CorrelatedEventFlow( + correlationWindow: correlationWindow, + minimumLevel: minimumLevel)); + return this; + } + + /// + /// Monitors performance metrics and detects anomalies using statistical analysis + /// + public LogBuilder WriteToPerformanceMonitoring( + IFlow? alertFlow = null, + int windowSize = 100, + double standardDeviationThreshold = 2.0) + { + _flows.Add(new PerformanceAnomalyDetectorFlow( + alertFlow, + windowSize, + standardDeviationThreshold)); + return this; + } + + /// + /// Adds a filtered flow using a named preset (ErrorOnly, PerformanceLogs, SecurityLogs, etc.) + /// + 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; + } + + /// + /// Adds a custom filtered flow using a predicate function + /// + public LogBuilder WriteToCustomFilteredFlow( + IFlow targetFlow, + string name, + Func predicate, + LogLevel minimumLevel = LogLevel.Trace) + { + var preset = new CustomPreset(name, predicate); + _flows.Add(new LogFilterPresetFlow(targetFlow, preset, minimumLevel)); + return this; + } + /// /// Boost logs with machine name /// diff --git a/EonaCat.LogStack/Policies/AutoPolicies.cs b/EonaCat.LogStack/Policies/AutoPolicies.cs new file mode 100644 index 0000000..ce14a8b --- /dev/null +++ b/EonaCat.LogStack/Policies/AutoPolicies.cs @@ -0,0 +1,177 @@ +using System; +using System.Collections.Generic; + +namespace EonaCat.LogStack.Policies +{ + /// + /// Automatic batching policy for flows + /// + 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; + } + + /// + /// Automatic flow scaling policy + /// + 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; + } + + /// + /// Automatic retention policy for log files + /// + 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; + } + + /// + /// Automatic optimization policy for performance + /// + 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; + } + + /// + /// Collection of auto-policies for logger + /// + public sealed class AutoPolicies + { + public AutoBatchingPolicy Batching { get; } = new(); + public AutoScalingPolicy Scaling { get; } = new(); + public AutoRetentionPolicy Retention { get; } = new(); + public AutoOptimizationPolicy Optimization { get; } = new(); + } + + /// + /// Policy engine for automatic optimization + /// + 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(); + } + + /// + /// Evaluates policies and returns recommended actions + /// + public List EvaluatePolicies() + { + var actions = new List(); + 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; + } + + /// + /// Gets all active policies as string + /// + public string GetPoliciesSummary() + { + var lines = new List + { + "=== 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); + } + } + + /// + /// Policy action types + /// + public enum PolicyActionType + { + None = 0, + AnalyzePerformance = 1, + CleanupRetentionPolicy = 2, + AdjustBatchSize = 3, + AdjustBufferSize = 4, + AlertHighErrorRate = 5, + AlertHighThroughput = 6, + CompressArchives = 7 + } + + /// + /// Represents a policy action to execute + /// + public sealed class PolicyAction + { + public PolicyActionType Type { get; set; } + public string? Reason { get; set; } + public Dictionary Parameters { get; set; } = new(); + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + } +} diff --git a/EonaCat.LogStack/SuperiorFeaturesStats.cs b/EonaCat.LogStack/SuperiorFeaturesStats.cs new file mode 100644 index 0000000..aa1ff97 --- /dev/null +++ b/EonaCat.LogStack/SuperiorFeaturesStats.cs @@ -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; + +/// +/// Aggregated statistics from all superior features. +/// +public class SuperiorFeaturesStats +{ + /// + /// Whether intelligent routing is enabled. + /// + public bool IntelligentRoutingEnabled { get; set; } + + /// + /// Statistics from the intelligent router. + /// + public RouterStats RouterStats { get; set; } + + /// + /// Whether adaptive sampling is enabled. + /// + public bool AdaptiveSamplingEnabled { get; set; } + + /// + /// Metrics from the adaptive sampler. + /// + public SamplingMetrics SamplingMetrics { get; set; } + + /// + /// Whether anomaly detection is enabled. + /// + public bool AnomalyDetectionEnabled { get; set; } + + /// + /// Statistics from the anomaly detector. + /// + public AnomalyDetectorStats AnomalyStats { get; set; } + + /// + /// Recent anomalies that were detected. + /// + public List RecentAnomalies { get; set; } + + /// + /// Whether context snapshots are enabled. + /// + public bool ContextSnapshotsEnabled { get; set; } + + /// + /// Statistics from the context snapshot collector. + /// + public SnapshotCollectorStats SnapshotStats { get; set; } + + /// + /// Whether the dead letter queue is enabled. + /// + public bool DeadLetterQueueEnabled { get; set; } + + /// + /// Statistics from the dead letter queue. + /// + public DeadLetterQueueStats DlqStats { get; set; } + + /// + /// Failure reasons grouped by count. + /// + public Dictionary DlqFailureReasons { get; set; } + + /// + /// Gets a summary string of all enabled features. + /// + public string GetEnabledFeaturesSummary() + { + var features = new List(); + + 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"); + } +} diff --git a/EonaCat.LogStack/Telemetry/HealthMonitor.cs b/EonaCat.LogStack/Telemetry/HealthMonitor.cs new file mode 100644 index 0000000..1c41385 --- /dev/null +++ b/EonaCat.LogStack/Telemetry/HealthMonitor.cs @@ -0,0 +1,239 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EonaCat.LogStack.Telemetry +{ + /// + /// Monitors the health status of the logger and flows. + /// Tracks degradation, failures, and recovery events. + /// + public sealed class HealthMonitor + { + private Dictionary _componentHealth = new(); + private OverallHealth _overallHealth = new(); + private readonly object _lock = new(); + + /// + /// Gets overall health status + /// + public HealthStatus OverallStatus + { + get + { + lock (_lock) + { + return _overallHealth.Status; + } + } + } + + /// + /// Gets health status of a component + /// + public ComponentHealth? GetComponentHealth(string name) + { + lock (_lock) + { + return _componentHealth.TryGetValue(name, out var health) ? health : null; + } + } + + /// + /// Updates health of a component + /// + 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(); + } + } + + /// + /// Records an error event + /// + 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(); + } + } + + /// + /// Recovers a component + /// + 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(); + } + } + } + + /// + /// Gets all component health statuses + /// + public IReadOnlyDictionary GetAllComponentHealth() + { + lock (_lock) + { + return new Dictionary(_componentHealth); + } + } + + /// + /// Gets health snapshot + /// + public HealthSnapshot GetSnapshot() + { + lock (_lock) + { + return new HealthSnapshot + { + OverallStatus = _overallHealth.Status, + OverallMessage = _overallHealth.Message, + CapturedAt = DateTime.UtcNow, + Components = new Dictionary(_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; + } + } + } + + /// + /// Health status enumeration + /// + public enum HealthStatus + { + Unknown = 0, + Healthy = 1, + Degraded = 2, + Unhealthy = 3 + } + + /// + /// Health status of a single component + /// + 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; } + } + + /// + /// Overall health information + /// + public sealed class OverallHealth + { + public HealthStatus Status { get; set; } = HealthStatus.Unknown; + public string? Message { get; set; } + } + + /// + /// Snapshot of health status + /// + public sealed class HealthSnapshot + { + public HealthStatus OverallStatus { get; set; } + public string? OverallMessage { get; set; } + public DateTime CapturedAt { get; set; } + public Dictionary 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}"; + } + } +} diff --git a/EonaCat.LogStack/Telemetry/TelemetryAggregator.cs b/EonaCat.LogStack/Telemetry/TelemetryAggregator.cs new file mode 100644 index 0000000..c829e6b --- /dev/null +++ b/EonaCat.LogStack/Telemetry/TelemetryAggregator.cs @@ -0,0 +1,293 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; + +namespace EonaCat.LogStack.Telemetry +{ + /// + /// Aggregates telemetry data from multiple sources and flows. + /// Correlates metrics and provides unified telemetry snapshots. + /// + public sealed class TelemetryAggregator + { + private readonly ConcurrentDictionary _metrics = new(); + private readonly ConcurrentDictionary _gauges = new(); + private readonly List _events = new(); + private readonly HealthMonitor _healthMonitor = new(); + private readonly object _eventsLock = new(); + private long _eventCount; + private DateTime _startTime = DateTime.UtcNow; + + /// + /// Gets the health monitor + /// + public HealthMonitor HealthMonitor => _healthMonitor; + + /// + /// Records a counter metric + /// + public void RecordCounter(string name, long value = 1, Dictionary? 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++; + } + + /// + /// Records a gauge metric + /// + public void RecordGauge(string name, double value, Dictionary? tags = null) + { + var key = $"{name}:{TagsKey(tags)}"; + _gauges[key] = new GaugeValue { Name = name, Value = value, Tags = tags ?? new(), RecordedAt = DateTime.UtcNow }; + } + + /// + /// Records a histogram metric + /// + public void RecordHistogram(string name, double value, Dictionary? 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); + } + } + + /// + /// Records a telemetry event + /// + public void RecordEvent(TelemetryEvent evt) + { + lock (_eventsLock) + { + _events.Add(evt); + if (_events.Count > 10000) // Keep last 10k events + { + _events.RemoveRange(0, 1000); + } + } + } + + /// + /// Gets metric value + /// + public MetricValue? GetMetric(string name) + { + foreach (var kvp in _metrics.Where(kvp => kvp.Value.Name == name)) + { + return kvp.Value; + } + return null; + } + + /// + /// Gets gauge value + /// + public GaugeValue? GetGauge(string name) + { + foreach (var kvp in _gauges.Where(kvp => kvp.Value.Name == name)) + { + return kvp.Value; + } + return null; + } + + /// + /// Gets all current metrics + /// + public IReadOnlyCollection GetAllMetrics() + { + return _metrics.Values.ToList().AsReadOnly(); + } + + /// + /// Gets all current gauges + /// + public IReadOnlyCollection GetAllGauges() + { + return _gauges.Values.ToList().AsReadOnly(); + } + + /// + /// Gets recent events + /// + public IReadOnlyList GetRecentEvents(int count = 100) + { + lock (_eventsLock) + { + var start = Math.Max(0, _events.Count - count); + return _events.Skip(start).ToList().AsReadOnly(); + } + } + + /// + /// Gets aggregated telemetry snapshot + /// + 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 + }; + } + + /// + /// Exports metrics in prometheus-like format + /// + public string ExportPrometheusFormat() + { + var lines = new List { "# 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); + } + + /// + /// Resets all metrics + /// + public void Reset() + { + _metrics.Clear(); + _gauges.Clear(); + lock (_eventsLock) + { + _events.Clear(); + } + _eventCount = 0; + _startTime = DateTime.UtcNow; + } + + private static string TagsKey(Dictionary? tags) + { + if (tags == null || tags.Count == 0) return ""; + return string.Join(",", tags.OrderBy(t => t.Key).Select(t => $"{t.Key}={t.Value}")); + } + } + + /// + /// Represents a metric value + /// + public class MetricValue + { + public string Name { get; set; } + public long Value { get; set; } + public Dictionary Tags { get; set; } = new(); + } + + /// + /// Represents a gauge value + /// + public sealed class GaugeValue + { + public string Name { get; set; } + public double Value { get; set; } + public Dictionary Tags { get; set; } = new(); + public DateTime RecordedAt { get; set; } + } + + /// + /// Histogram metric value + /// + public sealed class HistogramValue : MetricValue + { + private readonly List _values = new(); + public IReadOnlyList 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]; + } + } + + /// + /// Aggregated telemetry snapshot + /// + 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 RecordedMetrics { get; set; } = new(); + public List 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}"; + } + } +} diff --git a/README.md b/README.md index d8014e8..c37949b 100644 --- a/README.md +++ b/README.md @@ -31,26 +31,398 @@ It features a rich fluent API for routing log events to dozens of destinations - - **Lazy initialization** - Flows are only initialized when first used, reducing startup overhead. -### Telemetry & Observability +## Telemetry & Observability -EonaCat.LogStack includes built-in telemetry primitives for production monitoring: +EonaCat.LogStack includes **built-in, zero-dependency telemetry** for comprehensive production monitoring: -- **Metrics registry** - Low-overhead counters and snapshots for logging throughput. -- **Telemetry snapshots** - Capture service name, environment, runtime/process metrics, log counters and trace activity. -- **Runtime health data** - Export operational status for dashboards and monitoring systems. -- **Tracing support** - Activity/span integration for distributed request correlation. -- **Custom exporters** - Build exporters around telemetry snapshots for your monitoring backend. +### Enhanced Telemetry System + +#### Metrics Collection & Aggregation + +Track logging operations with built-in metrics: -Example: ```csharp -var options = new TelemetryOptions -{ - Enabled = true, - IncludeRuntimeMetrics = true, - IncludeTraceMetrics = true -}; +var aggregator = new EonaCat.LogStack.Telemetry.TelemetryAggregator(); + +// Record counters +aggregator.RecordCounter("logs_logged", 1); +aggregator.RecordCounter("logs_dropped", 1); + +// Record gauges (point-in-time measurements) +aggregator.RecordGauge("memory_usage_mb", GC.GetTotalMemory(false) / 1024 / 1024); + +// Record histograms (with percentiles) +aggregator.RecordHistogram("request_duration_ms", 42); + +// Get snapshot +var snapshot = aggregator.GetSnapshot(); +Console.WriteLine($"Uptime: {snapshot.Uptime.TotalSeconds}s"); +Console.WriteLine($"Total Events: {snapshot.TotalEvents}"); +Console.WriteLine($"Logs Logged: {snapshot.LogsLogged}"); +Console.WriteLine($"Logs Dropped: {snapshot.LogsDropped}"); ``` +#### Health Monitoring + +Track component health and detect degradation: + +```csharp +var health = new EonaCat.LogStack.Telemetry.HealthMonitor(); + +// Update component health +health.UpdateComponentHealth("EmailFlow", HealthStatus.Healthy); +health.UpdateComponentHealth("ElasticsearchFlow", HealthStatus.Degraded, "High latency detected"); + +// Record errors automatically impact health +health.RecordError("DatabaseFlow", new TimeoutException("Connection timeout")); + +// Get health snapshot +var snapshot = health.GetSnapshot(); +Console.WriteLine($"Overall Status: {snapshot.OverallStatus}"); +Console.WriteLine($"Healthy: {snapshot.HealthyComponents}"); +Console.WriteLine($"Degraded: {snapshot.DegradedComponents}"); +console.WriteLine($"Unhealthy: {snapshot.UnhealthyComponents}"); +``` + +#### Distributed Tracing + +Enable end-to-end request tracing across async boundaries: + +```csharp +// Enable tracing in DI +services.AddEonaCatTracing(); + +// Use trace context manager +var traceId = TraceContextManager.GetTraceId(); +TraceContextManager.SetCorrelationId("order-123"); +TraceContextManager.AddBaggage("UserId", "user-456"); + +// Create spans for operations +var spanFactory = sp.GetRequiredService(); + +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(); + +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(); + +// 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(); +``` + +#### 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 ProcessOrderAsync(string orderId) + { + using var span = _spans.CreateSpanScope("ProcessOrder"); + span.SetAttribute("orderId", orderId); + + try + { + _logger.Information("Processing order", + ("OrderId", orderId), + ("Timestamp", DateTime.UtcNow)); + + _telemetry.RecordCounter("orders.processed", 1); + + // ... processing logic ... + + _telemetry.RecordGauge("order.revenue", 99.99); + return order; + } + catch (Exception ex) + { + _telemetry.RecordCounter("orders.failed", 1); + span.RecordException(ex); + _logger.Error(ex, "Order processing failed", ("OrderId", orderId)); + throw; + } + } +} +``` + +### Telemetry Export + +Export metrics to external systems: + +```csharp +// Prometheus-format export +var prometheus = aggregator.ExportPrometheusFormat(); +File.WriteAllText("metrics.txt", prometheus); + +// Custom export +var snapshot = aggregator.GetSnapshot(); +await httpClient.PostAsJsonAsync( + "https://monitoring.example.com/metrics", + new + { + snapshot.Uptime, + snapshot.TotalEvents, + snapshot.HealthStatus, + Metrics = snapshot.RecordedMetrics + }); +``` + + + ## Supported Targets - .NET Standard 2.1