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> <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; }
}
+34
View File
@@ -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;
}
} }
+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. **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