Updated
This commit is contained in:
@@ -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>
|
||||
@@ -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;
|
||||
private IExternalScopeProvider? _scopeProvider;
|
||||
|
||||
public EonaCatLoggerProvider(EonaCatLogStack logStack) => _logStack = logStack;
|
||||
public Microsoft.Extensions.Logging.ILogger CreateLogger(string categoryName) => new CategoryLogger(categoryName,_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
|
||||
{
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
// 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,8 +905,15 @@ namespace EonaCat.LogStack.Flows
|
||||
return;
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user