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