This commit is contained in:
2026-07-21 11:46:29 +02:00
committed by Jeroen Saey
parent 5d43be8657
commit 9484891cc0
12 changed files with 3188 additions and 3 deletions
+4
View File
@@ -110,4 +110,8 @@ It features a rich fluent API for routing log events to dozens of destinations f
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="EonaCat.LogStack.Test\Features\" />
</ItemGroup>
</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; }
}
+34
View File
@@ -1,4 +1,5 @@
using EonaCat.LogStack.Boosters;
using EonaCat.LogStack.Chaining;
using EonaCat.LogStack.Core;
using EonaCat.LogStack.EonaCatLogStackCore;
using EonaCat.LogStack.EonaCatLogStackCore.Policies;
@@ -1461,4 +1462,37 @@ public sealed class LogBuilder
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;
}
}
+372 -3
View File
@@ -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.
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.
**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.
**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
@@ -3615,3 +3621,366 @@ EonaCat.LogStack combines:
- Security-focused audit logging
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