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>
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
<PackageIconUrl />
<FileVersion>0.2.0</FileVersion>
<FileVersion>0.2.1</FileVersion>
<PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
@@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
</PropertyGroup>
<PropertyGroup>
<EVRevisionFormat>0.2.0+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVRevisionFormat>0.2.1+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVDefault>true</EVDefault>
<EVInfo>true</EVInfo>
<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>
<Version>0.2.0</Version>
<Version>0.2.1</Version>
<PackageId>EonaCat.LogStack</PackageId>
<Product>EonaCat.LogStack</Product>
<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>
<Compile Remove="Configuration\**" />
<Compile Remove="EonaCat.LogStack.Test\**" />
<Compile Remove="EonaCatLoggerCore\Examples\**" />
<Compile Remove="Examples\**" />
<Compile Remove="Patterns\**" />
<Compile Remove="Utilities\**" />
<EmbeddedResource Remove="Configuration\**" />
<EmbeddedResource Remove="EonaCat.LogStack.Test\**" />
<EmbeddedResource Remove="EonaCatLoggerCore\Examples\**" />
<EmbeddedResource Remove="Examples\**" />
<EmbeddedResource Remove="Patterns\**" />
<EmbeddedResource Remove="Utilities\**" />
<None Remove="Configuration\**" />
<None Remove="EonaCat.LogStack.Test\**" />
<None Remove="EonaCatLoggerCore\Examples\**" />
<None Remove="Examples\**" />
<None Remove="Patterns\**" />
@@ -110,8 +113,4 @@ It features a rich fluent API for routing log events to dozens of destinations f
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="EonaCat.LogStack.Test\Features\" />
</ItemGroup>
</Project>
@@ -41,13 +41,17 @@ namespace EonaCat.LogStack.Flows
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
{
return WriteResult.Success;
}
try
{
var correlationId = ExtractCorrelationId(logEvent);
if (string.IsNullOrEmpty(correlationId))
{
return WriteResult.Success;
}
// Get or create correlation group
var group = _correlationGroups.AddOrUpdate(
@@ -119,7 +123,9 @@ namespace EonaCat.LogStack.Flows
public IEnumerable<LogEvent> GetCorrelatedEvents(string correlationId)
{
if (_correlationGroups.TryGetValue(correlationId, out var group))
{
return group.Events.ToList();
}
return new List<LogEvent>();
}
@@ -200,11 +206,19 @@ namespace EonaCat.LogStack.Flows
if (logEvent.Properties != null)
{
if (logEvent.Properties.TryGetValue("CorrelationId", out var corrId) && corrId is string str)
{
return str;
}
if (logEvent.Properties.TryGetValue("RequestId", out var reqId) && reqId is string str2)
{
return str2;
}
if (logEvent.Properties.TryGetValue("TraceId", out var traceId) && traceId is string str3)
{
return str3;
}
}
// Try thread-based correlation
@@ -230,10 +244,17 @@ namespace EonaCat.LogStack.Flows
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();
if (sorted.Count % 2 == 0)
{
return (sorted[sorted.Count / 2 - 1] + sorted[sorted.Count / 2]) / 2.0;
}
return sorted[sorted.Count / 2];
}
@@ -258,7 +279,9 @@ namespace EonaCat.LogStack.Flows
get
{
lock (_eventLock)
{
return _events.AsReadOnly();
}
}
}
public DateTime FirstEventTime { get; private set; }
@@ -287,7 +310,11 @@ namespace EonaCat.LogStack.Flows
{
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);
}
}
@@ -298,7 +325,9 @@ namespace EonaCat.LogStack.Flows
public IEnumerable<LogEvent> GetEventsByLevel(LogLevel level)
{
lock (_eventLock)
{
return _events.Where(e => e.Level == level).ToList();
}
}
/// <summary>
@@ -309,7 +338,9 @@ namespace EonaCat.LogStack.Flows
get
{
lock (_eventLock)
{
return _events.Any(e => e.Exception != null);
}
}
}
@@ -44,7 +44,9 @@ namespace EonaCat.LogStack.Flows
: base("LocalStorageQuery", minimumLevel)
{
if (maxCapacity <= 0)
{
throw new ArgumentException("Capacity must be greater than 0", nameof(maxCapacity));
}
_maxCapacity = maxCapacity;
}
@@ -52,7 +54,9 @@ namespace EonaCat.LogStack.Flows
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
{
return WriteResult.Success;
}
try
{
@@ -185,7 +189,9 @@ namespace EonaCat.LogStack.Flows
{
var events = _logBuffer.ToList();
if (events.Count == 0)
{
return 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)
{
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
{
return WriteResult.Success;
}
try
{
@@ -114,7 +116,10 @@ namespace EonaCat.LogStack.Flows
{
_channel.Writer.TryComplete();
if (_processingTask != null)
{
await _processingTask;
}
await _targetFlow.DisposeAsync();
await base.DisposeAsync();
}
@@ -44,13 +44,17 @@ namespace EonaCat.LogStack.Flows
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (!IsEnabled || !IsLogLevelEnabled(logEvent))
{
return WriteResult.Success;
}
try
{
var metrics = ExtractPerformanceMetrics(logEvent);
if (metrics.Count == 0)
{
return WriteResult.Success;
}
lock (_trackersLock)
{
@@ -102,7 +106,9 @@ namespace EonaCat.LogStack.Flows
{
var result = await BlastAsync(logEvent, cancellationToken);
if (result != WriteResult.Success)
{
return result;
}
}
return WriteResult.Success;
}
@@ -110,7 +116,10 @@ namespace EonaCat.LogStack.Flows
public override async Task FlushAsync(CancellationToken cancellationToken = default)
{
if (_alertTarget != null)
{
await _alertTarget.FlushAsync(cancellationToken);
}
await Task.CompletedTask;
}
@@ -165,7 +174,10 @@ namespace EonaCat.LogStack.Flows
lock (_trackersLock)
{
if (_trackers.TryGetValue(metricName, out var tracker))
{
return tracker.RecentAnomalies.ToList();
}
return new List<PerformanceAnomaly>();
}
}
@@ -199,7 +211,9 @@ namespace EonaCat.LogStack.Flows
var metrics = new Dictionary<string, double>();
if (logEvent.Properties == null)
{
return metrics;
}
// Look for common performance metric property names
var metricNames = new[] { "duration", "latency", "elapsed", "time_ms", "response_time", "memory", "cpu" };
@@ -235,7 +249,10 @@ namespace EonaCat.LogStack.Flows
{
Clear();
if (_alertTarget != null)
{
await _alertTarget.DisposeAsync();
}
await base.DisposeAsync();
}
}
@@ -310,7 +327,9 @@ namespace EonaCat.LogStack.Flows
_recentAnomalies.Add(anomaly);
if (_recentAnomalies.Count > 100)
{
_recentAnomalies.RemoveAt(0);
}
AnomalyCount++;
return anomaly;
@@ -324,7 +343,9 @@ namespace EonaCat.LogStack.Flows
private double CalculateStandardDeviation()
{
if (_measurements.Count < 2)
{
return 0;
}
var avg = Average;
var sumSquaredDiff = _measurements.Sum(x => Math.Pow(x - avg, 2));
@@ -207,7 +207,9 @@ namespace EonaCat.LogStack.Extensions
{
var engine = provider.GetService<LogSearchEngine>();
if (engine == null)
{
return Enumerable.Empty<LogSearchEntry>();
}
var query = new LogSearchQuery
{
@@ -35,7 +35,9 @@ namespace EonaCat.LogStack.Features
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);
@@ -72,7 +74,9 @@ namespace EonaCat.LogStack.Features
public void EvaluateLog(LogEvent logEvent, string formattedMessage)
{
if (!_isEnabled)
{
return;
}
lock (_lockObject)
{
@@ -107,31 +111,41 @@ namespace EonaCat.LogStack.Features
{
// 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
@@ -142,7 +156,9 @@ namespace EonaCat.LogStack.Features
{
var timeSinceLastTrigger = (now - rule.LastTriggeredAt.Value).TotalSeconds;
if (timeSinceLastTrigger < rule.ThrottleIntervalSeconds)
{
return false;
}
}
rule.LastTriggeredAt = now;
}
@@ -263,7 +279,10 @@ namespace EonaCat.LogStack.Features
public AlertRuleBuilder WithThrottling(int intervalSeconds)
{
if (intervalSeconds < 0)
{
throw new ArgumentException("Throttle interval must be >= 0", nameof(intervalSeconds));
}
_rule.ThrottleIntervalSeconds = intervalSeconds;
return this;
}
@@ -30,10 +30,14 @@ namespace EonaCat.LogStack.Features
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
{
@@ -48,7 +52,9 @@ namespace EonaCat.LogStack.Features
public ConfigurationValidationReport Validate(ILogger logger)
{
if (logger == null)
{
throw new ArgumentNullException(nameof(logger));
}
var context = new ConfigurationValidationContext { Logger = logger };
var results = new List<ValidationResult>();
@@ -247,7 +253,9 @@ namespace EonaCat.LogStack.Features
public void EnableHotReload(Action<ConfigurationChangeNotification> onConfigChanged)
{
if (onConfigChanged == null)
{
throw new ArgumentNullException(nameof(onConfigChanged));
}
lock (_lockObject)
{
@@ -274,7 +282,9 @@ namespace EonaCat.LogStack.Features
public void NotifyConfigurationChange(string configKey, object oldValue, object newValue, string reason = null)
{
if (!_isEnabled || _changeHandler == null)
{
return;
}
var notification = new ConfigurationChangeNotification
{
@@ -21,7 +21,9 @@ namespace EonaCat.LogStack.Features
string serviceName, ActivityStatus status, double durationMs, Exception exception = null)
{
if (string.IsNullOrWhiteSpace(correlationId))
{
throw new ArgumentNullException(nameof(correlationId));
}
lock (_lockObject)
{
@@ -59,7 +61,9 @@ namespace EonaCat.LogStack.Features
public CorrelatedActivityTrace GetTrace(string correlationId)
{
if (string.IsNullOrWhiteSpace(correlationId))
{
return null;
}
lock (_lockObject)
{
@@ -85,7 +89,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<CorrelatedActivityTrace> GetTracesByService(string serviceName)
{
if (string.IsNullOrWhiteSpace(serviceName))
{
return Enumerable.Empty<CorrelatedActivityTrace>();
}
lock (_lockObject)
{
@@ -129,7 +135,9 @@ namespace EonaCat.LogStack.Features
lock (_lockObject)
{
if (_traces.Count == 0)
{
return new DashboardStatistics();
}
var allActivities = _traces.SelectMany(t => t.Activities).ToList();
var byStatus = allActivities.GroupBy(a => a.Status)
@@ -174,7 +182,9 @@ namespace EonaCat.LogStack.Features
var to = services[i + 1];
if (!dependencies.ContainsKey(from))
{
dependencies[from] = new HashSet<string>();
}
dependencies[from].Add(to);
}
@@ -242,7 +252,9 @@ namespace EonaCat.LogStack.Features
{
var trace = GetTrace(correlationId);
if (trace == null)
{
return null;
}
var startTime = trace.Activities.Min(a => a.Timestamp);
var activities = trace.Activities
@@ -50,7 +50,9 @@ namespace EonaCat.LogStack.Features
public void CaptureLog(LogEvent logEvent, string formattedMessage)
{
if (!_isCapturing)
{
return;
}
lock (_lockObject)
{
@@ -95,17 +97,25 @@ namespace EonaCat.LogStack.Features
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();
}
@@ -134,7 +144,9 @@ namespace EonaCat.LogStack.Features
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);
@@ -151,7 +163,9 @@ namespace EonaCat.LogStack.Features
public void Replay(Action<CapturedLogEntry> onLogReplayed, bool respectTimings = false)
{
if (onLogReplayed == null)
{
throw new ArgumentNullException(nameof(onLogReplayed));
}
List<CapturedLogEntry> logsToReplay;
@@ -161,7 +175,9 @@ namespace EonaCat.LogStack.Features
}
if (logsToReplay.Count == 0)
{
return;
}
DateTime? previousTimestamp = null;
@@ -189,7 +205,9 @@ namespace EonaCat.LogStack.Features
lock (_lockObject)
{
if (_capturedLogs.Count == 0)
{
return new LogReplayStatistics();
}
var byLevel = _capturedLogs.GroupBy(l => l.Level)
.ToDictionary(g => g.Key, g => g.Count());
@@ -317,7 +335,9 @@ namespace EonaCat.LogStack.Features
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);
@@ -88,7 +88,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> SearchByKeyword(string keyword)
{
if (string.IsNullOrWhiteSpace(keyword))
{
return Enumerable.Empty<LogSearchEntry>();
}
var lower = keyword.ToLowerInvariant();
lock (_lockObject)
@@ -106,7 +108,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> SearchByLogger(string loggerName)
{
if (string.IsNullOrWhiteSpace(loggerName))
{
return Enumerable.Empty<LogSearchEntry>();
}
lock (_lockObject)
{
@@ -122,7 +126,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> SearchByCorrelationId(string correlationId)
{
if (string.IsNullOrWhiteSpace(correlationId))
{
return Enumerable.Empty<LogSearchEntry>();
}
lock (_lockObject)
{
@@ -151,7 +157,9 @@ namespace EonaCat.LogStack.Features
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)
{
@@ -167,7 +175,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> Search(LogSearchQuery query)
{
if (query == null)
{
return Enumerable.Empty<LogSearchEntry>();
}
lock (_lockObject)
{
@@ -241,7 +251,9 @@ namespace EonaCat.LogStack.Features
public IEnumerable<LogSearchEntry> GetRecent(int count = 100)
{
if (count <= 0)
{
count = 100;
}
lock (_lockObject)
{
@@ -261,7 +273,9 @@ namespace EonaCat.LogStack.Features
lock (_lockObject)
{
if (_logIndex.Count == 0)
{
return new LogSearchStatistics();
}
return new LogSearchStatistics
{
@@ -36,19 +36,27 @@ namespace EonaCat.LogStack
int height = 10)
{
if (timeResolutionMinutes <= 0)
{
throw new ArgumentException("Time resolution must be positive", nameof(timeResolutionMinutes));
}
var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0)
{
return "No events to visualize";
}
var timeBuckets = CreateTimeBuckets(eventList, timeResolutionMinutes);
if (timeBuckets.Count == 0)
{
return "No valid time data";
}
var maxCount = timeBuckets.Values.Max();
if (maxCount == 0)
{
return "All buckets empty";
}
return RenderHeatmap(timeBuckets, width, height, maxCount);
}
@@ -60,7 +68,9 @@ namespace EonaCat.LogStack
{
var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0)
{
return "No events to visualize";
}
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));
@@ -71,12 +81,17 @@ namespace EonaCat.LogStack
var maxCount = levelCounts.Values.Max();
if (maxCount == 0)
{
return "No events";
}
foreach (var level in levels)
{
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 barLength = (int)((double)count / maxCount * (width - 20));
@@ -95,7 +110,9 @@ namespace EonaCat.LogStack
{
var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0)
{
return "No events to visualize";
}
var categoryCounts = eventList
.GroupBy(e => e.Category ?? "Unknown")
@@ -109,7 +126,9 @@ namespace EonaCat.LogStack
var maxCount = categoryCounts.Values.Max();
if (maxCount == 0)
{
return "No categories";
}
foreach (var kvp in categoryCounts)
{
@@ -132,12 +151,16 @@ namespace EonaCat.LogStack
{
var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0)
{
return string.Empty;
}
var timeBuckets = CreateTimeBuckets(eventList, (int)Math.Ceiling((double)GetTimeSpanMinutes(eventList) / buckets));
var maxCount = timeBuckets.Values.Max();
if (maxCount == 0)
{
return string.Empty;
}
var sb = new StringBuilder();
foreach (var bucket in timeBuckets.OrderBy(kvp => kvp.Key))
@@ -155,7 +178,9 @@ namespace EonaCat.LogStack
{
var eventList = events?.ToList() ?? new List<LogEvent>();
if (eventList.Count == 0)
{
return "No events";
}
// Create a 24x7 matrix (hours x days)
var hourlyMatrix = new int[24, 7];
@@ -178,11 +203,17 @@ namespace EonaCat.LogStack
var maxValue = 0;
for (int h = 0; h < 24; h++)
{
for (int d = 0; d < 7; d++)
{
maxValue = Math.Max(maxValue, hourlyMatrix[h, d]);
}
}
if (maxValue == 0)
{
return "No hourly data";
}
for (int h = 0; h < 24; h++)
{
@@ -214,9 +245,13 @@ namespace EonaCat.LogStack
.AddMinutes(-(timestamp.Minute % bucketMinutes));
if (buckets.ContainsKey(bucketTime))
{
buckets[bucketTime]++;
}
else
{
buckets[bucketTime] = 1;
}
}
return buckets;
@@ -259,23 +294,45 @@ namespace EonaCat.LogStack
sb.AppendLine(new string('=', width + 4));
if (sortedBuckets.Count > 0)
{
sb.AppendLine($"From: {sortedBuckets.First().Key:yyyy-MM-dd HH:mm}, To: {sortedBuckets.Last().Key:yyyy-MM-dd HH:mm}");
}
return sb.ToString();
}
private static char GetHeatCharacter(double intensity)
{
if (intensity < 0.01) return EmptyCharacter;
if (intensity < 0.35) return ColdCharacter;
if (intensity < 0.65) return CoolCharacter;
if (intensity < 0.85) return WarmCharacter;
if (intensity < 0.01)
{
return EmptyCharacter;
}
if (intensity < 0.35)
{
return ColdCharacter;
}
if (intensity < 0.65)
{
return CoolCharacter;
}
if (intensity < 0.85)
{
return WarmCharacter;
}
return HotCharacter;
}
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 minTicks = events.Min(e => e.Timestamp);
var span = new TimeSpan(maxTicks - minTicks);
@@ -65,10 +65,14 @@ public class AdaptiveSamplingEngine
// Use random sampling based on current rate
if (_samplingRate >= 1.0)
{
return true;
}
if (_samplingRate <= 0.0)
{
return false;
}
return ThreadSafeRandom.NextDouble() < _samplingRate;
}
@@ -164,7 +168,9 @@ public class AdaptiveSamplingEngine
{
var now = DateTime.UtcNow;
if ((now - _lastCpuCheck).TotalMilliseconds < 1000)
{
return; // Update at most once per second to avoid overhead
}
_lastCpuCheck = now;
@@ -113,7 +113,9 @@ public class AnomalyDetector
{
_recentAlerts.Add(alert);
if (_recentAlerts.Count > 1000)
{
_recentAlerts.RemoveAt(0); // Keep last 1000 alerts
}
}
AnomalyDetected?.Invoke(this, alert);
@@ -180,10 +182,14 @@ public class AnomalyDetector
private AnomalySeverity DetermineAnomalySeverity(List<string> anomalies)
{
if (anomalies.Any(a => a.Contains("error rate")))
{
return AnomalySeverity.Critical;
}
if (anomalies.Any(a => a.Contains("Exception") || a.Contains("infinite loop")))
{
return AnomalySeverity.High;
}
return AnomalySeverity.Medium;
}
@@ -222,10 +228,14 @@ internal class CategoryStatistics
EventCount++;
_recentEventTimes.Enqueue(DateTime.UtcNow);
if (_recentEventTimes.Count > 1000)
{
_recentEventTimes.Dequeue();
}
if (logEvent.Level == LogLevel.Error || logEvent.Level == LogLevel.Critical)
{
_errorCount++;
}
}
}
@@ -244,7 +254,9 @@ internal class CategoryStatistics
lock (_lock)
{
if (_recentEventTimes.Count < 10)
{
return false;
}
var now = DateTime.UtcNow;
var times = _recentEventTimes.ToList();
@@ -254,11 +266,15 @@ internal class CategoryStatistics
{
var interval = (now - times[i]).TotalSeconds;
if (interval > 0)
{
recentIntervals.Add(interval);
}
}
if (recentIntervals.Count < 2)
{
return false;
}
var mean = recentIntervals.Average();
var variance = recentIntervals.Average(x => Math.Pow(x - mean, 2));
@@ -286,7 +302,9 @@ internal class LevelStatistics
_errorCount++;
_eventTimes.Enqueue(DateTime.UtcNow);
if (_eventTimes.Count > 10000)
{
_eventTimes.Dequeue();
}
}
}
@@ -295,13 +313,17 @@ internal class LevelStatistics
lock (_lock)
{
if (_eventTimes.Count < 2)
{
return 0;
}
var now = DateTime.UtcNow;
var span = (now - _eventTimes.Peek()).TotalSeconds;
if (span < 1)
{
return _eventTimes.Count;
}
return _eventTimes.Count / span;
}
@@ -38,7 +38,9 @@ public class ContextSnapshotCollector
public ContextSnapshot CaptureIfNeeded(LogEvent logEvent)
{
if (logEvent.Level < _triggerLevel)
{
return null;
}
return CaptureSnapshot(logEvent);
}
@@ -64,7 +66,9 @@ public class ContextSnapshotCollector
{
_snapshots.Add(snapshot);
if (_snapshots.Count > _maxSnapshots)
{
_snapshots.RemoveAt(0);
}
_totalSnapshotsCaptured++;
}
@@ -257,7 +261,9 @@ public class ContextSnapshotCollector
{
var varValue = Environment.GetEnvironmentVariable(varName);
if (varValue != null)
{
env[varName] = varValue;
}
}
// Add runtime info
@@ -111,7 +111,9 @@ public class DeadLetterQueue
public async Task<bool> ReplayAsync(DeadLetterEvent dlEvent, IEnumerable<IFlow> targetFlows)
{
if (dlEvent == null)
{
throw new ArgumentNullException(nameof(dlEvent));
}
dlEvent.RetryCount++;
dlEvent.LastRetryAt = DateTime.UtcNow;
@@ -207,7 +209,10 @@ public class DeadLetterQueue
{
var reason = evt.FailureReason ?? "Unknown";
if (!stats.ContainsKey(reason))
{
stats[reason] = 0;
}
stats[reason]++;
}
return stats.OrderByDescending(x => x.Value).ToDictionary(x => x.Key, x => x.Value);
@@ -135,7 +135,9 @@ public sealed class DelegatingLoggerFlow : FlowBase
lock (_queueLock)
{
if (_delegationQueue.Count == 0)
{
return;
}
pendingEvents = new Queue<LogEvent>(_delegationQueue);
_delegationQueue.Clear();
@@ -170,7 +172,9 @@ public sealed class DelegatingLoggerFlow : FlowBase
private void DelegateLogEvent(LogEvent logEvent)
{
if (_disposed != 0)
{
return;
}
try
{
@@ -199,7 +203,9 @@ public sealed class DelegatingLoggerFlow : FlowBase
public override async ValueTask DisposeAsync()
{
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
{
return;
}
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>
public long ReadCounter(string name)
{
Counter c;
return _counters.TryGetValue(name, out c) ? c.Value : 0;
return _counters.TryGetValue(name, out Counter c) ? c.Value : 0;
}
public override Task<WriteResult> BlastAsync(
@@ -40,7 +40,11 @@ public class DlqFlow : FlowBase
/// </summary>
public DlqFlow AddBackupFlow(IFlow flow)
{
if (flow == null) throw new ArgumentNullException(nameof(flow));
if (flow == null)
{
throw new ArgumentNullException(nameof(flow));
}
lock (_flowsLock)
{
_backupFlows.Add(flow);
@@ -54,10 +58,14 @@ public class DlqFlow : FlowBase
public override async Task<WriteResult> BlastAsync(LogEvent logEvent, CancellationToken cancellationToken = default)
{
if (_disposed != 0)
{
return WriteResult.Dropped;
}
if (!IsLogLevelEnabled(logEvent))
{
return WriteResult.Success;
}
// Try backup flows first before enqueueing to DLQ
List<IFlow> backupFlows;
@@ -96,7 +104,9 @@ public class DlqFlow : FlowBase
// All backups failed, enqueue to DLQ
var failureReason = string.Join("; ", failureReasons);
if (failureReason.Length > 500)
{
failureReason = failureReason.Substring(0, 500) + "...";
}
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)
{
if (_disposed != 0)
{
return WriteResult.Dropped;
}
var events = logEvents.ToArray();
int successCount = 0;
@@ -125,7 +137,9 @@ public class DlqFlow : FlowBase
{
var result = await BlastAsync(logEvent, cancellationToken).ConfigureAwait(false);
if (result == WriteResult.Success)
{
successCount++;
}
}
return successCount > 0 ? WriteResult.Success : WriteResult.Dropped;
@@ -180,7 +194,9 @@ public class DlqFlow : FlowBase
public override async ValueTask DisposeAsync()
{
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
{
return;
}
List<IFlow> flowsToDispose;
lock (_flowsLock)
@@ -2,6 +2,7 @@ using EonaCat.LogStack.Core;
using EonaCat.LogStack.EonaCatLogStackCore;
using EonaCat.LogStack.EonaCatLogStackCore.Policies;
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Diagnostics;
@@ -116,6 +117,7 @@ namespace EonaCat.LogStack.Flows
// Auto-flush on error
private volatile bool _autoFlushOnError;
private volatile bool _durableWrites;
// Scoped properties (AsyncLocal for ambient context)
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 List<Action<LogEvent, StringBuilder>> _compiledTemplate;
private readonly Dictionary<string, Action<LogEvent, StringBuilder>> _customTokens
= new Dictionary<string, Action<LogEvent, StringBuilder>>(StringComparer.OrdinalIgnoreCase);
private readonly Dictionary<string, Action<LogEvent, StringBuilder>> _customTokens = new Dictionary<string, Action<LogEvent, StringBuilder>>(StringComparer.OrdinalIgnoreCase);
private int _correlationSeed;
public string EncryptedFileExtension { get; set; } = ".eona";
public EncryptedFileFlow(
string directory,
@@ -148,7 +149,7 @@ namespace EonaCat.LogStack.Flows
long maxFileSize = 50L * 1024 * 1024,
long maxDirectorySize = 2L * 1024 * 1024 * 1024,
FileRetentionPolicy retention = null,
int flushIntervalMs = 3000,
int flushIntervalMs = 250,
int batchSize = 1,
LogLevel minimumLevel = LogLevel.Trace,
bool useCategoryRouting = false,
@@ -299,6 +300,28 @@ namespace EonaCat.LogStack.Flows
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>
public EncryptedFileFlow OnFileRotated(Action<string> callback)
{
@@ -365,6 +388,18 @@ namespace EonaCat.LogStack.Flows
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>
public EncryptedFileFlow WithDeduplication(TimeSpan window)
{
@@ -563,6 +598,11 @@ namespace EonaCat.LogStack.Flows
aes.Key = key;
aes.IV = iv;
if (!Directory.Exists(Path.GetDirectoryName(outputPath)))
{
Directory.CreateDirectory(Path.GetDirectoryName(outputPath));
}
using (ICryptoTransform dec = aes.CreateDecryptor())
using (StreamWriter out_ = new StreamWriter(outputPath, false, Encoding.UTF8))
{
@@ -585,8 +625,8 @@ namespace EonaCat.LogStack.Flows
ReadExact(source, cipher, blockLength);
byte[] plain = dec.TransformFinalBlock(cipher, 0, cipher.Length);
out_.WriteLine(Encoding.UTF8.GetString(plain));
return true;
}
return true;
}
}
}
@@ -729,10 +769,11 @@ namespace EonaCat.LogStack.Flows
IsEnabled = false;
_queue.CompleteAdding();
_cts.Cancel();
_writerThread.Join(TimeSpan.FromSeconds(5));
_flushThread.Join(TimeSpan.FromSeconds(2));
_retentionThread.Join(TimeSpan.FromSeconds(2));
_cts.Cancel();
lock (_lock) { CloseCurrentFile(); }
_cts.Dispose();
_queue.Dispose();
@@ -752,9 +793,8 @@ namespace EonaCat.LogStack.Flows
WriteEncryptedLine(entry);
QueueEntry extra;
int batch = 0;
while (batch < _batchSize && _queue.TryTake(out extra))
while (batch < _batchSize && _queue.TryTake(out QueueEntry extra))
{
WriteEncryptedLine(extra);
batch++;
@@ -767,8 +807,7 @@ namespace EonaCat.LogStack.Flows
}
finally
{
QueueEntry remaining;
while (_queue.TryTake(out remaining))
while (_queue.TryTake(out QueueEntry remaining))
{
WriteEncryptedLine(remaining);
}
@@ -781,8 +820,10 @@ namespace EonaCat.LogStack.Flows
{
while (!_cts.Token.IsCancellationRequested)
{
try { Thread.Sleep(_flushIntervalMs); }
catch (ThreadInterruptedException) { break; }
if (_cts.Token.WaitHandle.WaitOne(_flushIntervalMs))
{
break;
}
lock (_lock)
{
@@ -827,18 +868,36 @@ namespace EonaCat.LogStack.Flows
{
byte[] plain = Encoding.UTF8.GetBytes(entry.Line);
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;
if (_durableWrites)
{
try { _currentStream.Flush(true); } catch { /* surfaced via outer catch below on failure */ }
}
Interlocked.Increment(ref _totalWritten);
Interlocked.Increment(ref BlastedCount);
Interlocked.Add(ref _totalBytesWritten, 4 + cipher.Length);
// Auto-flush on error/critical
if (_autoFlushOnError && entry.Level >= LogLevel.Error)
if (!_durableWrites && _autoFlushOnError && entry.Level >= LogLevel.Error)
{
try { _currentStream.Flush(true); } catch { /* ignore */ }
}
@@ -848,6 +907,21 @@ namespace EonaCat.LogStack.Flows
_lastError = ex;
Interlocked.Increment(ref _totalErrors);
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);
}
}
@@ -856,29 +930,28 @@ namespace EonaCat.LogStack.Flows
private void OpenNewFile(DateTime date)
{
_currentDate = date;
_currentPath = Path.Combine(
_directory,
_filePrefix + "_" + Environment.MachineName + "_" + date.ToString(_dateFormat) + ".eona");
_currentPath = Path.Combine(_directory, _filePrefix + "_" + Environment.MachineName + "_" + date.ToString(_dateFormat) + EncryptedFileExtension);
bool isNew = !File.Exists(_currentPath) || new FileInfo(_currentPath).Length == 0;
_currentStream = new FileStream(
_currentPath, FileMode.Append, FileAccess.Write, FileShare.Read, 65536);
_currentStream = new FileStream(_currentPath, FileMode.Append, FileAccess.Write, FileShare.Read, 65536, _durableWrites ? FileOptions.WriteThrough : FileOptions.None);
byte[] salt = new byte[SaltSize];
byte[] iv = new byte[IvSize];
if (isNew)
{
using (RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider())
{
rng.GetBytes(salt);
rng.GetBytes(iv);
}
FillRandom(salt);
FillRandom(iv);
_currentStream.Write(Magic, 0, 4);
_currentStream.Write(salt, 0, SaltSize);
_currentStream.Write(iv, 0, IvSize);
_currentSize = 4 + SaltSize + IvSize;
if (_durableWrites)
{
try { _currentStream.Flush(true); } catch { /* ignore */ }
}
}
else
{
@@ -894,13 +967,45 @@ namespace EonaCat.LogStack.Flows
byte[] key = DeriveKey(_password, salt);
Aes aes = Aes.Create();
aes.KeySize = 256;
aes.Mode = CipherMode.CBC;
aes.Padding = PaddingMode.PKCS7;
aes.Key = key;
aes.IV = iv;
_encryptor = aes.CreateEncryptor();
// The Aes instance is only needed to produce the transform below; it must be
// disposed here rather than kept alive, otherwise every file open/rotation
// (daily, or on maxFileSize) leaks an Aes instance and its key material for
// the lifetime of the process.
using (Aes aes = Aes.Create())
{
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()
@@ -937,15 +1042,22 @@ namespace EonaCat.LogStack.Flows
string line = Format(logEvent);
var entry = new QueueEntry { Line = line, Level = logEvent.Level };
if (!_queue.TryAdd(entry))
try
{
Interlocked.Increment(ref DroppedCount);
Action<LogEvent> drop = _onDrop;
if (drop != null)
if (!_queue.TryAdd(entry))
{
drop(logEvent);
}
Interlocked.Increment(ref DroppedCount);
Action<LogEvent> drop = _onDrop;
if (drop != null)
{
drop(logEvent);
}
return WriteResult.Dropped;
}
}
catch (Exception)
{
return WriteResult.Dropped;
}
@@ -1022,8 +1134,7 @@ namespace EonaCat.LogStack.Flows
long nowTicks = DateTime.UtcNow.Ticks;
long windowTicks = _deduplicationWindow.Ticks;
long existing;
if (_deduplicationCache.TryGetValue(key, out existing))
if (_deduplicationCache.TryGetValue(key, out long existing))
{
if (nowTicks - existing < windowTicks)
{
@@ -1047,20 +1158,26 @@ namespace EonaCat.LogStack.Flows
{
if (nowTicks - kvp.Value >= windowTicks)
{
long removed;
_deduplicationCache.TryRemove(kvp.Key, out removed);
_deduplicationCache.TryRemove(kvp.Key, out long removed);
}
}
}
private string Format(LogEvent log)
{
var sb = new StringBuilder(256);
foreach (Action<LogEvent, StringBuilder> action in _compiledTemplate)
StringBuilder sb = StringBuilderPool.Rent();
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)
@@ -1174,8 +1291,7 @@ namespace EonaCat.LogStack.Flows
case "logtype":
return (log, sb) =>
{
string s;
sb.Append(LevelStrings.TryGetValue(log.Level, out s) ? s : log.Level.ToString());
sb.Append(LevelStrings.TryGetValue(log.Level, out string s) ? s : log.Level.ToString());
};
case "message":
return (log, sb) =>
@@ -1225,8 +1341,7 @@ namespace EonaCat.LogStack.Flows
string name = token;
return (log, sb) =>
{
Action<LogEvent, StringBuilder> custom;
if (_customTokens.TryGetValue(name, out custom))
if (_customTokens.TryGetValue(name, out Action<LogEvent, StringBuilder> custom))
{
custom(log, sb);
}
@@ -1267,10 +1382,13 @@ namespace EonaCat.LogStack.Flows
return;
}
FileInfo[] files = dir.GetFiles("*.eona")
.OrderByDescending(f => f.LastWriteTimeUtc)
.ToArray();
// failsafe
if (string.IsNullOrWhiteSpace(EncryptedFileExtension))
{
EncryptedFileExtension = ".eona";
}
FileInfo[] files = dir.GetFiles($"*.{EncryptedFileExtension}").OrderByDescending(f => f.LastWriteTimeUtc).ToArray();
long totalBytes = 0;
int kept = 0;
@@ -1315,23 +1433,21 @@ namespace EonaCat.LogStack.Flows
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)
{
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);
}
File diff suppressed because it is too large Load Diff
@@ -177,9 +177,8 @@ namespace EonaCat.LogStack.Flows
SendToRedis(msg);
string extra;
int batch = 0;
while (batch < 64 && _queue.TryTake(out extra))
while (batch < 64 && _queue.TryTake(out string extra))
{
SendToRedis(extra);
batch++;
@@ -132,7 +132,6 @@ namespace EonaCat.LogStack.Flows
if (_deduplicate)
{
string key = MakeDedupKey(logEvent);
DedupEntry entry;
// Flush expired entries to avoid unbounded growth
if (_dedupMap.Count >= _dedupMaxKeys)
@@ -140,7 +139,7 @@ namespace EonaCat.LogStack.Flows
PurgeExpiredDedupEntries();
}
if (_dedupMap.TryGetValue(key, out entry))
if (_dedupMap.TryGetValue(key, out DedupEntry entry))
{
TimeSpan age = DateTime.UtcNow - entry.FirstSeen;
if (age < _dedupWindow)
@@ -172,8 +171,7 @@ namespace EonaCat.LogStack.Flows
}
// token bucket pass
Bucket bucket;
if (!_buckets.TryGetValue(logEvent.Level, out bucket))
if (!_buckets.TryGetValue(logEvent.Level, out Bucket bucket))
{
bucket = new Bucket(_burstCapacity, _refillPerSecond);
_buckets[logEvent.Level] = bucket;
@@ -221,8 +219,7 @@ namespace EonaCat.LogStack.Flows
List<string> keys = new List<string>(_dedupMap.Keys);
foreach (string key in keys)
{
DedupEntry entry;
if (_dedupMap.TryGetValue(key, out entry) && entry.Count > 1)
if (_dedupMap.TryGetValue(key, out DedupEntry entry) && entry.Count > 1)
{
FlushDedupEntry(key, entry);
}
@@ -290,8 +287,7 @@ namespace EonaCat.LogStack.Flows
foreach (string k in expired)
{
DedupEntry entry;
if (_dedupMap.TryGetValue(k, out entry) && entry.Count > 1)
if (_dedupMap.TryGetValue(k, out DedupEntry entry) && entry.Count > 1)
{
FlushDedupEntry(k, entry);
}
@@ -30,7 +30,11 @@ public class IntelligentRouter : IAsyncDisposable
/// </summary>
public IntelligentRouter AddRule(RoutingRule rule)
{
if (rule == null) throw new ArgumentNullException(nameof(rule));
if (rule == null)
{
throw new ArgumentNullException(nameof(rule));
}
lock (_rulesLock)
{
_rules.Add(rule);
@@ -117,8 +121,15 @@ public class IntelligentRouter : IAsyncDisposable
/// </summary>
public IntelligentRouter RegisterFlow(string flowName, IFlow flow)
{
if (string.IsNullOrEmpty(flowName)) throw new ArgumentNullException(nameof(flowName));
if (flow == null) throw new ArgumentNullException(nameof(flow));
if (string.IsNullOrEmpty(flowName))
{
throw new ArgumentNullException(nameof(flowName));
}
if (flow == null)
{
throw new ArgumentNullException(nameof(flow));
}
lock (_rulesLock)
{
@@ -190,7 +201,9 @@ public class IntelligentRouter : IAsyncDisposable
.ContinueWith(t =>
{
if (t.Status == TaskStatus.RanToCompletion && t.Result == WriteResult.Success)
{
anySuccess = true;
}
}));
}
}
@@ -73,15 +73,21 @@ public sealed class LoggerChain : ILoggerChain
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
@@ -109,7 +115,9 @@ public sealed class LoggerChain : ILoggerChain
public ILoggerChain AddRange(params EonaCatLogStack[] loggers)
{
if (loggers == null)
{
throw new ArgumentNullException(nameof(loggers));
}
foreach (var logger in loggers)
{
@@ -127,7 +135,9 @@ public sealed class LoggerChain : ILoggerChain
ThrowIfDisposed();
if (logger == null)
{
return false;
}
lock (_chainLock)
{
@@ -192,7 +202,9 @@ public sealed class LoggerChain : ILoggerChain
public void DisableLogger(EonaCatLogStack logger)
{
if (logger == null)
{
return;
}
lock (_chainLock)
{
@@ -209,7 +221,9 @@ public sealed class LoggerChain : ILoggerChain
public void EnableLogger(EonaCatLogStack logger)
{
if (logger == null)
{
return;
}
lock (_chainLock)
{
@@ -226,7 +240,9 @@ public sealed class LoggerChain : ILoggerChain
public bool IsLoggerEnabled(EonaCatLogStack logger)
{
if (logger == null)
{
return false;
}
lock (_chainLock)
{
@@ -284,20 +300,26 @@ public sealed class LoggerChain : ILoggerChain
});
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);
@@ -406,9 +406,8 @@ public sealed class MessageTemplate
}
string[] parts = propertyPath.Split('.');
object? current = null;
if (!properties.TryGetValue(parts[0], out current))
if (!properties.TryGetValue(parts[0], out object current))
{
return null;
}
@@ -41,7 +41,10 @@ namespace EonaCat.LogStack.PerformanceInsights
tracker.RecordOperation(durationTicks, byteCount, isError);
_totalOperations++;
if (isError) _totalErrors++;
if (isError)
{
_totalErrors++;
}
}
/// <summary>
@@ -84,10 +84,25 @@ namespace EonaCat.LogStack.PerformanceInsights
{
_operationCount++;
_totalDurationTicks += durationTicks;
if (durationTicks < _minDurationTicks) _minDurationTicks = durationTicks;
if (durationTicks > _maxDurationTicks) _maxDurationTicks = durationTicks;
if (byteCount > 0) _byteCount += byteCount;
if (isError) _errorCount++;
if (durationTicks < _minDurationTicks)
{
_minDurationTicks = durationTicks;
}
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()
{
StringBuilder sb;
if (Pool.TryTake(out sb))
if (Pool.TryTake(out StringBuilder sb))
{
sb.Clear();
return sb;
@@ -387,9 +387,8 @@ public sealed class PropertyToken : TemplateToken
}
string[] parts = propertyPath.Split('.');
object? current = null;
if (!properties.TryGetValue(parts[0], out current))
if (!properties.TryGetValue(parts[0], out object current))
{
return null;
}
@@ -118,7 +118,11 @@ namespace EonaCat.LogStack.Tracing
public void Dispose()
{
if (_disposed) return;
if (_disposed)
{
return;
}
_disposed = true;
if (_span.Status == SpanStatus.Unset)
@@ -36,7 +36,11 @@ namespace EonaCat.LogStack.Tracing
/// </summary>
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;
}
@@ -102,7 +102,10 @@ namespace EonaCat.LogStack.Tracing
/// </summary>
public void RecordException(Exception exception, Dictionary<string, object>? attributes = null)
{
if (exception == null) return;
if (exception == null)
{
return;
}
_exceptions.Add(exception);
Status = SpanStatus.Error;
@@ -129,7 +132,11 @@ namespace EonaCat.LogStack.Tracing
/// </summary>
public void SetAttributes(Dictionary<string, object> attributes)
{
if (attributes == null) return;
if (attributes == null)
{
return;
}
foreach (var kvp in attributes)
{
Attributes[kvp.Key] = kvp.Value;
@@ -154,7 +161,10 @@ namespace EonaCat.LogStack.Tracing
/// </summary>
public void End(SpanStatus status = SpanStatus.Ok, string? description = null)
{
if (_disposed) return;
if (_disposed)
{
return;
}
_stopwatch.Stop();
EndTime = DateTime.UtcNow;
@@ -173,7 +183,11 @@ namespace EonaCat.LogStack.Tracing
public void Dispose()
{
if (_disposed) return;
if (_disposed)
{
return;
}
_disposed = true;
End();
}
@@ -350,7 +350,9 @@ public static class ServiceCollectionExtensions
LoggerDIOptions? options = null)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
options ??= new LoggerDIOptions();
services.AddSingleton(options);
@@ -386,7 +388,9 @@ public static class ServiceCollectionExtensions
this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddSingleton<Tracing.TraceContextManager>();
services.AddSingleton<Tracing.SpanFactory>();
@@ -401,7 +405,9 @@ public static class ServiceCollectionExtensions
this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddSingleton<PerformanceInsights.PerformanceInsightsCollector>();
services.AddSingleton<PerformanceInsights.PerformanceAnalyzer>();
@@ -415,7 +421,9 @@ public static class ServiceCollectionExtensions
this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddSingleton<Telemetry.HealthMonitor>();
services.AddSingleton<Telemetry.TelemetryAggregator>();
@@ -429,7 +437,9 @@ public static class ServiceCollectionExtensions
this IServiceCollection services)
{
if (services == null)
{
throw new ArgumentNullException(nameof(services));
}
services.AddSingleton<Telemetry.TelemetryAggregator>();
services.AddSingleton<Telemetry.HealthMonitor>();
+2
View File
@@ -1484,7 +1484,9 @@ public sealed class LogBuilder
public Chaining.ILoggerChain AddToChain(EonaCatLogStack primaryLogger, bool chainAsFlow = true)
{
if (primaryLogger == null)
{
throw new ArgumentNullException(nameof(primaryLogger));
}
var newLogger = Build();
+10
View File
@@ -84,19 +84,29 @@ public class SuperiorFeaturesStats
var features = new List<string>();
if (IntelligentRoutingEnabled)
{
features.Add($"IntelligentRouting({RouterStats?.TotalRules ?? 0} rules)");
}
if (AdaptiveSamplingEnabled)
{
features.Add($"AdaptiveSampling({SamplingMetrics?.SamplingRate:P1} rate)");
}
if (AnomalyDetectionEnabled)
{
features.Add($"AnomalyDetection({AnomalyStats?.RecentAnomalies ?? 0} recent)");
}
if (ContextSnapshotsEnabled)
{
features.Add($"ContextSnapshots({SnapshotStats?.CurrentSnapshots ?? 0} snapshots)");
}
if (DeadLetterQueueEnabled)
{
features.Add($"DeadLetterQueue({DlqStats?.CurrentQueueSize ?? 0}/{DlqStats?.MaxCapacity ?? 0})");
}
return "Superior Features: " + (features.Count > 0
? string.Join(", ", features)
@@ -25,14 +25,19 @@ public sealed class AdaptiveTelemetryEngine
Interlocked.Increment(ref _events);
if (DetectAnomalies)
{
signal.UpdateAnomalyState();
}
}
public TelemetryEngineSnapshot Snapshot()
{
var snapshot = new TelemetryEngineSnapshot { TotalEvents = Interlocked.Read(ref _events) };
foreach (var item in _signals)
{
snapshot.Signals[item.Key] = item.Value.Snapshot();
}
return snapshot;
}
}
+6 -2
View File
@@ -4,6 +4,10 @@ namespace EonaCat.LogStack.Telemetry;
public sealed class Histogram
{
private readonly ConcurrentQueue<double> _values = new();
public void Record(double value){ _values.Enqueue(value); while(_values.Count>10000)_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)]; }
public void Record(double value){ _values.Enqueue(value); while(_values.Count>10000)
{
_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)
{
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}"));
}
}
@@ -250,8 +254,16 @@ namespace EonaCat.LogStack.Telemetry
_values.Add(value);
Value++;
Sum += value;
if (value < Min) Min = value;
if (value > Max) Max = value;
if (value < Min)
{
Min = value;
}
if (value > Max)
{
Max = value;
}
if (_values.Count > 1000) // Keep reasonable history
{
_values.RemoveRange(0, 100);
@@ -260,7 +272,11 @@ namespace EonaCat.LogStack.Telemetry
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 index = (int)((percentile / 100.0) * sorted.Count);
index = Math.Min(index, sorted.Count - 1);
@@ -28,7 +28,9 @@ public sealed class TelemetrySignal
Max = Count == 1 ? value : Math.Max(Max, value);
_values.Enqueue(value);
while (_values.Count > limit)
{
_values.Dequeue();
}
}
}
@@ -36,7 +38,11 @@ public sealed class TelemetrySignal
{
lock (_sync)
{
if (_values.Count < 10) return;
if (_values.Count < 10)
{
return;
}
var avg = _values.Average();
var variance = _values.Average(v => Math.Pow(v - avg, 2));
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)
{
await using var logger = LogBuilder.CreateDefault("MyApp");
logger.Information("Application started");
logger.Warning("Low memory warning");
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.Json);
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.WriteToEncryptedFile("./logs");
//logBuilder.WriteDiagnostics();