Updated encryption file

Added braces
This commit is contained in:
2026-08-04 21:47:43 +02:00
parent 3e22c31460
commit 5c435fb2f5
41 changed files with 3079 additions and 2425 deletions
+6 -7
View File
@@ -14,7 +14,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
<Copyright>EonaCat (Jeroen Saey)</Copyright> <Copyright>EonaCat (Jeroen Saey)</Copyright>
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags> <PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
<PackageIconUrl /> <PackageIconUrl />
<FileVersion>0.2.0</FileVersion> <FileVersion>0.2.1</FileVersion>
<PackageReadmeFile>README.md</PackageReadmeFile> <PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile> <GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile> <PackageLicenseFile>LICENSE</PackageLicenseFile>
@@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<EVRevisionFormat>0.2.0+{chash:10}.{c:ymd}</EVRevisionFormat> <EVRevisionFormat>0.2.1+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVDefault>true</EVDefault> <EVDefault>true</EVDefault>
<EVInfo>true</EVInfo> <EVInfo>true</EVInfo>
<EVTagMatch>v[0-9]*</EVTagMatch> <EVTagMatch>v[0-9]*</EVTagMatch>
@@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
</PropertyGroup> </PropertyGroup>
<PropertyGroup> <PropertyGroup>
<Version>0.2.0</Version> <Version>0.2.1</Version>
<PackageId>EonaCat.LogStack</PackageId> <PackageId>EonaCat.LogStack</PackageId>
<Product>EonaCat.LogStack</Product> <Product>EonaCat.LogStack</Product>
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl> <RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl>
@@ -52,16 +52,19 @@ It features a rich fluent API for routing log events to dozens of destinations f
<ItemGroup> <ItemGroup>
<Compile Remove="Configuration\**" /> <Compile Remove="Configuration\**" />
<Compile Remove="EonaCat.LogStack.Test\**" />
<Compile Remove="EonaCatLoggerCore\Examples\**" /> <Compile Remove="EonaCatLoggerCore\Examples\**" />
<Compile Remove="Examples\**" /> <Compile Remove="Examples\**" />
<Compile Remove="Patterns\**" /> <Compile Remove="Patterns\**" />
<Compile Remove="Utilities\**" /> <Compile Remove="Utilities\**" />
<EmbeddedResource Remove="Configuration\**" /> <EmbeddedResource Remove="Configuration\**" />
<EmbeddedResource Remove="EonaCat.LogStack.Test\**" />
<EmbeddedResource Remove="EonaCatLoggerCore\Examples\**" /> <EmbeddedResource Remove="EonaCatLoggerCore\Examples\**" />
<EmbeddedResource Remove="Examples\**" /> <EmbeddedResource Remove="Examples\**" />
<EmbeddedResource Remove="Patterns\**" /> <EmbeddedResource Remove="Patterns\**" />
<EmbeddedResource Remove="Utilities\**" /> <EmbeddedResource Remove="Utilities\**" />
<None Remove="Configuration\**" /> <None Remove="Configuration\**" />
<None Remove="EonaCat.LogStack.Test\**" />
<None Remove="EonaCatLoggerCore\Examples\**" /> <None Remove="EonaCatLoggerCore\Examples\**" />
<None Remove="Examples\**" /> <None Remove="Examples\**" />
<None Remove="Patterns\**" /> <None Remove="Patterns\**" />
@@ -110,8 +113,4 @@ 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>
@@ -41,13 +41,17 @@ namespace EonaCat.LogStack.Flows
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{ {
if (!IsEnabled || !IsLogLevelEnabled(logEvent)) if (!IsEnabled || !IsLogLevelEnabled(logEvent))
{
return WriteResult.Success; return WriteResult.Success;
}
try try
{ {
var correlationId = ExtractCorrelationId(logEvent); var correlationId = ExtractCorrelationId(logEvent);
if (string.IsNullOrEmpty(correlationId)) if (string.IsNullOrEmpty(correlationId))
{
return WriteResult.Success; return WriteResult.Success;
}
// Get or create correlation group // Get or create correlation group
var group = _correlationGroups.AddOrUpdate( var group = _correlationGroups.AddOrUpdate(
@@ -119,7 +123,9 @@ namespace EonaCat.LogStack.Flows
public IEnumerable<LogEvent> GetCorrelatedEvents(string correlationId) public IEnumerable<LogEvent> GetCorrelatedEvents(string correlationId)
{ {
if (_correlationGroups.TryGetValue(correlationId, out var group)) if (_correlationGroups.TryGetValue(correlationId, out var group))
{
return group.Events.ToList(); return group.Events.ToList();
}
return new List<LogEvent>(); return new List<LogEvent>();
} }
@@ -200,11 +206,19 @@ namespace EonaCat.LogStack.Flows
if (logEvent.Properties != null) if (logEvent.Properties != null)
{ {
if (logEvent.Properties.TryGetValue("CorrelationId", out var corrId) && corrId is string str) if (logEvent.Properties.TryGetValue("CorrelationId", out var corrId) && corrId is string str)
{
return str; return str;
}
if (logEvent.Properties.TryGetValue("RequestId", out var reqId) && reqId is string str2) if (logEvent.Properties.TryGetValue("RequestId", out var reqId) && reqId is string str2)
{
return str2; return str2;
}
if (logEvent.Properties.TryGetValue("TraceId", out var traceId) && traceId is string str3) if (logEvent.Properties.TryGetValue("TraceId", out var traceId) && traceId is string str3)
{
return str3; return str3;
}
} }
// Try thread-based correlation // Try thread-based correlation
@@ -230,10 +244,17 @@ namespace EonaCat.LogStack.Flows
private double GetMedian(List<int> values) private double GetMedian(List<int> values)
{ {
if (values.Count == 0) return 0; if (values.Count == 0)
{
return 0;
}
var sorted = values.OrderBy(x => x).ToList(); var sorted = values.OrderBy(x => x).ToList();
if (sorted.Count % 2 == 0) if (sorted.Count % 2 == 0)
{
return (sorted[sorted.Count / 2 - 1] + sorted[sorted.Count / 2]) / 2.0; return (sorted[sorted.Count / 2 - 1] + sorted[sorted.Count / 2]) / 2.0;
}
return sorted[sorted.Count / 2]; return sorted[sorted.Count / 2];
} }
@@ -258,7 +279,9 @@ namespace EonaCat.LogStack.Flows
get get
{ {
lock (_eventLock) lock (_eventLock)
{
return _events.AsReadOnly(); return _events.AsReadOnly();
}
} }
} }
public DateTime FirstEventTime { get; private set; } public DateTime FirstEventTime { get; private set; }
@@ -287,7 +310,11 @@ namespace EonaCat.LogStack.Flows
{ {
lock (_eventLock) lock (_eventLock)
{ {
if (_events.Count == 0) return TimeSpan.Zero; if (_events.Count == 0)
{
return TimeSpan.Zero;
}
return new TimeSpan(_events[_events.Count - 1].Timestamp - _events[0].Timestamp); return new TimeSpan(_events[_events.Count - 1].Timestamp - _events[0].Timestamp);
} }
} }
@@ -298,7 +325,9 @@ namespace EonaCat.LogStack.Flows
public IEnumerable<LogEvent> GetEventsByLevel(LogLevel level) public IEnumerable<LogEvent> GetEventsByLevel(LogLevel level)
{ {
lock (_eventLock) lock (_eventLock)
{
return _events.Where(e => e.Level == level).ToList(); return _events.Where(e => e.Level == level).ToList();
}
} }
/// <summary> /// <summary>
@@ -309,7 +338,9 @@ namespace EonaCat.LogStack.Flows
get get
{ {
lock (_eventLock) lock (_eventLock)
{
return _events.Any(e => e.Exception != null); return _events.Any(e => e.Exception != null);
}
} }
} }
@@ -44,7 +44,9 @@ namespace EonaCat.LogStack.Flows
: base("LocalStorageQuery", minimumLevel) : base("LocalStorageQuery", minimumLevel)
{ {
if (maxCapacity <= 0) if (maxCapacity <= 0)
{
throw new ArgumentException("Capacity must be greater than 0", nameof(maxCapacity)); throw new ArgumentException("Capacity must be greater than 0", nameof(maxCapacity));
}
_maxCapacity = maxCapacity; _maxCapacity = maxCapacity;
} }
@@ -52,7 +54,9 @@ namespace EonaCat.LogStack.Flows
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{ {
if (!IsEnabled || !IsLogLevelEnabled(logEvent)) if (!IsEnabled || !IsLogLevelEnabled(logEvent))
{
return WriteResult.Success; return WriteResult.Success;
}
try try
{ {
@@ -185,7 +189,9 @@ namespace EonaCat.LogStack.Flows
{ {
var events = _logBuffer.ToList(); var events = _logBuffer.ToList();
if (events.Count == 0) if (events.Count == 0)
{
return new LogStorageStatistics(); return new LogStorageStatistics();
}
var stats = new LogStorageStatistics var stats = new LogStorageStatistics
{ {
@@ -72,7 +72,9 @@ namespace EonaCat.LogStack.Flows
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{ {
if (!IsEnabled || !IsLogLevelEnabled(logEvent)) if (!IsEnabled || !IsLogLevelEnabled(logEvent))
{
return WriteResult.Success; return WriteResult.Success;
}
try try
{ {
@@ -114,7 +116,10 @@ namespace EonaCat.LogStack.Flows
{ {
_channel.Writer.TryComplete(); _channel.Writer.TryComplete();
if (_processingTask != null) if (_processingTask != null)
{
await _processingTask; await _processingTask;
}
await _targetFlow.DisposeAsync(); await _targetFlow.DisposeAsync();
await base.DisposeAsync(); await base.DisposeAsync();
} }
@@ -44,13 +44,17 @@ namespace EonaCat.LogStack.Flows
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{ {
if (!IsEnabled || !IsLogLevelEnabled(logEvent)) if (!IsEnabled || !IsLogLevelEnabled(logEvent))
{
return WriteResult.Success; return WriteResult.Success;
}
try try
{ {
var metrics = ExtractPerformanceMetrics(logEvent); var metrics = ExtractPerformanceMetrics(logEvent);
if (metrics.Count == 0) if (metrics.Count == 0)
{
return WriteResult.Success; return WriteResult.Success;
}
lock (_trackersLock) lock (_trackersLock)
{ {
@@ -102,7 +106,9 @@ namespace EonaCat.LogStack.Flows
{ {
var result = await BlastAsync(logEvent, cancellationToken); var result = await BlastAsync(logEvent, cancellationToken);
if (result != WriteResult.Success) if (result != WriteResult.Success)
{
return result; return result;
}
} }
return WriteResult.Success; return WriteResult.Success;
} }
@@ -110,7 +116,10 @@ namespace EonaCat.LogStack.Flows
public override async Task FlushAsync(CancellationToken cancellationToken = default) public override async Task FlushAsync(CancellationToken cancellationToken = default)
{ {
if (_alertTarget != null) if (_alertTarget != null)
{
await _alertTarget.FlushAsync(cancellationToken); await _alertTarget.FlushAsync(cancellationToken);
}
await Task.CompletedTask; await Task.CompletedTask;
} }
@@ -165,7 +174,10 @@ namespace EonaCat.LogStack.Flows
lock (_trackersLock) lock (_trackersLock)
{ {
if (_trackers.TryGetValue(metricName, out var tracker)) if (_trackers.TryGetValue(metricName, out var tracker))
{
return tracker.RecentAnomalies.ToList(); return tracker.RecentAnomalies.ToList();
}
return new List<PerformanceAnomaly>(); return new List<PerformanceAnomaly>();
} }
} }
@@ -199,7 +211,9 @@ namespace EonaCat.LogStack.Flows
var metrics = new Dictionary<string, double>(); var metrics = new Dictionary<string, double>();
if (logEvent.Properties == null) if (logEvent.Properties == null)
{
return metrics; return metrics;
}
// Look for common performance metric property names // Look for common performance metric property names
var metricNames = new[] { "duration", "latency", "elapsed", "time_ms", "response_time", "memory", "cpu" }; var metricNames = new[] { "duration", "latency", "elapsed", "time_ms", "response_time", "memory", "cpu" };
@@ -235,7 +249,10 @@ namespace EonaCat.LogStack.Flows
{ {
Clear(); Clear();
if (_alertTarget != null) if (_alertTarget != null)
{
await _alertTarget.DisposeAsync(); await _alertTarget.DisposeAsync();
}
await base.DisposeAsync(); await base.DisposeAsync();
} }
} }
@@ -310,7 +327,9 @@ namespace EonaCat.LogStack.Flows
_recentAnomalies.Add(anomaly); _recentAnomalies.Add(anomaly);
if (_recentAnomalies.Count > 100) if (_recentAnomalies.Count > 100)
{
_recentAnomalies.RemoveAt(0); _recentAnomalies.RemoveAt(0);
}
AnomalyCount++; AnomalyCount++;
return anomaly; return anomaly;
@@ -324,7 +343,9 @@ namespace EonaCat.LogStack.Flows
private double CalculateStandardDeviation() private double CalculateStandardDeviation()
{ {
if (_measurements.Count < 2) if (_measurements.Count < 2)
{
return 0; return 0;
}
var avg = Average; var avg = Average;
var sumSquaredDiff = _measurements.Sum(x => Math.Pow(x - avg, 2)); var sumSquaredDiff = _measurements.Sum(x => Math.Pow(x - avg, 2));
@@ -207,7 +207,9 @@ namespace EonaCat.LogStack.Extensions
{ {
var engine = provider.GetService<LogSearchEngine>(); var engine = provider.GetService<LogSearchEngine>();
if (engine == null) if (engine == null)
{
return Enumerable.Empty<LogSearchEntry>(); return Enumerable.Empty<LogSearchEntry>();
}
var query = new LogSearchQuery var query = new LogSearchQuery
{ {
@@ -35,7 +35,9 @@ namespace EonaCat.LogStack.Features
public AlertRule AddRule(string ruleName, Action<AlertRuleBuilder> configureAction) public AlertRule AddRule(string ruleName, Action<AlertRuleBuilder> configureAction)
{ {
if (string.IsNullOrWhiteSpace(ruleName)) if (string.IsNullOrWhiteSpace(ruleName))
{
throw new ArgumentException("Rule name cannot be empty", nameof(ruleName)); throw new ArgumentException("Rule name cannot be empty", nameof(ruleName));
}
var builder = new AlertRuleBuilder(ruleName); var builder = new AlertRuleBuilder(ruleName);
configureAction(builder); configureAction(builder);
@@ -72,7 +74,9 @@ namespace EonaCat.LogStack.Features
public void EvaluateLog(LogEvent logEvent, string formattedMessage) public void EvaluateLog(LogEvent logEvent, string formattedMessage)
{ {
if (!_isEnabled) if (!_isEnabled)
{
return; return;
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -107,31 +111,41 @@ namespace EonaCat.LogStack.Features
{ {
// Level check // Level check
if (rule.MinimumLevel.HasValue && logEvent.Level < rule.MinimumLevel.Value) if (rule.MinimumLevel.HasValue && logEvent.Level < rule.MinimumLevel.Value)
{
return false; return false;
}
// Keyword check // Keyword check
if (!string.IsNullOrEmpty(rule.KeywordPattern)) if (!string.IsNullOrEmpty(rule.KeywordPattern))
{ {
if (!formattedMessage.Contains(rule.KeywordPattern, StringComparison.OrdinalIgnoreCase)) if (!formattedMessage.Contains(rule.KeywordPattern, StringComparison.OrdinalIgnoreCase))
{
return false; return false;
}
} }
// Logger check // Logger check
if (!string.IsNullOrEmpty(rule.LoggerNamePattern)) if (!string.IsNullOrEmpty(rule.LoggerNamePattern))
{ {
if (!(logEvent.Category?? "").Contains(rule.LoggerNamePattern, StringComparison.OrdinalIgnoreCase)) if (!(logEvent.Category?? "").Contains(rule.LoggerNamePattern, StringComparison.OrdinalIgnoreCase))
{
return false; return false;
}
} }
// Exception check // Exception check
if (rule.OnlyWithExceptions && logEvent.Exception == null) if (rule.OnlyWithExceptions && logEvent.Exception == null)
{
return false; return false;
}
// Custom predicate // Custom predicate
if (rule.CustomPredicate != null) if (rule.CustomPredicate != null)
{ {
if (!rule.CustomPredicate(logEvent, formattedMessage)) if (!rule.CustomPredicate(logEvent, formattedMessage))
{
return false; return false;
}
} }
// Check throttling // Check throttling
@@ -142,7 +156,9 @@ namespace EonaCat.LogStack.Features
{ {
var timeSinceLastTrigger = (now - rule.LastTriggeredAt.Value).TotalSeconds; var timeSinceLastTrigger = (now - rule.LastTriggeredAt.Value).TotalSeconds;
if (timeSinceLastTrigger < rule.ThrottleIntervalSeconds) if (timeSinceLastTrigger < rule.ThrottleIntervalSeconds)
{
return false; return false;
}
} }
rule.LastTriggeredAt = now; rule.LastTriggeredAt = now;
} }
@@ -263,7 +279,10 @@ namespace EonaCat.LogStack.Features
public AlertRuleBuilder WithThrottling(int intervalSeconds) public AlertRuleBuilder WithThrottling(int intervalSeconds)
{ {
if (intervalSeconds < 0) if (intervalSeconds < 0)
{
throw new ArgumentException("Throttle interval must be >= 0", nameof(intervalSeconds)); throw new ArgumentException("Throttle interval must be >= 0", nameof(intervalSeconds));
}
_rule.ThrottleIntervalSeconds = intervalSeconds; _rule.ThrottleIntervalSeconds = intervalSeconds;
return this; return this;
} }
@@ -30,10 +30,14 @@ namespace EonaCat.LogStack.Features
public void AddRule(string ruleName, Func<ConfigurationValidationContext, ValidationResult> validator) public void AddRule(string ruleName, Func<ConfigurationValidationContext, ValidationResult> validator)
{ {
if (string.IsNullOrWhiteSpace(ruleName)) if (string.IsNullOrWhiteSpace(ruleName))
{
throw new ArgumentException("Rule name cannot be empty", nameof(ruleName)); throw new ArgumentException("Rule name cannot be empty", nameof(ruleName));
}
if (validator == null) if (validator == null)
{
throw new ArgumentNullException(nameof(validator)); throw new ArgumentNullException(nameof(validator));
}
_rules.Add(new ConfigurationRule _rules.Add(new ConfigurationRule
{ {
@@ -48,7 +52,9 @@ namespace EonaCat.LogStack.Features
public ConfigurationValidationReport Validate(ILogger logger) public ConfigurationValidationReport Validate(ILogger logger)
{ {
if (logger == null) if (logger == null)
{
throw new ArgumentNullException(nameof(logger)); throw new ArgumentNullException(nameof(logger));
}
var context = new ConfigurationValidationContext { Logger = logger }; var context = new ConfigurationValidationContext { Logger = logger };
var results = new List<ValidationResult>(); var results = new List<ValidationResult>();
@@ -247,7 +253,9 @@ namespace EonaCat.LogStack.Features
public void EnableHotReload(Action<ConfigurationChangeNotification> onConfigChanged) public void EnableHotReload(Action<ConfigurationChangeNotification> onConfigChanged)
{ {
if (onConfigChanged == null) if (onConfigChanged == null)
{
throw new ArgumentNullException(nameof(onConfigChanged)); throw new ArgumentNullException(nameof(onConfigChanged));
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -274,7 +282,9 @@ namespace EonaCat.LogStack.Features
public void NotifyConfigurationChange(string configKey, object oldValue, object newValue, string reason = null) public void NotifyConfigurationChange(string configKey, object oldValue, object newValue, string reason = null)
{ {
if (!_isEnabled || _changeHandler == null) if (!_isEnabled || _changeHandler == null)
{
return; return;
}
var notification = new ConfigurationChangeNotification var notification = new ConfigurationChangeNotification
{ {
@@ -21,7 +21,9 @@ namespace EonaCat.LogStack.Features
string serviceName, ActivityStatus status, double durationMs, Exception exception = null) string serviceName, ActivityStatus status, double durationMs, Exception exception = null)
{ {
if (string.IsNullOrWhiteSpace(correlationId)) if (string.IsNullOrWhiteSpace(correlationId))
{
throw new ArgumentNullException(nameof(correlationId)); throw new ArgumentNullException(nameof(correlationId));
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -59,7 +61,9 @@ namespace EonaCat.LogStack.Features
public CorrelatedActivityTrace GetTrace(string correlationId) public CorrelatedActivityTrace GetTrace(string correlationId)
{ {
if (string.IsNullOrWhiteSpace(correlationId)) if (string.IsNullOrWhiteSpace(correlationId))
{
return null; return null;
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -85,7 +89,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<CorrelatedActivityTrace> GetTracesByService(string serviceName) public IEnumerable<CorrelatedActivityTrace> GetTracesByService(string serviceName)
{ {
if (string.IsNullOrWhiteSpace(serviceName)) if (string.IsNullOrWhiteSpace(serviceName))
{
return Enumerable.Empty<CorrelatedActivityTrace>(); return Enumerable.Empty<CorrelatedActivityTrace>();
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -129,7 +135,9 @@ namespace EonaCat.LogStack.Features
lock (_lockObject) lock (_lockObject)
{ {
if (_traces.Count == 0) if (_traces.Count == 0)
{
return new DashboardStatistics(); return new DashboardStatistics();
}
var allActivities = _traces.SelectMany(t => t.Activities).ToList(); var allActivities = _traces.SelectMany(t => t.Activities).ToList();
var byStatus = allActivities.GroupBy(a => a.Status) var byStatus = allActivities.GroupBy(a => a.Status)
@@ -174,7 +182,9 @@ namespace EonaCat.LogStack.Features
var to = services[i + 1]; var to = services[i + 1];
if (!dependencies.ContainsKey(from)) if (!dependencies.ContainsKey(from))
{
dependencies[from] = new HashSet<string>(); dependencies[from] = new HashSet<string>();
}
dependencies[from].Add(to); dependencies[from].Add(to);
} }
@@ -242,7 +252,9 @@ namespace EonaCat.LogStack.Features
{ {
var trace = GetTrace(correlationId); var trace = GetTrace(correlationId);
if (trace == null) if (trace == null)
{
return null; return null;
}
var startTime = trace.Activities.Min(a => a.Timestamp); var startTime = trace.Activities.Min(a => a.Timestamp);
var activities = trace.Activities var activities = trace.Activities
@@ -50,7 +50,9 @@ namespace EonaCat.LogStack.Features
public void CaptureLog(LogEvent logEvent, string formattedMessage) public void CaptureLog(LogEvent logEvent, string formattedMessage)
{ {
if (!_isCapturing) if (!_isCapturing)
{
return; return;
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -95,17 +97,25 @@ namespace EonaCat.LogStack.Features
var results = _capturedLogs.AsEnumerable(); var results = _capturedLogs.AsEnumerable();
if (minLevel.HasValue) if (minLevel.HasValue)
{
results = results.Where(l => l.Level >= minLevel.Value); results = results.Where(l => l.Level >= minLevel.Value);
}
if (!string.IsNullOrWhiteSpace(loggerFilter)) if (!string.IsNullOrWhiteSpace(loggerFilter))
{
results = results.Where(l => results = results.Where(l =>
l.Logger.Contains(loggerFilter, StringComparison.OrdinalIgnoreCase)); l.Logger.Contains(loggerFilter, StringComparison.OrdinalIgnoreCase));
}
if (fromUtc.HasValue) if (fromUtc.HasValue)
{
results = results.Where(l => l.Timestamp >= fromUtc.Value); results = results.Where(l => l.Timestamp >= fromUtc.Value);
}
if (toUtc.HasValue) if (toUtc.HasValue)
{
results = results.Where(l => l.Timestamp <= toUtc.Value); results = results.Where(l => l.Timestamp <= toUtc.Value);
}
return results.ToList(); return results.ToList();
} }
@@ -134,7 +144,9 @@ namespace EonaCat.LogStack.Features
public void ImportFromJson(string filePath) public void ImportFromJson(string filePath)
{ {
if (!File.Exists(filePath)) if (!File.Exists(filePath))
{
throw new FileNotFoundException($"File not found: {filePath}"); throw new FileNotFoundException($"File not found: {filePath}");
}
var json = File.ReadAllText(filePath); var json = File.ReadAllText(filePath);
var logs = JsonSerializer.Deserialize<List<CapturedLogEntry>>(json); var logs = JsonSerializer.Deserialize<List<CapturedLogEntry>>(json);
@@ -151,7 +163,9 @@ namespace EonaCat.LogStack.Features
public void Replay(Action<CapturedLogEntry> onLogReplayed, bool respectTimings = false) public void Replay(Action<CapturedLogEntry> onLogReplayed, bool respectTimings = false)
{ {
if (onLogReplayed == null) if (onLogReplayed == null)
{
throw new ArgumentNullException(nameof(onLogReplayed)); throw new ArgumentNullException(nameof(onLogReplayed));
}
List<CapturedLogEntry> logsToReplay; List<CapturedLogEntry> logsToReplay;
@@ -161,7 +175,9 @@ namespace EonaCat.LogStack.Features
} }
if (logsToReplay.Count == 0) if (logsToReplay.Count == 0)
{
return; return;
}
DateTime? previousTimestamp = null; DateTime? previousTimestamp = null;
@@ -189,7 +205,9 @@ namespace EonaCat.LogStack.Features
lock (_lockObject) lock (_lockObject)
{ {
if (_capturedLogs.Count == 0) if (_capturedLogs.Count == 0)
{
return new LogReplayStatistics(); return new LogReplayStatistics();
}
var byLevel = _capturedLogs.GroupBy(l => l.Level) var byLevel = _capturedLogs.GroupBy(l => l.Level)
.ToDictionary(g => g.Key, g => g.Count()); .ToDictionary(g => g.Key, g => g.Count());
@@ -317,7 +335,9 @@ namespace EonaCat.LogStack.Features
public static LogReplayScenario Load(string filePath) public static LogReplayScenario Load(string filePath)
{ {
if (!File.Exists(filePath)) if (!File.Exists(filePath))
{
throw new FileNotFoundException($"Scenario file not found: {filePath}"); throw new FileNotFoundException($"Scenario file not found: {filePath}");
}
var json = File.ReadAllText(filePath); var json = File.ReadAllText(filePath);
return JsonSerializer.Deserialize<LogReplayScenario>(json); return JsonSerializer.Deserialize<LogReplayScenario>(json);
@@ -88,7 +88,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> SearchByKeyword(string keyword) public IEnumerable<LogSearchEntry> SearchByKeyword(string keyword)
{ {
if (string.IsNullOrWhiteSpace(keyword)) if (string.IsNullOrWhiteSpace(keyword))
{
return Enumerable.Empty<LogSearchEntry>(); return Enumerable.Empty<LogSearchEntry>();
}
var lower = keyword.ToLowerInvariant(); var lower = keyword.ToLowerInvariant();
lock (_lockObject) lock (_lockObject)
@@ -106,7 +108,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> SearchByLogger(string loggerName) public IEnumerable<LogSearchEntry> SearchByLogger(string loggerName)
{ {
if (string.IsNullOrWhiteSpace(loggerName)) if (string.IsNullOrWhiteSpace(loggerName))
{
return Enumerable.Empty<LogSearchEntry>(); return Enumerable.Empty<LogSearchEntry>();
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -122,7 +126,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> SearchByCorrelationId(string correlationId) public IEnumerable<LogSearchEntry> SearchByCorrelationId(string correlationId)
{ {
if (string.IsNullOrWhiteSpace(correlationId)) if (string.IsNullOrWhiteSpace(correlationId))
{
return Enumerable.Empty<LogSearchEntry>(); return Enumerable.Empty<LogSearchEntry>();
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -151,7 +157,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> SearchByTimeRange(DateTime fromUtc, DateTime toUtc) public IEnumerable<LogSearchEntry> SearchByTimeRange(DateTime fromUtc, DateTime toUtc)
{ {
if (fromUtc > toUtc) if (fromUtc > toUtc)
{
throw new ArgumentException("fromUtc must be less than or equal to toUtc"); throw new ArgumentException("fromUtc must be less than or equal to toUtc");
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -167,7 +175,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> Search(LogSearchQuery query) public IEnumerable<LogSearchEntry> Search(LogSearchQuery query)
{ {
if (query == null) if (query == null)
{
return Enumerable.Empty<LogSearchEntry>(); return Enumerable.Empty<LogSearchEntry>();
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -241,7 +251,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> GetRecent(int count = 100) public IEnumerable<LogSearchEntry> GetRecent(int count = 100)
{ {
if (count <= 0) if (count <= 0)
{
count = 100; count = 100;
}
lock (_lockObject) lock (_lockObject)
{ {
@@ -261,7 +273,9 @@ namespace EonaCat.LogStack.Features
lock (_lockObject) lock (_lockObject)
{ {
if (_logIndex.Count == 0) if (_logIndex.Count == 0)
{
return new LogSearchStatistics(); return new LogSearchStatistics();
}
return new LogSearchStatistics return new LogSearchStatistics
{ {
@@ -36,19 +36,27 @@ namespace EonaCat.LogStack
int height = 10) int height = 10)
{ {
if (timeResolutionMinutes <= 0) if (timeResolutionMinutes <= 0)
{
throw new ArgumentException("Time resolution must be positive", nameof(timeResolutionMinutes)); throw new ArgumentException("Time resolution must be positive", nameof(timeResolutionMinutes));
}
var eventList = events?.ToList() ?? new List<LogEvent>(); var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0) if (eventList.Count == 0)
{
return "No events to visualize"; return "No events to visualize";
}
var timeBuckets = CreateTimeBuckets(eventList, timeResolutionMinutes); var timeBuckets = CreateTimeBuckets(eventList, timeResolutionMinutes);
if (timeBuckets.Count == 0) if (timeBuckets.Count == 0)
{
return "No valid time data"; return "No valid time data";
}
var maxCount = timeBuckets.Values.Max(); var maxCount = timeBuckets.Values.Max();
if (maxCount == 0) if (maxCount == 0)
{
return "All buckets empty"; return "All buckets empty";
}
return RenderHeatmap(timeBuckets, width, height, maxCount); return RenderHeatmap(timeBuckets, width, height, maxCount);
} }
@@ -60,7 +68,9 @@ namespace EonaCat.LogStack
{ {
var eventList = events?.ToList() ?? new List<LogEvent>(); var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0) if (eventList.Count == 0)
{
return "No events to visualize"; return "No events to visualize";
}
var levels = new[] { LogLevel.Trace, LogLevel.Debug, LogLevel.Information, LogLevel.Warning, LogLevel.Error, LogLevel.Critical }; var levels = new[] { LogLevel.Trace, LogLevel.Debug, LogLevel.Information, LogLevel.Warning, LogLevel.Error, LogLevel.Critical };
var levelCounts = levels.ToDictionary(l => l, l => eventList.Count(e => e.Level == l)); var levelCounts = levels.ToDictionary(l => l, l => eventList.Count(e => e.Level == l));
@@ -71,12 +81,17 @@ namespace EonaCat.LogStack
var maxCount = levelCounts.Values.Max(); var maxCount = levelCounts.Values.Max();
if (maxCount == 0) if (maxCount == 0)
{
return "No events"; return "No events";
}
foreach (var level in levels) foreach (var level in levels)
{ {
var count = levelCounts[level]; var count = levelCounts[level];
if (count == 0 && level != LogLevel.Information) continue; if (count == 0 && level != LogLevel.Information)
{
continue;
}
var percentage = (double)count / eventList.Count * 100; var percentage = (double)count / eventList.Count * 100;
var barLength = (int)((double)count / maxCount * (width - 20)); var barLength = (int)((double)count / maxCount * (width - 20));
@@ -95,7 +110,9 @@ namespace EonaCat.LogStack
{ {
var eventList = events?.ToList() ?? new List<LogEvent>(); var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0) if (eventList.Count == 0)
{
return "No events to visualize"; return "No events to visualize";
}
var categoryCounts = eventList var categoryCounts = eventList
.GroupBy(e => e.Category ?? "Unknown") .GroupBy(e => e.Category ?? "Unknown")
@@ -109,7 +126,9 @@ namespace EonaCat.LogStack
var maxCount = categoryCounts.Values.Max(); var maxCount = categoryCounts.Values.Max();
if (maxCount == 0) if (maxCount == 0)
{
return "No categories"; return "No categories";
}
foreach (var kvp in categoryCounts) foreach (var kvp in categoryCounts)
{ {
@@ -132,12 +151,16 @@ namespace EonaCat.LogStack
{ {
var eventList = events?.ToList() ?? new List<LogEvent>(); var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0) if (eventList.Count == 0)
{
return string.Empty; return string.Empty;
}
var timeBuckets = CreateTimeBuckets(eventList, (int)Math.Ceiling((double)GetTimeSpanMinutes(eventList) / buckets)); var timeBuckets = CreateTimeBuckets(eventList, (int)Math.Ceiling((double)GetTimeSpanMinutes(eventList) / buckets));
var maxCount = timeBuckets.Values.Max(); var maxCount = timeBuckets.Values.Max();
if (maxCount == 0) if (maxCount == 0)
{
return string.Empty; return string.Empty;
}
var sb = new StringBuilder(); var sb = new StringBuilder();
foreach (var bucket in timeBuckets.OrderBy(kvp => kvp.Key)) foreach (var bucket in timeBuckets.OrderBy(kvp => kvp.Key))
@@ -155,7 +178,9 @@ namespace EonaCat.LogStack
{ {
var eventList = events?.ToList() ?? new List<LogEvent>(); var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0) if (eventList.Count == 0)
{
return "No events"; return "No events";
}
// Create a 24x7 matrix (hours x days) // Create a 24x7 matrix (hours x days)
var hourlyMatrix = new int[24, 7]; var hourlyMatrix = new int[24, 7];
@@ -178,11 +203,17 @@ namespace EonaCat.LogStack
var maxValue = 0; var maxValue = 0;
for (int h = 0; h < 24; h++) for (int h = 0; h < 24; h++)
{
for (int d = 0; d < 7; d++) for (int d = 0; d < 7; d++)
{
maxValue = Math.Max(maxValue, hourlyMatrix[h, d]); maxValue = Math.Max(maxValue, hourlyMatrix[h, d]);
}
}
if (maxValue == 0) if (maxValue == 0)
{
return "No hourly data"; return "No hourly data";
}
for (int h = 0; h < 24; h++) for (int h = 0; h < 24; h++)
{ {
@@ -214,9 +245,13 @@ namespace EonaCat.LogStack
.AddMinutes(-(timestamp.Minute % bucketMinutes)); .AddMinutes(-(timestamp.Minute % bucketMinutes));
if (buckets.ContainsKey(bucketTime)) if (buckets.ContainsKey(bucketTime))
{
buckets[bucketTime]++; buckets[bucketTime]++;
}
else else
{
buckets[bucketTime] = 1; buckets[bucketTime] = 1;
}
} }
return buckets; return buckets;
@@ -259,23 +294,45 @@ namespace EonaCat.LogStack
sb.AppendLine(new string('=', width + 4)); sb.AppendLine(new string('=', width + 4));
if (sortedBuckets.Count > 0) if (sortedBuckets.Count > 0)
{
sb.AppendLine($"From: {sortedBuckets.First().Key:yyyy-MM-dd HH:mm}, To: {sortedBuckets.Last().Key:yyyy-MM-dd HH:mm}"); sb.AppendLine($"From: {sortedBuckets.First().Key:yyyy-MM-dd HH:mm}, To: {sortedBuckets.Last().Key:yyyy-MM-dd HH:mm}");
}
return sb.ToString(); return sb.ToString();
} }
private static char GetHeatCharacter(double intensity) private static char GetHeatCharacter(double intensity)
{ {
if (intensity < 0.01) return EmptyCharacter; if (intensity < 0.01)
if (intensity < 0.35) return ColdCharacter; {
if (intensity < 0.65) return CoolCharacter; return EmptyCharacter;
if (intensity < 0.85) return WarmCharacter; }
if (intensity < 0.35)
{
return ColdCharacter;
}
if (intensity < 0.65)
{
return CoolCharacter;
}
if (intensity < 0.85)
{
return WarmCharacter;
}
return HotCharacter; return HotCharacter;
} }
private static int GetTimeSpanMinutes(List<LogEvent> events) private static int GetTimeSpanMinutes(List<LogEvent> events)
{ {
if (events.Count == 0) return 1; if (events.Count == 0)
{
return 1;
}
var maxTicks = events.Max(e => e.Timestamp); var maxTicks = events.Max(e => e.Timestamp);
var minTicks = events.Min(e => e.Timestamp); var minTicks = events.Min(e => e.Timestamp);
var span = new TimeSpan(maxTicks - minTicks); var span = new TimeSpan(maxTicks - minTicks);
@@ -65,10 +65,14 @@ public class AdaptiveSamplingEngine
// Use random sampling based on current rate // Use random sampling based on current rate
if (_samplingRate >= 1.0) if (_samplingRate >= 1.0)
{
return true; return true;
}
if (_samplingRate <= 0.0) if (_samplingRate <= 0.0)
{
return false; return false;
}
return ThreadSafeRandom.NextDouble() < _samplingRate; return ThreadSafeRandom.NextDouble() < _samplingRate;
} }
@@ -164,7 +168,9 @@ public class AdaptiveSamplingEngine
{ {
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
if ((now - _lastCpuCheck).TotalMilliseconds < 1000) if ((now - _lastCpuCheck).TotalMilliseconds < 1000)
{
return; // Update at most once per second to avoid overhead return; // Update at most once per second to avoid overhead
}
_lastCpuCheck = now; _lastCpuCheck = now;
@@ -113,7 +113,9 @@ public class AnomalyDetector
{ {
_recentAlerts.Add(alert); _recentAlerts.Add(alert);
if (_recentAlerts.Count > 1000) if (_recentAlerts.Count > 1000)
{
_recentAlerts.RemoveAt(0); // Keep last 1000 alerts _recentAlerts.RemoveAt(0); // Keep last 1000 alerts
}
} }
AnomalyDetected?.Invoke(this, alert); AnomalyDetected?.Invoke(this, alert);
@@ -180,10 +182,14 @@ public class AnomalyDetector
private AnomalySeverity DetermineAnomalySeverity(List<string> anomalies) private AnomalySeverity DetermineAnomalySeverity(List<string> anomalies)
{ {
if (anomalies.Any(a => a.Contains("error rate"))) if (anomalies.Any(a => a.Contains("error rate")))
{
return AnomalySeverity.Critical; return AnomalySeverity.Critical;
}
if (anomalies.Any(a => a.Contains("Exception") || a.Contains("infinite loop"))) if (anomalies.Any(a => a.Contains("Exception") || a.Contains("infinite loop")))
{
return AnomalySeverity.High; return AnomalySeverity.High;
}
return AnomalySeverity.Medium; return AnomalySeverity.Medium;
} }
@@ -222,10 +228,14 @@ internal class CategoryStatistics
EventCount++; EventCount++;
_recentEventTimes.Enqueue(DateTime.UtcNow); _recentEventTimes.Enqueue(DateTime.UtcNow);
if (_recentEventTimes.Count > 1000) if (_recentEventTimes.Count > 1000)
{
_recentEventTimes.Dequeue(); _recentEventTimes.Dequeue();
}
if (logEvent.Level == LogLevel.Error || logEvent.Level == LogLevel.Critical) if (logEvent.Level == LogLevel.Error || logEvent.Level == LogLevel.Critical)
{
_errorCount++; _errorCount++;
}
} }
} }
@@ -244,7 +254,9 @@ internal class CategoryStatistics
lock (_lock) lock (_lock)
{ {
if (_recentEventTimes.Count < 10) if (_recentEventTimes.Count < 10)
{
return false; return false;
}
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var times = _recentEventTimes.ToList(); var times = _recentEventTimes.ToList();
@@ -254,11 +266,15 @@ internal class CategoryStatistics
{ {
var interval = (now - times[i]).TotalSeconds; var interval = (now - times[i]).TotalSeconds;
if (interval > 0) if (interval > 0)
{
recentIntervals.Add(interval); recentIntervals.Add(interval);
}
} }
if (recentIntervals.Count < 2) if (recentIntervals.Count < 2)
{
return false; return false;
}
var mean = recentIntervals.Average(); var mean = recentIntervals.Average();
var variance = recentIntervals.Average(x => Math.Pow(x - mean, 2)); var variance = recentIntervals.Average(x => Math.Pow(x - mean, 2));
@@ -286,7 +302,9 @@ internal class LevelStatistics
_errorCount++; _errorCount++;
_eventTimes.Enqueue(DateTime.UtcNow); _eventTimes.Enqueue(DateTime.UtcNow);
if (_eventTimes.Count > 10000) if (_eventTimes.Count > 10000)
{
_eventTimes.Dequeue(); _eventTimes.Dequeue();
}
} }
} }
@@ -295,13 +313,17 @@ internal class LevelStatistics
lock (_lock) lock (_lock)
{ {
if (_eventTimes.Count < 2) if (_eventTimes.Count < 2)
{
return 0; return 0;
}
var now = DateTime.UtcNow; var now = DateTime.UtcNow;
var span = (now - _eventTimes.Peek()).TotalSeconds; var span = (now - _eventTimes.Peek()).TotalSeconds;
if (span < 1) if (span < 1)
{
return _eventTimes.Count; return _eventTimes.Count;
}
return _eventTimes.Count / span; return _eventTimes.Count / span;
} }
@@ -38,7 +38,9 @@ public class ContextSnapshotCollector
public ContextSnapshot CaptureIfNeeded(LogEvent logEvent) public ContextSnapshot CaptureIfNeeded(LogEvent logEvent)
{ {
if (logEvent.Level < _triggerLevel) if (logEvent.Level < _triggerLevel)
{
return null; return null;
}
return CaptureSnapshot(logEvent); return CaptureSnapshot(logEvent);
} }
@@ -64,7 +66,9 @@ public class ContextSnapshotCollector
{ {
_snapshots.Add(snapshot); _snapshots.Add(snapshot);
if (_snapshots.Count > _maxSnapshots) if (_snapshots.Count > _maxSnapshots)
{
_snapshots.RemoveAt(0); _snapshots.RemoveAt(0);
}
_totalSnapshotsCaptured++; _totalSnapshotsCaptured++;
} }
@@ -257,7 +261,9 @@ public class ContextSnapshotCollector
{ {
var varValue = Environment.GetEnvironmentVariable(varName); var varValue = Environment.GetEnvironmentVariable(varName);
if (varValue != null) if (varValue != null)
{
env[varName] = varValue; env[varName] = varValue;
}
} }
// Add runtime info // Add runtime info
@@ -111,7 +111,9 @@ public class DeadLetterQueue
public async Task<bool> ReplayAsync(DeadLetterEvent dlEvent, IEnumerable<IFlow> targetFlows) public async Task<bool> ReplayAsync(DeadLetterEvent dlEvent, IEnumerable<IFlow> targetFlows)
{ {
if (dlEvent == null) if (dlEvent == null)
{
throw new ArgumentNullException(nameof(dlEvent)); throw new ArgumentNullException(nameof(dlEvent));
}
dlEvent.RetryCount++; dlEvent.RetryCount++;
dlEvent.LastRetryAt = DateTime.UtcNow; dlEvent.LastRetryAt = DateTime.UtcNow;
@@ -207,7 +209,10 @@ public class DeadLetterQueue
{ {
var reason = evt.FailureReason ?? "Unknown"; var reason = evt.FailureReason ?? "Unknown";
if (!stats.ContainsKey(reason)) if (!stats.ContainsKey(reason))
{
stats[reason] = 0; stats[reason] = 0;
}
stats[reason]++; stats[reason]++;
} }
return stats.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value); return stats.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value);
@@ -135,7 +135,9 @@ public sealed class DelegatingLoggerFlow : FlowBase
lock (_queueLock) lock (_queueLock)
{ {
if (_delegationQueue.Count == 0) if (_delegationQueue.Count == 0)
{
return; return;
}
pendingEvents = new Queue<LogEvent>(_delegationQueue); pendingEvents = new Queue<LogEvent>(_delegationQueue);
_delegationQueue.Clear(); _delegationQueue.Clear();
@@ -170,7 +172,9 @@ public sealed class DelegatingLoggerFlow : FlowBase
private void DelegateLogEvent(LogEvent logEvent) private void DelegateLogEvent(LogEvent logEvent)
{ {
if (_disposed != 0) if (_disposed != 0)
{
return; return;
}
try try
{ {
@@ -199,7 +203,9 @@ public sealed class DelegatingLoggerFlow : FlowBase
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
{ {
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
{
return; return;
}
await FlushAsync().ConfigureAwait(false); await FlushAsync().ConfigureAwait(false);
@@ -124,8 +124,7 @@ namespace EonaCat.LogStack.Flows
/// <summary>Current value of a named counter (0 if not yet created).</summary> /// <summary>Current value of a named counter (0 if not yet created).</summary>
public long ReadCounter(string name) public long ReadCounter(string name)
{ {
Counter c; return _counters.TryGetValue(name, out Counter c) ? c.Value : 0;
return _counters.TryGetValue(name, out c) ? c.Value : 0;
} }
public override Task<WriteResult> BlastAsync( public override Task<WriteResult> BlastAsync(
@@ -40,7 +40,11 @@ public class DlqFlow : FlowBase
/// </summary> /// </summary>
public DlqFlow AddBackupFlow(IFlow flow) public DlqFlow AddBackupFlow(IFlow flow)
{ {
if (flow == null) throw new ArgumentNullException(nameof(flow)); if (flow == null)
{
throw new ArgumentNullException(nameof(flow));
}
lock (_flowsLock) lock (_flowsLock)
{ {
_backupFlows.Add(flow); _backupFlows.Add(flow);
@@ -54,10 +58,14 @@ public class DlqFlow : FlowBase
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default) public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{ {
if (_disposed != 0) if (_disposed != 0)
{
return WriteResult.Dropped; return WriteResult.Dropped;
}
if (!IsLogLevelEnabled(logEvent)) if (!IsLogLevelEnabled(logEvent))
{
return WriteResult.Success; return WriteResult.Success;
}
// Try backup flows first before enqueueing to DLQ // Try backup flows first before enqueueing to DLQ
List<IFlow> backupFlows; List<IFlow> backupFlows;
@@ -96,7 +104,9 @@ public class DlqFlow : FlowBase
// All backups failed, enqueue to DLQ // All backups failed, enqueue to DLQ
var failureReason = string.Join("; ", failureReasons); var failureReason = string.Join("; ", failureReasons);
if (failureReason.Length > 500) if (failureReason.Length > 500)
{
failureReason = failureReason.Substring(0, 500) + "..."; failureReason = failureReason.Substring(0, 500) + "...";
}
var enqueued = _dlq.Enqueue(logEvent, failureReason ?? "Unknown failure"); var enqueued = _dlq.Enqueue(logEvent, failureReason ?? "Unknown failure");
@@ -116,7 +126,9 @@ public class DlqFlow : FlowBase
public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default) public override async Task<WriteResult> BlastBatchAsync(ReadOnlyMemory<LogEvent> logEvents, CancellationToken cancellationToken = default)
{ {
if (_disposed != 0) if (_disposed != 0)
{
return WriteResult.Dropped; return WriteResult.Dropped;
}
var events = logEvents.ToArray(); var events = logEvents.ToArray();
int successCount = 0; int successCount = 0;
@@ -125,7 +137,9 @@ public class DlqFlow : FlowBase
{ {
var result = await BlastAsync(logEvent, cancellationToken).ConfigureAwait(false); var result = await BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
if (result == WriteResult.Success) if (result == WriteResult.Success)
{
successCount++; successCount++;
}
} }
return successCount > 0 ? WriteResult.Success : WriteResult.Dropped; return successCount > 0 ? WriteResult.Success : WriteResult.Dropped;
@@ -180,7 +194,9 @@ public class DlqFlow : FlowBase
public override async ValueTask DisposeAsync() public override async ValueTask DisposeAsync()
{ {
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
{
return; return;
}
List<IFlow> flowsToDispose; List<IFlow> flowsToDispose;
lock (_flowsLock) lock (_flowsLock)
@@ -2,6 +2,7 @@ using EonaCat.LogStack.Core;
using EonaCat.LogStack.EonaCatLogStackCore; using EonaCat.LogStack.EonaCatLogStackCore;
using EonaCat.LogStack.EonaCatLogStackCore.Policies; using EonaCat.LogStack.EonaCatLogStackCore.Policies;
using System; using System;
using System.Buffers;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
@@ -116,6 +117,7 @@ namespace EonaCat.LogStack.Flows
// Auto-flush on error // Auto-flush on error
private volatile bool _autoFlushOnError; private volatile bool _autoFlushOnError;
private volatile bool _durableWrites;
// Scoped properties (AsyncLocal for ambient context) // Scoped properties (AsyncLocal for ambient context)
private static readonly AsyncLocal<Dictionary<string, object>> _scopeProperties private static readonly AsyncLocal<Dictionary<string, object>> _scopeProperties
@@ -136,10 +138,9 @@ namespace EonaCat.LogStack.Flows
private static readonly int CachedPid = Process.GetCurrentProcess().Id; private static readonly int CachedPid = Process.GetCurrentProcess().Id;
private List<Action<LogEvent, StringBuilder>> _compiledTemplate; private List<Action<LogEvent, StringBuilder>> _compiledTemplate;
private readonly Dictionary<string, Action<LogEvent, StringBuilder>> _customTokens private readonly Dictionary<string, Action<LogEvent, StringBuilder>> _customTokens = new Dictionary<string, Action<LogEvent, StringBuilder>>(StringComparer.OrdinalIgnoreCase);
= new Dictionary<string, Action<LogEvent, StringBuilder>>(StringComparer.OrdinalIgnoreCase);
private int _correlationSeed; private int _correlationSeed;
public string EncryptedFileExtension { get; set; } = ".eona";
public EncryptedFileFlow( public EncryptedFileFlow(
string directory, string directory,
@@ -148,7 +149,7 @@ namespace EonaCat.LogStack.Flows
long maxFileSize = 50L * 1024 * 1024, long maxFileSize = 50L * 1024 * 1024,
long maxDirectorySize = 2L * 1024 * 1024 * 1024, long maxDirectorySize = 2L * 1024 * 1024 * 1024,
FileRetentionPolicy retention = null, FileRetentionPolicy retention = null,
int flushIntervalMs = 3000, int flushIntervalMs = 250,
int batchSize = 1, int batchSize = 1,
LogLevel minimumLevel = LogLevel.Trace, LogLevel minimumLevel = LogLevel.Trace,
bool useCategoryRouting = false, bool useCategoryRouting = false,
@@ -299,6 +300,28 @@ namespace EonaCat.LogStack.Flows
return this; return this;
} }
/// <summary>Registers this flow to flush and dispose automatically on process exit
/// (ProcessExit) and Ctrl+C (CancelKeyPress). Opt-in only — call this explicitly if
/// your application doesn't already have its own shutdown/dispose sequence for this flow.
/// Not recommended if you're hosting inside ASP.NET Core, a Windows Service, or any
/// framework with its own graceful-shutdown lifecycle — hook into that instead.</summary>
public EncryptedFileFlow WithAutoShutdownHooks()
{
AppDomain.CurrentDomain.ProcessExit += (_, _) =>
{
try { DisposeAsync().AsTask().Wait(TimeSpan.FromSeconds(5)); } catch { }
};
Console.CancelKeyPress += (_, e) =>
{
e.Cancel = true;
try { DisposeAsync().AsTask().Wait(TimeSpan.FromSeconds(5)); } catch { }
Environment.Exit(0);
};
return this;
}
/// <summary>Callback invoked with the archived path after each file rotation.</summary> /// <summary>Callback invoked with the archived path after each file rotation.</summary>
public EncryptedFileFlow OnFileRotated(Action<string> callback) public EncryptedFileFlow OnFileRotated(Action<string> callback)
{ {
@@ -365,6 +388,18 @@ namespace EonaCat.LogStack.Flows
return this; return this;
} }
/// <summary>When enabled, every encrypted line is flushed to the physical disk
/// (StreamWriter-equivalent FileStream buffer + OS write cache) before the write call
/// returns, instead of relying on the periodic FlushLoop. Eliminates data loss on abrupt
/// process termination at the cost of significantly reduced throughput — each line becomes
/// a synchronous disk I/O. Recommended for audit-trail / compliance use cases, which is
/// arguably the common case for an encrypted log sink.</summary>
public EncryptedFileFlow WithDurableWrites(bool enabled = true)
{
_durableWrites = enabled;
return this;
}
/// <summary>Enable deduplication: suppress identical messages within the given time window.</summary> /// <summary>Enable deduplication: suppress identical messages within the given time window.</summary>
public EncryptedFileFlow WithDeduplication(TimeSpan window) public EncryptedFileFlow WithDeduplication(TimeSpan window)
{ {
@@ -563,6 +598,11 @@ namespace EonaCat.LogStack.Flows
aes.Key = key; aes.Key = key;
aes.IV = iv; aes.IV = iv;
if (!Directory.Exists(Path.GetDirectoryName(outputPath)))
{
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
}
using (ICryptoTransform dec = aes.CreateDecryptor()) using (ICryptoTransform dec = aes.CreateDecryptor())
using (StreamWriter out_ = new StreamWriter(outputPath, false, Encoding.UTF8)) using (StreamWriter out_ = new StreamWriter(outputPath, false, Encoding.UTF8))
{ {
@@ -585,8 +625,8 @@ namespace EonaCat.LogStack.Flows
ReadExact(source, cipher, blockLength); ReadExact(source, cipher, blockLength);
byte[] plain = dec.TransformFinalBlock(cipher, 0, cipher.Length); byte[] plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
out_.WriteLine(Encoding.UTF8.GetString(plain)); out_.WriteLine(Encoding.UTF8.GetString(plain));
return true;
} }
return true;
} }
} }
} }
@@ -729,10 +769,11 @@ namespace EonaCat.LogStack.Flows
IsEnabled = false; IsEnabled = false;
_queue.CompleteAdding(); _queue.CompleteAdding();
_cts.Cancel();
_writerThread.Join(TimeSpan.FromSeconds(5)); _writerThread.Join(TimeSpan.FromSeconds(5));
_flushThread.Join(TimeSpan.FromSeconds(2)); _flushThread.Join(TimeSpan.FromSeconds(2));
_retentionThread.Join(TimeSpan.FromSeconds(2)); _retentionThread.Join(TimeSpan.FromSeconds(2));
_cts.Cancel();
lock (_lock) { CloseCurrentFile(); } lock (_lock) { CloseCurrentFile(); }
_cts.Dispose(); _cts.Dispose();
_queue.Dispose(); _queue.Dispose();
@@ -752,9 +793,8 @@ namespace EonaCat.LogStack.Flows
WriteEncryptedLine(entry); WriteEncryptedLine(entry);
QueueEntry extra;
int batch = 0; int batch = 0;
while (batch < _batchSize && _queue.TryTake(out extra)) while (batch < _batchSize && _queue.TryTake(out QueueEntry extra))
{ {
WriteEncryptedLine(extra); WriteEncryptedLine(extra);
batch++; batch++;
@@ -767,8 +807,7 @@ namespace EonaCat.LogStack.Flows
} }
finally finally
{ {
QueueEntry remaining; while (_queue.TryTake(out QueueEntry remaining))
while (_queue.TryTake(out remaining))
{ {
WriteEncryptedLine(remaining); WriteEncryptedLine(remaining);
} }
@@ -781,8 +820,10 @@ namespace EonaCat.LogStack.Flows
{ {
while (!_cts.Token.IsCancellationRequested) while (!_cts.Token.IsCancellationRequested)
{ {
try { Thread.Sleep(_flushIntervalMs); } if (_cts.Token.WaitHandle.WaitOne(_flushIntervalMs))
catch (ThreadInterruptedException) { break; } {
break;
}
lock (_lock) lock (_lock)
{ {
@@ -827,18 +868,36 @@ namespace EonaCat.LogStack.Flows
{ {
byte[] plain = Encoding.UTF8.GetBytes(entry.Line); byte[] plain = Encoding.UTF8.GetBytes(entry.Line);
byte[] cipher = _encryptor.TransformFinalBlock(plain, 0, plain.Length); byte[] cipher = _encryptor.TransformFinalBlock(plain, 0, plain.Length);
byte[] lenBuf = BitConverter.GetBytes(cipher.Length); byte[] frame = ArrayPool<byte>.Shared.Rent(4 + cipher.Length);
try
{
frame[0] = (byte)cipher.Length;
frame[1] = (byte)(cipher.Length >> 8);
frame[2] = (byte)(cipher.Length >> 16);
frame[3] = (byte)(cipher.Length >> 24);
Buffer.BlockCopy(cipher, 0, frame, 4, cipher.Length);
_currentStream.Write(frame, 0, 4 + cipher.Length);
}
finally
{
ArrayPool<byte>.Shared.Return(frame);
}
_currentStream.Write(lenBuf, 0, 4);
_currentStream.Write(cipher, 0, cipher.Length);
_currentSize += 4 + cipher.Length; _currentSize += 4 + cipher.Length;
if (_durableWrites)
{
try { _currentStream.Flush(true); } catch { /* surfaced via outer catch below on failure */ }
}
Interlocked.Increment(ref _totalWritten); Interlocked.Increment(ref _totalWritten);
Interlocked.Increment(ref BlastedCount); Interlocked.Increment(ref BlastedCount);
Interlocked.Add(ref _totalBytesWritten, 4 + cipher.Length); Interlocked.Add(ref _totalBytesWritten, 4 + cipher.Length);
// Auto-flush on error/critical // Auto-flush on error/critical
if (_autoFlushOnError && entry.Level >= LogLevel.Error) if (!_durableWrites && _autoFlushOnError && entry.Level >= LogLevel.Error)
{ {
try { _currentStream.Flush(true); } catch { /* ignore */ } try { _currentStream.Flush(true); } catch { /* ignore */ }
} }
@@ -848,6 +907,21 @@ namespace EonaCat.LogStack.Flows
_lastError = ex; _lastError = ex;
Interlocked.Increment(ref _totalErrors); Interlocked.Increment(ref _totalErrors);
Interlocked.Exchange(ref _lastErrorTimestamp, DateTime.UtcNow.Ticks); Interlocked.Exchange(ref _lastErrorTimestamp, DateTime.UtcNow.Ticks);
// A write failure means this event is lost — make that visible via the
// same drop-reporting path used for backpressure/rate-limit drops, instead
// of only surfacing it through GetTotalErrors()/GetLastError().
Interlocked.Increment(ref DroppedCount);
Action<LogEvent> drop = _onDrop;
if (drop != null)
{
try
{
drop(new LogEvent { Level = entry.Level, Message = default });
}
catch { /* Do nothing */ }
}
OnException?.Invoke(null, "[EncryptedFileFlow] Write error: " + ex.Message); OnException?.Invoke(null, "[EncryptedFileFlow] Write error: " + ex.Message);
} }
} }
@@ -856,29 +930,28 @@ namespace EonaCat.LogStack.Flows
private void OpenNewFile(DateTime date) private void OpenNewFile(DateTime date)
{ {
_currentDate = date; _currentDate = date;
_currentPath = Path.Combine( _currentPath = Path.Combine(_directory, _filePrefix + "_" + Environment.MachineName + "_" + date.ToString(_dateFormat) + EncryptedFileExtension);
_directory,
_filePrefix + "_" + Environment.MachineName + "_" + date.ToString(_dateFormat) + ".eona");
bool isNew = !File.Exists(_currentPath) || new FileInfo(_currentPath).Length == 0; bool isNew = !File.Exists(_currentPath) || new FileInfo(_currentPath).Length == 0;
_currentStream = new FileStream( _currentStream = new FileStream(_currentPath, FileMode.Append, FileAccess.Write, FileShare.Read, 65536, _durableWrites ? FileOptions.WriteThrough : FileOptions.None);
_currentPath, FileMode.Append, FileAccess.Write, FileShare.Read, 65536);
byte[] salt = new byte[SaltSize]; byte[] salt = new byte[SaltSize];
byte[] iv = new byte[IvSize]; byte[] iv = new byte[IvSize];
if (isNew) if (isNew)
{ {
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider()) FillRandom(salt);
{ FillRandom(iv);
rng.GetBytes(salt);
rng.GetBytes(iv);
}
_currentStream.Write(Magic, 0, 4); _currentStream.Write(Magic, 0, 4);
_currentStream.Write(salt, 0, SaltSize); _currentStream.Write(salt, 0, SaltSize);
_currentStream.Write(iv, 0, IvSize); _currentStream.Write(iv, 0, IvSize);
_currentSize = 4 + SaltSize + IvSize; _currentSize = 4 + SaltSize + IvSize;
if (_durableWrites)
{
try { _currentStream.Flush(true); } catch { /* ignore */ }
}
} }
else else
{ {
@@ -894,13 +967,45 @@ namespace EonaCat.LogStack.Flows
byte[] key = DeriveKey(_password, salt); byte[] key = DeriveKey(_password, salt);
Aes aes = Aes.Create(); // The Aes instance is only needed to produce the transform below; it must be
aes.KeySize = 256; // disposed here rather than kept alive, otherwise every file open/rotation
aes.Mode = CipherMode.CBC; // (daily, or on maxFileSize) leaks an Aes instance and its key material for
aes.Padding = PaddingMode.PKCS7; // the lifetime of the process.
aes.Key = key; using (Aes aes = Aes.Create())
aes.IV = iv; {
_encryptor = aes.CreateEncryptor(); aes.KeySize = 256;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
aes.IV = iv;
_encryptor = aes.CreateEncryptor();
}
}
private static void ClearSecret(byte[] bytes)
{
if (bytes == null)
{
return;
}
#if NET6_0_OR_GREATER
CryptographicOperations.ZeroMemory(bytes);
#else
Array.Clear(bytes, 0, bytes.Length);
#endif
}
private static void FillRandom(byte[] buffer)
{
#if NET6_0_OR_GREATER
RandomNumberGenerator.Fill(buffer);
#else
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(buffer);
}
#endif
} }
private void CloseCurrentFile() private void CloseCurrentFile()
@@ -937,15 +1042,22 @@ namespace EonaCat.LogStack.Flows
string line = Format(logEvent); string line = Format(logEvent);
var entry = new QueueEntry { Line = line, Level = logEvent.Level }; var entry = new QueueEntry { Line = line, Level = logEvent.Level };
if (!_queue.TryAdd(entry)) try
{ {
Interlocked.Increment(ref DroppedCount); if (!_queue.TryAdd(entry))
Action<LogEvent> drop = _onDrop;
if (drop != null)
{ {
drop(logEvent); Interlocked.Increment(ref DroppedCount);
} Action<LogEvent> drop = _onDrop;
if (drop != null)
{
drop(logEvent);
}
return WriteResult.Dropped;
}
}
catch (Exception)
{
return WriteResult.Dropped; return WriteResult.Dropped;
} }
@@ -1022,8 +1134,7 @@ namespace EonaCat.LogStack.Flows
long nowTicks = DateTime.UtcNow.Ticks; long nowTicks = DateTime.UtcNow.Ticks;
long windowTicks = _deduplicationWindow.Ticks; long windowTicks = _deduplicationWindow.Ticks;
long existing; if (_deduplicationCache.TryGetValue(key, out long existing))
if (_deduplicationCache.TryGetValue(key, out existing))
{ {
if (nowTicks - existing < windowTicks) if (nowTicks - existing < windowTicks)
{ {
@@ -1047,20 +1158,26 @@ namespace EonaCat.LogStack.Flows
{ {
if (nowTicks - kvp.Value >= windowTicks) if (nowTicks - kvp.Value >= windowTicks)
{ {
long removed; _deduplicationCache.TryRemove(kvp.Key, out long removed);
_deduplicationCache.TryRemove(kvp.Key, out removed);
} }
} }
} }
private string Format(LogEvent log) private string Format(LogEvent log)
{ {
var sb = new StringBuilder(256); StringBuilder sb = StringBuilderPool.Rent();
foreach (Action<LogEvent, StringBuilder> action in _compiledTemplate) try
{ {
action(log, sb); foreach (Action<LogEvent, StringBuilder> action in _compiledTemplate)
{
action(log, sb);
}
return sb.ToString();
}
finally
{
StringBuilderPool.Return(sb);
} }
return sb.ToString();
} }
private void AppendProperties(LogEvent log, StringBuilder sb) private void AppendProperties(LogEvent log, StringBuilder sb)
@@ -1174,8 +1291,7 @@ namespace EonaCat.LogStack.Flows
case "logtype": case "logtype":
return (log, sb) => return (log, sb) =>
{ {
string s; sb.Append(LevelStrings.TryGetValue(log.Level, out string s) ? s : log.Level.ToString());
sb.Append(LevelStrings.TryGetValue(log.Level, out s) ? s : log.Level.ToString());
}; };
case "message": case "message":
return (log, sb) => return (log, sb) =>
@@ -1225,8 +1341,7 @@ namespace EonaCat.LogStack.Flows
string name = token; string name = token;
return (log, sb) => return (log, sb) =>
{ {
Action<LogEvent, StringBuilder> custom; if (_customTokens.TryGetValue(name, out Action<LogEvent, StringBuilder> custom))
if (_customTokens.TryGetValue(name, out custom))
{ {
custom(log, sb); custom(log, sb);
} }
@@ -1267,10 +1382,13 @@ namespace EonaCat.LogStack.Flows
return; return;
} }
FileInfo[] files = dir.GetFiles("*.eona") // failsafe
.OrderByDescending(f => f.LastWriteTimeUtc) if (string.IsNullOrWhiteSpace(EncryptedFileExtension))
.ToArray(); {
EncryptedFileExtension = ".eona";
}
FileInfo[] files = dir.GetFiles($"*.{EncryptedFileExtension}").OrderByDescending(f => f.LastWriteTimeUtc).ToArray();
long totalBytes = 0; long totalBytes = 0;
int kept = 0; int kept = 0;
@@ -1315,23 +1433,21 @@ namespace EonaCat.LogStack.Flows
return s; return s;
} }
private static string LevelString(LogLevel level)
{
switch (level)
{
case LogLevel.Trace: return "TRACE";
case LogLevel.Debug: return "DEBUG";
case LogLevel.Information: return "INFO";
case LogLevel.Warning: return "WARN";
case LogLevel.Error: return "ERROR";
case LogLevel.Critical: return "CRITICAL";
default: return level.ToString().ToUpperInvariant();
}
}
private static byte[] DeriveKey(string password, byte[] salt) private static byte[] DeriveKey(string password, byte[] salt)
{ {
using (Rfc2898DeriveBytes kdf = new Rfc2898DeriveBytes(password, salt, Pbkdf2Iter)) #if NET6_0_OR_GREATER
using (var kdf = new Rfc2898DeriveBytes(
password,
salt,
Pbkdf2Iter,
HashAlgorithmName.SHA256))
#else
// SHA1 is the only option on older .NET Framework
using (var kdf = new Rfc2898DeriveBytes(
password,
salt,
Pbkdf2Iter))
#endif
{ {
return kdf.GetBytes(KeySize); return kdf.GetBytes(KeySize);
} }
File diff suppressed because it is too large Load Diff
@@ -177,9 +177,8 @@ namespace EonaCat.LogStack.Flows
SendToRedis(msg); SendToRedis(msg);
string extra;
int batch = 0; int batch = 0;
while (batch < 64 && _queue.TryTake(out extra)) while (batch < 64 && _queue.TryTake(out string extra))
{ {
SendToRedis(extra); SendToRedis(extra);
batch++; batch++;
@@ -132,7 +132,6 @@ namespace EonaCat.LogStack.Flows
if (_deduplicate) if (_deduplicate)
{ {
string key = MakeDedupKey(logEvent); string key = MakeDedupKey(logEvent);
DedupEntry entry;
// Flush expired entries to avoid unbounded growth // Flush expired entries to avoid unbounded growth
if (_dedupMap.Count >= _dedupMaxKeys) if (_dedupMap.Count >= _dedupMaxKeys)
@@ -140,7 +139,7 @@ namespace EonaCat.LogStack.Flows
PurgeExpiredDedupEntries(); PurgeExpiredDedupEntries();
} }
if (_dedupMap.TryGetValue(key, out entry)) if (_dedupMap.TryGetValue(key, out DedupEntry entry))
{ {
TimeSpan age = DateTime.UtcNow - entry.FirstSeen; TimeSpan age = DateTime.UtcNow - entry.FirstSeen;
if (age < _dedupWindow) if (age < _dedupWindow)
@@ -172,8 +171,7 @@ namespace EonaCat.LogStack.Flows
} }
// token bucket pass // token bucket pass
Bucket bucket; if (!_buckets.TryGetValue(logEvent.Level, out Bucket bucket))
if (!_buckets.TryGetValue(logEvent.Level, out bucket))
{ {
bucket = new Bucket(_burstCapacity, _refillPerSecond); bucket = new Bucket(_burstCapacity, _refillPerSecond);
_buckets[logEvent.Level] = bucket; _buckets[logEvent.Level] = bucket;
@@ -221,8 +219,7 @@ namespace EonaCat.LogStack.Flows
List<string> keys = new List<string>(_dedupMap.Keys); List<string> keys = new List<string>(_dedupMap.Keys);
foreach (string key in keys) foreach (string key in keys)
{ {
DedupEntry entry; if (_dedupMap.TryGetValue(key, out DedupEntry entry) && entry.Count > 1)
if (_dedupMap.TryGetValue(key, out entry) && entry.Count > 1)
{ {
FlushDedupEntry(key, entry); FlushDedupEntry(key, entry);
} }
@@ -290,8 +287,7 @@ namespace EonaCat.LogStack.Flows
foreach (string k in expired) foreach (string k in expired)
{ {
DedupEntry entry; if (_dedupMap.TryGetValue(k, out DedupEntry entry) && entry.Count > 1)
if (_dedupMap.TryGetValue(k, out entry) && entry.Count > 1)
{ {
FlushDedupEntry(k, entry); FlushDedupEntry(k, entry);
} }
@@ -30,7 +30,11 @@ public class IntelligentRouter : IAsyncDisposable
/// </summary> /// </summary>
public IntelligentRouter AddRule(RoutingRule rule) public IntelligentRouter AddRule(RoutingRule rule)
{ {
if (rule == null) throw new ArgumentNullException(nameof(rule)); if (rule == null)
{
throw new ArgumentNullException(nameof(rule));
}
lock (_rulesLock) lock (_rulesLock)
{ {
_rules.Add(rule); _rules.Add(rule);
@@ -117,8 +121,15 @@ public class IntelligentRouter : IAsyncDisposable
/// </summary> /// </summary>
public IntelligentRouter RegisterFlow(string flowName, IFlow flow) public IntelligentRouter RegisterFlow(string flowName, IFlow flow)
{ {
if (string.IsNullOrEmpty(flowName)) throw new ArgumentNullException(nameof(flowName)); if (string.IsNullOrEmpty(flowName))
if (flow == null) throw new ArgumentNullException(nameof(flow)); {
throw new ArgumentNullException(nameof(flowName));
}
if (flow == null)
{
throw new ArgumentNullException(nameof(flow));
}
lock (_rulesLock) lock (_rulesLock)
{ {
@@ -190,7 +201,9 @@ public class IntelligentRouter : IAsyncDisposable
.ContinueWith(t => .ContinueWith(t =>
{ {
if (t.Status == TaskStatus.RanToCompletion && t.Result == WriteResult.Success) if (t.Status == TaskStatus.RanToCompletion && t.Result == WriteResult.Success)
{
anySuccess = true; anySuccess = true;
}
})); }));
} }
} }
@@ -73,15 +73,21 @@ public sealed class LoggerChain : ILoggerChain
ThrowIfDisposed(); ThrowIfDisposed();
if (logger == null) if (logger == null)
{
throw new ArgumentNullException(nameof(logger)); throw new ArgumentNullException(nameof(logger));
}
if (logger == _primary) if (logger == _primary)
{
throw new InvalidOperationException("Cannot chain the primary logger to itself"); throw new InvalidOperationException("Cannot chain the primary logger to itself");
}
lock (_chainLock) lock (_chainLock)
{ {
if (_chainedLoggers.Contains(logger)) if (_chainedLoggers.Contains(logger))
{
return this; // Already in chain return this; // Already in chain
}
_chainedLoggers.Add(logger); _chainedLoggers.Add(logger);
_loggerStates[logger] = true; // Enabled by default _loggerStates[logger] = true; // Enabled by default
@@ -109,7 +115,9 @@ public sealed class LoggerChain : ILoggerChain
public ILoggerChain AddRange(params EonaCatLogStack[] loggers) public ILoggerChain AddRange(params EonaCatLogStack[] loggers)
{ {
if (loggers == null) if (loggers == null)
{
throw new ArgumentNullException(nameof(loggers)); throw new ArgumentNullException(nameof(loggers));
}
foreach (var logger in loggers) foreach (var logger in loggers)
{ {
@@ -127,7 +135,9 @@ public sealed class LoggerChain : ILoggerChain
ThrowIfDisposed(); ThrowIfDisposed();
if (logger == null) if (logger == null)
{
return false; return false;
}
lock (_chainLock) lock (_chainLock)
{ {
@@ -192,7 +202,9 @@ public sealed class LoggerChain : ILoggerChain
public void DisableLogger(EonaCatLogStack logger) public void DisableLogger(EonaCatLogStack logger)
{ {
if (logger == null) if (logger == null)
{
return; return;
}
lock (_chainLock) lock (_chainLock)
{ {
@@ -209,7 +221,9 @@ public sealed class LoggerChain : ILoggerChain
public void EnableLogger(EonaCatLogStack logger) public void EnableLogger(EonaCatLogStack logger)
{ {
if (logger == null) if (logger == null)
{
return; return;
}
lock (_chainLock) lock (_chainLock)
{ {
@@ -226,7 +240,9 @@ public sealed class LoggerChain : ILoggerChain
public bool IsLoggerEnabled(EonaCatLogStack logger) public bool IsLoggerEnabled(EonaCatLogStack logger)
{ {
if (logger == null) if (logger == null)
{
return false; return false;
}
lock (_chainLock) lock (_chainLock)
{ {
@@ -284,20 +300,26 @@ public sealed class LoggerChain : ILoggerChain
}); });
if (ErrorBehavior == LoggerChainErrorBehavior.StopOnError) if (ErrorBehavior == LoggerChainErrorBehavior.StopOnError)
{
throw; throw;
}
} }
} }
private void ThrowIfDisposed() private void ThrowIfDisposed()
{ {
if (Interlocked.CompareExchange(ref _disposed, 0, 0) != 0) if (Interlocked.CompareExchange(ref _disposed, 0, 0) != 0)
{
throw new ObjectDisposedException(nameof(LoggerChain)); throw new ObjectDisposedException(nameof(LoggerChain));
}
} }
public async ValueTask DisposeAsync() public async ValueTask DisposeAsync()
{ {
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
{
return; return;
}
// Flush all loggers before disposing // Flush all loggers before disposing
await FlushAllAsync().ConfigureAwait(false); await FlushAllAsync().ConfigureAwait(false);
@@ -406,9 +406,8 @@ public sealed class MessageTemplate
} }
string[] parts = propertyPath.Split('.'); string[] parts = propertyPath.Split('.');
object? current = null;
if (!properties.TryGetValue(parts[0], out current)) if (!properties.TryGetValue(parts[0], out object current))
{ {
return null; return null;
} }
@@ -41,7 +41,10 @@ namespace EonaCat.LogStack.PerformanceInsights
tracker.RecordOperation(durationTicks, byteCount, isError); tracker.RecordOperation(durationTicks, byteCount, isError);
_totalOperations++; _totalOperations++;
if (isError) _totalErrors++; if (isError)
{
_totalErrors++;
}
} }
/// <summary> /// <summary>
@@ -84,10 +84,25 @@ namespace EonaCat.LogStack.PerformanceInsights
{ {
_operationCount++; _operationCount++;
_totalDurationTicks += durationTicks; _totalDurationTicks += durationTicks;
if (durationTicks < _minDurationTicks) _minDurationTicks = durationTicks; if (durationTicks < _minDurationTicks)
if (durationTicks > _maxDurationTicks) _maxDurationTicks = durationTicks; {
if (byteCount > 0) _byteCount += byteCount; _minDurationTicks = durationTicks;
if (isError) _errorCount++; }
if (durationTicks > _maxDurationTicks)
{
_maxDurationTicks = durationTicks;
}
if (byteCount > 0)
{
_byteCount += byteCount;
}
if (isError)
{
_errorCount++;
}
} }
} }
@@ -14,8 +14,7 @@ namespace EonaCat.LogStack.EonaCatLogStackCore
public static StringBuilder Rent() public static StringBuilder Rent()
{ {
StringBuilder sb; if (Pool.TryTake(out StringBuilder sb))
if (Pool.TryTake(out sb))
{ {
sb.Clear(); sb.Clear();
return sb; return sb;
@@ -387,9 +387,8 @@ public sealed class PropertyToken : TemplateToken
} }
string[] parts = propertyPath.Split('.'); string[] parts = propertyPath.Split('.');
object? current = null;
if (!properties.TryGetValue(parts[0], out current)) if (!properties.TryGetValue(parts[0], out object current))
{ {
return null; return null;
} }
@@ -118,7 +118,11 @@ namespace EonaCat.LogStack.Tracing
public void Dispose() public void Dispose()
{ {
if (_disposed) return; if (_disposed)
{
return;
}
_disposed = true; _disposed = true;
if (_span.Status == SpanStatus.Unset) if (_span.Status == SpanStatus.Unset)
@@ -36,7 +36,11 @@ namespace EonaCat.LogStack.Tracing
/// </summary> /// </summary>
public static void SetCurrent(TraceContext context) public static void SetCurrent(TraceContext context)
{ {
if (context == null) throw new ArgumentNullException(nameof(context)); if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
_current.Value = context; _current.Value = context;
} }
@@ -102,7 +102,10 @@ namespace EonaCat.LogStack.Tracing
/// </summary> /// </summary>
public void RecordException(Exception exception, Dictionary<string, object>? attributes = null) public void RecordException(Exception exception, Dictionary<string, object>? attributes = null)
{ {
if (exception == null) return; if (exception == null)
{
return;
}
_exceptions.Add(exception); _exceptions.Add(exception);
Status = SpanStatus.Error; Status = SpanStatus.Error;
@@ -129,7 +132,11 @@ namespace EonaCat.LogStack.Tracing
/// </summary> /// </summary>
public void SetAttributes(Dictionary<string, object> attributes) public void SetAttributes(Dictionary<string, object> attributes)
{ {
if (attributes == null) return; if (attributes == null)
{
return;
}
foreach (var kvp in attributes) foreach (var kvp in attributes)
{ {
Attributes[kvp.Key] = kvp.Value; Attributes[kvp.Key] = kvp.Value;
@@ -154,7 +161,10 @@ namespace EonaCat.LogStack.Tracing
/// </summary> /// </summary>
public void End(SpanStatus status = SpanStatus.Ok, string? description = null) public void End(SpanStatus status = SpanStatus.Ok, string? description = null)
{ {
if (_disposed) return; if (_disposed)
{
return;
}
_stopwatch.Stop(); _stopwatch.Stop();
EndTime = DateTime.UtcNow; EndTime = DateTime.UtcNow;
@@ -173,7 +183,11 @@ namespace EonaCat.LogStack.Tracing
public void Dispose() public void Dispose()
{ {
if (_disposed) return; if (_disposed)
{
return;
}
_disposed = true; _disposed = true;
End(); End();
} }
@@ -350,7 +350,9 @@ public static class ServiceCollectionExtensions
LoggerDIOptions? options = null) LoggerDIOptions? options = null)
{ {
if (services == null) if (services == null)
{
throw new ArgumentNullException(nameof(services)); throw new ArgumentNullException(nameof(services));
}
options ??= new LoggerDIOptions(); options ??= new LoggerDIOptions();
services.AddSingleton(options); services.AddSingleton(options);
@@ -386,7 +388,9 @@ public static class ServiceCollectionExtensions
this IServiceCollection services) this IServiceCollection services)
{ {
if (services == null) if (services == null)
{
throw new ArgumentNullException(nameof(services)); throw new ArgumentNullException(nameof(services));
}
services.AddSingleton<Tracing.TraceContextManager>(); services.AddSingleton<Tracing.TraceContextManager>();
services.AddSingleton<Tracing.SpanFactory>(); services.AddSingleton<Tracing.SpanFactory>();
@@ -401,7 +405,9 @@ public static class ServiceCollectionExtensions
this IServiceCollection services) this IServiceCollection services)
{ {
if (services == null) if (services == null)
{
throw new ArgumentNullException(nameof(services)); throw new ArgumentNullException(nameof(services));
}
services.AddSingleton<PerformanceInsights.PerformanceInsightsCollector>(); services.AddSingleton<PerformanceInsights.PerformanceInsightsCollector>();
services.AddSingleton<PerformanceInsights.PerformanceAnalyzer>(); services.AddSingleton<PerformanceInsights.PerformanceAnalyzer>();
@@ -415,7 +421,9 @@ public static class ServiceCollectionExtensions
this IServiceCollection services) this IServiceCollection services)
{ {
if (services == null) if (services == null)
{
throw new ArgumentNullException(nameof(services)); throw new ArgumentNullException(nameof(services));
}
services.AddSingleton<Telemetry.HealthMonitor>(); services.AddSingleton<Telemetry.HealthMonitor>();
services.AddSingleton<Telemetry.TelemetryAggregator>(); services.AddSingleton<Telemetry.TelemetryAggregator>();
@@ -429,7 +437,9 @@ public static class ServiceCollectionExtensions
this IServiceCollection services) this IServiceCollection services)
{ {
if (services == null) if (services == null)
{
throw new ArgumentNullException(nameof(services)); throw new ArgumentNullException(nameof(services));
}
services.AddSingleton<Telemetry.TelemetryAggregator>(); services.AddSingleton<Telemetry.TelemetryAggregator>();
services.AddSingleton<Telemetry.HealthMonitor>(); services.AddSingleton<Telemetry.HealthMonitor>();
+2
View File
@@ -1484,7 +1484,9 @@ public sealed class LogBuilder
public Chaining.ILoggerChain AddToChain(EonaCatLogStack primaryLogger, bool chainAsFlow = true) public Chaining.ILoggerChain AddToChain(EonaCatLogStack primaryLogger, bool chainAsFlow = true)
{ {
if (primaryLogger == null) if (primaryLogger == null)
{
throw new ArgumentNullException(nameof(primaryLogger)); throw new ArgumentNullException(nameof(primaryLogger));
}
var newLogger = Build(); var newLogger = Build();
+10
View File
@@ -84,19 +84,29 @@ public class SuperiorFeaturesStats
var features = new List<string>(); var features = new List<string>();
if (IntelligentRoutingEnabled) if (IntelligentRoutingEnabled)
{
features.Add($"IntelligentRouting({RouterStats?.TotalRules ?? 0} rules)"); features.Add($"IntelligentRouting({RouterStats?.TotalRules ?? 0} rules)");
}
if (AdaptiveSamplingEnabled) if (AdaptiveSamplingEnabled)
{
features.Add($"AdaptiveSampling({SamplingMetrics?.SamplingRate:P1} rate)"); features.Add($"AdaptiveSampling({SamplingMetrics?.SamplingRate:P1} rate)");
}
if (AnomalyDetectionEnabled) if (AnomalyDetectionEnabled)
{
features.Add($"AnomalyDetection({AnomalyStats?.RecentAnomalies ?? 0} recent)"); features.Add($"AnomalyDetection({AnomalyStats?.RecentAnomalies ?? 0} recent)");
}
if (ContextSnapshotsEnabled) if (ContextSnapshotsEnabled)
{
features.Add($"ContextSnapshots({SnapshotStats?.CurrentSnapshots ?? 0} snapshots)"); features.Add($"ContextSnapshots({SnapshotStats?.CurrentSnapshots ?? 0} snapshots)");
}
if (DeadLetterQueueEnabled) if (DeadLetterQueueEnabled)
{
features.Add($"DeadLetterQueue({DlqStats?.CurrentQueueSize ?? 0}/{DlqStats?.MaxCapacity ?? 0})"); features.Add($"DeadLetterQueue({DlqStats?.CurrentQueueSize ?? 0}/{DlqStats?.MaxCapacity ?? 0})");
}
return "Superior Features: " + (features.Count > 0 return "Superior Features: " + (features.Count > 0
? string.Join(", ", features) ? string.Join(", ", features)
@@ -25,14 +25,19 @@ public sealed class AdaptiveTelemetryEngine
Interlocked.Increment(ref _events); Interlocked.Increment(ref _events);
if (DetectAnomalies) if (DetectAnomalies)
{
signal.UpdateAnomalyState(); signal.UpdateAnomalyState();
}
} }
public TelemetryEngineSnapshot Snapshot() public TelemetryEngineSnapshot Snapshot()
{ {
var snapshot = new TelemetryEngineSnapshot { TotalEvents = Interlocked.Read(ref _events) }; var snapshot = new TelemetryEngineSnapshot { TotalEvents = Interlocked.Read(ref _events) };
foreach (var item in _signals) foreach (var item in _signals)
{
snapshot.Signals[item.Key] = item.Value.Snapshot(); snapshot.Signals[item.Key] = item.Value.Snapshot();
}
return snapshot; return snapshot;
} }
} }
+6 -2
View File
@@ -4,6 +4,10 @@ namespace EonaCat.LogStack.Telemetry;
public sealed class Histogram public sealed class Histogram
{ {
private readonly ConcurrentQueue<double> _values = new(); private readonly ConcurrentQueue<double> _values = new();
public void Record(double value){ _values.Enqueue(value); while(_values.Count>10000)_values.TryDequeue(out _); } public void Record(double value){ _values.Enqueue(value); while(_values.Count>10000)
public double Percentile(double p){ var a=_values.ToArray(); if(a.Length==0)return 0; System.Array.Sort(a); return a[(int)((a.Length-1)*p)]; } {
_values.TryDequeue(out _);
}
}
public double Percentile(double p){ var a=_values.ToArray(); if(a.Length==0) { return 0; } System.Array.Sort(a); return a[(int)((a.Length-1)*p)]; }
} }
@@ -205,7 +205,11 @@ namespace EonaCat.LogStack.Telemetry
private static string TagsKey(Dictionary<string, string>? tags) private static string TagsKey(Dictionary<string, string>? tags)
{ {
if (tags == null || tags.Count == 0) return ""; if (tags == null || tags.Count == 0)
{
return "";
}
return string.Join(",", tags.OrderBy(t => t.Key).Select(t => $"{t.Key}={t.Value}")); return string.Join(",", tags.OrderBy(t => t.Key).Select(t => $"{t.Key}={t.Value}"));
} }
} }
@@ -250,8 +254,16 @@ namespace EonaCat.LogStack.Telemetry
_values.Add(value); _values.Add(value);
Value++; Value++;
Sum += value; Sum += value;
if (value < Min) Min = value; if (value < Min)
if (value > Max) Max = value; {
Min = value;
}
if (value > Max)
{
Max = value;
}
if (_values.Count > 1000) // Keep reasonable history if (_values.Count > 1000) // Keep reasonable history
{ {
_values.RemoveRange(0, 100); _values.RemoveRange(0, 100);
@@ -260,7 +272,11 @@ namespace EonaCat.LogStack.Telemetry
private double CalculatePercentile(double percentile) private double CalculatePercentile(double percentile)
{ {
if (_values.Count == 0) return 0; if (_values.Count == 0)
{
return 0;
}
var sorted = _values.OrderBy(v => v).ToList(); var sorted = _values.OrderBy(v => v).ToList();
var index = (int)((percentile / 100.0) * sorted.Count); var index = (int)((percentile / 100.0) * sorted.Count);
index = Math.Min(index, sorted.Count - 1); index = Math.Min(index, sorted.Count - 1);
@@ -28,7 +28,9 @@ public sealed class TelemetrySignal
Max = Count == 1 ? value : Math.Max(Max, value); Max = Count == 1 ? value : Math.Max(Max, value);
_values.Enqueue(value); _values.Enqueue(value);
while (_values.Count > limit) while (_values.Count > limit)
{
_values.Dequeue(); _values.Dequeue();
}
} }
} }
@@ -36,7 +38,11 @@ public sealed class TelemetrySignal
{ {
lock (_sync) lock (_sync)
{ {
if (_values.Count < 10) return; if (_values.Count < 10)
{
return;
}
var avg = _values.Average(); var avg = _values.Average();
var variance = _values.Average(v => Math.Pow(v - avg, 2)); var variance = _values.Average(v => Math.Pow(v - avg, 2));
IsAnomaly = Math.Abs(_values.Last() - avg) > Math.Sqrt(variance) * 3; IsAnomaly = Math.Abs(_values.Last() - avg) > Math.Sqrt(variance) * 3;
+2 -1
View File
@@ -17,7 +17,6 @@ namespace EonaCat.LogStack.Test.Web
public static async Task Main(string[] args) public static async Task Main(string[] args)
{ {
await using var logger = LogBuilder.CreateDefault("MyApp"); await using var logger = LogBuilder.CreateDefault("MyApp");
logger.Information("Application started"); logger.Information("Application started");
logger.Warning("Low memory warning"); logger.Warning("Low memory warning");
logger.Error(new Exception("DIT IS MIJN TEST!"), "Unexpected error occurred"); logger.Error(new Exception("DIT IS MIJN TEST!"), "Unexpected error occurred");
@@ -58,6 +57,8 @@ namespace EonaCat.LogStack.Test.Web
logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.Text); logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.Text);
logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.Json); logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.Json);
logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.Xml); logBuilder.WriteToFile(logDirectory, outputFormat: FileOutputFormat.Xml);
logBuilder.WriteToEncryptedFile(logDirectory, filePrefix: "encrypted", password: "MySecretKey123456");
EncryptedFileFlow.DecryptToFile(@"C:\workdir\C#\EonaCat.LogStack\Testers\EonaCat.LogStack.Test.Web\bin\Debug\net8.0\logs\encrypted_EONACAT_20260804.eona", "./logs/decrypted.log", "MySecretKey123456");
logBuilder.WriteToTcp("127.0.0.1", 514); logBuilder.WriteToTcp("127.0.0.1", 514);
//logBuilder.WriteToEncryptedFile("./logs"); //logBuilder.WriteToEncryptedFile("./logs");
//logBuilder.WriteDiagnostics(); //logBuilder.WriteDiagnostics();