Updated
This commit is contained in:
@@ -110,4 +110,8 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
<PackagePath>\</PackagePath>
|
<PackagePath>\</PackagePath>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Folder Include="EonaCat.LogStack.Test\Features\" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
@@ -0,0 +1,329 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using EonaCat.LogStack.Logging;
|
||||||
|
using EonaCat.LogStack.Features;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Extensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Fluent extension methods for integrating advanced logging features.
|
||||||
|
/// </summary>
|
||||||
|
public static class LoggerFeatureExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Registers LogSearchEngine in the dependency injection container.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddLogSearchEngine(this IServiceCollection services, int maxIndexSize = 10000)
|
||||||
|
{
|
||||||
|
services.AddSingleton(new LogSearchEngine(maxIndexSize));
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers AlertingEngine in the dependency injection container.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddAlertingEngine(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton(new AlertingEngine());
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers LogReplayEngine in the dependency injection container.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddLogReplayEngine(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton(new LogReplayEngine());
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers ConfigurationValidator and ConfigurationHotReloadManager in the DI container.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddConfigurationValidation(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton(new ConfigurationValidator());
|
||||||
|
services.AddSingleton(new ConfigurationHotReloadManager());
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers CorrelationDashboardHelper in the dependency injection container.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddCorrelationDashboardHelper(this IServiceCollection services)
|
||||||
|
{
|
||||||
|
services.AddSingleton(new CorrelationDashboardHelper());
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers all logging features at once.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceCollection AddAllEonaCatLoggingFeatures(this IServiceCollection services,
|
||||||
|
LogFeaturesConfiguration config = null)
|
||||||
|
{
|
||||||
|
config ??= new LogFeaturesConfiguration();
|
||||||
|
|
||||||
|
services.AddLogSearchEngine(config.LogSearchMaxSize);
|
||||||
|
services.AddAlertingEngine();
|
||||||
|
services.AddLogReplayEngine();
|
||||||
|
services.AddConfigurationValidation();
|
||||||
|
services.AddCorrelationDashboardHelper();
|
||||||
|
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets LogSearchEngine from service provider and performs a search.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceProvider WithLogSearch(this IServiceProvider provider,
|
||||||
|
Action<LogSearchEngine> configureSearch)
|
||||||
|
{
|
||||||
|
var engine = provider.GetService<LogSearchEngine>();
|
||||||
|
if (engine != null)
|
||||||
|
{
|
||||||
|
configureSearch(engine);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets AlertingEngine from service provider and configures alerting.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceProvider WithAlerting(this IServiceProvider provider,
|
||||||
|
Action<AlertingEngine> configureAlerting)
|
||||||
|
{
|
||||||
|
var engine = provider.GetService<AlertingEngine>();
|
||||||
|
if (engine != null)
|
||||||
|
{
|
||||||
|
configureAlerting(engine);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets LogReplayEngine from service provider and performs log operations.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceProvider WithLogReplay(this IServiceProvider provider,
|
||||||
|
Action<LogReplayEngine> configureReplay)
|
||||||
|
{
|
||||||
|
var engine = provider.GetService<LogReplayEngine>();
|
||||||
|
if (engine != null)
|
||||||
|
{
|
||||||
|
configureReplay(engine);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates logger configuration on startup.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceProvider ValidateLoggerConfiguration(this IServiceProvider provider, ILogger logger)
|
||||||
|
{
|
||||||
|
var validator = provider.GetService<ConfigurationValidator>();
|
||||||
|
if (validator != null)
|
||||||
|
{
|
||||||
|
var report = validator.Validate(logger);
|
||||||
|
if (!report.IsValid)
|
||||||
|
{
|
||||||
|
var errors = report.GetErrors();
|
||||||
|
if (errors.Any())
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Logger configuration validation failed:\n{report.ToString()}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables hot-reload support with a handler callback.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceProvider EnableConfigurationHotReload(this IServiceProvider provider,
|
||||||
|
Action<ConfigurationChangeNotification> onConfigChanged)
|
||||||
|
{
|
||||||
|
var reloadManager = provider.GetService<ConfigurationHotReloadManager>();
|
||||||
|
if (reloadManager != null)
|
||||||
|
{
|
||||||
|
reloadManager.EnableHotReload(onConfigChanged);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets CorrelationDashboardHelper from service provider and records activity.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceProvider RecordCorrelatedActivity(this IServiceProvider provider,
|
||||||
|
string correlationId, string activityName, string serviceName, ActivityStatus status,
|
||||||
|
double durationMs, Exception exception = null)
|
||||||
|
{
|
||||||
|
var helper = provider.GetService<CorrelationDashboardHelper>();
|
||||||
|
if (helper != null)
|
||||||
|
{
|
||||||
|
helper.RecordActivity(correlationId, activityName, serviceName, status, durationMs, exception);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets tracing dashboard statistics from service provider.
|
||||||
|
/// </summary>
|
||||||
|
public static DashboardStatistics GetTracingStatistics(this IServiceProvider provider)
|
||||||
|
{
|
||||||
|
var helper = provider.GetService<CorrelationDashboardHelper>();
|
||||||
|
return helper?.GetStatistics() ?? new DashboardStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets search statistics from service provider.
|
||||||
|
/// </summary>
|
||||||
|
public static LogSearchStatistics GetSearchStatistics(this IServiceProvider provider)
|
||||||
|
{
|
||||||
|
var engine = provider.GetService<LogSearchEngine>();
|
||||||
|
return engine?.GetStatistics() ?? new LogSearchStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets alerting statistics from service provider.
|
||||||
|
/// </summary>
|
||||||
|
public static AlertingStatistics GetAlertingStatistics(this IServiceProvider provider)
|
||||||
|
{
|
||||||
|
var engine = provider.GetService<AlertingEngine>();
|
||||||
|
return engine?.GetStatistics() ?? new AlertingStatistics();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Performs a complex log search using fluent API.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<LogSearchEntry> SearchLogs(this IServiceProvider provider,
|
||||||
|
LogLevel? level = null, string keyword = null, string loggerName = null,
|
||||||
|
string correlationId = null, DateTime? fromUtc = null, DateTime? toUtc = null,
|
||||||
|
int limit = 0)
|
||||||
|
{
|
||||||
|
var engine = provider.GetService<LogSearchEngine>();
|
||||||
|
if (engine == null)
|
||||||
|
return Enumerable.Empty<LogSearchEntry>();
|
||||||
|
|
||||||
|
var query = new LogSearchQuery
|
||||||
|
{
|
||||||
|
Keyword = keyword,
|
||||||
|
Level = level,
|
||||||
|
LoggerName = loggerName,
|
||||||
|
CorrelationId = correlationId,
|
||||||
|
FromUtc = fromUtc,
|
||||||
|
ToUtc = toUtc,
|
||||||
|
Limit = limit
|
||||||
|
};
|
||||||
|
|
||||||
|
return engine.Search(query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds an alert rule using fluent configuration.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceProvider AddAlertRule(this IServiceProvider provider,
|
||||||
|
string ruleName, Action<AlertRuleBuilder> configureRule)
|
||||||
|
{
|
||||||
|
var engine = provider.GetService<AlertingEngine>();
|
||||||
|
if (engine != null)
|
||||||
|
{
|
||||||
|
engine.AddRule(ruleName, configureRule);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exports captured logs to JSON.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceProvider ExportLogsToJson(this IServiceProvider provider, string filePath)
|
||||||
|
{
|
||||||
|
var engine = provider.GetService<LogReplayEngine>();
|
||||||
|
if (engine != null)
|
||||||
|
{
|
||||||
|
engine.ExportToJson(filePath);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Imports captured logs from JSON.
|
||||||
|
/// </summary>
|
||||||
|
public static IServiceProvider ImportLogsFromJson(this IServiceProvider provider, string filePath)
|
||||||
|
{
|
||||||
|
var engine = provider.GetService<LogReplayEngine>();
|
||||||
|
if (engine != null)
|
||||||
|
{
|
||||||
|
engine.ImportFromJson(filePath);
|
||||||
|
}
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a correlation trace for visualization.
|
||||||
|
/// </summary>
|
||||||
|
public static GanttChartData GetTraceAsGanttChart(this IServiceProvider provider, string correlationId)
|
||||||
|
{
|
||||||
|
var helper = provider.GetService<CorrelationDashboardHelper>();
|
||||||
|
return helper?.GetAsGanttChart(correlationId) ?? null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the service dependency map for the system.
|
||||||
|
/// </summary>
|
||||||
|
public static ServiceDependencyMap GetServiceDependencies(this IServiceProvider provider)
|
||||||
|
{
|
||||||
|
var helper = provider.GetService<CorrelationDashboardHelper>();
|
||||||
|
return helper?.GetDependencyMap() ?? new ServiceDependencyMap();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets critical paths (slow traces) from the system.
|
||||||
|
/// </summary>
|
||||||
|
public static IEnumerable<CriticalPath> GetCriticalPaths(this IServiceProvider provider, int topCount = 10)
|
||||||
|
{
|
||||||
|
var helper = provider.GetService<CorrelationDashboardHelper>();
|
||||||
|
return helper?.FindCriticalPaths(topCount) ?? Enumerable.Empty<CriticalPath>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Configuration for logging features.
|
||||||
|
/// </summary>
|
||||||
|
public class LogFeaturesConfiguration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum number of logs to keep in the search index.
|
||||||
|
/// </summary>
|
||||||
|
public int LogSearchMaxSize { get; set; } = 10000;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to enable search indexing by default.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableSearchByDefault { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to enable alerting by default.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableAlertingByDefault { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to enable log capture for replay by default.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableReplayByDefault { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether to enable tracing dashboard by default.
|
||||||
|
/// </summary>
|
||||||
|
public bool EnableTracingByDefault { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Maximum age of trace data to keep (in hours).
|
||||||
|
/// </summary>
|
||||||
|
public int TraceDataMaxAgeHours { get; set; } = 24;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,347 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Features
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Rule-based alerting engine for proactive log monitoring and notifications.
|
||||||
|
/// Monitors log streams and triggers alerts based on configurable conditions.
|
||||||
|
/// </summary>
|
||||||
|
public class AlertingEngine
|
||||||
|
{
|
||||||
|
private readonly List<AlertRule> _rules = new();
|
||||||
|
private readonly object _lockObject = new();
|
||||||
|
private bool _isEnabled = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event fired when an alert is triggered.
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<AlertTriggeredEventArgs> AlertTriggered;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables or disables the alerting engine.
|
||||||
|
/// </summary>
|
||||||
|
public void SetEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
_isEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a new alert rule.
|
||||||
|
/// </summary>
|
||||||
|
public AlertRule AddRule(string ruleName, Action<AlertRuleBuilder> configureAction)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ruleName))
|
||||||
|
throw new ArgumentException("Rule name cannot be empty", nameof(ruleName));
|
||||||
|
|
||||||
|
var builder = new AlertRuleBuilder(ruleName);
|
||||||
|
configureAction(builder);
|
||||||
|
var rule = builder.Build();
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_rules.Add(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a rule by name.
|
||||||
|
/// </summary>
|
||||||
|
public bool RemoveRule(string ruleName)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
var rule = _rules.FirstOrDefault(r => r.Name.Equals(ruleName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
if (rule != null)
|
||||||
|
{
|
||||||
|
_rules.Remove(rule);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Evaluates a log event against all active rules.
|
||||||
|
/// </summary>
|
||||||
|
public void EvaluateLog(LogEvent logEvent, string formattedMessage)
|
||||||
|
{
|
||||||
|
if (!_isEnabled)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
foreach (var rule in _rules.Where(r => r.IsEnabled))
|
||||||
|
{
|
||||||
|
if (EvaluateRule(rule, logEvent, formattedMessage))
|
||||||
|
{
|
||||||
|
var alert = new Alert
|
||||||
|
{
|
||||||
|
RuleName = rule.Name,
|
||||||
|
RuleSeverity = rule.Severity,
|
||||||
|
TriggeredAt = DateTime.UtcNow,
|
||||||
|
LogLevel = logEvent.Level,
|
||||||
|
LoggerName = logEvent.Category ?? "Default",
|
||||||
|
Message = formattedMessage,
|
||||||
|
LogEvent = logEvent
|
||||||
|
};
|
||||||
|
|
||||||
|
AlertTriggered?.Invoke(this, new AlertTriggeredEventArgs { Alert = alert });
|
||||||
|
|
||||||
|
// Execute async action if configured
|
||||||
|
if (rule.OnAlertAsync != null)
|
||||||
|
{
|
||||||
|
_ = Task.Run(async () => await rule.OnAlertAsync(alert));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EvaluateRule(AlertRule rule, LogEvent logEvent, string formattedMessage)
|
||||||
|
{
|
||||||
|
// Level check
|
||||||
|
if (rule.MinimumLevel.HasValue && logEvent.Level < rule.MinimumLevel.Value)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Keyword check
|
||||||
|
if (!string.IsNullOrEmpty(rule.KeywordPattern))
|
||||||
|
{
|
||||||
|
if (!formattedMessage.Contains(rule.KeywordPattern, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Logger check
|
||||||
|
if (!string.IsNullOrEmpty(rule.LoggerNamePattern))
|
||||||
|
{
|
||||||
|
if (!(logEvent.Category?? "").Contains(rule.LoggerNamePattern, StringComparison.OrdinalIgnoreCase))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exception check
|
||||||
|
if (rule.OnlyWithExceptions && logEvent.Exception == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
// Custom predicate
|
||||||
|
if (rule.CustomPredicate != null)
|
||||||
|
{
|
||||||
|
if (!rule.CustomPredicate(logEvent, formattedMessage))
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check throttling
|
||||||
|
if (rule.ThrottleIntervalSeconds > 0)
|
||||||
|
{
|
||||||
|
var now = DateTime.UtcNow;
|
||||||
|
if (rule.LastTriggeredAt.HasValue)
|
||||||
|
{
|
||||||
|
var timeSinceLastTrigger = (now - rule.LastTriggeredAt.Value).TotalSeconds;
|
||||||
|
if (timeSinceLastTrigger < rule.ThrottleIntervalSeconds)
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
rule.LastTriggeredAt = now;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all active rules.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<AlertRule> GetRules()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _rules.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets alerting statistics.
|
||||||
|
/// </summary>
|
||||||
|
public AlertingStatistics GetStatistics()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return new AlertingStatistics
|
||||||
|
{
|
||||||
|
TotalRules = _rules.Count,
|
||||||
|
EnabledRules = _rules.Count(r => r.IsEnabled),
|
||||||
|
Rules = _rules.Select(r => new AlertRuleStats
|
||||||
|
{
|
||||||
|
Name = r.Name,
|
||||||
|
IsEnabled = r.IsEnabled,
|
||||||
|
TimesTriggered = r.TimesTriggered,
|
||||||
|
LastTriggered = r.LastTriggeredAt
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all alert statistics.
|
||||||
|
/// </summary>
|
||||||
|
public void ClearStatistics()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
foreach (var rule in _rules)
|
||||||
|
{
|
||||||
|
rule.TimesTriggered = 0;
|
||||||
|
rule.LastTriggeredAt = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents an alert rule.
|
||||||
|
/// </summary>
|
||||||
|
public class AlertRule
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool IsEnabled { get; set; } = true;
|
||||||
|
public AlertSeverity Severity { get; set; } = AlertSeverity.Warning;
|
||||||
|
public LogLevel? MinimumLevel { get; set; }
|
||||||
|
public string KeywordPattern { get; set; }
|
||||||
|
public string LoggerNamePattern { get; set; }
|
||||||
|
public bool OnlyWithExceptions { get; set; }
|
||||||
|
public int ThrottleIntervalSeconds { get; set; } = 0; // 0 = no throttling
|
||||||
|
public Func<LogEvent, string, bool> CustomPredicate { get; set; }
|
||||||
|
public Func<Alert, Task> OnAlertAsync { get; set; }
|
||||||
|
public int TimesTriggered { get; set; }
|
||||||
|
public DateTime? LastTriggeredAt { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Fluent builder for alert rules.
|
||||||
|
/// </summary>
|
||||||
|
public class AlertRuleBuilder
|
||||||
|
{
|
||||||
|
private readonly AlertRule _rule;
|
||||||
|
|
||||||
|
public AlertRuleBuilder(string ruleName)
|
||||||
|
{
|
||||||
|
_rule = new AlertRule { Name = ruleName };
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertRuleBuilder WithMinimumLevel(LogLevel level)
|
||||||
|
{
|
||||||
|
_rule.MinimumLevel = level;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertRuleBuilder WithKeywordPattern(string pattern)
|
||||||
|
{
|
||||||
|
_rule.KeywordPattern = pattern;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertRuleBuilder WithLoggerNamePattern(string pattern)
|
||||||
|
{
|
||||||
|
_rule.LoggerNamePattern = pattern;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertRuleBuilder OnlyWhenExceptionPresent()
|
||||||
|
{
|
||||||
|
_rule.OnlyWithExceptions = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertRuleBuilder WithSeverity(AlertSeverity severity)
|
||||||
|
{
|
||||||
|
_rule.Severity = severity;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertRuleBuilder WithThrottling(int intervalSeconds)
|
||||||
|
{
|
||||||
|
if (intervalSeconds < 0)
|
||||||
|
throw new ArgumentException("Throttle interval must be >= 0", nameof(intervalSeconds));
|
||||||
|
_rule.ThrottleIntervalSeconds = intervalSeconds;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertRuleBuilder WithCustomCondition(Func<LogEvent, string, bool> predicate)
|
||||||
|
{
|
||||||
|
_rule.CustomPredicate = predicate;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertRuleBuilder OnAlert(Func<Alert, Task> handler)
|
||||||
|
{
|
||||||
|
_rule.OnAlertAsync = handler;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertRuleBuilder Disable()
|
||||||
|
{
|
||||||
|
_rule.IsEnabled = false;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public AlertRule Build()
|
||||||
|
{
|
||||||
|
return _rule;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a triggered alert.
|
||||||
|
/// </summary>
|
||||||
|
public class Alert
|
||||||
|
{
|
||||||
|
public string RuleName { get; set; }
|
||||||
|
public AlertSeverity RuleSeverity { get; set; }
|
||||||
|
public DateTime TriggeredAt { get; set; }
|
||||||
|
public LogLevel LogLevel { get; set; }
|
||||||
|
public string LoggerName { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public LogEvent LogEvent { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Alert severity levels.
|
||||||
|
/// </summary>
|
||||||
|
public enum AlertSeverity
|
||||||
|
{
|
||||||
|
Info = 0,
|
||||||
|
Warning = 1,
|
||||||
|
Critical = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event args for alert triggered event.
|
||||||
|
/// </summary>
|
||||||
|
public class AlertTriggeredEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
public Alert Alert { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics about alerting engine.
|
||||||
|
/// </summary>
|
||||||
|
public class AlertingStatistics
|
||||||
|
{
|
||||||
|
public int TotalRules { get; set; }
|
||||||
|
public int EnabledRules { get; set; }
|
||||||
|
public List<AlertRuleStats> Rules { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics for a single alert rule.
|
||||||
|
/// </summary>
|
||||||
|
public class AlertRuleStats
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
public int TimesTriggered { get; set; }
|
||||||
|
public DateTime? LastTriggered { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,324 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using EonaCat.LogStack.Logging;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Features
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Validates logger configuration for common issues and best practices.
|
||||||
|
/// Provides detailed diagnostics to catch configuration errors early.
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigurationValidator
|
||||||
|
{
|
||||||
|
private readonly List<ConfigurationRule> _rules = new();
|
||||||
|
|
||||||
|
public ConfigurationValidator()
|
||||||
|
{
|
||||||
|
// Register default validation rules
|
||||||
|
InitializeDefaultRules();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeDefaultRules()
|
||||||
|
{
|
||||||
|
// Add common validation checks
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a custom validation rule.
|
||||||
|
/// </summary>
|
||||||
|
public void AddRule(string ruleName, Func<ConfigurationValidationContext, ValidationResult> validator)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ruleName))
|
||||||
|
throw new ArgumentException("Rule name cannot be empty", nameof(ruleName));
|
||||||
|
|
||||||
|
if (validator == null)
|
||||||
|
throw new ArgumentNullException(nameof(validator));
|
||||||
|
|
||||||
|
_rules.Add(new ConfigurationRule
|
||||||
|
{
|
||||||
|
Name = ruleName,
|
||||||
|
Validator = validator
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Validates the logger configuration.
|
||||||
|
/// </summary>
|
||||||
|
public ConfigurationValidationReport Validate(ILogger logger)
|
||||||
|
{
|
||||||
|
if (logger == null)
|
||||||
|
throw new ArgumentNullException(nameof(logger));
|
||||||
|
|
||||||
|
var context = new ConfigurationValidationContext { Logger = logger };
|
||||||
|
var results = new List<ValidationResult>();
|
||||||
|
|
||||||
|
foreach (var rule in _rules)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = rule.Validator(context);
|
||||||
|
if (result != null)
|
||||||
|
{
|
||||||
|
result.RuleName = rule.Name;
|
||||||
|
results.Add(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
results.Add(new ValidationResult
|
||||||
|
{
|
||||||
|
RuleName = rule.Name,
|
||||||
|
IsValid = false,
|
||||||
|
Message = $"Rule evaluation failed: {ex.Message}",
|
||||||
|
Severity = ValidationSeverity.Error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ConfigurationValidationReport
|
||||||
|
{
|
||||||
|
ValidatedAt = DateTime.UtcNow,
|
||||||
|
Results = results,
|
||||||
|
IsValid = !results.Any(r => !r.IsValid && r.Severity == ValidationSeverity.Error)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a standard validator with common checks.
|
||||||
|
/// </summary>
|
||||||
|
public static ConfigurationValidator CreateStandard()
|
||||||
|
{
|
||||||
|
var validator = new ConfigurationValidator();
|
||||||
|
|
||||||
|
validator.AddRule("HasOutputFlows", ctx =>
|
||||||
|
{
|
||||||
|
// Check if at least one output flow is configured
|
||||||
|
// This is a placeholder - actual implementation depends on logger structure
|
||||||
|
return new ValidationResult
|
||||||
|
{
|
||||||
|
IsValid = true,
|
||||||
|
Message = "At least one output flow should be configured",
|
||||||
|
Severity = ValidationSeverity.Warning
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
validator.AddRule("LogLevelConfiguration", ctx =>
|
||||||
|
{
|
||||||
|
return new ValidationResult
|
||||||
|
{
|
||||||
|
IsValid = true,
|
||||||
|
Message = "Log level is properly configured",
|
||||||
|
Severity = ValidationSeverity.Info
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
return validator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all registered rules.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<ConfigurationRule> GetRules() => _rules.AsReadOnly();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all rules.
|
||||||
|
/// </summary>
|
||||||
|
public void ClearRules() => _rules.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Context for configuration validation.
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigurationValidationContext
|
||||||
|
{
|
||||||
|
public ILogger Logger { get; set; }
|
||||||
|
public Dictionary<string, object> CustomData { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Result of a validation check.
|
||||||
|
/// </summary>
|
||||||
|
public class ValidationResult
|
||||||
|
{
|
||||||
|
public string RuleName { get; set; }
|
||||||
|
public bool IsValid { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public ValidationSeverity Severity { get; set; } = ValidationSeverity.Info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Severity levels for validation messages.
|
||||||
|
/// </summary>
|
||||||
|
public enum ValidationSeverity
|
||||||
|
{
|
||||||
|
Info = 0,
|
||||||
|
Warning = 1,
|
||||||
|
Error = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Complete validation report.
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigurationValidationReport
|
||||||
|
{
|
||||||
|
public DateTime ValidatedAt { get; set; }
|
||||||
|
public bool IsValid { get; set; }
|
||||||
|
public List<ValidationResult> Results { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all errors from the report.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<ValidationResult> GetErrors() =>
|
||||||
|
Results.Where(r => r.Severity == ValidationSeverity.Error);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all warnings from the report.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<ValidationResult> GetWarnings() =>
|
||||||
|
Results.Where(r => r.Severity == ValidationSeverity.Warning);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all info messages from the report.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<ValidationResult> GetInfos() =>
|
||||||
|
Results.Where(r => r.Severity == ValidationSeverity.Info);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a formatted report as string.
|
||||||
|
/// </summary>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
var lines = new List<string>
|
||||||
|
{
|
||||||
|
"=== Configuration Validation Report ===",
|
||||||
|
$"Validated: {ValidatedAt:O}",
|
||||||
|
$"Status: {(IsValid ? "VALID" : "INVALID")}",
|
||||||
|
"",
|
||||||
|
"Results:"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (Results.Count == 0)
|
||||||
|
{
|
||||||
|
lines.Add(" No validation results");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (var result in Results.OrderBy(r => r.Severity))
|
||||||
|
{
|
||||||
|
var icon = result.Severity switch
|
||||||
|
{
|
||||||
|
ValidationSeverity.Error => "[✗]",
|
||||||
|
ValidationSeverity.Warning => "[!]",
|
||||||
|
_ => "[✓]"
|
||||||
|
};
|
||||||
|
|
||||||
|
lines.Add($" {icon} [{result.RuleName}] {result.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lines.Add("=====================================");
|
||||||
|
|
||||||
|
return string.Join(Environment.NewLine, lines);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Internal rule configuration.
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigurationRule
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public Func<ConfigurationValidationContext, ValidationResult> Validator { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hot-reload manager for runtime configuration updates.
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigurationHotReloadManager
|
||||||
|
{
|
||||||
|
private readonly object _lockObject = new();
|
||||||
|
private bool _isEnabled = false;
|
||||||
|
private Action<ConfigurationChangeNotification> _changeHandler;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables hot-reload mode.
|
||||||
|
/// </summary>
|
||||||
|
public void EnableHotReload(Action<ConfigurationChangeNotification> onConfigChanged)
|
||||||
|
{
|
||||||
|
if (onConfigChanged == null)
|
||||||
|
throw new ArgumentNullException(nameof(onConfigChanged));
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_changeHandler = onConfigChanged;
|
||||||
|
_isEnabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disables hot-reload mode.
|
||||||
|
/// </summary>
|
||||||
|
public void DisableHotReload()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_isEnabled = false;
|
||||||
|
_changeHandler = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Notifies about a configuration change.
|
||||||
|
/// </summary>
|
||||||
|
public void NotifyConfigurationChange(string configKey, object oldValue, object newValue, string reason = null)
|
||||||
|
{
|
||||||
|
if (!_isEnabled || _changeHandler == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var notification = new ConfigurationChangeNotification
|
||||||
|
{
|
||||||
|
ConfigurationKey = configKey,
|
||||||
|
OldValue = oldValue,
|
||||||
|
NewValue = newValue,
|
||||||
|
Reason = reason,
|
||||||
|
ChangedAt = DateTime.UtcNow
|
||||||
|
};
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_changeHandler(notification);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Suppress exceptions from handlers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether hot-reload is enabled.
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEnabled
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _isEnabled;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a configuration change notification.
|
||||||
|
/// </summary>
|
||||||
|
public class ConfigurationChangeNotification
|
||||||
|
{
|
||||||
|
public string ConfigurationKey { get; set; }
|
||||||
|
public object OldValue { get; set; }
|
||||||
|
public object NewValue { get; set; }
|
||||||
|
public string Reason { get; set; }
|
||||||
|
public DateTime ChangedAt { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,385 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using EonaCat.LogStack;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Features
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Helper utilities for building distributed tracing and correlation dashboards.
|
||||||
|
/// Aggregates log data for visualization and analysis across service boundaries.
|
||||||
|
/// </summary>
|
||||||
|
public class CorrelationDashboardHelper
|
||||||
|
{
|
||||||
|
private readonly List<CorrelatedActivityTrace> _traces = new();
|
||||||
|
private readonly object _lockObject = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Records a correlated activity for tracing.
|
||||||
|
/// </summary>
|
||||||
|
public void RecordActivity(string correlationId, string activityName,
|
||||||
|
string serviceName, ActivityStatus status, double durationMs, Exception exception = null)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(correlationId))
|
||||||
|
throw new ArgumentNullException(nameof(correlationId));
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
var trace = _traces.FirstOrDefault(t =>
|
||||||
|
t.CorrelationId.Equals(correlationId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (trace == null)
|
||||||
|
{
|
||||||
|
trace = new CorrelatedActivityTrace
|
||||||
|
{
|
||||||
|
CorrelationId = correlationId,
|
||||||
|
StartTime = DateTime.UtcNow,
|
||||||
|
Activities = new List<ActivityRecord>()
|
||||||
|
};
|
||||||
|
_traces.Add(trace);
|
||||||
|
}
|
||||||
|
|
||||||
|
trace.Activities.Add(new ActivityRecord
|
||||||
|
{
|
||||||
|
Name = activityName,
|
||||||
|
Service = serviceName,
|
||||||
|
Status = status,
|
||||||
|
DurationMs = durationMs,
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
Exception = exception?.Message
|
||||||
|
});
|
||||||
|
|
||||||
|
trace.EndTime = DateTime.UtcNow;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a trace by correlation ID.
|
||||||
|
/// </summary>
|
||||||
|
public CorrelatedActivityTrace GetTrace(string correlationId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(correlationId))
|
||||||
|
return null;
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _traces.FirstOrDefault(t =>
|
||||||
|
t.CorrelationId.Equals(correlationId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all traces.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CorrelatedActivityTrace> GetAllTraces()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _traces.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets traces by service name.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CorrelatedActivityTrace> GetTracesByService(string serviceName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(serviceName))
|
||||||
|
return Enumerable.Empty<CorrelatedActivityTrace>();
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _traces
|
||||||
|
.Where(t => t.Activities.Any(a => a.Service.Equals(serviceName, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets traces with performance issues (slow activities).
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CorrelatedActivityTrace> GetSlowTraces(double thresholdMs = 1000)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _traces
|
||||||
|
.Where(t => t.TotalDurationMs > thresholdMs)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets traces with errors.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CorrelatedActivityTrace> GetFailedTraces()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _traces
|
||||||
|
.Where(t => t.Activities.Any(a => a.Status == ActivityStatus.Failed))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets dashboard statistics.
|
||||||
|
/// </summary>
|
||||||
|
public DashboardStatistics GetStatistics()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
if (_traces.Count == 0)
|
||||||
|
return new DashboardStatistics();
|
||||||
|
|
||||||
|
var allActivities = _traces.SelectMany(t => t.Activities).ToList();
|
||||||
|
var byStatus = allActivities.GroupBy(a => a.Status)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
var byService = allActivities.GroupBy(a => a.Service)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
|
||||||
|
return new DashboardStatistics
|
||||||
|
{
|
||||||
|
TotalTraces = _traces.Count,
|
||||||
|
TotalActivities = allActivities.Count,
|
||||||
|
SuccessfulTraces = _traces.Count(t => !t.HasErrors),
|
||||||
|
FailedTraces = _traces.Count(t => t.HasErrors),
|
||||||
|
AverageDurationMs = _traces.Average(t => t.TotalDurationMs),
|
||||||
|
MinDurationMs = _traces.Min(t => t.TotalDurationMs),
|
||||||
|
MaxDurationMs = _traces.Max(t => t.TotalDurationMs),
|
||||||
|
ServiceCount = byService.Count,
|
||||||
|
ActivityCountByStatus = byStatus,
|
||||||
|
ActivityCountByService = byService,
|
||||||
|
OldestTrace = _traces.Min(t => t.StartTime),
|
||||||
|
NewestTrace = _traces.Max(t => t.EndTime)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Generates a service dependency map.
|
||||||
|
/// </summary>
|
||||||
|
public ServiceDependencyMap GetDependencyMap()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
var dependencies = new Dictionary<string, HashSet<string>>();
|
||||||
|
|
||||||
|
foreach (var trace in _traces)
|
||||||
|
{
|
||||||
|
var services = trace.Activities.Select(a => a.Service).Distinct().ToList();
|
||||||
|
|
||||||
|
for (int i = 0; i < services.Count - 1; i++)
|
||||||
|
{
|
||||||
|
var from = services[i];
|
||||||
|
var to = services[i + 1];
|
||||||
|
|
||||||
|
if (!dependencies.ContainsKey(from))
|
||||||
|
dependencies[from] = new HashSet<string>();
|
||||||
|
|
||||||
|
dependencies[from].Add(to);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new ServiceDependencyMap
|
||||||
|
{
|
||||||
|
Dependencies = dependencies.ToDictionary(
|
||||||
|
kvp => kvp.Key,
|
||||||
|
kvp => kvp.Value.ToList())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds critical paths (slow traces).
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CriticalPath> FindCriticalPaths(int topCount = 10)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _traces
|
||||||
|
.OrderByDescending(t => t.TotalDurationMs)
|
||||||
|
.Take(topCount)
|
||||||
|
.Select(t => new CriticalPath
|
||||||
|
{
|
||||||
|
CorrelationId = t.CorrelationId,
|
||||||
|
TotalDurationMs = t.TotalDurationMs,
|
||||||
|
ServiceSequence = t.Activities.Select(a => a.Service).Distinct().ToList(),
|
||||||
|
SlowestActivity = t.Activities.OrderByDescending(a => a.DurationMs).FirstOrDefault(),
|
||||||
|
HasErrors = t.HasErrors
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all traces.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_traces.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes traces older than specified age.
|
||||||
|
/// </summary>
|
||||||
|
public int PruneOldTraces(TimeSpan maxAge)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
var cutoffTime = DateTime.UtcNow.Subtract(maxAge);
|
||||||
|
var removed = _traces.RemoveAll(t => t.EndTime < cutoffTime);
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets trace as Gantt chart data representation.
|
||||||
|
/// </summary>
|
||||||
|
public GanttChartData GetAsGanttChart(string correlationId)
|
||||||
|
{
|
||||||
|
var trace = GetTrace(correlationId);
|
||||||
|
if (trace == null)
|
||||||
|
return null;
|
||||||
|
|
||||||
|
var startTime = trace.Activities.Min(a => a.Timestamp);
|
||||||
|
var activities = trace.Activities
|
||||||
|
.Select(a => new GanttChartItem
|
||||||
|
{
|
||||||
|
Name = $"{a.Service}.{a.Name}",
|
||||||
|
Start = (a.Timestamp - startTime).TotalMilliseconds,
|
||||||
|
Duration = a.DurationMs,
|
||||||
|
Status = a.Status,
|
||||||
|
Color = GetStatusColor(a.Status)
|
||||||
|
})
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
return new GanttChartData
|
||||||
|
{
|
||||||
|
CorrelationId = correlationId,
|
||||||
|
Items = activities,
|
||||||
|
StartTime = startTime,
|
||||||
|
EndTime = trace.EndTime,
|
||||||
|
TotalDurationMs = trace.TotalDurationMs
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private string GetStatusColor(ActivityStatus status)
|
||||||
|
{
|
||||||
|
return status switch
|
||||||
|
{
|
||||||
|
ActivityStatus.Success => "#4CAF50",
|
||||||
|
ActivityStatus.Failed => "#F44336",
|
||||||
|
ActivityStatus.Warning => "#FF9800",
|
||||||
|
ActivityStatus.Pending => "#2196F3",
|
||||||
|
_ => "#9E9E9E"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a correlated activity trace for a request/transaction.
|
||||||
|
/// </summary>
|
||||||
|
public class CorrelatedActivityTrace
|
||||||
|
{
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public DateTime EndTime { get; set; }
|
||||||
|
public List<ActivityRecord> Activities { get; set; } = new();
|
||||||
|
|
||||||
|
public double TotalDurationMs => (EndTime - StartTime).TotalMilliseconds;
|
||||||
|
public bool HasErrors => Activities.Any(a => a.Status == ActivityStatus.Failed);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single activity in a trace.
|
||||||
|
/// </summary>
|
||||||
|
public class ActivityRecord
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public string Service { get; set; }
|
||||||
|
public ActivityStatus Status { get; set; }
|
||||||
|
public double DurationMs { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public string Exception { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Activity status enumeration.
|
||||||
|
/// </summary>
|
||||||
|
public enum ActivityStatus
|
||||||
|
{
|
||||||
|
Pending = 0,
|
||||||
|
Success = 1,
|
||||||
|
Failed = 2,
|
||||||
|
Warning = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Dashboard statistics.
|
||||||
|
/// </summary>
|
||||||
|
public class DashboardStatistics
|
||||||
|
{
|
||||||
|
public int TotalTraces { get; set; }
|
||||||
|
public int TotalActivities { get; set; }
|
||||||
|
public int SuccessfulTraces { get; set; }
|
||||||
|
public int FailedTraces { get; set; }
|
||||||
|
public double AverageDurationMs { get; set; }
|
||||||
|
public double MinDurationMs { get; set; }
|
||||||
|
public double MaxDurationMs { get; set; }
|
||||||
|
public int ServiceCount { get; set; }
|
||||||
|
public Dictionary<ActivityStatus, int> ActivityCountByStatus { get; set; } = new();
|
||||||
|
public Dictionary<string, int> ActivityCountByService { get; set; } = new();
|
||||||
|
public DateTime OldestTrace { get; set; }
|
||||||
|
public DateTime NewestTrace { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Service dependency relationships.
|
||||||
|
/// </summary>
|
||||||
|
public class ServiceDependencyMap
|
||||||
|
{
|
||||||
|
public Dictionary<string, List<string>> Dependencies { get; set; } = new();
|
||||||
|
|
||||||
|
public IEnumerable<string> GetAllServices() =>
|
||||||
|
Dependencies.Keys.Union(Dependencies.Values.SelectMany(v => v)).Distinct();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a critical path in request flow.
|
||||||
|
/// </summary>
|
||||||
|
public class CriticalPath
|
||||||
|
{
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
public double TotalDurationMs { get; set; }
|
||||||
|
public List<string> ServiceSequence { get; set; }
|
||||||
|
public ActivityRecord SlowestActivity { get; set; }
|
||||||
|
public bool HasErrors { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gantt chart representation of a trace.
|
||||||
|
/// </summary>
|
||||||
|
public class GanttChartData
|
||||||
|
{
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public DateTime EndTime { get; set; }
|
||||||
|
public double TotalDurationMs { get; set; }
|
||||||
|
public List<GanttChartItem> Items { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Item in a Gantt chart.
|
||||||
|
/// </summary>
|
||||||
|
public class GanttChartItem
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public double Start { get; set; }
|
||||||
|
public double Duration { get; set; }
|
||||||
|
public ActivityStatus Status { get; set; }
|
||||||
|
public string Color { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,338 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Text.Json.Serialization;
|
||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Features
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Captures and replays log sequences for debugging and analysis.
|
||||||
|
/// Useful for reproducing issues, testing scenarios, and performance analysis.
|
||||||
|
/// </summary>
|
||||||
|
public class LogReplayEngine
|
||||||
|
{
|
||||||
|
private readonly List<CapturedLogEntry> _capturedLogs = new();
|
||||||
|
private readonly object _lockObject = new();
|
||||||
|
private bool _isCapturing = false;
|
||||||
|
private string _currentCaptureSessionId = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Starts a new log capture session.
|
||||||
|
/// </summary>
|
||||||
|
public string StartCapture(string sessionName = null)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_currentCaptureSessionId = sessionName ?? $"session_{Guid.NewGuid().ToString("N").Substring(0, 8)}";
|
||||||
|
_isCapturing = true;
|
||||||
|
_capturedLogs.Clear();
|
||||||
|
return _currentCaptureSessionId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops capturing logs.
|
||||||
|
/// </summary>
|
||||||
|
public void StopCapture()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_isCapturing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Captures a log entry.
|
||||||
|
/// </summary>
|
||||||
|
public void CaptureLog(LogEvent logEvent, string formattedMessage)
|
||||||
|
{
|
||||||
|
if (!_isCapturing)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_capturedLogs.Add(new CapturedLogEntry
|
||||||
|
{
|
||||||
|
SessionId = _currentCaptureSessionId,
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
Level = logEvent.Level,
|
||||||
|
Logger = logEvent.Category ?? "Default",
|
||||||
|
Message = formattedMessage?.Trim() ?? string.Empty,
|
||||||
|
Exception = logEvent.Exception?.ToString() ?? string.Empty,
|
||||||
|
CorrelationId = logEvent.CustomData ?? string.Empty,
|
||||||
|
Properties = logEvent.Properties != null
|
||||||
|
? new Dictionary<string, string>(
|
||||||
|
logEvent.Properties.Where(x => x.Value != null).ToDictionary(
|
||||||
|
kvp => kvp.Key,
|
||||||
|
kvp => kvp.Value.ToString() ?? string.Empty))
|
||||||
|
: new Dictionary<string, string>()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets all captured logs in current session.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CapturedLogEntry> GetCapturedLogs()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _capturedLogs.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets captured logs filtered by criteria.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<CapturedLogEntry> GetCapturedLogs(LogLevel? minLevel = null,
|
||||||
|
string loggerFilter = null, DateTime? fromUtc = null, DateTime? toUtc = null)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
var results = _capturedLogs.AsEnumerable();
|
||||||
|
|
||||||
|
if (minLevel.HasValue)
|
||||||
|
results = results.Where(l => l.Level >= minLevel.Value);
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(loggerFilter))
|
||||||
|
results = results.Where(l =>
|
||||||
|
l.Logger.Contains(loggerFilter, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
if (fromUtc.HasValue)
|
||||||
|
results = results.Where(l => l.Timestamp >= fromUtc.Value);
|
||||||
|
|
||||||
|
if (toUtc.HasValue)
|
||||||
|
results = results.Where(l => l.Timestamp <= toUtc.Value);
|
||||||
|
|
||||||
|
return results.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Exports captured logs to JSON file.
|
||||||
|
/// </summary>
|
||||||
|
public void ExportToJson(string filePath)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(_capturedLogs, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true,
|
||||||
|
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||||
|
});
|
||||||
|
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Imports captured logs from JSON file.
|
||||||
|
/// </summary>
|
||||||
|
public void ImportFromJson(string filePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"File not found: {filePath}");
|
||||||
|
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
var logs = JsonSerializer.Deserialize<List<CapturedLogEntry>>(json);
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_capturedLogs.AddRange(logs ?? new List<CapturedLogEntry>());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Replays captured logs with optional delays.
|
||||||
|
/// </summary>
|
||||||
|
public void Replay(Action<CapturedLogEntry> onLogReplayed, bool respectTimings = false)
|
||||||
|
{
|
||||||
|
if (onLogReplayed == null)
|
||||||
|
throw new ArgumentNullException(nameof(onLogReplayed));
|
||||||
|
|
||||||
|
List<CapturedLogEntry> logsToReplay;
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
logsToReplay = _capturedLogs.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (logsToReplay.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
DateTime? previousTimestamp = null;
|
||||||
|
|
||||||
|
foreach (var log in logsToReplay)
|
||||||
|
{
|
||||||
|
if (respectTimings && previousTimestamp.HasValue)
|
||||||
|
{
|
||||||
|
var delay = (long)(log.Timestamp - previousTimestamp.Value).TotalMilliseconds;
|
||||||
|
if (delay > 0)
|
||||||
|
{
|
||||||
|
System.Threading.Thread.Sleep((int)Math.Min(delay, int.MaxValue));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onLogReplayed(log);
|
||||||
|
previousTimestamp = log.Timestamp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets statistics about captured logs.
|
||||||
|
/// </summary>
|
||||||
|
public LogReplayStatistics GetStatistics()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
if (_capturedLogs.Count == 0)
|
||||||
|
return new LogReplayStatistics();
|
||||||
|
|
||||||
|
var byLevel = _capturedLogs.GroupBy(l => l.Level)
|
||||||
|
.ToDictionary(g => g.Key, g => g.Count());
|
||||||
|
|
||||||
|
var duration = _capturedLogs.Max(l => l.Timestamp) - _capturedLogs.Min(l => l.Timestamp);
|
||||||
|
|
||||||
|
return new LogReplayStatistics
|
||||||
|
{
|
||||||
|
TotalCaptured = _capturedLogs.Count,
|
||||||
|
SessionId = _currentCaptureSessionId,
|
||||||
|
StartTime = _capturedLogs.First().Timestamp,
|
||||||
|
EndTime = _capturedLogs.Last().Timestamp,
|
||||||
|
DurationSeconds = duration.TotalSeconds,
|
||||||
|
LogsByLevel = byLevel,
|
||||||
|
UniqueLoggers = _capturedLogs.Select(l => l.Logger).Distinct().Count(),
|
||||||
|
HasExceptions = _capturedLogs.Any(l => !string.IsNullOrEmpty(l.Exception))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all captured logs.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_capturedLogs.Clear();
|
||||||
|
_isCapturing = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets detailed replay scenario for testing.
|
||||||
|
/// </summary>
|
||||||
|
public LogReplayScenario CreateScenario(string name)
|
||||||
|
{
|
||||||
|
return new LogReplayScenario
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
CreatedAt = DateTime.UtcNow,
|
||||||
|
Logs = GetCapturedLogs().ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Finds similar log patterns for diagnosis.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogPattern> FindPatterns(int minOccurrences = 2)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
var patterns = _capturedLogs
|
||||||
|
.GroupBy(l => l.Logger)
|
||||||
|
.SelectMany(g =>
|
||||||
|
g.GroupBy(l => l.Level)
|
||||||
|
.SelectMany(lg => new[]
|
||||||
|
{
|
||||||
|
new LogPattern
|
||||||
|
{
|
||||||
|
Logger = g.Key,
|
||||||
|
Level = lg.Key,
|
||||||
|
Count = lg.Count(),
|
||||||
|
FirstOccurrence = lg.Min(l => l.Timestamp),
|
||||||
|
LastOccurrence = lg.Max(l => l.Timestamp)
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
.Where(p => p.Count >= minOccurrences)
|
||||||
|
.OrderByDescending(p => p.Count);
|
||||||
|
|
||||||
|
return patterns.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single captured log entry.
|
||||||
|
/// </summary>
|
||||||
|
public class CapturedLogEntry
|
||||||
|
{
|
||||||
|
public string SessionId { get; set; }
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public LogLevel Level { get; set; }
|
||||||
|
public string Logger { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public string Exception { get; set; }
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
public Dictionary<string, string> Properties { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics about captured logs.
|
||||||
|
/// </summary>
|
||||||
|
public class LogReplayStatistics
|
||||||
|
{
|
||||||
|
public int TotalCaptured { get; set; }
|
||||||
|
public string SessionId { get; set; }
|
||||||
|
public DateTime StartTime { get; set; }
|
||||||
|
public DateTime EndTime { get; set; }
|
||||||
|
public double DurationSeconds { get; set; }
|
||||||
|
public Dictionary<LogLevel, int> LogsByLevel { get; set; } = new();
|
||||||
|
public int UniqueLoggers { get; set; }
|
||||||
|
public bool HasExceptions { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a pre-configured log replay scenario.
|
||||||
|
/// </summary>
|
||||||
|
public class LogReplayScenario
|
||||||
|
{
|
||||||
|
public string Name { get; set; }
|
||||||
|
public DateTime CreatedAt { get; set; }
|
||||||
|
public string Description { get; set; }
|
||||||
|
public List<CapturedLogEntry> Logs { get; set; } = new();
|
||||||
|
|
||||||
|
public void Save(string filePath)
|
||||||
|
{
|
||||||
|
var json = JsonSerializer.Serialize(this, new JsonSerializerOptions
|
||||||
|
{
|
||||||
|
WriteIndented = true
|
||||||
|
});
|
||||||
|
File.WriteAllText(filePath, json);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static LogReplayScenario Load(string filePath)
|
||||||
|
{
|
||||||
|
if (!File.Exists(filePath))
|
||||||
|
throw new FileNotFoundException($"Scenario file not found: {filePath}");
|
||||||
|
|
||||||
|
var json = File.ReadAllText(filePath);
|
||||||
|
return JsonSerializer.Deserialize<LogReplayScenario>(json);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a detected log pattern.
|
||||||
|
/// </summary>
|
||||||
|
public class LogPattern
|
||||||
|
{
|
||||||
|
public string Logger { get; set; }
|
||||||
|
public LogLevel Level { get; set; }
|
||||||
|
public int Count { get; set; }
|
||||||
|
public DateTime FirstOccurrence { get; set; }
|
||||||
|
public DateTime LastOccurrence { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,340 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Features
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// High-performance in-memory log search and query engine with fluent API.
|
||||||
|
/// Indexes logs for fast retrieval and supports complex queries.
|
||||||
|
/// </summary>
|
||||||
|
public class LogSearchEngine
|
||||||
|
{
|
||||||
|
private readonly List<LogSearchEntry> _logIndex = new();
|
||||||
|
private readonly int _maxEntries;
|
||||||
|
private readonly object _lockObject = new();
|
||||||
|
private bool _isEnabled = true;
|
||||||
|
|
||||||
|
public LogSearchEngine(int maxIndexSize = 10000)
|
||||||
|
{
|
||||||
|
_maxEntries = maxIndexSize > 0 ? maxIndexSize : 10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a log entry to the search index.
|
||||||
|
/// </summary>
|
||||||
|
public void IndexLog(LogEvent logEvent, string formattedMessage)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
// Maintain max size with FIFO removal
|
||||||
|
if (_logIndex.Count >= _maxEntries)
|
||||||
|
{
|
||||||
|
_logIndex.RemoveAt(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
_logIndex.Add(new LogSearchEntry
|
||||||
|
{
|
||||||
|
Timestamp = DateTime.UtcNow,
|
||||||
|
Level = logEvent.Level,
|
||||||
|
Logger = logEvent.Category ?? "Default",
|
||||||
|
Message = formattedMessage?.Trim() ?? string.Empty,
|
||||||
|
Exception = logEvent.Exception?.Message ?? string.Empty,
|
||||||
|
CorrelationId = logEvent.CustomData?? string.Empty,
|
||||||
|
Properties = logEvent.Properties != null
|
||||||
|
? new Dictionary<string, object>(logEvent.Properties.Where(x => x.Value != null).ToDictionary(x => x.Key, x => x.Value))
|
||||||
|
: new Dictionary<string, object>()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables or disables indexing.
|
||||||
|
/// </summary>
|
||||||
|
public void SetIndexingEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
_isEnabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all indexed logs.
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
_logIndex.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns the total number of indexed logs.
|
||||||
|
/// </summary>
|
||||||
|
public int Count
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _logIndex.Count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches logs by keyword (searches message and exception fields).
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogSearchEntry> SearchByKeyword(string keyword)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(keyword))
|
||||||
|
return Enumerable.Empty<LogSearchEntry>();
|
||||||
|
|
||||||
|
var lower = keyword.ToLowerInvariant();
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _logIndex
|
||||||
|
.Where(e => e.Message.Contains(lower, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
e.Exception.Contains(lower, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches logs by logger name (partial match).
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogSearchEntry> SearchByLogger(string loggerName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(loggerName))
|
||||||
|
return Enumerable.Empty<LogSearchEntry>();
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _logIndex
|
||||||
|
.Where(e => e.Logger.Contains(loggerName, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches logs by correlation ID.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogSearchEntry> SearchByCorrelationId(string correlationId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(correlationId))
|
||||||
|
return Enumerable.Empty<LogSearchEntry>();
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _logIndex
|
||||||
|
.Where(e => e.CorrelationId.Equals(correlationId, StringComparison.OrdinalIgnoreCase))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches logs by log level.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogSearchEntry> SearchByLevel(LogLevel level)
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _logIndex
|
||||||
|
.Where(e => e.Level == level)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches logs within a time range.
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogSearchEntry> SearchByTimeRange(DateTime fromUtc, DateTime toUtc)
|
||||||
|
{
|
||||||
|
if (fromUtc > toUtc)
|
||||||
|
throw new ArgumentException("fromUtc must be less than or equal to toUtc");
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _logIndex
|
||||||
|
.Where(e => e.Timestamp >= fromUtc && e.Timestamp <= toUtc)
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Searches logs with multiple criteria (AND logic).
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogSearchEntry> Search(LogSearchQuery query)
|
||||||
|
{
|
||||||
|
if (query == null)
|
||||||
|
return Enumerable.Empty<LogSearchEntry>();
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
var results = _logIndex.AsEnumerable();
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.Keyword))
|
||||||
|
{
|
||||||
|
var lower = query.Keyword.ToLowerInvariant();
|
||||||
|
results = results.Where(e =>
|
||||||
|
e.Message.Contains(lower, StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
e.Exception.Contains(lower, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.Level.HasValue)
|
||||||
|
{
|
||||||
|
results = results.Where(e => e.Level == query.Level.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.LoggerName))
|
||||||
|
{
|
||||||
|
results = results.Where(e =>
|
||||||
|
e.Logger.Contains(query.LoggerName, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(query.CorrelationId))
|
||||||
|
{
|
||||||
|
results = results.Where(e =>
|
||||||
|
e.CorrelationId.Equals(query.CorrelationId, StringComparison.OrdinalIgnoreCase));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.FromUtc.HasValue)
|
||||||
|
{
|
||||||
|
results = results.Where(e => e.Timestamp >= query.FromUtc.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.ToUtc.HasValue)
|
||||||
|
{
|
||||||
|
results = results.Where(e => e.Timestamp <= query.ToUtc.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply sorting
|
||||||
|
switch (query.SortBy)
|
||||||
|
{
|
||||||
|
case LogSearchSortBy.TimestampDescending:
|
||||||
|
results = results.OrderByDescending(e => e.Timestamp);
|
||||||
|
break;
|
||||||
|
case LogSearchSortBy.TimestampAscending:
|
||||||
|
results = results.OrderBy(e => e.Timestamp);
|
||||||
|
break;
|
||||||
|
case LogSearchSortBy.Level:
|
||||||
|
results = results.OrderBy(e => e.Level);
|
||||||
|
break;
|
||||||
|
case LogSearchSortBy.Logger:
|
||||||
|
results = results.OrderBy(e => e.Logger);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply limit
|
||||||
|
if (query.Limit > 0)
|
||||||
|
{
|
||||||
|
results = results.Take(query.Limit);
|
||||||
|
}
|
||||||
|
|
||||||
|
return results.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets recent logs (last N entries).
|
||||||
|
/// </summary>
|
||||||
|
public IEnumerable<LogSearchEntry> GetRecent(int count = 100)
|
||||||
|
{
|
||||||
|
if (count <= 0)
|
||||||
|
count = 100;
|
||||||
|
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
return _logIndex
|
||||||
|
.AsEnumerable()
|
||||||
|
.Skip(_logIndex.Count > count ? _logIndex.Count - count : 0)
|
||||||
|
.Reverse()
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets statistics about indexed logs.
|
||||||
|
/// </summary>
|
||||||
|
public LogSearchStatistics GetStatistics()
|
||||||
|
{
|
||||||
|
lock (_lockObject)
|
||||||
|
{
|
||||||
|
if (_logIndex.Count == 0)
|
||||||
|
return new LogSearchStatistics();
|
||||||
|
|
||||||
|
return new LogSearchStatistics
|
||||||
|
{
|
||||||
|
TotalLogs = _logIndex.Count,
|
||||||
|
Errors = _logIndex.Count(e => e.Level == LogLevel.Error),
|
||||||
|
Warnings = _logIndex.Count(e => e.Level == LogLevel.Warning),
|
||||||
|
Infos = _logIndex.Count(e => e.Level == LogLevel.Information),
|
||||||
|
OldestLog = _logIndex.First().Timestamp,
|
||||||
|
NewestLog = _logIndex.Last().Timestamp,
|
||||||
|
UniqueLooggers = _logIndex.Select(e => e.Logger).Distinct().Count(),
|
||||||
|
UniqueCorrelationIds = _logIndex
|
||||||
|
.Where(e => !string.IsNullOrEmpty(e.CorrelationId))
|
||||||
|
.Select(e => e.CorrelationId)
|
||||||
|
.Distinct()
|
||||||
|
.Count()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a single indexed log entry.
|
||||||
|
/// </summary>
|
||||||
|
public class LogSearchEntry
|
||||||
|
{
|
||||||
|
public DateTime Timestamp { get; set; }
|
||||||
|
public LogLevel Level { get; set; }
|
||||||
|
public string Logger { get; set; }
|
||||||
|
public string Message { get; set; }
|
||||||
|
public string Exception { get; set; }
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
public Dictionary<string, object> Properties { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Query builder for log searches.
|
||||||
|
/// </summary>
|
||||||
|
public class LogSearchQuery
|
||||||
|
{
|
||||||
|
public string Keyword { get; set; }
|
||||||
|
public LogLevel? Level { get; set; }
|
||||||
|
public string LoggerName { get; set; }
|
||||||
|
public string CorrelationId { get; set; }
|
||||||
|
public DateTime? FromUtc { get; set; }
|
||||||
|
public DateTime? ToUtc { get; set; }
|
||||||
|
public LogSearchSortBy SortBy { get; set; } = LogSearchSortBy.TimestampDescending;
|
||||||
|
public int Limit { get; set; } = 0; // 0 = no limit
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sort options for log searches.
|
||||||
|
/// </summary>
|
||||||
|
public enum LogSearchSortBy
|
||||||
|
{
|
||||||
|
None = 0,
|
||||||
|
TimestampAscending = 1,
|
||||||
|
TimestampDescending = 2,
|
||||||
|
Level = 3,
|
||||||
|
Logger = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Statistics about indexed logs.
|
||||||
|
/// </summary>
|
||||||
|
public class LogSearchStatistics
|
||||||
|
{
|
||||||
|
public int TotalLogs { get; set; }
|
||||||
|
public int Errors { get; set; }
|
||||||
|
public int Warnings { get; set; }
|
||||||
|
public int Infos { get; set; }
|
||||||
|
public DateTime OldestLog { get; set; }
|
||||||
|
public DateTime NewestLog { get; set; }
|
||||||
|
public int UniqueLooggers { get; set; }
|
||||||
|
public int UniqueCorrelationIds { get; set; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
using EonaCat.LogStack.Core;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
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 delegates log events to another logger instance.
|
||||||
|
/// This enables chaining multiple logger instances together, allowing logs to flow
|
||||||
|
/// through different logging backends or implementations.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DelegatingLoggerFlow : FlowBase
|
||||||
|
{
|
||||||
|
private readonly EonaCatLogStack _targetLogger;
|
||||||
|
private readonly Queue<LogEvent> _delegationQueue = new Queue<LogEvent>();
|
||||||
|
private readonly object _queueLock = new object();
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new delegating logger flow
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="targetLogger">The logger to delegate log events to</param>
|
||||||
|
/// <param name="minimumLevel">The minimum log level to process</param>
|
||||||
|
public DelegatingLoggerFlow(
|
||||||
|
EonaCatLogStack targetLogger,
|
||||||
|
LogLevel minimumLevel = LogLevel.Trace)
|
||||||
|
: base($"DelegatingFlow({targetLogger?.GetType().Name ?? "UnknownLogger"})", minimumLevel)
|
||||||
|
{
|
||||||
|
_targetLogger = targetLogger ?? throw new ArgumentNullException(nameof(targetLogger));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The target logger this flow delegates to
|
||||||
|
/// </summary>
|
||||||
|
public EonaCatLogStack TargetLogger => _targetLogger;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blasts (sends) a single log event to the target logger
|
||||||
|
/// </summary>
|
||||||
|
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref DroppedCount);
|
||||||
|
return WriteResult.LevelFiltered;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Queue the event for delegation with cancellation support
|
||||||
|
lock (_queueLock)
|
||||||
|
{
|
||||||
|
if (_disposed != 0)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref DroppedCount);
|
||||||
|
return WriteResult.Dropped;
|
||||||
|
}
|
||||||
|
|
||||||
|
_delegationQueue.Enqueue(logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delegate to the target logger's logging mechanism
|
||||||
|
// This is done asynchronously to prevent blocking
|
||||||
|
await Task.Run(() => DelegateLogEvent(logEvent), cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
Interlocked.Increment(ref BlastedCount);
|
||||||
|
return WriteResult.Success;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref DroppedCount);
|
||||||
|
System.Diagnostics.Debug.WriteLine($"DelegatingLoggerFlow error: {ex.Message}");
|
||||||
|
return WriteResult.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Blasts (sends) a batch of log events to the target logger
|
||||||
|
/// </summary>
|
||||||
|
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
Interlocked.Add(ref DroppedCount, logEvents.Length);
|
||||||
|
return WriteResult.LevelFiltered;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = WriteResult.Success;
|
||||||
|
var eventsArray = logEvents.ToArray();
|
||||||
|
|
||||||
|
foreach (var logEvent in eventsArray)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (IsLogLevelEnabled(logEvent))
|
||||||
|
{
|
||||||
|
lock (_queueLock)
|
||||||
|
{
|
||||||
|
if (_disposed == 0)
|
||||||
|
{
|
||||||
|
_delegationQueue.Enqueue(logEvent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.Run(() => DelegateLogEvent(logEvent), cancellationToken).ConfigureAwait(false);
|
||||||
|
Interlocked.Increment(ref BlastedCount);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref DroppedCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"DelegatingLoggerFlow batch error: {ex.Message}");
|
||||||
|
Interlocked.Increment(ref DroppedCount);
|
||||||
|
result = WriteResult.Failed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flushes any pending log events to the target logger
|
||||||
|
/// </summary>
|
||||||
|
public override async Task FlushAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
Queue<LogEvent> pendingEvents;
|
||||||
|
lock (_queueLock)
|
||||||
|
{
|
||||||
|
if (_delegationQueue.Count == 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
pendingEvents = new Queue<LogEvent>(_delegationQueue);
|
||||||
|
_delegationQueue.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process remaining events
|
||||||
|
foreach (var logEvent in pendingEvents)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
DelegateLogEvent(logEvent);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"DelegatingLoggerFlow flush error: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
await Task.CompletedTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets diagnostic information about this flow
|
||||||
|
/// </summary>
|
||||||
|
public override FlowDiagnostics GetDiagnostics()
|
||||||
|
{
|
||||||
|
var diagnostics = base.GetDiagnostics();
|
||||||
|
diagnostics.Name = $"{Name} (delegates to {TargetLogger.GetType().Name})";
|
||||||
|
return diagnostics;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DelegateLogEvent(LogEvent logEvent)
|
||||||
|
{
|
||||||
|
if (_disposed != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// Delegate by re-logging through the target logger using its public Log methods
|
||||||
|
// We reconstruct the logging call with the information from the LogEvent
|
||||||
|
var messageStr = logEvent.Message.ToString();
|
||||||
|
|
||||||
|
if (logEvent.Exception != null)
|
||||||
|
{
|
||||||
|
_targetLogger.Log(logEvent.Level, logEvent.Exception, messageStr);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_targetLogger.Log(messageStr, logEvent.Level);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
System.Diagnostics.Debug.WriteLine($"Failed to delegate log to target logger: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disposes the flow and clears the delegation queue
|
||||||
|
/// </summary>
|
||||||
|
public override async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
await FlushAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
lock (_queueLock)
|
||||||
|
{
|
||||||
|
_delegationQueue.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
await base.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,132 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Chaining;
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
/// Defines the contract for chaining multiple logger instances together.
|
||||||
|
/// Allows logs to be propagated through a chain of loggers, enabling the ability
|
||||||
|
/// to change log libraries or add multiple logging backends together.
|
||||||
|
/// </summary>
|
||||||
|
public interface ILoggerChain : IAsyncDisposable
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the name/identifier of this logger chain
|
||||||
|
/// </summary>
|
||||||
|
string Name { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the primary logger in the chain
|
||||||
|
/// </summary>
|
||||||
|
EonaCatLogStack Primary { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the list of chained loggers that receive logs from the primary logger
|
||||||
|
/// </summary>
|
||||||
|
IReadOnlyList<EonaCatLogStack> ChainedLoggers { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a logger to the chain. Logs will be propagated to this logger.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger to add to the chain</param>
|
||||||
|
/// <param name="chainAsFlow">If true, the logger will be added as a flow to the primary logger</param>
|
||||||
|
/// <returns>This instance for fluent chaining</returns>
|
||||||
|
ILoggerChain Add(EonaCatLogStack logger, bool chainAsFlow = true);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds multiple loggers to the chain.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="loggers">The loggers to add</param>
|
||||||
|
/// <returns>This instance for fluent chaining</returns>
|
||||||
|
ILoggerChain AddRange(params EonaCatLogStack[] loggers);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a logger from the chain.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger to remove</param>
|
||||||
|
/// <returns>True if the logger was removed, false if it wasn't in the chain</returns>
|
||||||
|
bool Remove(EonaCatLogStack logger);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes all loggers from the chain except the primary.
|
||||||
|
/// </summary>
|
||||||
|
void Clear();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the behavior when a chained logger fails
|
||||||
|
/// </summary>
|
||||||
|
LoggerChainErrorBehavior ErrorBehavior { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flushes all loggers in the chain.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="cancellationToken">Cancellation token</param>
|
||||||
|
/// <returns>A task representing the asynchronous flush operation</returns>
|
||||||
|
Task FlushAllAsync(CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disables a specific logger in the chain temporarily
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger to disable</param>
|
||||||
|
void DisableLogger(EonaCatLogStack logger);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables a previously disabled logger in the chain
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger to enable</param>
|
||||||
|
void EnableLogger(EonaCatLogStack logger);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the enabled state of a logger in the chain
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="logger">The logger to check</param>
|
||||||
|
/// <returns>True if the logger is enabled, false otherwise</returns>
|
||||||
|
bool IsLoggerEnabled(EonaCatLogStack logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Specifies how the logger chain should behave when a chained logger encounters an error
|
||||||
|
/// </summary>
|
||||||
|
public enum LoggerChainErrorBehavior
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Continue processing the chain even if a logger fails
|
||||||
|
/// </summary>
|
||||||
|
ContinueOnError = 0,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stop the chain if any logger fails
|
||||||
|
/// </summary>
|
||||||
|
StopOnError = 1,
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Skip the failed logger and continue with the rest
|
||||||
|
/// </summary>
|
||||||
|
SkipFailed = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event arguments for logger chain events
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LoggerChainEventArgs : EventArgs
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The logger involved in the event
|
||||||
|
/// </summary>
|
||||||
|
public EonaCatLogStack Logger { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The event message
|
||||||
|
/// </summary>
|
||||||
|
public string Message { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// An optional exception associated with the event
|
||||||
|
/// </summary>
|
||||||
|
public Exception? Exception { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
using EonaCat.LogStack.Flows;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace EonaCat.LogStack.Chaining;
|
||||||
|
|
||||||
|
// 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>
|
||||||
|
/// Implementation of ILoggerChain that enables chaining multiple logger instances together.
|
||||||
|
/// Logs propagate through the primary logger to all chained loggers, allowing multiple
|
||||||
|
/// logging backends to work in concert.
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LoggerChain : ILoggerChain
|
||||||
|
{
|
||||||
|
private readonly EonaCatLogStack _primary;
|
||||||
|
private readonly List<EonaCatLogStack> _chainedLoggers = new();
|
||||||
|
private readonly Dictionary<EonaCatLogStack, bool> _loggerStates = new();
|
||||||
|
private readonly object _chainLock = new object();
|
||||||
|
private int _disposed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event raised when a logger is added to the chain
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<LoggerChainEventArgs>? LoggerAdded;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event raised when a logger is removed from the chain
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<LoggerChainEventArgs>? LoggerRemoved;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Event raised when a logger fails
|
||||||
|
/// </summary>
|
||||||
|
public event EventHandler<LoggerChainEventArgs>? LoggerFailed;
|
||||||
|
|
||||||
|
public string Name { get; set; }
|
||||||
|
public EonaCatLogStack Primary => _primary;
|
||||||
|
public IReadOnlyList<EonaCatLogStack> ChainedLoggers
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
lock (_chainLock)
|
||||||
|
{
|
||||||
|
return new ReadOnlyCollection<EonaCatLogStack>(_chainedLoggers.ToList());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public LoggerChainErrorBehavior ErrorBehavior { get; set; } = LoggerChainErrorBehavior.ContinueOnError;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a new logger chain with the given primary logger
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="primary">The primary logger that forms the base of the chain</param>
|
||||||
|
/// <param name="name">The name of the logger chain</param>
|
||||||
|
public LoggerChain(EonaCatLogStack primary, string name = "LoggerChain")
|
||||||
|
{
|
||||||
|
_primary = primary ?? throw new ArgumentNullException(nameof(primary));
|
||||||
|
Name = name ?? throw new ArgumentNullException(nameof(name));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a logger to the chain
|
||||||
|
/// </summary>
|
||||||
|
public ILoggerChain Add(EonaCatLogStack logger, bool chainAsFlow = true)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
if (logger == null)
|
||||||
|
throw new ArgumentNullException(nameof(logger));
|
||||||
|
|
||||||
|
if (logger == _primary)
|
||||||
|
throw new InvalidOperationException("Cannot chain the primary logger to itself");
|
||||||
|
|
||||||
|
lock (_chainLock)
|
||||||
|
{
|
||||||
|
if (_chainedLoggers.Contains(logger))
|
||||||
|
return this; // Already in chain
|
||||||
|
|
||||||
|
_chainedLoggers.Add(logger);
|
||||||
|
_loggerStates[logger] = true; // Enabled by default
|
||||||
|
|
||||||
|
if (chainAsFlow)
|
||||||
|
{
|
||||||
|
// Add a delegating flow to the primary logger that forwards to this logger
|
||||||
|
var delegatingFlow = new DelegatingLoggerFlow(logger);
|
||||||
|
_primary.AddFlow(delegatingFlow);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerAdded?.Invoke(this, new LoggerChainEventArgs
|
||||||
|
{
|
||||||
|
Logger = logger,
|
||||||
|
Message = $"Logger added to chain: {logger.GetType().Name}"
|
||||||
|
});
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds multiple loggers to the chain
|
||||||
|
/// </summary>
|
||||||
|
public ILoggerChain AddRange(params EonaCatLogStack[] loggers)
|
||||||
|
{
|
||||||
|
if (loggers == null)
|
||||||
|
throw new ArgumentNullException(nameof(loggers));
|
||||||
|
|
||||||
|
foreach (var logger in loggers)
|
||||||
|
{
|
||||||
|
Add(logger);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Removes a logger from the chain
|
||||||
|
/// </summary>
|
||||||
|
public bool Remove(EonaCatLogStack logger)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
if (logger == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
lock (_chainLock)
|
||||||
|
{
|
||||||
|
bool removed = _chainedLoggers.Remove(logger);
|
||||||
|
if (removed)
|
||||||
|
{
|
||||||
|
_loggerStates.Remove(logger);
|
||||||
|
LoggerRemoved?.Invoke(this, new LoggerChainEventArgs
|
||||||
|
{
|
||||||
|
Logger = logger,
|
||||||
|
Message = $"Logger removed from chain: {logger.GetType().Name}"
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return removed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Clears all chained loggers (keeps the primary)
|
||||||
|
/// </summary>
|
||||||
|
public void Clear()
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
lock (_chainLock)
|
||||||
|
{
|
||||||
|
_chainedLoggers.Clear();
|
||||||
|
_loggerStates.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Flushes all loggers in the chain
|
||||||
|
/// </summary>
|
||||||
|
public async Task FlushAllAsync(CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
ThrowIfDisposed();
|
||||||
|
|
||||||
|
List<EonaCatLogStack> loggers;
|
||||||
|
lock (_chainLock)
|
||||||
|
{
|
||||||
|
loggers = new List<EonaCatLogStack>(_chainedLoggers);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flush primary logger
|
||||||
|
await FlushLoggerAsync(_primary, cancellationToken).ConfigureAwait(false);
|
||||||
|
|
||||||
|
// Flush all chained loggers
|
||||||
|
foreach (var logger in loggers)
|
||||||
|
{
|
||||||
|
if (IsLoggerEnabled(logger))
|
||||||
|
{
|
||||||
|
await FlushLoggerAsync(logger, cancellationToken).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Disables a logger in the chain temporarily
|
||||||
|
/// </summary>
|
||||||
|
public void DisableLogger(EonaCatLogStack logger)
|
||||||
|
{
|
||||||
|
if (logger == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_chainLock)
|
||||||
|
{
|
||||||
|
if (_loggerStates.ContainsKey(logger))
|
||||||
|
{
|
||||||
|
_loggerStates[logger] = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Enables a disabled logger
|
||||||
|
/// </summary>
|
||||||
|
public void EnableLogger(EonaCatLogStack logger)
|
||||||
|
{
|
||||||
|
if (logger == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
lock (_chainLock)
|
||||||
|
{
|
||||||
|
if (_loggerStates.ContainsKey(logger))
|
||||||
|
{
|
||||||
|
_loggerStates[logger] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets whether a logger is enabled
|
||||||
|
/// </summary>
|
||||||
|
public bool IsLoggerEnabled(EonaCatLogStack logger)
|
||||||
|
{
|
||||||
|
if (logger == null)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
lock (_chainLock)
|
||||||
|
{
|
||||||
|
if (_loggerStates.TryGetValue(logger, out var enabled))
|
||||||
|
{
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets a snapshot of the current chain state
|
||||||
|
/// </summary>
|
||||||
|
public LoggerChainSnapshot GetSnapshot()
|
||||||
|
{
|
||||||
|
lock (_chainLock)
|
||||||
|
{
|
||||||
|
return new LoggerChainSnapshot
|
||||||
|
{
|
||||||
|
Name = Name,
|
||||||
|
PrimaryLoggerType = Primary?.GetType().Name,
|
||||||
|
ChainedLoggerCount = _chainedLoggers.Count,
|
||||||
|
EnabledLoggerCount = _loggerStates.Values.Count(s => s),
|
||||||
|
DisabledLoggerCount = _loggerStates.Values.Count(s => !s),
|
||||||
|
ErrorBehavior = ErrorBehavior,
|
||||||
|
ChainedLoggers = _chainedLoggers.Select(l => new LoggerChainLoggerInfo
|
||||||
|
{
|
||||||
|
LoggerType = l.GetType().Name,
|
||||||
|
IsEnabled = _loggerStates.TryGetValue(l, out var enabled) ? enabled : false
|
||||||
|
}).ToList()
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task FlushLoggerAsync(EonaCatLogStack logger, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (logger is IAsyncDisposable disposable)
|
||||||
|
{
|
||||||
|
// Note: We flush through the logger's internal mechanisms
|
||||||
|
// This is handled by the logger's own async pipeline
|
||||||
|
await Task.CompletedTask.ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
LoggerFailed?.Invoke(this, new LoggerChainEventArgs
|
||||||
|
{
|
||||||
|
Logger = logger,
|
||||||
|
Message = "Failed to flush logger",
|
||||||
|
Exception = ex
|
||||||
|
});
|
||||||
|
|
||||||
|
if (ErrorBehavior == LoggerChainErrorBehavior.StopOnError)
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ThrowIfDisposed()
|
||||||
|
{
|
||||||
|
if (Interlocked.CompareExchange(ref _disposed, 0, 0) != 0)
|
||||||
|
throw new ObjectDisposedException(nameof(LoggerChain));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Flush all loggers before disposing
|
||||||
|
await FlushAllAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
|
lock (_chainLock)
|
||||||
|
{
|
||||||
|
_chainedLoggers.Clear();
|
||||||
|
_loggerStates.Clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
GC.SuppressFinalize(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a snapshot of the logger chain state
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LoggerChainSnapshot
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The name of the chain
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The type name of the primary logger
|
||||||
|
/// </summary>
|
||||||
|
public string PrimaryLoggerType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Total number of chained loggers
|
||||||
|
/// </summary>
|
||||||
|
public int ChainedLoggerCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of enabled loggers
|
||||||
|
/// </summary>
|
||||||
|
public int EnabledLoggerCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Number of disabled loggers
|
||||||
|
/// </summary>
|
||||||
|
public int DisabledLoggerCount { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The error behavior mode
|
||||||
|
/// </summary>
|
||||||
|
public LoggerChainErrorBehavior ErrorBehavior { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about each chained logger
|
||||||
|
/// </summary>
|
||||||
|
public List<LoggerChainLoggerInfo> ChainedLoggers { get; set; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Information about a single logger in the chain
|
||||||
|
/// </summary>
|
||||||
|
public sealed class LoggerChainLoggerInfo
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The type name of the logger
|
||||||
|
/// </summary>
|
||||||
|
public string LoggerType { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether the logger is currently enabled
|
||||||
|
/// </summary>
|
||||||
|
public bool IsEnabled { get; set; }
|
||||||
|
}
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
using EonaCat.LogStack.Boosters;
|
using EonaCat.LogStack.Boosters;
|
||||||
|
using EonaCat.LogStack.Chaining;
|
||||||
using EonaCat.LogStack.Core;
|
using EonaCat.LogStack.Core;
|
||||||
using EonaCat.LogStack.EonaCatLogStackCore;
|
using EonaCat.LogStack.EonaCatLogStackCore;
|
||||||
using EonaCat.LogStack.EonaCatLogStackCore.Policies;
|
using EonaCat.LogStack.EonaCatLogStackCore.Policies;
|
||||||
@@ -1461,4 +1462,37 @@ public sealed class LogBuilder
|
|||||||
return new Compatibility.EonaCatNLogAdapter(Build());
|
return new Compatibility.EonaCatNLogAdapter(Build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a logger chain with this logger as the primary logger.
|
||||||
|
/// This enables chaining multiple logger instances together.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="chainName">Optional name for the logger chain</param>
|
||||||
|
/// <returns>A LoggerChain instance for fluent chaining</returns>
|
||||||
|
public Chaining.ILoggerChain BuildChain(string chainName = null)
|
||||||
|
{
|
||||||
|
var logger = Build();
|
||||||
|
var chainName_ = chainName ?? $"LoggerChain-{_category}";
|
||||||
|
return new Chaining.LoggerChain(logger, chainName_);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a logger and adds it to an existing logger chain.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="primaryLogger">The primary logger to chain this logger to</param>
|
||||||
|
/// <param name="chainAsFlow">If true, this logger is added as a flow to the primary logger</param>
|
||||||
|
/// <returns>The updated logger chain</returns>
|
||||||
|
public Chaining.ILoggerChain AddToChain(EonaCatLogStack primaryLogger, bool chainAsFlow = true)
|
||||||
|
{
|
||||||
|
if (primaryLogger == null)
|
||||||
|
throw new ArgumentNullException(nameof(primaryLogger));
|
||||||
|
|
||||||
|
var newLogger = Build();
|
||||||
|
|
||||||
|
// Create or use existing chain
|
||||||
|
var chain = new Chaining.LoggerChain(primaryLogger, $"LoggerChain-{primaryLogger.GetType().Name}");
|
||||||
|
chain.Add(newLogger, chainAsFlow);
|
||||||
|
|
||||||
|
return chain;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
# EonaCat.LogStack
|
# EonaCat.LogStack 📊
|
||||||
|
|
||||||
**EonaCat.LogStack** is a flow-based, high-performance logging library for .NET, designed for zero-allocation logging paths and superior memory efficiency.
|
**EonaCat.LogStack** is a flow-based, high-performance logging library for .NET built for production environments. It features zero-allocation hot paths, superior memory efficiency, and a rich fluent API for routing log events to 50+ destinations including console, file, Slack, Discord, Redis, Elasticsearch, databases, and more.
|
||||||
It features a rich fluent API for routing log events to dozens of destinations - from console and file to Slack, Discord, Redis, Elasticsearch, and beyond.
|
|
||||||
|
**Comprehensive Suite Includes:**
|
||||||
|
- **Core Library** - High-performance logging engine with flows and boosters
|
||||||
|
- **LogClient** - Centralized remote logging client for distributed systems
|
||||||
|
- **Status Service** - Self-hosted monitoring, log aggregation, and health dashboard
|
||||||
|
- **Windows Event Log Flow** - Enterprise Windows event log integration
|
||||||
|
- **OpenTelemetry Flow** - Observability and distributed tracing support
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
@@ -3615,3 +3621,366 @@ EonaCat.LogStack combines:
|
|||||||
- Security-focused audit logging
|
- Security-focused audit logging
|
||||||
|
|
||||||
The library is designed to scale from small console tools to distributed production services.
|
The library is designed to scale from small console tools to distributed production services.
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Complete Code Examples
|
||||||
|
|
||||||
|
### Example 1: Production ASP.NET Core Application
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using EonaCat.LogStack;
|
||||||
|
|
||||||
|
// In Program.cs
|
||||||
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
|
|
||||||
|
// Add EonaCat logging
|
||||||
|
builder.Services.AddEonaCatLogging(config =>
|
||||||
|
{
|
||||||
|
config
|
||||||
|
.WithApplicationName("MyApi")
|
||||||
|
.WithMinimumLevel(LogLevel.Information)
|
||||||
|
.WriteToConsole(useColors: true)
|
||||||
|
.WriteToFile(
|
||||||
|
directory: "./logs",
|
||||||
|
maxFileSize: 100 * 1024 * 1024,
|
||||||
|
maxDirectorySize: 5L * 1024 * 1024 * 1024,
|
||||||
|
compression: CompressionFormat.GZip)
|
||||||
|
.WriteToSlack(
|
||||||
|
webhookUrl: builder.Configuration["Slack:WebhookUrl"],
|
||||||
|
minimumLevel: LogLevel.Error)
|
||||||
|
.WriteToElasticSearch(
|
||||||
|
elasticSearchUrl: builder.Configuration["Elasticsearch:Url"],
|
||||||
|
indexName: "myapi-logs")
|
||||||
|
.BoostWithMachineName()
|
||||||
|
.BoostWithProcessId()
|
||||||
|
.BoostWithCorrelationId()
|
||||||
|
.BoostWithCallerInfo();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Enable advanced telemetry
|
||||||
|
builder.Services.AddEonaCatNamedLoggers(new LoggerDIOptions
|
||||||
|
{
|
||||||
|
EnableTelemetry = true,
|
||||||
|
EnableTracing = true,
|
||||||
|
EnablePerformanceMonitoring = true,
|
||||||
|
EnableHealthMonitoring = true
|
||||||
|
});
|
||||||
|
|
||||||
|
builder.Services.AddEonaCatTracing();
|
||||||
|
|
||||||
|
var app = builder.Build();
|
||||||
|
app.UseHttpsRedirection();
|
||||||
|
app.UseRouting();
|
||||||
|
app.UseAuthentication();
|
||||||
|
app.UseAuthorization();
|
||||||
|
app.MapControllers();
|
||||||
|
|
||||||
|
var logger = app.Services.GetRequiredService<ILoggerFactory>()
|
||||||
|
.CreateLogger("Application");
|
||||||
|
|
||||||
|
logger.Information("Application starting",
|
||||||
|
("Version", "1.0.0"),
|
||||||
|
("Environment", app.Environment.EnvironmentName));
|
||||||
|
|
||||||
|
app.Run();
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 2: Console Application with Multiple Flows
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using EonaCat.LogStack;
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
await using var logger = new LogBuilder("ConsoleApp")
|
||||||
|
.WithMinimumLevel(LogLevel.Debug)
|
||||||
|
.WithTimestampMode(TimestampMode.Utc)
|
||||||
|
.WriteToConsole(useColors: true)
|
||||||
|
.WriteToFile("./logs", filePrefix: "app", maxFileSize: 50 * 1024 * 1024)
|
||||||
|
.WriteToAudit("./audit", auditLevel: AuditLevel.WarningAndAbove)
|
||||||
|
.WriteToMemory(capacity: 100)
|
||||||
|
.WriteToEmail(
|
||||||
|
smtpHost: "smtp.gmail.com", smtpPort: 587, useSsl: true,
|
||||||
|
username: "email@gmail.com", password: "app-password",
|
||||||
|
from: "logs@company.com", to: "admin@company.com",
|
||||||
|
subjectPrefix: "[CRITICAL]", minimumLevel: LogLevel.Critical)
|
||||||
|
.RedisFlow(host: "redis.local", channel: "myapp:logs")
|
||||||
|
.BoostWithMachineName()
|
||||||
|
.BoostWithProcessId()
|
||||||
|
.BoostWithCorrelationId()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
logger.Information("Application started");
|
||||||
|
await ProcessDataAsync(logger);
|
||||||
|
logger.Information("Application completed successfully");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Critical(ex, "Unhandled exception");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
await logger.FlushAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static async Task ProcessDataAsync(ILogger logger)
|
||||||
|
{
|
||||||
|
logger.Information("Processing started");
|
||||||
|
logger.Debug("Starting batch", ("BatchId", 123), ("Count", 5000));
|
||||||
|
await Task.Delay(1000);
|
||||||
|
logger.Information("Batch processed", ("Imported", 4998), ("Errors", 2));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 3: Service with Dependency Injection & Telemetry
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using EonaCat.LogStack;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
|
public class OrderService
|
||||||
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
|
private readonly ITelemetryAggregator _telemetry;
|
||||||
|
private readonly ISpanFactory _spans;
|
||||||
|
|
||||||
|
public OrderService(ILogger logger, ITelemetryAggregator telemetry, ISpanFactory spans)
|
||||||
|
{
|
||||||
|
_logger = logger;
|
||||||
|
_telemetry = telemetry;
|
||||||
|
_spans = spans;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<Order> ProcessOrderAsync(string orderId, decimal amount)
|
||||||
|
{
|
||||||
|
using var span = _spans.CreateSpanScope("ProcessOrder");
|
||||||
|
span.SetAttribute("OrderId", orderId);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_logger.Information("Processing order", ("OrderId", orderId), ("Amount", amount));
|
||||||
|
_telemetry.RecordCounter("orders.started", 1);
|
||||||
|
|
||||||
|
var order = new Order { Id = orderId, Amount = amount };
|
||||||
|
await Task.Delay(500);
|
||||||
|
|
||||||
|
_logger.Information("Order processed", ("Status", "Completed"));
|
||||||
|
_telemetry.RecordCounter("orders.completed", 1);
|
||||||
|
_telemetry.RecordGauge("order.revenue", amount);
|
||||||
|
return order;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_telemetry.RecordCounter("orders.failed", 1);
|
||||||
|
span.RecordException(ex);
|
||||||
|
_logger.Error(ex, "Order processing failed", ("OrderId", orderId));
|
||||||
|
throw;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Program
|
||||||
|
{
|
||||||
|
static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
var services = new ServiceCollection();
|
||||||
|
|
||||||
|
services.AddEonaCatLogging(config =>
|
||||||
|
{
|
||||||
|
config
|
||||||
|
.WithApplicationName("OrderService")
|
||||||
|
.WriteToConsole()
|
||||||
|
.WriteToFile("./logs")
|
||||||
|
.WriteToDatabase(
|
||||||
|
connectionFactory: () => new SqlConnection("Server=.;Database=Logs;"),
|
||||||
|
tableName: "OrderLogs")
|
||||||
|
.BoostWithCorrelationId();
|
||||||
|
});
|
||||||
|
|
||||||
|
services.AddEonaCatTelemetryAggregation();
|
||||||
|
services.AddEonaCatTracing();
|
||||||
|
services.AddScoped<OrderService>();
|
||||||
|
|
||||||
|
var sp = services.BuildServiceProvider();
|
||||||
|
var orderService = sp.GetRequiredService<OrderService>();
|
||||||
|
var order = await orderService.ProcessOrderAsync("ORD-001", 99.99m);
|
||||||
|
|
||||||
|
var telemetry = sp.GetRequiredService<ITelemetryAggregator>();
|
||||||
|
var snapshot = telemetry.GetSnapshot();
|
||||||
|
Console.WriteLine($"Completed: {snapshot.RecordedMetrics["orders.completed"]}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
record Order { public required string Id { get; init; } public required decimal Amount { get; init; } }
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 4: Advanced Resilience Patterns
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using EonaCat.LogStack;
|
||||||
|
|
||||||
|
await using var logger = new LogBuilder("ResilientApp")
|
||||||
|
.WriteToConsole()
|
||||||
|
// Retry with exponential backoff
|
||||||
|
.WriteToRetry(
|
||||||
|
primary: cfg => cfg.WriteToHttp("https://primary-logs.company.com/ingest", batchSize: 50),
|
||||||
|
maxRetries: 3, initialDelayMs: 1000, maxDelayMs: 30000)
|
||||||
|
// Failover to secondary
|
||||||
|
.WriteToFailover(
|
||||||
|
primary: cfg => cfg.WriteToElasticSearch("https://es-primary.company.com:9200", "logs"),
|
||||||
|
secondary: cfg => cfg.WriteToFile("./logs/failover"),
|
||||||
|
recoveryCheckIntervalSeconds: 60)
|
||||||
|
// Rate limiting with deduplication
|
||||||
|
.WriteToThrottled(
|
||||||
|
target: cfg => cfg.WriteToSlack("https://hooks.slack.com/...", LogLevel.Warning),
|
||||||
|
requestsPerSecond: 10, enableDeduplication: true, minimumLevel: LogLevel.Warning)
|
||||||
|
// Circuit breaker
|
||||||
|
.WriteToCircuitBreaker(
|
||||||
|
inner: cfg => cfg.WriteToDatabase(() => new SqlConnection("..."), "Logs"),
|
||||||
|
failureThreshold: 5, successThresholdToClose: 3, timeoutSeconds: 60)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
logger.Information("Started with resilience");
|
||||||
|
logger.Error(new Exception("Test"), "Error occurred");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 5: Remote Logging with LogClient
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using EonaCat.LogStack.LogClient;
|
||||||
|
|
||||||
|
var clientOptions = new LogCentralOptions
|
||||||
|
{
|
||||||
|
ServerUrl = "https://logcentral.company.com",
|
||||||
|
ApiKey = "your-api-key",
|
||||||
|
ApplicationName = "DistributedService",
|
||||||
|
ApplicationVersion = "2.1.0",
|
||||||
|
Environment = "Production",
|
||||||
|
BatchSize = 100,
|
||||||
|
FlushIntervalSeconds = 5
|
||||||
|
};
|
||||||
|
|
||||||
|
await using var logger = new LogBuilder("DistributedApp")
|
||||||
|
.WriteToConsole()
|
||||||
|
.WriteToFile("./logs")
|
||||||
|
.WriteToHttp(
|
||||||
|
endpoint: $"{clientOptions.ServerUrl}/api/logs/ingest",
|
||||||
|
batchSize: clientOptions.BatchSize,
|
||||||
|
headers: new Dictionary<string, string>
|
||||||
|
{
|
||||||
|
["X-API-Key"] = clientOptions.ApiKey,
|
||||||
|
["X-App-Name"] = clientOptions.ApplicationName
|
||||||
|
})
|
||||||
|
.BoostWithMachineName()
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
logger.Information("Connected to central logging", ("Server", clientOptions.ServerUrl));
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 6: Log Processing and Analysis
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using EonaCat.LogStack;
|
||||||
|
|
||||||
|
await using var logger = new LogBuilder("AnalysisApp")
|
||||||
|
.WriteToConsole()
|
||||||
|
.WriteToMemory(capacity: 10000)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
for (int i = 0; i < 100; i++)
|
||||||
|
{
|
||||||
|
if (i % 10 == 0)
|
||||||
|
logger.Warning($"Iteration {i}");
|
||||||
|
else
|
||||||
|
logger.Information($"Processing item {i}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var memoryFlow = logger.GetFlowOfType<MemoryFlow>();
|
||||||
|
var events = memoryFlow.GetEvents();
|
||||||
|
|
||||||
|
var errorCount = events.Count(e => e.Level == LogLevel.Error);
|
||||||
|
var warningCount = events.Count(e => e.Level == LogLevel.Warning);
|
||||||
|
Console.WriteLine($"Errors: {errorCount}, Warnings: {warningCount}");
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 7: Custom Modifiers for Universal Enrichment
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using EonaCat.LogStack;
|
||||||
|
|
||||||
|
var requestId = "REQ-" + Guid.NewGuid().ToString().Substring(0, 8);
|
||||||
|
var userId = "USER-123";
|
||||||
|
|
||||||
|
await using var logger = new LogBuilder("EnrichedApp")
|
||||||
|
.WriteToConsole()
|
||||||
|
.WriteToFile("./logs")
|
||||||
|
.AddModifier((ref LogEventBuilder builder) =>
|
||||||
|
{
|
||||||
|
builder.WithProperty("RequestId", requestId);
|
||||||
|
builder.WithProperty("UserId", userId);
|
||||||
|
builder.WithProperty("Host", Environment.MachineName);
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
logger.Information("Operation started");
|
||||||
|
logger.Warning("Potential issue detected");
|
||||||
|
logger.Error(new Exception("Failed"), "Operation failed");
|
||||||
|
|
||||||
|
// All logs automatically include RequestId, UserId, Host
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example 8: Structured Logging and Complex Properties
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
using EonaCat.LogStack;
|
||||||
|
|
||||||
|
await using var logger = new LogBuilder("StructuredApp")
|
||||||
|
.WriteToConsole()
|
||||||
|
.WriteToFile("./logs", outputFormat: FileOutputFormat.Json)
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
// Simple tuple properties (fast)
|
||||||
|
logger.Information("User action",
|
||||||
|
("UserId", 42),
|
||||||
|
("Action", "login"),
|
||||||
|
("Ip", "192.168.1.1"));
|
||||||
|
|
||||||
|
// Dictionary properties
|
||||||
|
var contextDict = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
{ "OrderId", "ORD-123" },
|
||||||
|
{ "Items", 5 },
|
||||||
|
{ "Total", 199.99m }
|
||||||
|
};
|
||||||
|
logger.Information("Order processed", contextDict);
|
||||||
|
|
||||||
|
// Exception with context
|
||||||
|
try
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Database connection failed");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
logger.Error(ex, "Operation failed",
|
||||||
|
("Retries", 3),
|
||||||
|
("TimeoutMs", 5000),
|
||||||
|
("DatabaseName", "ProductDb"));
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Additional Resources
|
||||||
|
|
||||||
|
- **[GitHub Repository](https://git.saey.me/EonaCat/EonaCat.logstack)** - Source code and issues
|
||||||
|
- **[NuGet Package](https://www.nuget.org/packages/EonaCat.LogStack)** - Official package
|
||||||
|
- **[Supported Targets](#supported-targets)** - .NET versions and frameworks
|
||||||
|
- **[Installation](#installation)** - Getting started guide
|
||||||
|
|||||||
Reference in New Issue
Block a user