From 79428652afd31fad83df990448ff54a6c65da586 Mon Sep 17 00:00:00 2001 From: EonaCat Date: Tue, 16 Jun 2026 21:46:16 +0200 Subject: [PATCH] Updated --- EonaCat.LogStack/EonaCat.LogStack.csproj | 6 +- EonaCat.LogStack/EonaCatLogger.cs | 5 +- .../Boosters/AdvancedBoosters.cs | 287 -- .../Boosters/ContextEnrichmentBooster.cs | 31 + .../Boosters/DeduplicationBooster.cs | 44 + .../Boosters/DistributedTracingBooster.cs | 51 + .../Boosters/ExceptionFingerprintBooster.cs | 39 + .../Boosters/HealthSnapshotBooster.cs | 28 + .../Boosters/PerformanceBooster.cs | 40 + .../Boosters/PropertyFilterBooster.cs | 39 + .../Boosters/RateLimitingBooster.cs | 55 + .../Boosters/SamplingBooster.cs | 51 + .../Boosters/SchemaVersionBooster.cs | 23 + .../Boosters/SequenceBooster.cs | 20 + .../Compatibility/Log4NetCompatibility.cs | 25 + .../LoggingCompatibilityFacade.cs | 21 + .../Compatibility/NLogCompatibility.cs | 25 + .../Compatibility/SerilogCompatibility.cs | 25 + .../EonaCatLoggerProvider.cs | 76 +- .../EonaCatLoggerCore/Flows/AuditFlow.cs | 27 +- .../Flows/EncryptedFileFlow.cs | 26 +- .../EonaCatLoggerCore/Flows/FileFlow.cs | 38 +- .../EonaCatLoggerCore/Flows/StatusFlow.cs | 27 +- .../Logging/EonaCatLoggingExtensions.cs | 41 + EonaCat.LogStack/LogBuilder.cs | 2499 +++++++++-------- EonaCat.LogStack/Telemetry/TelemetryClient.cs | 19 + .../Telemetry/TelemetryDashboard.cs | 11 + EonaCat.LogStack/Telemetry/TelemetryEvent.cs | 13 + EonaCat.LogStack/Telemetry/TelemetryServer.cs | 47 + 29 files changed, 2114 insertions(+), 1525 deletions(-) delete mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/AdvancedBoosters.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/ContextEnrichmentBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/DeduplicationBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/DistributedTracingBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/ExceptionFingerprintBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/HealthSnapshotBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/PerformanceBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/PropertyFilterBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/RateLimitingBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/SamplingBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/SchemaVersionBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Boosters/SequenceBooster.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Compatibility/Log4NetCompatibility.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Compatibility/LoggingCompatibilityFacade.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Compatibility/NLogCompatibility.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Compatibility/SerilogCompatibility.cs create mode 100644 EonaCat.LogStack/EonaCatLoggerCore/Logging/EonaCatLoggingExtensions.cs create mode 100644 EonaCat.LogStack/Telemetry/TelemetryClient.cs create mode 100644 EonaCat.LogStack/Telemetry/TelemetryDashboard.cs create mode 100644 EonaCat.LogStack/Telemetry/TelemetryEvent.cs create mode 100644 EonaCat.LogStack/Telemetry/TelemetryServer.cs diff --git a/EonaCat.LogStack/EonaCat.LogStack.csproj b/EonaCat.LogStack/EonaCat.LogStack.csproj index 1ab53a9..cfdfd9d 100644 --- a/EonaCat.LogStack/EonaCat.LogStack.csproj +++ b/EonaCat.LogStack/EonaCat.LogStack.csproj @@ -51,8 +51,11 @@ It features a rich fluent API for routing log events to dozens of destinations f + + + @@ -97,7 +100,4 @@ It features a rich fluent API for routing log events to dozens of destinations f \ - - - \ No newline at end of file diff --git a/EonaCat.LogStack/EonaCatLogger.cs b/EonaCat.LogStack/EonaCatLogger.cs index 20cbe60..9e25c1b 100644 --- a/EonaCat.LogStack/EonaCatLogger.cs +++ b/EonaCat.LogStack/EonaCatLogger.cs @@ -162,7 +162,10 @@ namespace EonaCat.LogStack { var keep = _concurrentFlows.Where(f => f.Name != name).ToArray(); while (_concurrentFlows.TryTake(out _)) { } - foreach (var f in keep) _concurrentFlows.Add(f); + foreach (var f in keep) + { + _concurrentFlows.Add(f); + } } return this; } diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AdvancedBoosters.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AdvancedBoosters.cs deleted file mode 100644 index 7e7d73f..0000000 --- a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/AdvancedBoosters.cs +++ /dev/null @@ -1,287 +0,0 @@ -using EonaCat.LogStack.Core; -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using System.Threading; - -namespace EonaCat.LogStack.Boosters; - -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -/// -/// Performance monitoring booster that adds timing and resource metrics -/// -public sealed class PerformanceBooster : BoosterBase -{ - private readonly long _startTime = Stopwatch.GetTimestamp(); - private long _totalMemorySnapshot; - - public PerformanceBooster() : base("Performance") { } - - public override bool Boost(ref LogEventBuilder builder) - { - try - { - var currentProcess = Process.GetCurrentProcess(); - var elapsed = (long)((DateTime.UtcNow.Ticks - _startTime) / 10000.0); - - builder.WithProperty("uptime_ms", elapsed); - builder.WithProperty("working_set_mb", currentProcess.WorkingSet64 / (1024 * 1024)); - builder.WithProperty("virtual_memory_mb", currentProcess.VirtualMemorySize64 / (1024 * 1024)); - builder.WithProperty("gc_total_memory", GC.GetTotalMemory(false) / 1024); - builder.WithProperty("thread_count", Process.GetCurrentProcess().Threads.Count); - - return true; - } - catch - { - return true; // Don't filter on error - } - } -} - -/// -/// Distributed tracing booster that extracts and propagates trace context -/// -public sealed class DistributedTracingBooster : BoosterBase -{ - private readonly AsyncLocal _correlationId = new AsyncLocal(); - private readonly AsyncLocal _parentSpanId = new AsyncLocal(); - private int _spanIdCounter; - - public DistributedTracingBooster() : base("DistributedTracing") { } - - /// - /// Sets the correlation ID for the current async context - /// - public void SetCorrelationId(string correlationId) - { - _correlationId.Value = correlationId; - } - - /// - /// Gets the current correlation ID - /// - public string? GetCorrelationId() - { - return _correlationId.Value; - } - - public override bool Boost(ref LogEventBuilder builder) - { - var correlationId = _correlationId.Value; - if (string.IsNullOrEmpty(correlationId)) - { - correlationId = Guid.NewGuid().ToString("N"); - _correlationId.Value = correlationId; - } - - builder.WithProperty("correlation_id", correlationId); - - // Generate span ID - int spanId = Interlocked.Increment(ref _spanIdCounter); - builder.WithProperty("span_id", spanId.ToString("X8")); - - return true; - } -} - -/// -/// Sampling booster that probabilistically filters log events -/// -public sealed class SamplingBooster : BoosterBase -{ - private readonly double _samplingRate; - private readonly Random _random; - private long _totalLogged; - private long _totalSampled; - - public SamplingBooster(double samplingRate = 0.1) : base("Sampling") - { - if (samplingRate < 0.0 || samplingRate > 1.0) - { - throw new ArgumentOutOfRangeException(nameof(samplingRate), "Must be between 0.0 and 1.0"); - } - - _samplingRate = samplingRate; - _random = new Random(); - } - - /// - /// Always log critical and error events, sample the rest - /// - public override bool Boost(ref LogEventBuilder builder) - { - _totalLogged++; - - // Sample based on configured rate - bool shouldLog = _random.NextDouble() < _samplingRate; - if (shouldLog) - { - _totalSampled++; - } - - return shouldLog; - } - - /// - /// Gets sampling statistics - /// - public (long total, long sampled, double rate) GetStatistics() - { - return (_totalLogged, _totalSampled, _totalLogged > 0 ? (double)_totalSampled / _totalLogged : 0.0); - } -} - -/// -/// Property filter booster that includes/excludes events based on properties -/// -public sealed class PropertyFilterBooster : BoosterBase -{ - private readonly Func? _includeFilter; - private readonly Func? _excludeFilter; - - public PropertyFilterBooster( - Func? includeFilter = null, - Func? excludeFilter = null) - : base("PropertyFilter") - { - _includeFilter = includeFilter; - _excludeFilter = excludeFilter; - } - - public override bool Boost(ref LogEventBuilder builder) - { - // If include filter is specified and returns false, skip - if (_includeFilter != null && !_includeFilter(builder)) - { - return false; - } - - // If exclude filter is specified and returns true, skip - if (_excludeFilter != null && _excludeFilter(builder)) - { - return false; - } - - return true; - } -} - -/// -/// Rate limiting booster that throttles log events -/// -public sealed class RateLimitingBooster : BoosterBase -{ - private readonly int _maxEventsPerSecond; - private DateTime _lastResetTime; - private int _eventCount; - private readonly object _lock = new object(); - - public RateLimitingBooster(int maxEventsPerSecond = 1000) : base("RateLimiting") - { - if (maxEventsPerSecond <= 0) - { - throw new ArgumentOutOfRangeException(nameof(maxEventsPerSecond)); - } - - _maxEventsPerSecond = maxEventsPerSecond; - _lastResetTime = DateTime.UtcNow; - } - - public override bool Boost(ref LogEventBuilder builder) - { - lock (_lock) - { - var now = DateTime.UtcNow; - var elapsed = (now - _lastResetTime).TotalSeconds; - - if (elapsed >= 1.0) - { - _lastResetTime = now; - _eventCount = 0; - } - - _eventCount++; - return _eventCount <= _maxEventsPerSecond; - } - } - - /// - /// Gets current rate limiting statistics - /// - public (int current, int limit) GetStatistics() - { - lock (_lock) - { - return (_eventCount, _maxEventsPerSecond); - } - } -} - -/// -/// Context enrichment booster that adds contextual information -/// -public sealed class ContextEnrichmentBooster : BoosterBase -{ - private readonly Func _enricher; - - public ContextEnrichmentBooster(Func enricher) - : base("ContextEnrichment") - { - _enricher = enricher ?? throw new ArgumentNullException(nameof(enricher)); - } - - public override bool Boost(ref LogEventBuilder builder) - { - try - { - builder = _enricher(builder); - return true; - } - catch - { - return true; // Don't filter on enrichment error - } - } -} - -/// -/// Deduplication booster that filters duplicate messages within a time window -/// -public sealed class DeduplicationBooster : BoosterBase -{ - private readonly TimeSpan _window; - private readonly Dictionary _seenMessages = new(); - private readonly object _lock = new object(); - - public DeduplicationBooster(TimeSpan? window = null) : base("Deduplication") - { - _window = window ?? TimeSpan.FromSeconds(10); - } - - public override bool Boost(ref LogEventBuilder builder) - { - lock (_lock) - { - var now = DateTime.UtcNow; - - // Clean old entries - var oldEntries = _seenMessages - .Where(kvp => (now - kvp.Value) > _window) - .Select(kvp => kvp.Key) - .ToList(); - - foreach (var key in oldEntries) - { - _seenMessages.Remove(key); - } - - // For now, don't deduplicate as we don't have access to message in ref struct - // This can be enhanced when builder provides access - return true; - } - } -} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ContextEnrichmentBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ContextEnrichmentBooster.cs new file mode 100644 index 0000000..d0dcbd1 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ContextEnrichmentBooster.cs @@ -0,0 +1,31 @@ +using EonaCat.LogStack.Core; +using System; + +namespace EonaCat.LogStack.Boosters; + +/// +/// Context enrichment booster that adds contextual information +/// +public sealed class ContextEnrichmentBooster : BoosterBase +{ + private readonly Func _enricher; + + public ContextEnrichmentBooster(Func enricher) + : base("ContextEnrichment") + { + _enricher = enricher ?? throw new ArgumentNullException(nameof(enricher)); + } + + public override bool Boost(ref LogEventBuilder builder) + { + try + { + builder = _enricher(builder); + return true; + } + catch + { + return true; // Don't filter on enrichment error + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DeduplicationBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DeduplicationBooster.cs new file mode 100644 index 0000000..b67d89d --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DeduplicationBooster.cs @@ -0,0 +1,44 @@ +using EonaCat.LogStack.Core; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace EonaCat.LogStack.Boosters; + +/// +/// Deduplication booster that filters duplicate messages within a time window +/// +public sealed class DeduplicationBooster : BoosterBase +{ + private readonly TimeSpan _window; + private readonly Dictionary _seenMessages = new(); + private readonly object _lock = new object(); + + public DeduplicationBooster(TimeSpan? window = null) : base("Deduplication") + { + _window = window ?? TimeSpan.FromSeconds(10); + } + + public override bool Boost(ref LogEventBuilder builder) + { + lock (_lock) + { + var now = DateTime.UtcNow; + + // Clean old entries + var oldEntries = _seenMessages + .Where(kvp => (now - kvp.Value) > _window) + .Select(kvp => kvp.Key) + .ToList(); + + foreach (var key in oldEntries) + { + _seenMessages.Remove(key); + } + + // For now, don't deduplicate as we don't have access to message in ref struct + // This can be enhanced when builder provides access + return true; + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DistributedTracingBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DistributedTracingBooster.cs new file mode 100644 index 0000000..5aa1a33 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/DistributedTracingBooster.cs @@ -0,0 +1,51 @@ +using EonaCat.LogStack.Core; +using System; +using System.Threading; + +namespace EonaCat.LogStack.Boosters; + +/// +/// Distributed tracing booster that extracts and propagates trace context +/// +public sealed class DistributedTracingBooster : BoosterBase +{ + private readonly AsyncLocal _correlationId = new AsyncLocal(); + private readonly AsyncLocal _parentSpanId = new AsyncLocal(); + private int _spanIdCounter; + + public DistributedTracingBooster() : base("DistributedTracing") { } + + /// + /// Sets the correlation ID for the current async context + /// + public void SetCorrelationId(string correlationId) + { + _correlationId.Value = correlationId; + } + + /// + /// Gets the current correlation ID + /// + public string? GetCorrelationId() + { + return _correlationId.Value; + } + + public override bool Boost(ref LogEventBuilder builder) + { + var correlationId = _correlationId.Value; + if (string.IsNullOrEmpty(correlationId)) + { + correlationId = Guid.NewGuid().ToString("N"); + _correlationId.Value = correlationId; + } + + builder.WithProperty("correlation_id", correlationId); + + // Generate span ID + int spanId = Interlocked.Increment(ref _spanIdCounter); + builder.WithProperty("span_id", spanId.ToString("X8")); + + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ExceptionFingerprintBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ExceptionFingerprintBooster.cs new file mode 100644 index 0000000..d3c281d --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/ExceptionFingerprintBooster.cs @@ -0,0 +1,39 @@ +using EonaCat.LogStack.Core; +using System; + +namespace EonaCat.LogStack.Boosters; + +/// +/// Adds a stable fingerprint for exceptions/errors so incidents can be grouped. +/// Useful for alerting systems and production error aggregation. +/// +public sealed class ExceptionFingerprintBooster : BoosterBase +{ + public ExceptionFingerprintBooster() : base("ExceptionFingerprint") { } + + public override bool Boost(ref LogEventBuilder builder) + { + try + { + using (var sha256 = System.Security.Cryptography.SHA256.Create()) + { + var input = System.Text.Encoding.UTF8.GetBytes( + builder.ToString() ?? string.Empty); + + var hash = sha256.ComputeHash(input); + + var hex = BitConverter.ToString(hash) + .Replace("-", string.Empty) + .Substring(0, 16); + + builder.WithProperty("event_fingerprint", hex); + } + } + catch + { + // Do not allow logging failures to break the application + } + + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/HealthSnapshotBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/HealthSnapshotBooster.cs new file mode 100644 index 0000000..8ed0272 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/HealthSnapshotBooster.cs @@ -0,0 +1,28 @@ +using EonaCat.LogStack.Core; +using System; +using System.Diagnostics; + +namespace EonaCat.LogStack.Boosters; + +/// +/// Adds a health snapshot: CPU, memory pressure and thread information. +/// +public sealed class HealthSnapshotBooster : BoosterBase +{ + public HealthSnapshotBooster() : base("HealthSnapshot") { } + + public override bool Boost(ref LogEventBuilder builder) + { + try + { + using var process = Process.GetCurrentProcess(); + builder.WithProperty("health_memory_mb", process.WorkingSet64 / 1024 / 1024); + builder.WithProperty("health_threads", process.Threads.Count); + builder.WithProperty("health_gc_memory_mb", GC.GetTotalMemory(false) / 1024 / 1024); + builder.WithProperty("health_processor_count", Environment.ProcessorCount); + } + catch { } + + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/PerformanceBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/PerformanceBooster.cs new file mode 100644 index 0000000..51cbd69 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/PerformanceBooster.cs @@ -0,0 +1,40 @@ +using EonaCat.LogStack.Core; +using System; +using System.Diagnostics; + +namespace EonaCat.LogStack.Boosters; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Performance monitoring booster that adds timing and resource metrics +/// +public sealed class PerformanceBooster : BoosterBase +{ + private readonly long _startTime = Stopwatch.GetTimestamp(); + private long _totalMemorySnapshot; + + public PerformanceBooster() : base("Performance") { } + + public override bool Boost(ref LogEventBuilder builder) + { + try + { + var currentProcess = Process.GetCurrentProcess(); + var elapsed = (long)((DateTime.UtcNow.Ticks - _startTime) / 10000.0); + + builder.WithProperty("uptime_ms", elapsed); + builder.WithProperty("working_set_mb", currentProcess.WorkingSet64 / (1024 * 1024)); + builder.WithProperty("virtual_memory_mb", currentProcess.VirtualMemorySize64 / (1024 * 1024)); + builder.WithProperty("gc_total_memory", GC.GetTotalMemory(false) / 1024); + builder.WithProperty("thread_count", Process.GetCurrentProcess().Threads.Count); + + return true; + } + catch + { + return true; // Don't filter on error + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/PropertyFilterBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/PropertyFilterBooster.cs new file mode 100644 index 0000000..0bcb700 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/PropertyFilterBooster.cs @@ -0,0 +1,39 @@ +using EonaCat.LogStack.Core; +using System; + +namespace EonaCat.LogStack.Boosters; + +/// +/// Property filter booster that includes/excludes events based on properties +/// +public sealed class PropertyFilterBooster : BoosterBase +{ + private readonly Func? _includeFilter; + private readonly Func? _excludeFilter; + + public PropertyFilterBooster( + Func? includeFilter = null, + Func? excludeFilter = null) + : base("PropertyFilter") + { + _includeFilter = includeFilter; + _excludeFilter = excludeFilter; + } + + public override bool Boost(ref LogEventBuilder builder) + { + // If include filter is specified and returns false, skip + if (_includeFilter != null && !_includeFilter(builder)) + { + return false; + } + + // If exclude filter is specified and returns true, skip + if (_excludeFilter != null && _excludeFilter(builder)) + { + return false; + } + + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/RateLimitingBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/RateLimitingBooster.cs new file mode 100644 index 0000000..6ab730c --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/RateLimitingBooster.cs @@ -0,0 +1,55 @@ +using EonaCat.LogStack.Core; +using System; + +namespace EonaCat.LogStack.Boosters; + +/// +/// Rate limiting booster that throttles log events +/// +public sealed class RateLimitingBooster : BoosterBase +{ + private readonly int _maxEventsPerSecond; + private DateTime _lastResetTime; + private int _eventCount; + private readonly object _lock = new object(); + + public RateLimitingBooster(int maxEventsPerSecond = 1000) : base("RateLimiting") + { + if (maxEventsPerSecond <= 0) + { + throw new ArgumentOutOfRangeException(nameof(maxEventsPerSecond)); + } + + _maxEventsPerSecond = maxEventsPerSecond; + _lastResetTime = DateTime.UtcNow; + } + + public override bool Boost(ref LogEventBuilder builder) + { + lock (_lock) + { + var now = DateTime.UtcNow; + var elapsed = (now - _lastResetTime).TotalSeconds; + + if (elapsed >= 1.0) + { + _lastResetTime = now; + _eventCount = 0; + } + + _eventCount++; + return _eventCount <= _maxEventsPerSecond; + } + } + + /// + /// Gets current rate limiting statistics + /// + public (int current, int limit) GetStatistics() + { + lock (_lock) + { + return (_eventCount, _maxEventsPerSecond); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/SamplingBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/SamplingBooster.cs new file mode 100644 index 0000000..f0ee617 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/SamplingBooster.cs @@ -0,0 +1,51 @@ +using EonaCat.LogStack.Core; +using System; + +namespace EonaCat.LogStack.Boosters; + +/// +/// Sampling booster that probabilistically filters log events +/// +public sealed class SamplingBooster : BoosterBase +{ + private readonly double _samplingRate; + private readonly Random _random; + private long _totalLogged; + private long _totalSampled; + + public SamplingBooster(double samplingRate = 0.1) : base("Sampling") + { + if (samplingRate < 0.0 || samplingRate > 1.0) + { + throw new ArgumentOutOfRangeException(nameof(samplingRate), "Must be between 0.0 and 1.0"); + } + + _samplingRate = samplingRate; + _random = new Random(); + } + + /// + /// Always log critical and error events, sample the rest + /// + public override bool Boost(ref LogEventBuilder builder) + { + _totalLogged++; + + // Sample based on configured rate + bool shouldLog = _random.NextDouble() < _samplingRate; + if (shouldLog) + { + _totalSampled++; + } + + return shouldLog; + } + + /// + /// Gets sampling statistics + /// + public (long total, long sampled, double rate) GetStatistics() + { + return (_totalLogged, _totalSampled, _totalLogged > 0 ? (double)_totalSampled / _totalLogged : 0.0); + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/SchemaVersionBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/SchemaVersionBooster.cs new file mode 100644 index 0000000..190ecd1 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/SchemaVersionBooster.cs @@ -0,0 +1,23 @@ +using EonaCat.LogStack.Core; + +namespace EonaCat.LogStack.Boosters; + +/// +/// Adds a log schema version to make future log format migrations easier. +/// +public sealed class SchemaVersionBooster : BoosterBase +{ + private readonly string _version; + + public SchemaVersionBooster(string version = "1.0") + : base("SchemaVersion") + { + _version = version; + } + + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("log_schema_version", _version); + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Boosters/SequenceBooster.cs b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/SequenceBooster.cs new file mode 100644 index 0000000..740dacc --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Boosters/SequenceBooster.cs @@ -0,0 +1,20 @@ +using EonaCat.LogStack.Core; +using System.Threading; + +namespace EonaCat.LogStack.Boosters; + +/// +/// Adds a monotonic event sequence number. Helpful for ordering events across async pipelines. +/// +public sealed class SequenceBooster : BoosterBase +{ + private long _sequence; + + public SequenceBooster() : base("Sequence") { } + + public override bool Boost(ref LogEventBuilder builder) + { + builder.WithProperty("sequence", Interlocked.Increment(ref _sequence)); + return true; + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/Log4NetCompatibility.cs b/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/Log4NetCompatibility.cs new file mode 100644 index 0000000..6ae9be6 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/Log4NetCompatibility.cs @@ -0,0 +1,25 @@ +using System; +using EonaCat.LogStack.Core; + +namespace EonaCat.LogStack.Compatibility +{ + public interface ILog4NetLogger + { + void Log(string level, string message, Exception exception = null); + } + + public sealed class EonaCatLog4NetAdapter : ILog4NetLogger + { + private readonly EonaCatLogStack _logger; + public EonaCatLog4NetAdapter(EonaCatLogStack logger) { _logger = logger; } + public void Log(string level, string message, Exception exception = null) + { + if (!Enum.TryParse(level, true, out LogLevel parsed)) + { + parsed = LogLevel.Information; + } + + _logger.Log(parsed, exception, message); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/LoggingCompatibilityFacade.cs b/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/LoggingCompatibilityFacade.cs new file mode 100644 index 0000000..d0bacaa --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/LoggingCompatibilityFacade.cs @@ -0,0 +1,21 @@ +using EonaCat.LogStack.Core; + +namespace EonaCat.LogStack.Compatibility; + +/// +/// Single entry point for dependency-free adapters. This allows applications +/// migrating from Serilog, log4net or NLog to keep their integration layer. +/// +public sealed class LoggingCompatibilityFacade +{ + public EonaCatSerilogAdapter Serilog { get; } + public EonaCatLog4NetAdapter Log4Net { get; } + public EonaCatNLogAdapter NLog { get; } + + public LoggingCompatibilityFacade(EonaCatLogStack logger) + { + Serilog = new EonaCatSerilogAdapter(logger); + Log4Net = new EonaCatLog4NetAdapter(logger); + NLog = new EonaCatNLogAdapter(logger); + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/NLogCompatibility.cs b/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/NLogCompatibility.cs new file mode 100644 index 0000000..9585bd8 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/NLogCompatibility.cs @@ -0,0 +1,25 @@ +using System; +using EonaCat.LogStack.Core; + +namespace EonaCat.LogStack.Compatibility +{ + public interface INLogLogger + { + void Log(string level, string message, Exception exception = null); + } + + public sealed class EonaCatNLogAdapter : INLogLogger + { + private readonly EonaCatLogStack _logger; + public EonaCatNLogAdapter(EonaCatLogStack logger) { _logger = logger; } + public void Log(string level, string message, Exception exception = null) + { + if (!Enum.TryParse(level, true, out LogLevel parsed)) + { + parsed = LogLevel.Information; + } + + _logger.Log(parsed, exception, message); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/SerilogCompatibility.cs b/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/SerilogCompatibility.cs new file mode 100644 index 0000000..18a03c2 --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Compatibility/SerilogCompatibility.cs @@ -0,0 +1,25 @@ +using System; +using EonaCat.LogStack.Core; + +namespace EonaCat.LogStack.Compatibility +{ + public interface ISerilogLogger + { + void Write(string level, string message, Exception exception = null); + } + + public sealed class EonaCatSerilogAdapter : ISerilogLogger + { + private readonly EonaCatLogStack _logger; + public EonaCatSerilogAdapter(EonaCatLogStack logger) { _logger = logger; } + public void Write(string level, string message, Exception exception = null) + { + if (!Enum.TryParse(level, true, out LogLevel parsed)) + { + parsed = LogLevel.Information; + } + + _logger.Log(parsed, exception, message); + } + } +} diff --git a/EonaCat.LogStack/EonaCatLoggerCore/EonaCatLoggerProvider.cs b/EonaCat.LogStack/EonaCatLoggerCore/EonaCatLoggerProvider.cs index 9e2238e..ef7d2d8 100644 --- a/EonaCat.LogStack/EonaCatLoggerCore/EonaCatLoggerProvider.cs +++ b/EonaCat.LogStack/EonaCatLoggerCore/EonaCatLoggerProvider.cs @@ -3,21 +3,75 @@ using System; namespace EonaCat.LogStack.Logging; -public sealed class EonaCatLoggerProvider : ILoggerProvider +/// +/// Microsoft.Extensions.Logging bridge. Works with ASP.NET Core, worker services, +/// console applications, VS extensions and any .NET project that uses ILogger. +/// +public sealed class EonaCatLoggerProvider : ILoggerProvider, ISupportExternalScope { private readonly EonaCatLogStack _logStack; - public EonaCatLoggerProvider(EonaCatLogStack logStack)=>_logStack=logStack; - public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) => new CategoryLogger(categoryName,_logStack); - public void Dispose(){} + private IExternalScopeProvider? _scopeProvider; + + public EonaCatLoggerProvider(EonaCatLogStack logStack) => _logStack = logStack; + + public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) + => new CategoryLogger(categoryName, _logStack, _scopeProvider); + + public void SetScopeProvider(IExternalScopeProvider scopeProvider) + => _scopeProvider = scopeProvider; + + public void Dispose() { } } -internal sealed class CategoryLogger : Microsoft.Extensions.Logging.ILogger +internal sealed class CategoryLogger : Microsoft.Extensions.Logging.ILogger { private readonly string _category; - private readonly EonaCatLogStack _stack; - public CategoryLogger(string category,EonaCatLogStack stack){_category=category;_stack=stack;} - public IDisposable BeginScope(TState state) where TState:notnull => NullScope.Instance; - public bool IsEnabled(LogLevel logLevel)=>true; - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter){var msg=$"[{_category}] {formatter(state,exception)}"; _stack.Log(msg);} + private readonly EonaCatLogStack _EonaCatLogStack; + private readonly IExternalScopeProvider? _scopes; + + public CategoryLogger(string category, EonaCatLogStack EonaCatLogStack, IExternalScopeProvider? scopes) + { + _category = category; + _EonaCatLogStack = EonaCatLogStack; + _scopes = scopes; + } + + public IDisposable BeginScope(TState state) where TState : notnull + => _scopes?.Push(state) ?? NullScope.Instance; + + public bool IsEnabled(LogLevel logLevel) + => logLevel != LogLevel.None; + + public void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter) + { + if (!IsEnabled(logLevel)) + { + return; + } + + var message = formatter(state, exception); + + _scopes?.ForEachScope((scope, _) => + { + message = $"{message} | Scope={scope}"; + }, null); + + if (exception != null) + { + message = $"{message} | Exception={exception}"; + } + + _EonaCatLogStack.Log($"[{logLevel}] [{_category}] {message}"); + } +} + +internal sealed class NullScope : IDisposable +{ + public static readonly NullScope Instance = new(); + public void Dispose() { } } -internal sealed class NullScope:IDisposable { public static readonly NullScope Instance= new(); public void Dispose(){} } diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs index 0b16a6d..86a64a0 100644 --- a/EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/AuditFlow.cs @@ -3,6 +3,7 @@ using EonaCat.LogStack.EonaCatLogStackCore; using EonaCat.LogStack.Flows; using System; using System.Collections.Concurrent; +using System.Diagnostics; using System.IO; using System.Linq; using System.Security.Cryptography; @@ -43,6 +44,7 @@ namespace EonaCat.LogStack.Flows /// public sealed class AuditFlow : FlowBase { + public event EventHandler OnDirectoryException; private const string Delimiter = "|"; private const int HashLength = 64; // hex SHA-256 @@ -86,7 +88,30 @@ namespace EonaCat.LogStack.Flows directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, directory.Substring(2)); } - Directory.CreateDirectory(directory); + try + { + Directory.CreateDirectory(directory); + } + catch + { + try + { + var processId = Process.GetCurrentProcess().Id; + var newDirectory = Path.Combine(Path.GetTempPath(), "EonaCat.LogStack", processId.ToString()); + Directory.CreateDirectory(newDirectory); + OnDirectoryException?.Invoke(this, $"AuditFlow: Could not create directory: '{directory}', using directory '{newDirectory}' instead"); + directory = newDirectory; + } + catch + { + var newDirectory = Path.GetTempPath(); + OnDirectoryException?.Invoke(this, $"AuditFlow: Could not create directory: '{directory}', using directory '{newDirectory}' instead"); + directory = newDirectory; + + // Last resort: disable file output by pointing to a safe-ish temp path. + // The writer thread still runs and swallows failures. + } + } // One file per day, named with date stamp string date = DateTime.UtcNow.ToString("yyyyMMdd"); diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/EncryptedFileFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/EncryptedFileFlow.cs index f62eec7..6f69362 100644 --- a/EonaCat.LogStack/EonaCatLoggerCore/Flows/EncryptedFileFlow.cs +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/EncryptedFileFlow.cs @@ -33,6 +33,7 @@ namespace EonaCat.LogStack.Flows /// public sealed class EncryptedFileFlow : FlowBase { + public event EventHandler OnDirectoryException; private static readonly byte[] Magic = new byte[] { 0x45, 0x4F, 0x4E, 0x41 }; // "EONA" private const int SaltSize = 32; private const int IvSize = 16; @@ -211,7 +212,30 @@ namespace EonaCat.LogStack.Flows _directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _directory.Substring(2)); } - Directory.CreateDirectory(_directory); + try + { + Directory.CreateDirectory(_directory); + } + catch + { + try + { + var processId = Process.GetCurrentProcess().Id; + var newDirectory = Path.Combine(Path.GetTempPath(), "EonaCat.LogStack", processId.ToString()); + Directory.CreateDirectory(newDirectory); + OnDirectoryException?.Invoke(this, $"Could not create directory: '{_directory}', using directory '{newDirectory}' instead"); + _directory = newDirectory; + } + catch + { + var newDirectory = Path.GetTempPath(); + OnDirectoryException?.Invoke(this, $"Could not create directory: '{_directory}', using directory '{newDirectory}' instead"); + _directory = newDirectory; + + // Last resort: disable file output by pointing to a safe-ish temp path. + // The writer thread still runs and swallows failures. + } + } _queue = new BlockingCollection(new ConcurrentQueue(), QueueCapacity); diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs index 9670ba4..10ac1fa 100644 --- a/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/FileFlow.cs @@ -24,6 +24,7 @@ namespace EonaCat.LogStack.Flows /// public sealed class FileFlow : FlowBase { + public event EventHandler OnDirectoryException; private const int FileBufferSize = 131072; // 128 KB private const int WriterBufferSize = 131072; // 128 KB private readonly int _batchSize; @@ -211,7 +212,33 @@ namespace EonaCat.LogStack.Flows _directory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, _directory.Substring(2)); } - Directory.CreateDirectory(_directory); + // Never allow logging initialization to crash the host because of a bad + // directory (ACLs, antivirus locks, read-only locations, etc.). + // Fall back to a per-user temp directory. + try + { + Directory.CreateDirectory(_directory); + } + catch + { + try + { + var processId = Process.GetCurrentProcess().Id; + var newDirectory = Path.Combine(Path.GetTempPath(), "EonaCat.LogStack", processId.ToString()); + Directory.CreateDirectory(newDirectory); + OnDirectoryException?.Invoke(this, $"FileFlow: Could not create directory: '{_directory}', using directory '{newDirectory}' instead"); + _directory = newDirectory; + } + catch + { + var newDirectory = Path.GetTempPath(); + OnDirectoryException?.Invoke(this, $"FileFlow: Could not create directory: '{_directory}', using directory '{newDirectory}' instead"); + _directory = newDirectory; + + // Last resort: disable file output by pointing to a safe-ish temp path. + // The writer thread still runs and swallows failures. + } + } // BlockingCollection with bounded capacity _queue = new BlockingCollection(new ConcurrentQueue(), QueueCapacity); @@ -878,7 +905,14 @@ namespace EonaCat.LogStack.Flows return; } - Console.Error.WriteLine(text); + try + { + Console.Error.WriteLine(text); + } + catch + { + // Logging must never bring down the application. + } } private void WriteLogEvent(LogEvent log) diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Flows/StatusFlow.cs b/EonaCat.LogStack/EonaCatLoggerCore/Flows/StatusFlow.cs index 8d44b09..c4394fb 100644 --- a/EonaCat.LogStack/EonaCatLoggerCore/Flows/StatusFlow.cs +++ b/EonaCat.LogStack/EonaCatLoggerCore/Flows/StatusFlow.cs @@ -2,6 +2,7 @@ using EonaCat.LogStack.Flows; using System; using System.Collections.Generic; +using System.Diagnostics; using System.IO; using System.Linq; using System.Net; @@ -36,6 +37,7 @@ namespace ServiceMonitoring public sealed class StatusFlow : FlowBase { + public event EventHandler OnDirectoryException; private readonly List _servicesToMonitor; private readonly TimeSpan _checkInterval; private readonly string _statusDirectory; @@ -86,7 +88,30 @@ namespace ServiceMonitoring statusDirectory = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, statusDirectory.Substring(2)); } - Directory.CreateDirectory(statusDirectory); + try + { + Directory.CreateDirectory(statusDirectory); + } + catch + { + try + { + var processId = Process.GetCurrentProcess().Id; + var newDirectory = Path.Combine(Path.GetTempPath(), "EonaCat.LogStack", processId.ToString()); + Directory.CreateDirectory(newDirectory); + OnDirectoryException?.Invoke(this, $"StatusFlow: Could not create directory: '{statusDirectory}', using directory '{newDirectory}' instead"); + statusDirectory = newDirectory; + } + catch + { + var newDirectory = Path.GetTempPath(); + OnDirectoryException?.Invoke(this, $"StatusFlow: Could not create directory: '{statusDirectory}', using directory '{newDirectory}' instead"); + statusDirectory = newDirectory; + + // Last resort: disable file output by pointing to a safe-ish temp path. + // The writer thread still runs and swallows failures. + } + } _statusDirectory = statusDirectory; _cts = new CancellationTokenSource(); diff --git a/EonaCat.LogStack/EonaCatLoggerCore/Logging/EonaCatLoggingExtensions.cs b/EonaCat.LogStack/EonaCatLoggerCore/Logging/EonaCatLoggingExtensions.cs new file mode 100644 index 0000000..4db014c --- /dev/null +++ b/EonaCat.LogStack/EonaCatLoggerCore/Logging/EonaCatLoggingExtensions.cs @@ -0,0 +1,41 @@ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using System; + +namespace EonaCat.LogStack.Logging; + +/// +/// DI integration for every .NET host type. +/// +public static class EonaCatLoggingExtensions +{ + public static ILoggingBuilder AddEonaCatLogStack( + this ILoggingBuilder builder, + Action? configure = null) + { + var stack = new EonaCatLogStack(); + configure?.Invoke(stack); + + builder.Services.AddSingleton(stack); + builder.Services.AddSingleton( + new EonaCatLoggerProvider(stack)); + + return builder; + } + + public static IServiceCollection AddEonaCatLogStack( + this IServiceCollection services, + Action? configure = null) + { + var stack = new EonaCatLogStack(); + configure?.Invoke(stack); + + services.AddSingleton(stack); + services.AddSingleton( + new EonaCatLoggerProvider(stack)); + + services.AddSingleton(); + return services; + } +} diff --git a/EonaCat.LogStack/LogBuilder.cs b/EonaCat.LogStack/LogBuilder.cs index 69b4587..ed04815 100644 --- a/EonaCat.LogStack/LogBuilder.cs +++ b/EonaCat.LogStack/LogBuilder.cs @@ -1,1218 +1,1281 @@ -using EonaCat.LogStack.Boosters; -using EonaCat.LogStack.Core; -using EonaCat.LogStack.EonaCatLogStackCore; -using EonaCat.LogStack.EonaCatLogStackCore.Policies; -using EonaCat.LogStack.Flows; -using ServiceMonitoring; -using System; -using System.Collections.Generic; -using System.Data.Common; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Net.Security; -using System.Security.Cryptography.X509Certificates; - -namespace EonaCat.LogStack.Configuration; - -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -/// -/// Fluent builder for configuring the logger with flows and boosters -/// -public sealed class LogBuilder -{ - private readonly string _category; - private LogLevel _minimumLevel = LogLevel.Trace; - private TimestampMode _timestampMode = TimestampMode.Utc; - private readonly List _flows = new(); - private readonly List _boosters = new(); - private DynamicLevelController? _dynamicLevel; - private bool _useAsyncPipeline; - private int _asyncPipelineCapacity = 65536; - - public event EventHandler OnLog; - - public LogBuilder(string category = "Application") - { - _category = category ?? throw new ArgumentNullException(nameof(category)); - UseAsyncPipeline(); - } - - /// - /// Sets the minimum log level - /// - public LogBuilder WithMinimumLevel(LogLevel level) - { - _minimumLevel = level; - return this; - } - - /// - /// Sets the timestamp mode - /// - public LogBuilder WithTimestampMode(TimestampMode mode) - { - _timestampMode = mode; - return this; - } - - /// - /// Adds console output - /// - public LogBuilder WriteToConsole( - LogLevel minimumLevel = LogLevel.Trace, - bool useColors = true) - { - _flows.Add(new ConsoleFlow(minimumLevel, useColors, _timestampMode)); - return this; - } - - /// - /// Adds diagnostics - /// - public LogBuilder WriteDiagnostics( - TimeSpan snapshotInterval = default(TimeSpan), - bool injectIntoEvents = false, - bool writeSnapshotEvents = true, - string snapshotCategory = "Diagnostics", - IFlow forwardTo = null, - LogLevel minimumLevel = LogLevel.Trace, - Func> customMetrics = null) - { - _flows.Add(new DiagnosticsFlow( - snapshotInterval, - injectIntoEvents, - writeSnapshotEvents, - snapshotCategory, - forwardTo, - minimumLevel, - customMetrics)); - return this; - } - - /// - /// Adds file output - /// - public LogBuilder WriteToFile( - string directory, - string filePrefix = "log", - long maxFileSize = 100 * 1024 * 1024, - long maxDirectorySize = 2L * 1024 * 1024 * 1024, - FileRetentionPolicy fileRetentionPolicy = null, - int flushIntervalInMilliSeconds = 2000, - bool useCategoryRouting = false, - int batchSize = 1, - LogLevel[]? logLevelsForSeparateFiles = null, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, - FileOutputFormat outputFormat = FileOutputFormat.Text, - CompressionFormat compression = CompressionFormat.GZip, - string template = "[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}") - { - _flows.Add(new FileFlow( - directory, - filePrefix, - maxFileSize, - maxDirectorySize, - fileRetentionPolicy, - flushIntervalInMilliSeconds, - batchSize, - minimumLevel, - useCategoryRouting, - logLevelsForSeparateFiles, - _timestampMode, - backpressureStrategy, - outputFormat, - compression, - template)); - return this; - } - - public LogBuilder WriteToEncryptedFile( - string directory, - string filePrefix = "log", - string password = "EonaCat", - long maxFileSize = 100 * 1024 * 1024, - FileRetentionPolicy fileRetentionPolicy = null, - int flushIntervalInMilliSeconds = 2000, - bool useCategoryRouting = false, - LogLevel[]? logLevelsForSeparateFiles = null, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new EncryptedFileFlow( - directory, - password, - filePrefix, - maxFileSize, - maxDirectorySize: 2L * 1024 * 1024 * 1024, - retention: fileRetentionPolicy, - flushIntervalMs: flushIntervalInMilliSeconds, - batchSize: 1, - minimumLevel: minimumLevel, - useCategoryRouting: useCategoryRouting, - logLevelsForSeparateFiles: logLevelsForSeparateFiles, - tsMode: _timestampMode)); - return this; - } - - /// - /// Write to a rolling buffer - /// - /// Maximum number of events to retain. - /// Minimum level to store in the buffer. - /// - /// When a log event reaches this level or above, the current buffer - /// contents are immediately forwarded to . - /// Set to LogLevel.None (or omit) to disable. - /// - /// - /// Flow to forward the buffered context to when the trigger fires. - /// Can be null even when is set. - /// - /// - /// How many buffered lines to forward before the triggering event. - /// Defaults to entire buffer (int.MaxValue). - /// - /// - public LogBuilder WriteToRollingBuffer( - int capacity = 500, - LogLevel minimumLevel = LogLevel.Trace, - LogLevel triggerLevel = LogLevel.Error, - IFlow triggerTarget = null, - int preContextLines = int.MaxValue) - { - _flows.Add(new RollingBufferFlow( - capacity, - minimumLevel, - triggerLevel, - triggerTarget, - preContextLines)); - return this; - } - - /// - /// Decrypt a file which is encrypted by EonaCat Logger - /// - /// encrypted file source path - /// destination path for decrypted file - /// password used by encryption - /// - public static bool DecryptFile(string encryptedPath, string outputPath, string password) - { - return EncryptedFileFlow.DecryptToFile(encryptedPath, outputPath, password); - } - - /// - /// Adds Database output - /// - public LogBuilder WriteToDatabase( - Func connectionFactory, - string tableName = "logs", - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new DatabaseFlow( - connectionFactory, - tableName, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds Snmp traps - /// - public LogBuilder WriteToSnmpTrap(string host, int port = 162, string oid = "1.3.6.1.4.1.9999", LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new SnmpTrapFlow( - host, - port, - oid, - minimumLevel)); - return this; - } - - /// - /// Adds Discord - /// - public LogBuilder WriteToDiscord( - string webHookUrl, - string botName = "EonaCatBot", - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new DiscordFlow( - webHookUrl, - botName, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds ElasticSearch - /// - public LogBuilder WriteToElasticSearch( - string elasticSearchUrl, - string indexName = "EonaCatIndex", - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new ElasticSearchFlow( - elasticSearchUrl, - indexName, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds Telegram - /// - public LogBuilder WriteToTelegram(string botToken, string chatId = "EonaCat", int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new TelegramFlow( - botToken, - chatId, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds a tamper-evident audit trail. - /// - /// Each audit entry is hash-chained: every line stores a SHA-256 of the previous - /// line's hash + the current entry body, so deletion or modification of any past - /// entry invalidates all subsequent hashes. - /// - /// Use to verify file integrity at any time. - /// - /// Directory where the .audit file is written. - /// File name prefix (default: "audit"). - /// - /// Which severity levels are recorded in the audit trail: - /// - /// – every log event (default) - /// – Warning, Error, Critical - /// – Error and Critical only - /// – Critical only - /// - /// - /// Minimum for audit capture. - /// Whether structured properties are appended to each entry. - public LogBuilder WriteToAudit( - string directory, - string filePrefix = "audit", - AuditLevel auditLevel = AuditLevel.All, - LogLevel minimumLevel = LogLevel.Trace, - bool includeProperties = true) - { - _flows.Add(new AuditFlow( - directory, - filePrefix, - auditLevel, - minimumLevel, - includeProperties)); - return this; - } - - /// - /// Adds Slack - /// - public LogBuilder WriteToSlack(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new SlackFlow( - webhookUrl, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds Slack - /// - public LogBuilder WriteToMicrosoftTeams(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new MicrosoftTeamsFlow( - webhookUrl, - batchSize, - minimumLevel)); - return this; - } - - /// - /// Adds a TCP flow. - /// - public LogBuilder WriteToTcp( - string host, - int port, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, - bool useTls = false, - RemoteCertificateValidationCallback certValidationCallback = null, - X509CertificateCollection clientCertificates = null) - { - if (string.IsNullOrWhiteSpace(host)) - { - throw new ArgumentException("Host cannot be null or empty.", nameof(host)); - } - - if (port <= 0 || port > 65535) - { - throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 1 and 65535."); - } - - _flows.Add(new TcpFlow( - host, - port, - batchSize, - minimumLevel, - backpressureStrategy, - useTls, - certValidationCallback, - clientCertificates)); - - return this; - } - - - - /// - /// Adds a Retry flow to retry failed log writes. - /// - public LogBuilder WriteToRetry( - IFlow primaryFlow, - int maxRetries = 3, - TimeSpan? initialDelay = null, - bool exponentialBackoff = true) - { - if (primaryFlow == null) - { - throw new ArgumentNullException(nameof(primaryFlow)); - } - - _flows.Add(new RetryFlow( - primaryFlow, - maxRetries, - initialDelay ?? TimeSpan.FromMilliseconds(200), - exponentialBackoff)); - - return this; - } - - /// - /// Adds an EventLog flow for sending logs to a remote destination. - /// - public LogBuilder WriteToEventLogFlow( - string destination, - int port = 514, - LogLevel minimumLevel = LogLevel.Trace, - int bufferSize = 100, - TimeSpan? flushInterval = null, - bool useTls = false, - RemoteCertificateValidationCallback? certificateValidationCallback = null, - X509CertificateCollection? clientCertificates = null) - { - if (string.IsNullOrWhiteSpace(destination)) - { - throw new ArgumentException("Destination cannot be null or empty.", nameof(destination)); - } - - _flows.Add(new EventLogFlow( - destination, - port, - minimumLevel, - bufferSize, - flushInterval ?? TimeSpan.FromSeconds(5), - useTls, - certificateValidationCallback, - clientCertificates)); - return this; - } - - /// - /// Adds a Failover flow that switches to a secondary flow if the primary fails. - /// - public LogBuilder WriteToFailover(IFlow primaryFlow, IFlow secondaryFlow, TimeSpan? recoveryCheckInterval = null, int failureThreshold = 5) - { - if (primaryFlow == null) - { - throw new ArgumentNullException(nameof(primaryFlow)); - } - - if (secondaryFlow == null) - { - throw new ArgumentNullException(nameof(secondaryFlow)); - } - - _flows.Add(new FailoverFlow(primaryFlow, secondaryFlow, recoveryCheckInterval, failureThreshold)); - - return this; - } - - /// - /// Adds a Syslog TCP flow. - /// - public LogBuilder WriteToSyslogTcp( - string host, - int port = 514, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, - bool useTls = false, - RemoteCertificateValidationCallback certValidationCallback = null, - X509CertificateCollection clientCertificates = null) - { - if (string.IsNullOrWhiteSpace(host)) - { - throw new ArgumentException("Host cannot be null or empty.", nameof(host)); - } - - if (port <= 0 || port > 65535) - { - throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 1 and 65535."); - } - - _flows.Add(new SyslogTcpFlow( - host, - port, - batchSize, - minimumLevel, - backpressureStrategy, - useTls, - certValidationCallback, - clientCertificates)); - - return this; - } - - /// - /// Adds a Status Monitoring flow. - /// - public LogBuilder WriteToStatusFlow( - List servicesToMonitor, - TimeSpan? checkInterval = null, - string statusDirectory = null, - Action statusChangedTrigger = null) - { - _flows.Add(new StatusFlow( - servicesToMonitor, - checkInterval, - statusDirectory, - statusChangedTrigger)); - - return this; - } - - /// - /// Adds Syslog Tcp - /// - public LogBuilder WriteToSyslogUdp( - string host, - int port, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) - { - _flows.Add(new SyslogUdpFlow( - host, - port, - batchSize, - minimumLevel, - backpressureStrategy)); - return this; - } - - /// - /// Adds Zabbix - /// - public LogBuilder WriteToZabbixFlow( - string host, - int port = 10051, - string zabbixHostname = null, - string zabbixKey = "log_event", - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) - { - _flows.Add(new ZabbixFlow( - host, - port, - zabbixHostname, - zabbixKey, - batchSize, - minimumLevel, - backpressureStrategy)); - return this; - } - - /// - /// Adds Graylog - /// - public LogBuilder WriteToGraylogFlow( - string host, - int port = 12201, - bool useTcp = false, - string graylogHostName = null, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) - { - _flows.Add(new GraylogFlow( - host, - port, - useTcp, - graylogHostName, - batchSize, - minimumLevel, - backpressureStrategy)); - return this; - } - - /// - /// Publishes log events to a Redis channel using the PUBLISH command (Pub/Sub) - /// and optionally appends them to a Redis List (LPUSH) for persistence. - /// - /// Uses raw TCP + RESP protocol, so there arent additional dependencies - /// - /// Features: - /// - Reconnect with exponential back-off on connection failure - /// - Optional LPUSH to a list key with LTRIM to cap list length - /// - Optional password authentication (AUTH command) - /// - Optional DB selection (SELECT command) - /// - Background writer thread (non-blocking callers) - /// - public LogBuilder RedisFlow(string host = "localhost", - int port = 6379, - string password = null, - int database = 0, - string channel = "eonacat:logs", - string listKey = null, - int maxListLength = 10000, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new RedisFlow( - host, - port, - password, - database, - channel, - listKey, - maxListLength, - minimumLevel)); - return this; - } - - /// - /// Wraps any flow with token-bucket rate limiting and optional message deduplication. - /// Ideal for protecting high-latency sinks (email, Slack, HTTP) from log storms. - /// - /// The downstream flow to protect. - /// - /// Max events that can be emitted in a burst per level (token bucket capacity). - /// - /// - /// How many tokens are added per second per level. E.g. 5.0 = 5 events/second steady state. - /// - /// - /// If true, identical messages within are collapsed. - /// The suppressed count is appended to the message when the window expires. - /// - /// Deduplication window (default 60 s). - /// Maximum number of distinct messages tracked (default 1000). - /// Minimum level this flow processes. - public LogBuilder WriteToThrottled(IFlow inner, - int burstCapacity = 10, - double refillPerSecond = 1.0, - bool deduplicate = false, - TimeSpan dedupWindow = default(TimeSpan), - int dedupMaxKeys = 1000, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new ThrottledFlow( - inner, - burstCapacity, - refillPerSecond, - deduplicate, - dedupWindow, - dedupMaxKeys, - minimumLevel)); - return this; - } - - /// - /// Adds Splunk - /// - public LogBuilder WriteToSplunkFlow( - string splunkUrl, - string token, - string sourceType = "splunk_logs", - string hostName = null, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) - { - _flows.Add(new SplunkFlow( - splunkUrl, - token, - sourceType, - hostName, - batchSize, - minimumLevel, - backpressureStrategy)); - return this; - } - - /// - /// Pushes log events to a SignalR hub via HTTP POST to the hub's /send endpoint. - /// Works with ASP.NET SignalR (classic) and ASP.NET Core SignalR server-side REST API. - /// - /// A lightweight alternative to the SignalR client library - /// - /// On the server side you need a minimal hub endpoint that accepts POST: - /// POST {hubUrl}/send body: { "target": "...", "arguments": [ { log json } ] } - /// - /// For live dashboards: the hub broadcasts to a "logs" group; clients subscribe and - /// render events in real time. - /// - public LogBuilder WriteToSignalR( - string hubUrl, - string hubMethod = "ReceiveLog", - HttpClient httpClient = null, - int batchSize = 20, - int batchIntervalMs = 500, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new SignalRFlow( - hubUrl, - hubMethod, - httpClient, - batchSize, - batchIntervalMs, - minimumLevel)); - return this; - } - - /// - /// Sends log events as HTML email digests via SMTP. - /// Batches events for before sending, - /// unless flushOnCritical is true (Critical events bypass batching). - /// - public LogBuilder WriteToEmail( - string smtpHost, - int smtpPort = 587, - bool useSsl = true, - string username = null, - string password = null, - string from = null, - string to = null, - string subjectPrefix = "[EonaCatLogStack]", - int digestMinutes = 5, - bool flushOnCritical = true, - int maxEventsPerDigest = 100, - string headerName = null, - LogLevel minimumLevel = LogLevel.Error) - { - _flows.Add(new EmailFlow( - smtpHost, - smtpPort, - useSsl, - username, - password, - from, - to, - subjectPrefix, - digestMinutes, - flushOnCritical, - maxEventsPerDigest, - headerName, - minimumLevel)); - return this; - } - - /// - /// Adds Udp - /// - public LogBuilder WriteToUdp( - string host, - int port, - int flushIntervalInMilliseconds = 1000, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) - { - _flows.Add(new UdpFlow( - host, - port, - flushIntervalInMilliseconds, - batchSize, - minimumLevel, - backpressureStrategy)); - return this; - } - - /// - /// Adds in-memory buffer output - /// - public LogBuilder WriteToMemory( - int capacity = 10000, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new MemoryFlow(capacity, minimumLevel)); - return this; - } - - /// - /// Adds HTTP endpoint output - /// - public LogBuilder WriteToHttp( - string endpoint, - HttpClient? httpClient = null, - int batchSize = 1, - LogLevel minimumLevel = LogLevel.Trace, - TimeSpan? batchInterval = null, - Dictionary? headers = null) - { - _flows.Add(new HttpFlow( - endpoint, - httpClient, - batchSize, - minimumLevel, - batchInterval, - headers)); - return this; - } - - /// - /// Adds a custom flow - /// - public LogBuilder WriteTo(IFlow flow) - { - _flows.Add(flow ?? throw new ArgumentNullException(nameof(flow))); - return this; - } - - /// - /// Boost logs with machine name - /// - public LogBuilder BoostWithMachineName() - { - _boosters.Add(new MachineNameBooster()); - return this; - } - - /// - /// Boost logs with process ID - /// - public LogBuilder BoostWithProcessId() - { - _boosters.Add(new ProcessIdBooster()); - return this; - } - - /// - /// Boost logs with the current date (yyyy-MM-dd) - /// - public LogBuilder BoostWithDate() - { - _boosters.Add(new DateBooster()); - return this; - } - - /// - /// Boost logs with the current time (HH:mm:ss.fff) - /// - public LogBuilder BoostWithTime() - { - _boosters.Add(new TimeBooster()); - return this; - } - - /// - /// Boost logs with the current timestamp ticks - /// - public LogBuilder BoostWithTicks() - { - _boosters.Add(new TicksBooster()); - return this; - } - - /// - /// Boost logs with the process start time - /// - public LogBuilder BoostWithProcStart() - { - _boosters.Add(new ProcStartBooster()); - return this; - } - - /// - /// Boost logs with the uptime of the process in seconds - /// - public LogBuilder BoostWithUptime() - { - _boosters.Add(new UptimeBooster()); - return this; - } - - /// - /// Boost logs with the current thread name - /// - public LogBuilder BoostWithThreadName() - { - _boosters.Add(new ThreadNameBooster()); - return this; - } - - /// - /// Boost logs with memory usage in MB - /// - public LogBuilder BoostWithMemory() - { - _boosters.Add(new MemoryBooster()); - return this; - } - - /// - /// Boost logs with the operating system description - /// - public LogBuilder BoostWithOS() - { - _boosters.Add(new OSBooster()); - return this; - } - - /// - /// Boost logs with the runtime/framework description - /// - public LogBuilder BoostWithFramework() - { - _boosters.Add(new FrameworkBooster()); - return this; - } - - /// - /// Boost logs with the application name and base directory - /// - public LogBuilder BoostWithApp() - { - _boosters.Add(new AppBooster()); - return this; - } - - /// - /// Boost logs with the current user name - /// - public LogBuilder BoostWithUser() - { - _boosters.Add(new UserBooster()); - return this; - } - - /// - /// Boost logs with the current thread ID - /// - public LogBuilder BoostWithThreadId() - { - _boosters.Add(new ThreadIdBooster()); - return this; - } - - - /// - /// Boost logs with custom text - /// - public LogBuilder BoostWithCustomText(string key, string value) - { - _boosters.Add(new CustomTextBooster(key, value)); - return this; - } - - /// - /// Boost logs with environment name - /// - public LogBuilder BoostWithEnvironment(string environmentName) - { - _boosters.Add(new EnvironmentBooster(environmentName)); - return this; - } - - /// - /// Boost logs with application name and version - /// - public LogBuilder BoostWithApplication(string applicationName, string? version = null) - { - _boosters.Add(new ApplicationBooster(applicationName, version)); - return this; - } - - /// - /// Boost logs with correlation ID from Activity - /// - public LogBuilder BoostWithCorrelationId() - { - _boosters.Add(new CorrelationIdBooster()); - return this; - } - - /// - /// Adds a custom booster - /// - public LogBuilder Boost(IBooster booster) - { - _boosters.Add(booster ?? throw new ArgumentNullException(nameof(booster))); - return this; - } - - /// - /// Adds a callback-based booster - /// - public LogBuilder Boost(string name, Func> callback) - { - _boosters.Add(new CallbackBooster(name, callback)); - return this; - } - - /// - /// Builds the configured logger - /// - public EonaCatLogStack Build() - { - var logger = new EonaCatLogStack(_category, _minimumLevel, _timestampMode); - logger.OnLog += (sender, message) => OnLog?.Invoke(sender, message); - - foreach (var flow in _flows) - { - logger.AddFlow(flow); - } - - foreach (var booster in _boosters) - { - logger.AddBooster(booster); - } - - if (_dynamicLevel != null) - { - logger.UseDynamicLevel(_dynamicLevel); - } - - if (_useAsyncPipeline) - { - logger.UseAsyncPipeline(_asyncPipelineCapacity); - } - - return logger; - } - - /// - /// Creates a default logger with console and file output - /// - public static EonaCatLogStack CreateDefault( - string category = "Application", - string? logDirectory = null) - { - var directory = logDirectory ?? Path.Combine(AppContext.BaseDirectory, "logs"); - - return new LogBuilder(category) - .WithMinimumLevel(LogLevel.Information) - .WriteToConsole() - .WriteToFile(directory) - .BoostWithMachineName() - .BoostWithProcessId() - .Build(); - } - - /// - /// Get a flow by name - /// - /// - public IFlow GetFlow(string name) - { - lock (_flows) - { - var flow = _flows.Find(x => x.Name == name); - return flow; - } - } - - /// - /// Get a flow by type - /// - /// - public IFlow GetFlow(Type type) - { - lock (_flows) - { - var flow = _flows.Find(x => x.GetType() == type); - return flow; - } - } - - /// - /// Add a flow to the logBuilder - /// - /// - public void AddFlow(IFlow flow) - { - lock (_flows) - { - _flows.Add(flow); - } - } - - /// - /// Removes a flow from the logBuilder - /// - /// To be removed flow - public void RemoveFlow(IFlow flow) - { - lock (_flows) - { - _flows.Remove(flow); - } - } - - /// - /// Removes a flow from the logBuilder by name - /// - /// To be removed flow name - public void RemoveFlow(string name) - { - if (name == null) - { - throw new ArgumentNullException("name"); - } - - if (string.IsNullOrWhiteSpace(name)) - { - return; - } - - lock (_flows) { _flows.RemoveAll(f => f.Name == name); } - } - - /// - /// Enables the Channel-based async dispatch pipeline for zero-blocking logging. - /// Events are enqueued to a and - /// consumed by a dedicated background Task. - /// - /// Bounded capacity (0 = unbounded). - public LogBuilder UseAsyncPipeline(int capacity = 65536) - { - _useAsyncPipeline = true; - _asyncPipelineCapacity = capacity; - return this; - } - - /// - /// Disables the async dispatch pipeline and uses a synchronous flow instead. - /// - /// - /// - public LogBuilder UseSyncPipeline(int capacity = 65536) - { - _useAsyncPipeline = false; - _asyncPipelineCapacity = capacity; - return this; - } - - /// - /// Attaches a so the minimum log level can - /// be changed at runtime without restarting the application. - /// - public LogBuilder WithDynamicLevelController(DynamicLevelController controller) - { - _dynamicLevel = controller ?? throw new ArgumentNullException(nameof(controller)); - return this; - } - - /// - /// Wraps any flow with a Circuit Breaker that opens after repeated failures - /// and probes for recovery after a configurable timeout. - /// - public LogBuilder WriteToCircuitBreaker( - IFlow inner, - int failureThreshold = 5, - TimeSpan? recoveryTimeout = null, - LogLevel minimumLevel = LogLevel.Trace) - { - if (inner == null) - { - throw new ArgumentNullException(nameof(inner)); - } - - _flows.Add(new CircuitBreakerFlow(inner, failureThreshold, recoveryTimeout, minimumLevel)); - return this; - } - - /// - /// Wraps any flow with a predicate — events are forwarded only when the predicate returns true. - /// - public LogBuilder WriteToConditional( - IFlow inner, - Func predicate, - LogLevel minimumLevel = LogLevel.Trace) - { - if (inner == null) - { - throw new ArgumentNullException(nameof(inner)); - } - - if (predicate == null) - { - throw new ArgumentNullException(nameof(predicate)); - } - - _flows.Add(new ConditionalFlow(inner, predicate, minimumLevel)); - return this; - } - - /// - /// Adds a that fans events out to multiple inner flows in parallel. - /// Use to attach targets. - /// - public LogBuilder WriteToMulticast( - MulticastFlow multicast) - { - if (multicast == null) - { - throw new ArgumentNullException(nameof(multicast)); - } - - _flows.Add(multicast); - return this; - } - - /// - /// Pushes log events to a Grafana Loki instance. - /// - public LogBuilder WriteToLoki( - string lokiUrl, - Dictionary? labels = null, - int batchSize = 50, - int batchIntervalMs = 1000, - string? bearerToken = null, - string? basicUser = null, - string? basicPassword = null, - HttpClient? httpClient = null, - LogLevel minimumLevel = LogLevel.Trace) - { - _flows.Add(new LokiFlow( - lokiUrl, labels, batchSize, batchIntervalMs, - bearerToken, basicUser, basicPassword, httpClient, minimumLevel)); - return this; - } - - /// - /// Adds the which captures caller member/file/line. - /// - public LogBuilder BoostWithCallerInfo() - { - _boosters.Add(new CallerInfoBooster()); - return this; - } -} \ No newline at end of file +using EonaCat.LogStack.Boosters; +using EonaCat.LogStack.Core; +using EonaCat.LogStack.EonaCatLogStackCore; +using EonaCat.LogStack.EonaCatLogStackCore.Policies; +using EonaCat.LogStack.Flows; +using ServiceMonitoring; +using System; +using System.Collections.Generic; +using System.Data.Common; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Security; +using System.Security.Cryptography.X509Certificates; + +namespace EonaCat.LogStack.Configuration; + +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +/// +/// Fluent builder for configuring the logger with flows and boosters +/// +public sealed class LogBuilder +{ + private readonly string _category; + private LogLevel _minimumLevel = LogLevel.Trace; + private TimestampMode _timestampMode = TimestampMode.Utc; + private readonly List _flows = new(); + private readonly List _boosters = new(); + private DynamicLevelController? _dynamicLevel; + private bool _useAsyncPipeline; + private int _asyncPipelineCapacity = 65536; + + public event EventHandler OnLog; + + public LogBuilder(string category = "Application") + { + _category = category ?? throw new ArgumentNullException(nameof(category)); + UseAsyncPipeline(); + } + + /// + /// Sets the minimum log level + /// + public LogBuilder WithMinimumLevel(LogLevel level) + { + _minimumLevel = level; + return this; + } + + /// + /// Sets the timestamp mode + /// + public LogBuilder WithTimestampMode(TimestampMode mode) + { + _timestampMode = mode; + return this; + } + + /// + /// Adds console output + /// + public LogBuilder WriteToConsole( + LogLevel minimumLevel = LogLevel.Trace, + bool useColors = true) + { + _flows.Add(new ConsoleFlow(minimumLevel, useColors, _timestampMode)); + return this; + } + + /// + /// Adds diagnostics + /// + public LogBuilder WriteDiagnostics( + TimeSpan snapshotInterval = default(TimeSpan), + bool injectIntoEvents = false, + bool writeSnapshotEvents = true, + string snapshotCategory = "Diagnostics", + IFlow forwardTo = null, + LogLevel minimumLevel = LogLevel.Trace, + Func> customMetrics = null) + { + _flows.Add(new DiagnosticsFlow( + snapshotInterval, + injectIntoEvents, + writeSnapshotEvents, + snapshotCategory, + forwardTo, + minimumLevel, + customMetrics)); + return this; + } + + /// + /// Adds file output + /// + public LogBuilder WriteToFile( + string directory, + string filePrefix = "log", + long maxFileSize = 100 * 1024 * 1024, + long maxDirectorySize = 2L * 1024 * 1024 * 1024, + FileRetentionPolicy fileRetentionPolicy = null, + int flushIntervalInMilliSeconds = 2000, + bool useCategoryRouting = false, + int batchSize = 1, + LogLevel[]? logLevelsForSeparateFiles = null, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, + FileOutputFormat outputFormat = FileOutputFormat.Text, + CompressionFormat compression = CompressionFormat.GZip, + string template = "[{ts}] [Host: {host}] [Category: {category}] [Thread: {thread}] [{logtype}] {message}{props}") + { + _flows.Add(new FileFlow( + directory, + filePrefix, + maxFileSize, + maxDirectorySize, + fileRetentionPolicy, + flushIntervalInMilliSeconds, + batchSize, + minimumLevel, + useCategoryRouting, + logLevelsForSeparateFiles, + _timestampMode, + backpressureStrategy, + outputFormat, + compression, + template)); + return this; + } + + public LogBuilder WriteToEncryptedFile( + string directory, + string filePrefix = "log", + string password = "EonaCat", + long maxFileSize = 100 * 1024 * 1024, + FileRetentionPolicy fileRetentionPolicy = null, + int flushIntervalInMilliSeconds = 2000, + bool useCategoryRouting = false, + LogLevel[]? logLevelsForSeparateFiles = null, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new EncryptedFileFlow( + directory, + password, + filePrefix, + maxFileSize, + maxDirectorySize: 2L * 1024 * 1024 * 1024, + retention: fileRetentionPolicy, + flushIntervalMs: flushIntervalInMilliSeconds, + batchSize: 1, + minimumLevel: minimumLevel, + useCategoryRouting: useCategoryRouting, + logLevelsForSeparateFiles: logLevelsForSeparateFiles, + tsMode: _timestampMode)); + return this; + } + + /// + /// Write to a rolling buffer + /// + /// Maximum number of events to retain. + /// Minimum level to store in the buffer. + /// + /// When a log event reaches this level or above, the current buffer + /// contents are immediately forwarded to . + /// Set to LogLevel.None (or omit) to disable. + /// + /// + /// Flow to forward the buffered context to when the trigger fires. + /// Can be null even when is set. + /// + /// + /// How many buffered lines to forward before the triggering event. + /// Defaults to entire buffer (int.MaxValue). + /// + /// + public LogBuilder WriteToRollingBuffer( + int capacity = 500, + LogLevel minimumLevel = LogLevel.Trace, + LogLevel triggerLevel = LogLevel.Error, + IFlow triggerTarget = null, + int preContextLines = int.MaxValue) + { + _flows.Add(new RollingBufferFlow( + capacity, + minimumLevel, + triggerLevel, + triggerTarget, + preContextLines)); + return this; + } + + /// + /// Decrypt a file which is encrypted by EonaCat Logger + /// + /// encrypted file source path + /// destination path for decrypted file + /// password used by encryption + /// + public static bool DecryptFile(string encryptedPath, string outputPath, string password) + { + return EncryptedFileFlow.DecryptToFile(encryptedPath, outputPath, password); + } + + /// + /// Adds Database output + /// + public LogBuilder WriteToDatabase( + Func connectionFactory, + string tableName = "logs", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new DatabaseFlow( + connectionFactory, + tableName, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds Snmp traps + /// + public LogBuilder WriteToSnmpTrap(string host, int port = 162, string oid = "1.3.6.1.4.1.9999", LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new SnmpTrapFlow( + host, + port, + oid, + minimumLevel)); + return this; + } + + /// + /// Adds Discord + /// + public LogBuilder WriteToDiscord( + string webHookUrl, + string botName = "EonaCatBot", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new DiscordFlow( + webHookUrl, + botName, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds ElasticSearch + /// + public LogBuilder WriteToElasticSearch( + string elasticSearchUrl, + string indexName = "EonaCatIndex", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new ElasticSearchFlow( + elasticSearchUrl, + indexName, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds Telegram + /// + public LogBuilder WriteToTelegram(string botToken, string chatId = "EonaCat", int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new TelegramFlow( + botToken, + chatId, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds a tamper-evident audit trail. + /// + /// Each audit entry is hash-chained: every line stores a SHA-256 of the previous + /// line's hash + the current entry body, so deletion or modification of any past + /// entry invalidates all subsequent hashes. + /// + /// Use to verify file integrity at any time. + /// + /// Directory where the .audit file is written. + /// File name prefix (default: "audit"). + /// + /// Which severity levels are recorded in the audit trail: + /// + /// – every log event (default) + /// – Warning, Error, Critical + /// – Error and Critical only + /// – Critical only + /// + /// + /// Minimum for audit capture. + /// Whether structured properties are appended to each entry. + public LogBuilder WriteToAudit( + string directory, + string filePrefix = "audit", + AuditLevel auditLevel = AuditLevel.All, + LogLevel minimumLevel = LogLevel.Trace, + bool includeProperties = true) + { + _flows.Add(new AuditFlow( + directory, + filePrefix, + auditLevel, + minimumLevel, + includeProperties)); + return this; + } + + /// + /// Adds Slack + /// + public LogBuilder WriteToSlack(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new SlackFlow( + webhookUrl, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds Slack + /// + public LogBuilder WriteToMicrosoftTeams(string webhookUrl, int batchSize = 1, LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new MicrosoftTeamsFlow( + webhookUrl, + batchSize, + minimumLevel)); + return this; + } + + /// + /// Adds a TCP flow. + /// + public LogBuilder WriteToTcp( + string host, + int port, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, + bool useTls = false, + RemoteCertificateValidationCallback certValidationCallback = null, + X509CertificateCollection clientCertificates = null) + { + if (string.IsNullOrWhiteSpace(host)) + { + throw new ArgumentException("Host cannot be null or empty.", nameof(host)); + } + + if (port <= 0 || port > 65535) + { + throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 1 and 65535."); + } + + _flows.Add(new TcpFlow( + host, + port, + batchSize, + minimumLevel, + backpressureStrategy, + useTls, + certValidationCallback, + clientCertificates)); + + return this; + } + + + + /// + /// Adds a Retry flow to retry failed log writes. + /// + public LogBuilder WriteToRetry( + IFlow primaryFlow, + int maxRetries = 3, + TimeSpan? initialDelay = null, + bool exponentialBackoff = true) + { + if (primaryFlow == null) + { + throw new ArgumentNullException(nameof(primaryFlow)); + } + + _flows.Add(new RetryFlow( + primaryFlow, + maxRetries, + initialDelay ?? TimeSpan.FromMilliseconds(200), + exponentialBackoff)); + + return this; + } + + /// + /// Adds an EventLog flow for sending logs to a remote destination. + /// + public LogBuilder WriteToEventLogFlow( + string destination, + int port = 514, + LogLevel minimumLevel = LogLevel.Trace, + int bufferSize = 100, + TimeSpan? flushInterval = null, + bool useTls = false, + RemoteCertificateValidationCallback? certificateValidationCallback = null, + X509CertificateCollection? clientCertificates = null) + { + if (string.IsNullOrWhiteSpace(destination)) + { + throw new ArgumentException("Destination cannot be null or empty.", nameof(destination)); + } + + _flows.Add(new EventLogFlow( + destination, + port, + minimumLevel, + bufferSize, + flushInterval ?? TimeSpan.FromSeconds(5), + useTls, + certificateValidationCallback, + clientCertificates)); + return this; + } + + /// + /// Adds a Failover flow that switches to a secondary flow if the primary fails. + /// + public LogBuilder WriteToFailover(IFlow primaryFlow, IFlow secondaryFlow, TimeSpan? recoveryCheckInterval = null, int failureThreshold = 5) + { + if (primaryFlow == null) + { + throw new ArgumentNullException(nameof(primaryFlow)); + } + + if (secondaryFlow == null) + { + throw new ArgumentNullException(nameof(secondaryFlow)); + } + + _flows.Add(new FailoverFlow(primaryFlow, secondaryFlow, recoveryCheckInterval, failureThreshold)); + + return this; + } + + /// + /// Adds a Syslog TCP flow. + /// + public LogBuilder WriteToSyslogTcp( + string host, + int port = 514, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait, + bool useTls = false, + RemoteCertificateValidationCallback certValidationCallback = null, + X509CertificateCollection clientCertificates = null) + { + if (string.IsNullOrWhiteSpace(host)) + { + throw new ArgumentException("Host cannot be null or empty.", nameof(host)); + } + + if (port <= 0 || port > 65535) + { + throw new ArgumentOutOfRangeException(nameof(port), "Port must be between 1 and 65535."); + } + + _flows.Add(new SyslogTcpFlow( + host, + port, + batchSize, + minimumLevel, + backpressureStrategy, + useTls, + certValidationCallback, + clientCertificates)); + + return this; + } + + /// + /// Adds a Status Monitoring flow. + /// + public LogBuilder WriteToStatusFlow( + List servicesToMonitor, + TimeSpan? checkInterval = null, + string statusDirectory = null, + Action statusChangedTrigger = null) + { + _flows.Add(new StatusFlow( + servicesToMonitor, + checkInterval, + statusDirectory, + statusChangedTrigger)); + + return this; + } + + /// + /// Adds Syslog Tcp + /// + public LogBuilder WriteToSyslogUdp( + string host, + int port, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new SyslogUdpFlow( + host, + port, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Adds Zabbix + /// + public LogBuilder WriteToZabbixFlow( + string host, + int port = 10051, + string zabbixHostname = null, + string zabbixKey = "log_event", + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new ZabbixFlow( + host, + port, + zabbixHostname, + zabbixKey, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Adds Graylog + /// + public LogBuilder WriteToGraylogFlow( + string host, + int port = 12201, + bool useTcp = false, + string graylogHostName = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new GraylogFlow( + host, + port, + useTcp, + graylogHostName, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Publishes log events to a Redis channel using the PUBLISH command (Pub/Sub) + /// and optionally appends them to a Redis List (LPUSH) for persistence. + /// + /// Uses raw TCP + RESP protocol, so there arent additional dependencies + /// + /// Features: + /// - Reconnect with exponential back-off on connection failure + /// - Optional LPUSH to a list key with LTRIM to cap list length + /// - Optional password authentication (AUTH command) + /// - Optional DB selection (SELECT command) + /// - Background writer thread (non-blocking callers) + /// + public LogBuilder RedisFlow(string host = "localhost", + int port = 6379, + string password = null, + int database = 0, + string channel = "eonacat:logs", + string listKey = null, + int maxListLength = 10000, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new RedisFlow( + host, + port, + password, + database, + channel, + listKey, + maxListLength, + minimumLevel)); + return this; + } + + /// + /// Wraps any flow with token-bucket rate limiting and optional message deduplication. + /// Ideal for protecting high-latency sinks (email, Slack, HTTP) from log storms. + /// + /// The downstream flow to protect. + /// + /// Max events that can be emitted in a burst per level (token bucket capacity). + /// + /// + /// How many tokens are added per second per level. E.g. 5.0 = 5 events/second steady state. + /// + /// + /// If true, identical messages within are collapsed. + /// The suppressed count is appended to the message when the window expires. + /// + /// Deduplication window (default 60 s). + /// Maximum number of distinct messages tracked (default 1000). + /// Minimum level this flow processes. + public LogBuilder WriteToThrottled(IFlow inner, + int burstCapacity = 10, + double refillPerSecond = 1.0, + bool deduplicate = false, + TimeSpan dedupWindow = default(TimeSpan), + int dedupMaxKeys = 1000, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new ThrottledFlow( + inner, + burstCapacity, + refillPerSecond, + deduplicate, + dedupWindow, + dedupMaxKeys, + minimumLevel)); + return this; + } + + /// + /// Adds Splunk + /// + public LogBuilder WriteToSplunkFlow( + string splunkUrl, + string token, + string sourceType = "splunk_logs", + string hostName = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new SplunkFlow( + splunkUrl, + token, + sourceType, + hostName, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Pushes log events to a SignalR hub via HTTP POST to the hub's /send endpoint. + /// Works with ASP.NET SignalR (classic) and ASP.NET Core SignalR server-side REST API. + /// + /// A lightweight alternative to the SignalR client library + /// + /// On the server side you need a minimal hub endpoint that accepts POST: + /// POST {hubUrl}/send body: { "target": "...", "arguments": [ { log json } ] } + /// + /// For live dashboards: the hub broadcasts to a "logs" group; clients subscribe and + /// render events in real time. + /// + public LogBuilder WriteToSignalR( + string hubUrl, + string hubMethod = "ReceiveLog", + HttpClient httpClient = null, + int batchSize = 20, + int batchIntervalMs = 500, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new SignalRFlow( + hubUrl, + hubMethod, + httpClient, + batchSize, + batchIntervalMs, + minimumLevel)); + return this; + } + + /// + /// Sends log events as HTML email digests via SMTP. + /// Batches events for before sending, + /// unless flushOnCritical is true (Critical events bypass batching). + /// + public LogBuilder WriteToEmail( + string smtpHost, + int smtpPort = 587, + bool useSsl = true, + string username = null, + string password = null, + string from = null, + string to = null, + string subjectPrefix = "[EonaCatLogStack]", + int digestMinutes = 5, + bool flushOnCritical = true, + int maxEventsPerDigest = 100, + string headerName = null, + LogLevel minimumLevel = LogLevel.Error) + { + _flows.Add(new EmailFlow( + smtpHost, + smtpPort, + useSsl, + username, + password, + from, + to, + subjectPrefix, + digestMinutes, + flushOnCritical, + maxEventsPerDigest, + headerName, + minimumLevel)); + return this; + } + + /// + /// Adds Udp + /// + public LogBuilder WriteToUdp( + string host, + int port, + int flushIntervalInMilliseconds = 1000, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + BackpressureStrategy backpressureStrategy = BackpressureStrategy.Wait) + { + _flows.Add(new UdpFlow( + host, + port, + flushIntervalInMilliseconds, + batchSize, + minimumLevel, + backpressureStrategy)); + return this; + } + + /// + /// Adds in-memory buffer output + /// + public LogBuilder WriteToMemory( + int capacity = 10000, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new MemoryFlow(capacity, minimumLevel)); + return this; + } + + /// + /// Adds HTTP endpoint output + /// + public LogBuilder WriteToHttp( + string endpoint, + HttpClient? httpClient = null, + int batchSize = 1, + LogLevel minimumLevel = LogLevel.Trace, + TimeSpan? batchInterval = null, + Dictionary? headers = null) + { + _flows.Add(new HttpFlow( + endpoint, + httpClient, + batchSize, + minimumLevel, + batchInterval, + headers)); + return this; + } + + /// + /// Adds a custom flow + /// + public LogBuilder WriteTo(IFlow flow) + { + _flows.Add(flow ?? throw new ArgumentNullException(nameof(flow))); + return this; + } + + /// + /// Boost logs with machine name + /// + public LogBuilder BoostWithMachineName() + { + _boosters.Add(new MachineNameBooster()); + return this; + } + + /// + /// Boost logs with process ID + /// + public LogBuilder BoostWithProcessId() + { + _boosters.Add(new ProcessIdBooster()); + return this; + } + + /// + /// Boost logs with the current date (yyyy-MM-dd) + /// + public LogBuilder BoostWithDate() + { + _boosters.Add(new DateBooster()); + return this; + } + + /// + /// Boost logs with the current time (HH:mm:ss.fff) + /// + public LogBuilder BoostWithTime() + { + _boosters.Add(new TimeBooster()); + return this; + } + + /// + /// Boost logs with the current timestamp ticks + /// + public LogBuilder BoostWithTicks() + { + _boosters.Add(new TicksBooster()); + return this; + } + + /// + /// Boost logs with the process start time + /// + public LogBuilder BoostWithProcStart() + { + _boosters.Add(new ProcStartBooster()); + return this; + } + + /// + /// Boost logs with the uptime of the process in seconds + /// + public LogBuilder BoostWithUptime() + { + _boosters.Add(new UptimeBooster()); + return this; + } + + /// + /// Boost logs with the current thread name + /// + public LogBuilder BoostWithThreadName() + { + _boosters.Add(new ThreadNameBooster()); + return this; + } + + /// + /// Boost logs with memory usage in MB + /// + public LogBuilder BoostWithMemory() + { + _boosters.Add(new MemoryBooster()); + return this; + } + + /// + /// Boost logs with the operating system description + /// + public LogBuilder BoostWithOS() + { + _boosters.Add(new OSBooster()); + return this; + } + + /// + /// Boost logs with the runtime/framework description + /// + public LogBuilder BoostWithFramework() + { + _boosters.Add(new FrameworkBooster()); + return this; + } + + /// + /// Boost logs with the application name and base directory + /// + public LogBuilder BoostWithApp() + { + _boosters.Add(new AppBooster()); + return this; + } + + /// + /// Boost logs with the current user name + /// + public LogBuilder BoostWithUser() + { + _boosters.Add(new UserBooster()); + return this; + } + + /// + /// Boost logs with the current thread ID + /// + public LogBuilder BoostWithThreadId() + { + _boosters.Add(new ThreadIdBooster()); + return this; + } + + + /// + /// Boost logs with custom text + /// + public LogBuilder BoostWithCustomText(string key, string value) + { + _boosters.Add(new CustomTextBooster(key, value)); + return this; + } + + /// + /// Boost logs with environment name + /// + public LogBuilder BoostWithEnvironment(string environmentName) + { + _boosters.Add(new EnvironmentBooster(environmentName)); + return this; + } + + /// + /// Boost logs with application name and version + /// + public LogBuilder BoostWithApplication(string applicationName, string? version = null) + { + _boosters.Add(new ApplicationBooster(applicationName, version)); + return this; + } + + /// + /// Boost logs with correlation ID from Activity + /// + public LogBuilder BoostWithCorrelationId() + { + _boosters.Add(new CorrelationIdBooster()); + return this; + } + + /// + /// Adds a custom booster + /// + public LogBuilder Boost(IBooster booster) + { + _boosters.Add(booster ?? throw new ArgumentNullException(nameof(booster))); + return this; + } + + /// + /// Adds a callback-based booster + /// + public LogBuilder Boost(string name, Func> callback) + { + _boosters.Add(new CallbackBooster(name, callback)); + return this; + } + + /// + /// Builds the configured logger + /// + public EonaCatLogStack Build() + { + var logger = new EonaCatLogStack(_category, _minimumLevel, _timestampMode); + logger.OnLog += (sender, message) => OnLog?.Invoke(sender, message); + + foreach (var flow in _flows) + { + logger.AddFlow(flow); + } + + foreach (var booster in _boosters) + { + logger.AddBooster(booster); + } + + if (_dynamicLevel != null) + { + logger.UseDynamicLevel(_dynamicLevel); + } + + if (_useAsyncPipeline) + { + logger.UseAsyncPipeline(_asyncPipelineCapacity); + } + + return logger; + } + + /// + /// Creates a default logger with console and file output + /// + public static EonaCatLogStack CreateDefault( + string category = "Application", + string? logDirectory = null) + { + var directory = logDirectory ?? Path.Combine(AppContext.BaseDirectory, "logs"); + + return new LogBuilder(category) + .WithMinimumLevel(LogLevel.Information) + .WriteToConsole() + .WriteToFile(directory) + .BoostWithMachineName() + .BoostWithProcessId() + .Build(); + } + + /// + /// Get a flow by name + /// + /// + public IFlow GetFlow(string name) + { + lock (_flows) + { + var flow = _flows.Find(x => x.Name == name); + return flow; + } + } + + /// + /// Get a flow by type + /// + /// + public IFlow GetFlow(Type type) + { + lock (_flows) + { + var flow = _flows.Find(x => x.GetType() == type); + return flow; + } + } + + /// + /// Add a flow to the logBuilder + /// + /// + public void AddFlow(IFlow flow) + { + lock (_flows) + { + _flows.Add(flow); + } + } + + /// + /// Removes a flow from the logBuilder + /// + /// To be removed flow + public void RemoveFlow(IFlow flow) + { + lock (_flows) + { + _flows.Remove(flow); + } + } + + /// + /// Removes a flow from the logBuilder by name + /// + /// To be removed flow name + public void RemoveFlow(string name) + { + if (name == null) + { + throw new ArgumentNullException("name"); + } + + if (string.IsNullOrWhiteSpace(name)) + { + return; + } + + lock (_flows) { _flows.RemoveAll(f => f.Name == name); } + } + + /// + /// Enables the Channel-based async dispatch pipeline for zero-blocking logging. + /// Events are enqueued to a and + /// consumed by a dedicated background Task. + /// + /// Bounded capacity (0 = unbounded). + public LogBuilder UseAsyncPipeline(int capacity = 65536) + { + _useAsyncPipeline = true; + _asyncPipelineCapacity = capacity; + return this; + } + + /// + /// Disables the async dispatch pipeline and uses a synchronous flow instead. + /// + /// + /// + public LogBuilder UseSyncPipeline(int capacity = 65536) + { + _useAsyncPipeline = false; + _asyncPipelineCapacity = capacity; + return this; + } + + /// + /// Attaches a so the minimum log level can + /// be changed at runtime without restarting the application. + /// + public LogBuilder WithDynamicLevelController(DynamicLevelController controller) + { + _dynamicLevel = controller ?? throw new ArgumentNullException(nameof(controller)); + return this; + } + + /// + /// Wraps any flow with a Circuit Breaker that opens after repeated failures + /// and probes for recovery after a configurable timeout. + /// + public LogBuilder WriteToCircuitBreaker( + IFlow inner, + int failureThreshold = 5, + TimeSpan? recoveryTimeout = null, + LogLevel minimumLevel = LogLevel.Trace) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + _flows.Add(new CircuitBreakerFlow(inner, failureThreshold, recoveryTimeout, minimumLevel)); + return this; + } + + /// + /// Wraps any flow with a predicate — events are forwarded only when the predicate returns true. + /// + public LogBuilder WriteToConditional( + IFlow inner, + Func predicate, + LogLevel minimumLevel = LogLevel.Trace) + { + if (inner == null) + { + throw new ArgumentNullException(nameof(inner)); + } + + if (predicate == null) + { + throw new ArgumentNullException(nameof(predicate)); + } + + _flows.Add(new ConditionalFlow(inner, predicate, minimumLevel)); + return this; + } + + /// + /// Adds a that fans events out to multiple inner flows in parallel. + /// Use to attach targets. + /// + public LogBuilder WriteToMulticast( + MulticastFlow multicast) + { + if (multicast == null) + { + throw new ArgumentNullException(nameof(multicast)); + } + + _flows.Add(multicast); + return this; + } + + /// + /// Pushes log events to a Grafana Loki instance. + /// + public LogBuilder WriteToLoki( + string lokiUrl, + Dictionary? labels = null, + int batchSize = 50, + int batchIntervalMs = 1000, + string? bearerToken = null, + string? basicUser = null, + string? basicPassword = null, + HttpClient? httpClient = null, + LogLevel minimumLevel = LogLevel.Trace) + { + _flows.Add(new LokiFlow( + lokiUrl, labels, batchSize, batchIntervalMs, + bearerToken, basicUser, basicPassword, httpClient, minimumLevel)); + return this; + } + + /// + /// Adds the which captures caller member/file/line. + /// + public LogBuilder BoostWithCallerInfo() + { + _boosters.Add(new CallerInfoBooster()); + return this; + } + + + /// Adds a stable event fingerprint for grouping failures. + public LogBuilder BoostWithExceptionFingerprint() + { + _boosters.Add(new ExceptionFingerprintBooster()); + return this; + } + + /// Adds runtime health information to every event. + public LogBuilder BoostWithHealthSnapshot() + { + _boosters.Add(new HealthSnapshotBooster()); + return this; + } + + /// Adds a schema version field to every log event. + public LogBuilder BoostWithSchemaVersion(string version = "1.0") + { + _boosters.Add(new SchemaVersionBooster(version)); + return this; + } + + /// Adds a monotonic sequence number to every event. + public LogBuilder BoostWithSequence() + { + _boosters.Add(new SequenceBooster()); + return this; + } + + /// + /// Creates a dependency-free compatibility facade exposing adapters for + /// Serilog, log4net and NLog style integrations. + /// + public Compatibility.LoggingCompatibilityFacade AsCompatibility() + { + return new Compatibility.LoggingCompatibilityFacade(Build()); + } + + /// + /// Creates a Serilog compatible adapter without adding Serilog dependency. + /// + public Compatibility.EonaCatSerilogAdapter AsSerilog() + { + return new Compatibility.EonaCatSerilogAdapter(Build()); + } + + /// + /// Creates a log4net compatible adapter without adding log4net dependency. + /// + public Compatibility.EonaCatLog4NetAdapter AsLog4Net() + { + return new Compatibility.EonaCatLog4NetAdapter(Build()); + } + + /// + /// Creates an NLog compatible adapter without adding NLog dependency. + /// + public Compatibility.EonaCatNLogAdapter AsNLog() + { + return new Compatibility.EonaCatNLogAdapter(Build()); + } + +} diff --git a/EonaCat.LogStack/Telemetry/TelemetryClient.cs b/EonaCat.LogStack/Telemetry/TelemetryClient.cs new file mode 100644 index 0000000..efa476f --- /dev/null +++ b/EonaCat.LogStack/Telemetry/TelemetryClient.cs @@ -0,0 +1,19 @@ +using System; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +namespace EonaCat.LogStack.Telemetry; +public sealed class TelemetryClient : IDisposable +{ + private readonly HttpClient _http = new HttpClient(); + private readonly Uri _endpoint; + public TelemetryClient(string endpoint) => _endpoint = new Uri(endpoint.TrimEnd('/') + "/telemetry"); + public Task TrackAsync(TelemetryEvent evt, CancellationToken token = default) + { + var json = JsonSerializer.Serialize(evt); + return _http.PostAsync(_endpoint, new StringContent(json, Encoding.UTF8, "application/json"), token); + } + public void Dispose() => _http.Dispose(); +} diff --git a/EonaCat.LogStack/Telemetry/TelemetryDashboard.cs b/EonaCat.LogStack/Telemetry/TelemetryDashboard.cs new file mode 100644 index 0000000..4e54b4a --- /dev/null +++ b/EonaCat.LogStack/Telemetry/TelemetryDashboard.cs @@ -0,0 +1,11 @@ +namespace EonaCat.LogStack.Telemetry; +public static class TelemetryDashboard +{ + public static string Html => @" +EonaCat Telemetry +

