190 lines
7.0 KiB
C#
190 lines
7.0 KiB
C#
using EonaCat.LogStack.Core;
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
|
|
namespace EonaCat.LogStack.Flows
|
|
{
|
|
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
|
|
|
/// <summary>
|
|
/// A flow that aggregates log events and periodically emits summary logs.
|
|
/// Groups logs by category or level and provides statistics.
|
|
/// </summary>
|
|
public sealed class AggregationFlow : FlowBase
|
|
{
|
|
private readonly IFlow _innerFlow;
|
|
private readonly TimeSpan _aggregationInterval;
|
|
private readonly Dictionary<string, AggregationStats> _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<string, AggregationStats>();
|
|
_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<string> Messages { get; } = new HashSet<string>();
|
|
}
|
|
|
|
public override async Task<WriteResult> 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<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> 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<string, AggregationStats> currentStats;
|
|
lock (_statsLock)
|
|
{
|
|
if (_stats.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
currentStats = new Dictionary<string, AggregationStats>(_stats);
|
|
_stats.Clear();
|
|
}
|
|
|
|
var summaryMessages = new List<string>();
|
|
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<string, object>
|
|
{
|
|
{ "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);
|
|
}
|
|
}
|
|
}
|