This commit is contained in:
2026-06-16 21:46:16 +02:00
parent 3134ffba17
commit 79428652af
29 changed files with 2114 additions and 1525 deletions
+3 -3
View File
@@ -51,8 +51,11 @@ It features a rich fluent API for routing log events to dozens of destinations f
</Target>
<ItemGroup>
<Compile Remove="EonaCatLoggerCore\Examples\**" />
<Compile Remove="Examples\**" />
<EmbeddedResource Remove="EonaCatLoggerCore\Examples\**" />
<EmbeddedResource Remove="Examples\**" />
<None Remove="EonaCatLoggerCore\Examples\**" />
<None Remove="Examples\**" />
</ItemGroup>
@@ -97,7 +100,4 @@ It features a rich fluent API for routing log events to dozens of destinations f
<PackagePath>\</PackagePath>
</None>
</ItemGroup>
<ItemGroup>
<Folder Include="EonaCatLoggerCore\Examples\" />
</ItemGroup>
</Project>
+4 -1
View File
@@ -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;
}
@@ -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.
/// <summary>
/// Performance monitoring booster that adds timing and resource metrics
/// </summary>
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
}
}
}
/// <summary>
/// Distributed tracing booster that extracts and propagates trace context
/// </summary>
public sealed class DistributedTracingBooster : BoosterBase
{
private readonly AsyncLocal<string> _correlationId = new AsyncLocal<string>();
private readonly AsyncLocal<string> _parentSpanId = new AsyncLocal<string>();
private int _spanIdCounter;
public DistributedTracingBooster() : base("DistributedTracing") { }
/// <summary>
/// Sets the correlation ID for the current async context
/// </summary>
public void SetCorrelationId(string correlationId)
{
_correlationId.Value = correlationId;
}
/// <summary>
/// Gets the current correlation ID
/// </summary>
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;
}
}
/// <summary>
/// Sampling booster that probabilistically filters log events
/// </summary>
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();
}
/// <summary>
/// Always log critical and error events, sample the rest
/// </summary>
public override bool Boost(ref LogEventBuilder builder)
{
_totalLogged++;
// Sample based on configured rate
bool shouldLog = _random.NextDouble() < _samplingRate;
if (shouldLog)
{
_totalSampled++;
}
return shouldLog;
}
/// <summary>
/// Gets sampling statistics
/// </summary>
public (long total, long sampled, double rate) GetStatistics()
{
return (_totalLogged, _totalSampled, _totalLogged > 0 ? (double)_totalSampled / _totalLogged : 0.0);
}
}
/// <summary>
/// Property filter booster that includes/excludes events based on properties
/// </summary>
public sealed class PropertyFilterBooster : BoosterBase
{
private readonly Func<LogEventBuilder, bool>? _includeFilter;
private readonly Func<LogEventBuilder, bool>? _excludeFilter;
public PropertyFilterBooster(
Func<LogEventBuilder, bool>? includeFilter = null,
Func<LogEventBuilder, bool>? 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;
}
}
/// <summary>
/// Rate limiting booster that throttles log events
/// </summary>
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;
}
}
/// <summary>
/// Gets current rate limiting statistics
/// </summary>
public (int current, int limit) GetStatistics()
{
lock (_lock)
{
return (_eventCount, _maxEventsPerSecond);
}
}
}
/// <summary>
/// Context enrichment booster that adds contextual information
/// </summary>
public sealed class ContextEnrichmentBooster : BoosterBase
{
private readonly Func<LogEventBuilder, LogEventBuilder> _enricher;
public ContextEnrichmentBooster(Func<LogEventBuilder, LogEventBuilder> 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
}
}
}
/// <summary>
/// Deduplication booster that filters duplicate messages within a time window
/// </summary>
public sealed class DeduplicationBooster : BoosterBase
{
private readonly TimeSpan _window;
private readonly Dictionary<string, DateTime> _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;
}
}
}
@@ -0,0 +1,31 @@
using EonaCat.LogStack.Core;
using System;
namespace EonaCat.LogStack.Boosters;
/// <summary>
/// Context enrichment booster that adds contextual information
/// </summary>
public sealed class ContextEnrichmentBooster : BoosterBase
{
private readonly Func<LogEventBuilder, LogEventBuilder> _enricher;
public ContextEnrichmentBooster(Func<LogEventBuilder, LogEventBuilder> 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
}
}
}
@@ -0,0 +1,44 @@
using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Linq;
namespace EonaCat.LogStack.Boosters;
/// <summary>
/// Deduplication booster that filters duplicate messages within a time window
/// </summary>
public sealed class DeduplicationBooster : BoosterBase
{
private readonly TimeSpan _window;
private readonly Dictionary<string, DateTime> _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;
}
}
}
@@ -0,0 +1,51 @@
using EonaCat.LogStack.Core;
using System;
using System.Threading;
namespace EonaCat.LogStack.Boosters;
/// <summary>
/// Distributed tracing booster that extracts and propagates trace context
/// </summary>
public sealed class DistributedTracingBooster : BoosterBase
{
private readonly AsyncLocal<string> _correlationId = new AsyncLocal<string>();
private readonly AsyncLocal<string> _parentSpanId = new AsyncLocal<string>();
private int _spanIdCounter;
public DistributedTracingBooster() : base("DistributedTracing") { }
/// <summary>
/// Sets the correlation ID for the current async context
/// </summary>
public void SetCorrelationId(string correlationId)
{
_correlationId.Value = correlationId;
}
/// <summary>
/// Gets the current correlation ID
/// </summary>
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;
}
}
@@ -0,0 +1,39 @@
using EonaCat.LogStack.Core;
using System;
namespace EonaCat.LogStack.Boosters;
/// <summary>
/// Adds a stable fingerprint for exceptions/errors so incidents can be grouped.
/// Useful for alerting systems and production error aggregation.
/// </summary>
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;
}
}
@@ -0,0 +1,28 @@
using EonaCat.LogStack.Core;
using System;
using System.Diagnostics;
namespace EonaCat.LogStack.Boosters;
/// <summary>
/// Adds a health snapshot: CPU, memory pressure and thread information.
/// </summary>
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;
}
}
@@ -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.
/// <summary>
/// Performance monitoring booster that adds timing and resource metrics
/// </summary>
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
}
}
}
@@ -0,0 +1,39 @@
using EonaCat.LogStack.Core;
using System;
namespace EonaCat.LogStack.Boosters;
/// <summary>
/// Property filter booster that includes/excludes events based on properties
/// </summary>
public sealed class PropertyFilterBooster : BoosterBase
{
private readonly Func<LogEventBuilder, bool>? _includeFilter;
private readonly Func<LogEventBuilder, bool>? _excludeFilter;
public PropertyFilterBooster(
Func<LogEventBuilder, bool>? includeFilter = null,
Func<LogEventBuilder, bool>? 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;
}
}
@@ -0,0 +1,55 @@
using EonaCat.LogStack.Core;
using System;
namespace EonaCat.LogStack.Boosters;
/// <summary>
/// Rate limiting booster that throttles log events
/// </summary>
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;
}
}
/// <summary>
/// Gets current rate limiting statistics
/// </summary>
public (int current, int limit) GetStatistics()
{
lock (_lock)
{
return (_eventCount, _maxEventsPerSecond);
}
}
}
@@ -0,0 +1,51 @@
using EonaCat.LogStack.Core;
using System;
namespace EonaCat.LogStack.Boosters;
/// <summary>
/// Sampling booster that probabilistically filters log events
/// </summary>
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();
}
/// <summary>
/// Always log critical and error events, sample the rest
/// </summary>
public override bool Boost(ref LogEventBuilder builder)
{
_totalLogged++;
// Sample based on configured rate
bool shouldLog = _random.NextDouble() < _samplingRate;
if (shouldLog)
{
_totalSampled++;
}
return shouldLog;
}
/// <summary>
/// Gets sampling statistics
/// </summary>
public (long total, long sampled, double rate) GetStatistics()
{
return (_totalLogged, _totalSampled, _totalLogged > 0 ? (double)_totalSampled / _totalLogged : 0.0);
}
}
@@ -0,0 +1,23 @@
using EonaCat.LogStack.Core;
namespace EonaCat.LogStack.Boosters;
/// <summary>
/// Adds a log schema version to make future log format migrations easier.
/// </summary>
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;
}
}
@@ -0,0 +1,20 @@
using EonaCat.LogStack.Core;
using System.Threading;
namespace EonaCat.LogStack.Boosters;
/// <summary>
/// Adds a monotonic event sequence number. Helpful for ordering events across async pipelines.
/// </summary>
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;
}
}
@@ -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);
}
}
}
@@ -0,0 +1,21 @@
using EonaCat.LogStack.Core;
namespace EonaCat.LogStack.Compatibility;
/// <summary>
/// Single entry point for dependency-free adapters. This allows applications
/// migrating from Serilog, log4net or NLog to keep their integration layer.
/// </summary>
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);
}
}
@@ -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);
}
}
}
@@ -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);
}
}
}
@@ -3,21 +3,75 @@ using System;
namespace EonaCat.LogStack.Logging;
public sealed class EonaCatLoggerProvider : ILoggerProvider
/// <summary>
/// Microsoft.Extensions.Logging bridge. Works with ASP.NET Core, worker services,
/// console applications, VS extensions and any .NET project that uses ILogger.
/// </summary>
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>(TState state) where TState:notnull => NullScope.Instance;
public bool IsEnabled(LogLevel logLevel)=>true;
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> 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>(TState state) where TState : notnull
=> _scopes?.Push(state) ?? NullScope.Instance;
public bool IsEnabled(LogLevel logLevel)
=> logLevel != LogLevel.None;
public void Log<TState>(
LogLevel logLevel,
EventId eventId,
TState state,
Exception? exception,
Func<TState, Exception?, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
var message = formatter(state, exception);
_scopes?.ForEachScope<object?>((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(){} }
@@ -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
/// </summary>
public sealed class AuditFlow : FlowBase
{
public event EventHandler<string> 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");
@@ -33,6 +33,7 @@ namespace EonaCat.LogStack.Flows
/// </summary>
public sealed class EncryptedFileFlow : FlowBase
{
public event EventHandler<string> 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<QueueEntry>(new ConcurrentQueue<QueueEntry>(), QueueCapacity);
@@ -24,6 +24,7 @@ namespace EonaCat.LogStack.Flows
/// </summary>
public sealed class FileFlow : FlowBase
{
public event EventHandler<string> 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<LogEvent>(new ConcurrentQueue<LogEvent>(), 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)
@@ -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<string> OnDirectoryException;
private readonly List<ServiceStatus> _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();
@@ -0,0 +1,41 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace EonaCat.LogStack.Logging;
/// <summary>
/// DI integration for every .NET host type.
/// </summary>
public static class EonaCatLoggingExtensions
{
public static ILoggingBuilder AddEonaCatLogStack(
this ILoggingBuilder builder,
Action<EonaCatLogStack>? configure = null)
{
var stack = new EonaCatLogStack();
configure?.Invoke(stack);
builder.Services.AddSingleton(stack);
builder.Services.AddSingleton<ILoggerProvider>(
new EonaCatLoggerProvider(stack));
return builder;
}
public static IServiceCollection AddEonaCatLogStack(
this IServiceCollection services,
Action<EonaCatLogStack>? configure = null)
{
var stack = new EonaCatLogStack();
configure?.Invoke(stack);
services.AddSingleton(stack);
services.AddSingleton<ILoggerProvider>(
new EonaCatLoggerProvider(stack));
services.AddSingleton<ILoggerFactory, LoggerFactory>();
return services;
}
}
+63
View File
@@ -1215,4 +1215,67 @@ public sealed class LogBuilder
_boosters.Add(new CallerInfoBooster());
return this;
}
/// <summary>Adds a stable event fingerprint for grouping failures.</summary>
public LogBuilder BoostWithExceptionFingerprint()
{
_boosters.Add(new ExceptionFingerprintBooster());
return this;
}
/// <summary>Adds runtime health information to every event.</summary>
public LogBuilder BoostWithHealthSnapshot()
{
_boosters.Add(new HealthSnapshotBooster());
return this;
}
/// <summary>Adds a schema version field to every log event.</summary>
public LogBuilder BoostWithSchemaVersion(string version = "1.0")
{
_boosters.Add(new SchemaVersionBooster(version));
return this;
}
/// <summary>Adds a monotonic sequence number to every event.</summary>
public LogBuilder BoostWithSequence()
{
_boosters.Add(new SequenceBooster());
return this;
}
/// <summary>
/// Creates a dependency-free compatibility facade exposing adapters for
/// Serilog, log4net and NLog style integrations.
/// </summary>
public Compatibility.LoggingCompatibilityFacade AsCompatibility()
{
return new Compatibility.LoggingCompatibilityFacade(Build());
}
/// <summary>
/// Creates a Serilog compatible adapter without adding Serilog dependency.
/// </summary>
public Compatibility.EonaCatSerilogAdapter AsSerilog()
{
return new Compatibility.EonaCatSerilogAdapter(Build());
}
/// <summary>
/// Creates a log4net compatible adapter without adding log4net dependency.
/// </summary>
public Compatibility.EonaCatLog4NetAdapter AsLog4Net()
{
return new Compatibility.EonaCatLog4NetAdapter(Build());
}
/// <summary>
/// Creates an NLog compatible adapter without adding NLog dependency.
/// </summary>
public Compatibility.EonaCatNLogAdapter AsNLog()
{
return new Compatibility.EonaCatNLogAdapter(Build());
}
}
@@ -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();
}
@@ -0,0 +1,11 @@
namespace EonaCat.LogStack.Telemetry;
public static class TelemetryDashboard
{
public static string Html => @"<!doctype html>
<html><head><title>EonaCat Telemetry</title></head>
<body><h1>EonaCat LogStack Telemetry</h1>
<p>No external dependencies. Connect to /telemetry to ingest.</p>
<script>
setInterval(async()=>{document.body.dataset.events=await (await fetch('/stats')).text()},1000);
</script></body></html>";
}
@@ -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<string,string>? Tags { get; set; }
}
@@ -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<TelemetryEvent> 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<TelemetryEvent>(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();
}