EonaCat LogStack Telemetry

+

No external dependencies. Connect to /telemetry to ingest.

+"; +} diff --git a/EonaCat.LogStack/Telemetry/TelemetryEvent.cs b/EonaCat.LogStack/Telemetry/TelemetryEvent.cs new file mode 100644 index 0000000..b27bdf0 --- /dev/null +++ b/EonaCat.LogStack/Telemetry/TelemetryEvent.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +namespace EonaCat.LogStack.Telemetry; +public sealed class TelemetryEvent +{ + public string Name { get; set; } = ""; + public string Level { get; set; } = "Information"; + public long TimestampUnixMs { get; set; } = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); + public double DurationMs { get; set; } + public long Value { get; set; } + public string? TraceId { get; set; } + public Dictionary? Tags { get; set; } +} diff --git a/EonaCat.LogStack/Telemetry/TelemetryServer.cs b/EonaCat.LogStack/Telemetry/TelemetryServer.cs new file mode 100644 index 0000000..2d171ba --- /dev/null +++ b/EonaCat.LogStack/Telemetry/TelemetryServer.cs @@ -0,0 +1,47 @@ +using System; +using System.Collections.Concurrent; +using System.IO; +using System.Net; +using System.Text.Json; +using System.Threading; +using System.Threading.Tasks; +namespace EonaCat.LogStack.Telemetry; +public sealed class TelemetryServer : IDisposable +{ + private readonly HttpListener _listener = new(); + public ConcurrentQueue Events { get; } = new(); + public int Count => Events.Count; + public TelemetryServer(string prefix = "http://localhost:5155/") + { + _listener.Prefixes.Add(prefix); + } + public async Task StartAsync(CancellationToken token = default) + { + _listener.Start(); + while (!token.IsCancellationRequested) + { + var ctx = await _listener.GetContextAsync(); + _ = Task.Run(async () => + { + if (ctx.Request.HttpMethod == "POST" && ctx.Request.Url?.AbsolutePath == "/telemetry") + { + using var r = new StreamReader(ctx.Request.InputStream); + var item = JsonSerializer.Deserialize(await r.ReadToEndAsync()); + if (item != null) + { + Events.Enqueue(item); + } + } + else if (ctx.Request.Url?.AbsolutePath == "/") + { + var html = TelemetryDashboard.Html; + var bytes = System.Text.Encoding.UTF8.GetBytes(html); + ctx.Response.ContentType = "text/html"; + await ctx.Response.OutputStream.WriteAsync(bytes, 0, bytes.Length); + } + ctx.Response.Close(); + }); + } + } + public void Dispose() => _listener.Close(); +}