Updated encryption file
Added braces
This commit is contained in:
@@ -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();
|
||||
}
|
||||
|
||||
+21
@@ -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;
|
||||
}
|
||||
|
||||
+4
-1
@@ -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>();
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user