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. /// /// A flow that aggregates log events and periodically emits summary logs. /// Groups logs by category or level and provides statistics. /// public sealed class AggregationFlow : FlowBase { private readonly IFlow _innerFlow; private readonly TimeSpan _aggregationInterval; private readonly Dictionary _stats; private readonly Timer _flushTimer; private readonly object _statsLock = new(); private bool _disposed; public AggregationFlow( IFlow innerFlow, int aggregationIntervalMs = 60000, LogLevel minimumLevel = LogLevel.Trace) : base(innerFlow?.Name + "_Aggregated" ?? "AggregationFlow", minimumLevel) { _innerFlow = innerFlow ?? throw new ArgumentNullException(nameof(innerFlow)); _aggregationInterval = TimeSpan.FromMilliseconds(aggregationIntervalMs > 0 ? aggregationIntervalMs : 60000); _stats = new Dictionary(); _flushTimer = new Timer(FlushAggregationCallback, null, _aggregationInterval, _aggregationInterval); } private class AggregationStats { public int Count { get; set; } public int ErrorCount { get; set; } public int WarningCount { get; set; } public DateTime FirstOccurrence { get; set; } public DateTime LastOccurrence { get; set; } public HashSet Messages { get; } = new HashSet(); } public override async Task BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) { if (!IsEnabled || !IsLogLevelEnabled(logEvent)) { return WriteResult.LevelFiltered; } lock (_statsLock) { var key = $"{logEvent.Category}_{logEvent.Level}"; if (!_stats.TryGetValue(key, out var stats)) { stats = new AggregationStats { FirstOccurrence = DateTime.UtcNow }; _stats[key] = stats; } stats.Count++; stats.LastOccurrence = DateTime.UtcNow; if (logEvent.Level >= LogLevel.Error) { stats.ErrorCount++; } else if (logEvent.Level == LogLevel.Warning) { stats.WarningCount++; } var messageStr = logEvent.Message.ToString(); if (!string.IsNullOrEmpty(messageStr) && stats.Messages.Count < 10) { stats.Messages.Add(messageStr); } } Interlocked.Increment(ref BlastedCount); return WriteResult.Success; } public override async Task BlastBatchAsync(ReadOnlyMemory logEvents, CancellationToken cancellationToken = default) { if (!IsEnabled) { return WriteResult.FlowDisabled; } var eventsArray = logEvents.ToArray(); foreach (var logEvent in eventsArray) { if (IsLogLevelEnabled(logEvent)) { await BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); } } return WriteResult.Success; } public override async Task FlushAsync(CancellationToken cancellationToken = default) { await EmitAggregationSummaryAsync(cancellationToken).ConfigureAwait(false); } private async void FlushAggregationCallback(object? state) { if (!_disposed) { await EmitAggregationSummaryAsync(CancellationToken.None).ConfigureAwait(false); } } private async Task EmitAggregationSummaryAsync(CancellationToken cancellationToken) { Dictionary currentStats; lock (_statsLock) { if (_stats.Count == 0) { return; } currentStats = new Dictionary(_stats); _stats.Clear(); } var summaryMessages = new List(); summaryMessages.Add($"=== Log Aggregation Summary (Period: {_aggregationInterval.TotalSeconds:F0}s) ==="); foreach (var kvp in currentStats.OrderBy(x => x.Key)) { var key = kvp.Key; var stats = kvp.Value; var duration = (stats.LastOccurrence - stats.FirstOccurrence).TotalSeconds; summaryMessages.Add( $"Category-Level: {key} | Count: {stats.Count} | " + $"Errors: {stats.ErrorCount} | Warnings: {stats.WarningCount} | " + $"Duration: {duration:F2}s"); if (stats.Messages.Count > 0) { summaryMessages.Add($" Sample messages: {string.Join("; ", stats.Messages.Take(3))}"); } } var summaryMessage = string.Join(Environment.NewLine, summaryMessages); var summaryEvent = new LogEvent { Message = summaryMessage.AsMemory(), Category = "AggregationFlow", Level = LogLevel.Information, Timestamp = LogEvent.CreateTimestamp(DateTime.UtcNow), Exception = null, Properties = new Dictionary { { "AggregationType", "Summary" }, { "StatsCount", currentStats.Count } } }; try { await _innerFlow.BlastAsync(summaryEvent, cancellationToken).ConfigureAwait(false); } catch (Exception ex) { System.Diagnostics.Debug.WriteLine($"AggregationFlow summary emit error: {ex.Message}"); } } public override async ValueTask DisposeAsync() { _disposed = true; _flushTimer?.Dispose(); await EmitAggregationSummaryAsync(default).ConfigureAwait(false); await _innerFlow.DisposeAsync().ConfigureAwait(false); await base.DisposeAsync().ConfigureAwait(false); } } }