Added max folder capacity
Added some methods in the EncryptedFileFlow Added filters for FileFlow
This commit is contained in:
@@ -25,7 +25,7 @@
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Net.Http.Json" Version="10.0.3" />
|
<PackageReference Include="System.Net.Http.Json" Version="10.0.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\EonaCat.LogStack\EonaCat.LogStack.csproj" />
|
<ProjectReference Include="..\EonaCat.LogStack\EonaCat.LogStack.csproj" />
|
||||||
|
|||||||
@@ -35,8 +35,8 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="System.Diagnostics.EventLog" Version="10.0.3" />
|
<PackageReference Include="System.Diagnostics.EventLog" Version="10.0.7" />
|
||||||
<PackageReference Include="System.Threading.AccessControl" Version="10.0.3" />
|
<PackageReference Include="System.Threading.AccessControl" Version="10.0.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||||
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
|
<PackageTags>EonaCat;Logger;EonaCatLogStack;Log;Writer;Flows;LogStack;Memory;Speed;Jeroen;Saey</PackageTags>
|
||||||
<PackageIconUrl />
|
<PackageIconUrl />
|
||||||
<FileVersion>0.0.3</FileVersion>
|
<FileVersion>0.0.4</FileVersion>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
@@ -25,7 +25,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<EVRevisionFormat>0.0.3+{chash:10}.{c:ymd}</EVRevisionFormat>
|
<EVRevisionFormat>0.0.4+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||||
<EVDefault>true</EVDefault>
|
<EVDefault>true</EVDefault>
|
||||||
<EVInfo>true</EVInfo>
|
<EVInfo>true</EVInfo>
|
||||||
<EVTagMatch>v[0-9]*</EVTagMatch>
|
<EVTagMatch>v[0-9]*</EVTagMatch>
|
||||||
@@ -36,7 +36,7 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<Version>0.0.3</Version>
|
<Version>0.0.4</Version>
|
||||||
<PackageId>EonaCat.LogStack</PackageId>
|
<PackageId>EonaCat.LogStack</PackageId>
|
||||||
<Product>EonaCat.LogStack</Product>
|
<Product>EonaCat.LogStack</Product>
|
||||||
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl>
|
<RepositoryUrl>https://git.saey.me/EonaCat/EonaCat.LogStack</RepositoryUrl>
|
||||||
@@ -66,18 +66,18 @@ It features a rich fluent API for routing log events to dozens of destinations f
|
|||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="EonaCat.Json" Version="2.2.0" />
|
<PackageReference Include="EonaCat.Json" Version="2.2.2" />
|
||||||
<PackageReference Include="EonaCat.Versioning" Version="1.2.8">
|
<PackageReference Include="EonaCat.Versioning" Version="1.2.9">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
<PackageReference Include="EonaCat.Versioning.Helpers" Version="1.0.2" />
|
<PackageReference Include="EonaCat.Versioning.Helpers" Version="1.0.2" />
|
||||||
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Logging" Version="10.0.7" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.3" />
|
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="10.0.7" />
|
||||||
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
<PackageReference Include="System.Net.Http" Version="4.3.4" />
|
||||||
<PackageReference Include="System.Threading.Channels" Version="10.0.3" />
|
<PackageReference Include="System.Threading.Channels" Version="10.0.7" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="LICENSE.md">
|
<None Update="LICENSE.md">
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +1,4 @@
|
|||||||
using EonaCat.Json;
|
using EonaCat.LogStack.Core;
|
||||||
using EonaCat.LogStack.Core;
|
|
||||||
using EonaCat.LogStack.EonaCatLogStackCore;
|
using EonaCat.LogStack.EonaCatLogStackCore;
|
||||||
using EonaCat.LogStack.EonaCatLogStackCore.Policies;
|
using EonaCat.LogStack.EonaCatLogStackCore.Policies;
|
||||||
using System;
|
using System;
|
||||||
@@ -42,6 +41,10 @@ namespace EonaCat.LogStack.Flows
|
|||||||
{ LogLevel.Critical, "CRITICAL" },
|
{ LogLevel.Critical, "CRITICAL" },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly char[] CsvSpecialChars = { ',', '"', '\n', '\r' };
|
||||||
|
private static readonly string CachedMachineName = Environment.MachineName;
|
||||||
|
private static readonly int CachedPid = Process.GetCurrentProcess().Id;
|
||||||
|
|
||||||
private const string CsvHeader = "timestamp,level,category,message,exception,properties\r\n";
|
private const string CsvHeader = "timestamp,level,category,message,exception,properties\r\n";
|
||||||
|
|
||||||
private readonly BlockingCollection<LogEvent> _queue;
|
private readonly BlockingCollection<LogEvent> _queue;
|
||||||
@@ -57,6 +60,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
private readonly string _directory;
|
private readonly string _directory;
|
||||||
private readonly string _filePrefix;
|
private readonly string _filePrefix;
|
||||||
private readonly long _maxFileSize;
|
private readonly long _maxFileSize;
|
||||||
|
private readonly long _maxDirectorySize;
|
||||||
private readonly FileRetentionPolicy _retention;
|
private readonly FileRetentionPolicy _retention;
|
||||||
private readonly TimestampMode _timestampMode;
|
private readonly TimestampMode _timestampMode;
|
||||||
private readonly TimeSpan _flushInterval;
|
private readonly TimeSpan _flushInterval;
|
||||||
@@ -74,11 +78,38 @@ namespace EonaCat.LogStack.Flows
|
|||||||
private readonly object _secondaryWritersLock = new object();
|
private readonly object _secondaryWritersLock = new object();
|
||||||
private int _correlationSeed;
|
private int _correlationSeed;
|
||||||
|
|
||||||
|
private readonly List<Func<LogEvent, bool>> _filters = new List<Func<LogEvent, bool>>();
|
||||||
|
private readonly object _filtersLock = new object();
|
||||||
|
|
||||||
|
private readonly ConcurrentDictionary<string, long> _deduplicationCache
|
||||||
|
= new ConcurrentDictionary<string, long>(StringComparer.Ordinal);
|
||||||
|
private TimeSpan _deduplicationWindow = TimeSpan.Zero;
|
||||||
|
private volatile bool _deduplicationEnabled;
|
||||||
|
|
||||||
|
private string _dateFormat = "yyyyMMdd";
|
||||||
|
private volatile Exception _lastError;
|
||||||
|
private long _lastErrorTimestamp;
|
||||||
|
private long _totalErrors;
|
||||||
|
|
||||||
private readonly List<KeyValuePair<string, Func<LogEvent, object>>> _enrichers
|
private readonly List<KeyValuePair<string, Func<LogEvent, object>>> _enrichers
|
||||||
= new List<KeyValuePair<string, Func<LogEvent, object>>>();
|
= new List<KeyValuePair<string, Func<LogEvent, object>>>();
|
||||||
|
|
||||||
private long _currentMemoryBytes;
|
private long _currentMemoryBytes;
|
||||||
|
|
||||||
|
// Rate limiting
|
||||||
|
private int _maxEventsPerSecond;
|
||||||
|
private bool _rateLimitEnabled => _maxEventsPerSecond > 0;
|
||||||
|
private long _rateLimitWindowStart;
|
||||||
|
private int _rateLimitCounter;
|
||||||
|
private readonly object _rateLimitLock = new object();
|
||||||
|
|
||||||
|
// Auto-flush on error
|
||||||
|
private volatile bool _autoFlushOnError;
|
||||||
|
|
||||||
|
// Scoped properties (AsyncLocal for ambient context)
|
||||||
|
private static readonly AsyncLocal<Dictionary<string, object>> _scopeProperties
|
||||||
|
= new AsyncLocal<Dictionary<string, object>>();
|
||||||
|
|
||||||
private long _totalBytesWritten;
|
private long _totalBytesWritten;
|
||||||
private long _totalRotations;
|
private long _totalRotations;
|
||||||
|
|
||||||
@@ -121,6 +152,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
string directory,
|
string directory,
|
||||||
string filePrefix = "log",
|
string filePrefix = "log",
|
||||||
long maxFileSize = 200 * 1024 * 1024,
|
long maxFileSize = 200 * 1024 * 1024,
|
||||||
|
long maxDirectorySize = 2L * 1024 * 1024 * 1024,
|
||||||
FileRetentionPolicy retention = null,
|
FileRetentionPolicy retention = null,
|
||||||
int flushIntervalMs = 2000,
|
int flushIntervalMs = 2000,
|
||||||
int batchSize = 1,
|
int batchSize = 1,
|
||||||
@@ -157,6 +189,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
_filePrefix = filePrefix;
|
_filePrefix = filePrefix;
|
||||||
_template = template;
|
_template = template;
|
||||||
_maxFileSize = maxFileSize;
|
_maxFileSize = maxFileSize;
|
||||||
|
_maxDirectorySize = maxDirectorySize > 0 ? maxDirectorySize : 10L * maxFileSize;
|
||||||
_retention = retention ?? new FileRetentionPolicy();
|
_retention = retention ?? new FileRetentionPolicy();
|
||||||
_timestampMode = timestampMode;
|
_timestampMode = timestampMode;
|
||||||
_useCategoryRouting = useCategoryRouting;
|
_useCategoryRouting = useCategoryRouting;
|
||||||
@@ -318,6 +351,176 @@ namespace EonaCat.LogStack.Flows
|
|||||||
MinimumLevel = level;
|
MinimumLevel = level;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>Add a custom filter predicate. Events are logged only if ALL filters return true.</summary>
|
||||||
|
public FileFlow WithFilter(Func<LogEvent, bool> predicate)
|
||||||
|
{
|
||||||
|
if (predicate == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("predicate");
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_filtersLock)
|
||||||
|
{
|
||||||
|
_filters.Add(predicate);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Enable deduplication: suppress identical messages within the given time window.</summary>
|
||||||
|
public FileFlow WithDeduplication(TimeSpan window)
|
||||||
|
{
|
||||||
|
if (window <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
throw new ArgumentOutOfRangeException("window", "Deduplication window must be positive.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_deduplicationWindow = window;
|
||||||
|
_deduplicationEnabled = true;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Configure a custom date format for log file names (default: yyyyMMdd).</summary>
|
||||||
|
public FileFlow WithDateFormat(string dateFormat)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(dateFormat))
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("dateFormat");
|
||||||
|
}
|
||||||
|
|
||||||
|
_dateFormat = dateFormat;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Limit the flow to a maximum number of events per second. Events exceeding the limit are dropped.</summary>
|
||||||
|
public FileFlow WithRateLimit(int maxEventsPerSecond)
|
||||||
|
{
|
||||||
|
_maxEventsPerSecond = maxEventsPerSecond;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>When enabled, the file stream is flushed immediately after writing Error or Critical level events.</summary>
|
||||||
|
public FileFlow WithAutoFlushOnError(bool enabled = true)
|
||||||
|
{
|
||||||
|
_autoFlushOnError = enabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Push scoped properties that will be included in all log events written on the current async context.</summary>
|
||||||
|
public IDisposable BeginScope(params KeyValuePair<string, object>[] properties)
|
||||||
|
{
|
||||||
|
var previous = _scopeProperties.Value;
|
||||||
|
var merged = previous != null
|
||||||
|
? new Dictionary<string, object>(previous)
|
||||||
|
: new Dictionary<string, object>();
|
||||||
|
|
||||||
|
foreach (var kv in properties)
|
||||||
|
{
|
||||||
|
merged[kv.Key] = kv.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
_scopeProperties.Value = merged;
|
||||||
|
return new ScopeDisposable(previous);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Push a single scoped property.</summary>
|
||||||
|
public IDisposable BeginScope(string key, object value)
|
||||||
|
{
|
||||||
|
return BeginScope(new KeyValuePair<string, object>(key, value));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the current queue depth (number of pending events).</summary>
|
||||||
|
public int GetQueueDepth()
|
||||||
|
{
|
||||||
|
return _queue.Count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the current estimated memory usage of the queue in bytes.</summary>
|
||||||
|
public long GetMemoryPressureBytes()
|
||||||
|
{
|
||||||
|
return Interlocked.Read(ref _currentMemoryBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Generates a fingerprint hash for an exception to assist with grouping.</summary>
|
||||||
|
public static string GetExceptionFingerprint(Exception ex)
|
||||||
|
{
|
||||||
|
if (ex == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
string source = string.Concat(
|
||||||
|
ex.GetType().FullName, "|",
|
||||||
|
ex.TargetSite?.Name ?? string.Empty, "|",
|
||||||
|
ex.StackTrace != null && ex.StackTrace.Length > 0
|
||||||
|
? ex.StackTrace.Substring(0, Math.Min(200, ex.StackTrace.Length))
|
||||||
|
: string.Empty);
|
||||||
|
|
||||||
|
// Simple FNV-1a hash
|
||||||
|
unchecked
|
||||||
|
{
|
||||||
|
uint hash = 2166136261;
|
||||||
|
foreach (char c in source)
|
||||||
|
{
|
||||||
|
hash ^= c;
|
||||||
|
hash *= 16777619;
|
||||||
|
}
|
||||||
|
return hash.ToString("x8");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class ScopeDisposable : IDisposable
|
||||||
|
{
|
||||||
|
private readonly Dictionary<string, object> _previous;
|
||||||
|
|
||||||
|
public ScopeDisposable(Dictionary<string, object> previous)
|
||||||
|
{
|
||||||
|
_previous = previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_scopeProperties.Value = _previous;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns true if the flow is healthy (no recent errors and writer thread alive).</summary>
|
||||||
|
public bool IsHealthy()
|
||||||
|
{
|
||||||
|
if (!IsEnabled)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_writerThread.IsAlive)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
long lastErr = Interlocked.Read(ref _lastErrorTimestamp);
|
||||||
|
if (lastErr > 0)
|
||||||
|
{
|
||||||
|
TimeSpan since = TimeSpan.FromTicks(DateTime.UtcNow.Ticks - lastErr);
|
||||||
|
if (since < TimeSpan.FromMinutes(1))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the last error encountered by the writer, or null if none.</summary>
|
||||||
|
public Exception GetLastError()
|
||||||
|
{
|
||||||
|
return _lastError;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Returns the total number of write errors encountered.</summary>
|
||||||
|
public long GetTotalErrors()
|
||||||
|
{
|
||||||
|
return Interlocked.Read(ref _totalErrors);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Returns live throughput and health metrics.</summary>
|
/// <summary>Returns live throughput and health metrics.</summary>
|
||||||
public LogStats GetStats()
|
public LogStats GetStats()
|
||||||
{
|
{
|
||||||
@@ -347,6 +550,28 @@ namespace EonaCat.LogStack.Flows
|
|||||||
return Task.FromResult(WriteResult.LevelFiltered);
|
return Task.FromResult(WriteResult.LevelFiltered);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!PassesFilters(logEvent))
|
||||||
|
{
|
||||||
|
return Task.FromResult(WriteResult.LevelFiltered);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_deduplicationEnabled && IsDuplicate(logEvent))
|
||||||
|
{
|
||||||
|
return Task.FromResult(WriteResult.LevelFiltered);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_rateLimitEnabled && !TryPassRateLimit())
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref DroppedCount);
|
||||||
|
Action<LogEvent> drop = _onDrop;
|
||||||
|
if (drop != null)
|
||||||
|
{
|
||||||
|
drop(logEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
return Task.FromResult(WriteResult.Dropped);
|
||||||
|
}
|
||||||
|
|
||||||
return Task.FromResult(TryEnqueue(logEvent));
|
return Task.FromResult(TryEnqueue(logEvent));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,9 +586,11 @@ namespace EonaCat.LogStack.Flows
|
|||||||
|
|
||||||
WriteResult result = WriteResult.Success;
|
WriteResult result = WriteResult.Success;
|
||||||
SamplingPolicy sp = _samplingPolicy;
|
SamplingPolicy sp = _samplingPolicy;
|
||||||
|
ReadOnlySpan<LogEvent> span = logEvents.Span;
|
||||||
|
|
||||||
foreach (LogEvent e in logEvents.ToArray())
|
for (int i = 0; i < span.Length; i++)
|
||||||
{
|
{
|
||||||
|
LogEvent e = span[i];
|
||||||
if (e.Level < MinimumLevel)
|
if (e.Level < MinimumLevel)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -374,6 +601,29 @@ namespace EonaCat.LogStack.Flows
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!PassesFilters(e))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_deduplicationEnabled && IsDuplicate(e))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_rateLimitEnabled && !TryPassRateLimit())
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref DroppedCount);
|
||||||
|
Action<LogEvent> drop = _onDrop;
|
||||||
|
if (drop != null)
|
||||||
|
{
|
||||||
|
drop(e);
|
||||||
|
}
|
||||||
|
|
||||||
|
result = WriteResult.Dropped;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (TryEnqueue(e) == WriteResult.Dropped)
|
if (TryEnqueue(e) == WriteResult.Dropped)
|
||||||
{
|
{
|
||||||
result = WriteResult.Dropped;
|
result = WriteResult.Dropped;
|
||||||
@@ -418,6 +668,88 @@ namespace EonaCat.LogStack.Flows
|
|||||||
return WriteResult.Success;
|
return WriteResult.Success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private bool TryPassRateLimit()
|
||||||
|
{
|
||||||
|
long now = DateTime.UtcNow.Ticks;
|
||||||
|
lock (_rateLimitLock)
|
||||||
|
{
|
||||||
|
long elapsed = now - _rateLimitWindowStart;
|
||||||
|
if (elapsed >= TimeSpan.TicksPerSecond)
|
||||||
|
{
|
||||||
|
_rateLimitWindowStart = now;
|
||||||
|
_rateLimitCounter = 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_rateLimitEnabled && _rateLimitCounter >= _maxEventsPerSecond)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_rateLimitCounter++;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
|
private bool PassesFilters(LogEvent log)
|
||||||
|
{
|
||||||
|
lock (_filtersLock)
|
||||||
|
{
|
||||||
|
for (int i = 0; i < _filters.Count; i++)
|
||||||
|
{
|
||||||
|
if (!_filters[i](log))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool IsDuplicate(LogEvent log)
|
||||||
|
{
|
||||||
|
string key = string.Concat(
|
||||||
|
log.Level.ToString(), "|",
|
||||||
|
log.Category ?? string.Empty, "|",
|
||||||
|
log.Message.Length > 0 ? log.Message.ToString() : string.Empty);
|
||||||
|
|
||||||
|
long nowTicks = DateTime.UtcNow.Ticks;
|
||||||
|
long windowTicks = _deduplicationWindow.Ticks;
|
||||||
|
|
||||||
|
long existing;
|
||||||
|
if (_deduplicationCache.TryGetValue(key, out existing))
|
||||||
|
{
|
||||||
|
if (nowTicks - existing < windowTicks)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_deduplicationCache[key] = nowTicks;
|
||||||
|
|
||||||
|
// Periodic cleanup: remove expired entries when cache grows large
|
||||||
|
if (_deduplicationCache.Count > 10000)
|
||||||
|
{
|
||||||
|
CleanDeduplicationCache(nowTicks, windowTicks);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CleanDeduplicationCache(long nowTicks, long windowTicks)
|
||||||
|
{
|
||||||
|
foreach (var kvp in _deduplicationCache)
|
||||||
|
{
|
||||||
|
if (nowTicks - kvp.Value >= windowTicks)
|
||||||
|
{
|
||||||
|
long removed;
|
||||||
|
_deduplicationCache.TryRemove(kvp.Key, out removed);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
|
public override Task FlushAsync(CancellationToken cancellationToken = default(CancellationToken))
|
||||||
{
|
{
|
||||||
@@ -642,6 +974,9 @@ namespace EonaCat.LogStack.Flows
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
_lastError = ex;
|
||||||
|
Interlocked.Increment(ref _totalErrors);
|
||||||
|
Interlocked.Exchange(ref _lastErrorTimestamp, DateTime.UtcNow.Ticks);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
WriteToConsoleError("[FileFlow] Write error for '" + path + "': " + ex.Message);
|
WriteToConsoleError("[FileFlow] Write error for '" + path + "': " + ex.Message);
|
||||||
@@ -651,6 +986,19 @@ namespace EonaCat.LogStack.Flows
|
|||||||
}
|
}
|
||||||
|
|
||||||
Interlocked.Add(ref _totalBytesWritten, line.Length + 1);
|
Interlocked.Add(ref _totalBytesWritten, line.Length + 1);
|
||||||
|
|
||||||
|
// Auto-flush on error/critical
|
||||||
|
if (_autoFlushOnError && (log.Level >= LogLevel.Error))
|
||||||
|
{
|
||||||
|
lock (_fileLock)
|
||||||
|
{
|
||||||
|
OpenFile autoFlushOf;
|
||||||
|
if (_openFiles.TryGetValue(path, out autoFlushOf))
|
||||||
|
{
|
||||||
|
try { autoFlushOf.Writer.Flush(); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -719,11 +1067,30 @@ namespace EonaCat.LogStack.Flows
|
|||||||
sb.Append("\",");
|
sb.Append("\",");
|
||||||
|
|
||||||
sb.Append("\"host\":\"");
|
sb.Append("\"host\":\"");
|
||||||
AppendJsonEscaped(Environment.MachineName, sb);
|
AppendJsonEscaped(CachedMachineName, sb);
|
||||||
sb.Append("\",");
|
sb.Append("\",");
|
||||||
|
|
||||||
sb.Append("\"pid\":");
|
sb.Append("\"pid\":");
|
||||||
sb.Append(Process.GetCurrentProcess().Id);
|
sb.Append(CachedPid);
|
||||||
|
sb.Append(',');
|
||||||
|
|
||||||
|
// Distributed tracing
|
||||||
|
if (log.TraceId != default(ActivityTraceId))
|
||||||
|
{
|
||||||
|
sb.Append("\"traceId\":\"");
|
||||||
|
sb.Append(log.TraceId.ToHexString());
|
||||||
|
sb.Append("\",");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (log.SpanId != default(ActivitySpanId))
|
||||||
|
{
|
||||||
|
sb.Append("\"spanId\":\"");
|
||||||
|
sb.Append(log.SpanId.ToHexString());
|
||||||
|
sb.Append("\",");
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append("\"threadId\":");
|
||||||
|
sb.Append(log.ThreadId);
|
||||||
sb.Append(',');
|
sb.Append(',');
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -777,7 +1144,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
sb.Append('"');
|
sb.Append('"');
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var property in log.Properties.ToArray())
|
foreach (var property in log.Properties)
|
||||||
{
|
{
|
||||||
if (!first)
|
if (!first)
|
||||||
{
|
{
|
||||||
@@ -927,7 +1294,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
sb.Append("</property>");
|
sb.Append("</property>");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var property in log.Properties.ToArray())
|
foreach (var property in log.Properties)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrEmpty(property.Key))
|
if (string.IsNullOrEmpty(property.Key))
|
||||||
{
|
{
|
||||||
@@ -1025,7 +1392,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
object val = kv.Value(log);
|
object val = kv.Value(log);
|
||||||
AppendCsvInner(val != null ? val.ToString() : "null", sb);
|
AppendCsvInner(val != null ? val.ToString() : "null", sb);
|
||||||
}
|
}
|
||||||
foreach (var property in log.Properties.ToArray())
|
foreach (var property in log.Properties)
|
||||||
{
|
{
|
||||||
if (!first)
|
if (!first)
|
||||||
{
|
{
|
||||||
@@ -1047,7 +1414,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
value = string.Empty;
|
value = string.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool needsQuote = value.IndexOfAny(new[] { ',', '"', '\n', '\r' }) >= 0;
|
bool needsQuote = value.IndexOfAny(CsvSpecialChars) >= 0;
|
||||||
if (needsQuote)
|
if (needsQuote)
|
||||||
{
|
{
|
||||||
sb.Append('"');
|
sb.Append('"');
|
||||||
@@ -1253,7 +1620,20 @@ namespace EonaCat.LogStack.Flows
|
|||||||
for (int i = maxFiles - 1; i >= 1; i--)
|
for (int i = maxFiles - 1; i >= 1; i--)
|
||||||
{
|
{
|
||||||
string src = Path.Combine(dir, baseName + "_" + i + _fileExtension);
|
string src = Path.Combine(dir, baseName + "_" + i + _fileExtension);
|
||||||
|
string srcGz = src + ".gz";
|
||||||
string dst = Path.Combine(dir, baseName + "_" + (i + 1) + _fileExtension);
|
string dst = Path.Combine(dir, baseName + "_" + (i + 1) + _fileExtension);
|
||||||
|
string dstGz = dst + ".gz";
|
||||||
|
|
||||||
|
// Move .gz variant if it exists
|
||||||
|
if (File.Exists(srcGz))
|
||||||
|
{
|
||||||
|
if (File.Exists(dstGz))
|
||||||
|
{
|
||||||
|
File.Delete(dstGz);
|
||||||
|
}
|
||||||
|
File.Move(srcGz, dstGz);
|
||||||
|
}
|
||||||
|
|
||||||
if (!File.Exists(src))
|
if (!File.Exists(src))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@@ -1261,8 +1641,6 @@ namespace EonaCat.LogStack.Flows
|
|||||||
|
|
||||||
if (File.Exists(dst))
|
if (File.Exists(dst))
|
||||||
{
|
{
|
||||||
_compressionQueue.Enqueue(dst);
|
|
||||||
_compressionSignal.Release(1);
|
|
||||||
File.Delete(dst);
|
File.Delete(dst);
|
||||||
}
|
}
|
||||||
File.Move(src, dst);
|
File.Move(src, dst);
|
||||||
@@ -1275,6 +1653,14 @@ namespace EonaCat.LogStack.Flows
|
|||||||
}
|
}
|
||||||
|
|
||||||
File.Move(filePath, archive);
|
File.Move(filePath, archive);
|
||||||
|
|
||||||
|
// Queue the archived file for compression
|
||||||
|
if (_compressionFormat != CompressionFormat.None)
|
||||||
|
{
|
||||||
|
_compressionQueue.Enqueue(archive);
|
||||||
|
_compressionSignal.Release(1);
|
||||||
|
}
|
||||||
|
|
||||||
return archive;
|
return archive;
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -1343,6 +1729,9 @@ namespace EonaCat.LogStack.Flows
|
|||||||
gz.Write(buffer, 0, read);
|
gz.Write(buffer, 0, read);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete the original uncompressed file after successful compression
|
||||||
|
try { File.Delete(path); } catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
private void PeriodicFlushLoop()
|
private void PeriodicFlushLoop()
|
||||||
@@ -1361,16 +1750,12 @@ namespace EonaCat.LogStack.Flows
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
OpenFile[] snapshot;
|
|
||||||
lock (_fileLock)
|
lock (_fileLock)
|
||||||
{
|
{
|
||||||
snapshot = new OpenFile[_openFiles.Count];
|
foreach (OpenFile of in _openFiles.Values)
|
||||||
_openFiles.Values.CopyTo(snapshot, 0);
|
{
|
||||||
}
|
try { of.Writer.Flush(); } catch { /* ignore */ }
|
||||||
|
}
|
||||||
foreach (OpenFile of in snapshot)
|
|
||||||
{
|
|
||||||
try { of.Writer.Flush(); } catch { /* ignore */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (ThreadInterruptedException) { break; }
|
catch (ThreadInterruptedException) { break; }
|
||||||
@@ -1405,6 +1790,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
}
|
}
|
||||||
|
|
||||||
FileInfo[] files = dir.GetFiles("*" + _fileExtension)
|
FileInfo[] files = dir.GetFiles("*" + _fileExtension)
|
||||||
|
.Concat(dir.GetFiles("*" + _fileExtension + ".gz"))
|
||||||
.OrderByDescending(f => f.LastWriteTimeUtc)
|
.OrderByDescending(f => f.LastWriteTimeUtc)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
@@ -1418,8 +1804,9 @@ namespace EonaCat.LogStack.Flows
|
|||||||
bool tooMany = _retention.MaxRolledFiles > 0 && kept >= _retention.MaxRolledFiles;
|
bool tooMany = _retention.MaxRolledFiles > 0 && kept >= _retention.MaxRolledFiles;
|
||||||
bool tooLarge = _retention.MaxTotalArchiveBytes > 0
|
bool tooLarge = _retention.MaxTotalArchiveBytes > 0
|
||||||
&& totalBytes + f.Length > _retention.MaxTotalArchiveBytes;
|
&& totalBytes + f.Length > _retention.MaxTotalArchiveBytes;
|
||||||
|
bool directoryTooLarge = totalBytes + f.Length > _maxDirectorySize;
|
||||||
|
|
||||||
if (tooOld || tooMany || tooLarge)
|
if (tooOld || tooMany || tooLarge || directoryTooLarge)
|
||||||
{
|
{
|
||||||
try { f.Delete(); } catch { /* ignore */ }
|
try { f.Delete(); } catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
@@ -1460,7 +1847,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
|
|
||||||
return Path.Combine(
|
return Path.Combine(
|
||||||
_directory,
|
_directory,
|
||||||
prefix + "_" + Environment.MachineName + "_" + date.ToString("yyyyMMdd") + _fileExtension);
|
string.Concat(prefix, "_", CachedMachineName, "_", date.ToString(_dateFormat), _fileExtension));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SetFileExtension(FileOutputFormat fmt)
|
private void SetFileExtension(FileOutputFormat fmt)
|
||||||
@@ -1488,7 +1875,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
+ log.Properties.Count * 40;
|
+ log.Properties.Count * 40;
|
||||||
if (log.Exception != null)
|
if (log.Exception != null)
|
||||||
{
|
{
|
||||||
s += log.Exception.ToString().Length * 2;
|
s += 2048; // Avoid calling Exception.ToString() just for size estimation
|
||||||
}
|
}
|
||||||
|
|
||||||
return s;
|
return s;
|
||||||
@@ -1541,7 +1928,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
sb.Append(_timestampMode == TimestampMode.Local
|
sb.Append(_timestampMode == TimestampMode.Local
|
||||||
? TimeZoneInfo.Local.StandardName : "UTC");
|
? TimeZoneInfo.Local.StandardName : "UTC");
|
||||||
case "host":
|
case "host":
|
||||||
return (log, sb) => sb.Append(Environment.MachineName);
|
return (log, sb) => sb.Append(CachedMachineName);
|
||||||
case "category":
|
case "category":
|
||||||
return (log, sb) => { if (log.Category != null) { sb.Append(log.Category); } };
|
return (log, sb) => { if (log.Category != null) { sb.Append(log.Category); } };
|
||||||
case "thread":
|
case "thread":
|
||||||
@@ -1573,7 +1960,23 @@ namespace EonaCat.LogStack.Flows
|
|||||||
case "newline":
|
case "newline":
|
||||||
return (log, sb) => sb.AppendLine();
|
return (log, sb) => sb.AppendLine();
|
||||||
case "pid":
|
case "pid":
|
||||||
return (log, sb) => sb.Append(Process.GetCurrentProcess().Id);
|
return (log, sb) => sb.Append(CachedPid);
|
||||||
|
case "traceid":
|
||||||
|
return (log, sb) =>
|
||||||
|
{
|
||||||
|
if (log.TraceId != default(ActivityTraceId))
|
||||||
|
{
|
||||||
|
sb.Append(log.TraceId.ToHexString());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
case "spanid":
|
||||||
|
return (log, sb) =>
|
||||||
|
{
|
||||||
|
if (log.SpanId != default(ActivitySpanId))
|
||||||
|
{
|
||||||
|
sb.Append(log.SpanId.ToHexString());
|
||||||
|
}
|
||||||
|
};
|
||||||
default:
|
default:
|
||||||
return BuildCustomOrLiteralToken(token);
|
return BuildCustomOrLiteralToken(token);
|
||||||
}
|
}
|
||||||
@@ -1598,9 +2001,11 @@ namespace EonaCat.LogStack.Flows
|
|||||||
|
|
||||||
private void AppendProperties(LogEvent log, StringBuilder sb)
|
private void AppendProperties(LogEvent log, StringBuilder sb)
|
||||||
{
|
{
|
||||||
|
var scopeProps = _scopeProperties.Value;
|
||||||
bool hasEnrichers = _enrichers.Count > 0;
|
bool hasEnrichers = _enrichers.Count > 0;
|
||||||
bool hasProps = log.Properties.Count > 0;
|
bool hasProps = log.Properties.Count > 0;
|
||||||
if (!hasEnrichers && !hasProps)
|
bool hasScope = scopeProps != null && scopeProps.Count > 0;
|
||||||
|
if (!hasEnrichers && !hasProps && !hasScope)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1620,7 +2025,7 @@ namespace EonaCat.LogStack.Flows
|
|||||||
sb.Append(kv.Key).Append('=').Append(val != null ? val.ToString() : "null");
|
sb.Append(kv.Key).Append('=').Append(val != null ? val.ToString() : "null");
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var property in log.Properties.ToArray())
|
foreach (var property in log.Properties)
|
||||||
{
|
{
|
||||||
if (!first)
|
if (!first)
|
||||||
{
|
{
|
||||||
@@ -1632,6 +2037,21 @@ namespace EonaCat.LogStack.Flows
|
|||||||
.Append(property.Value != null ? property.Value.ToString() : "null");
|
.Append(property.Value != null ? property.Value.ToString() : "null");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hasScope)
|
||||||
|
{
|
||||||
|
foreach (var kv in scopeProps)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
sb.Append(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
first = false;
|
||||||
|
sb.Append(kv.Key).Append('=')
|
||||||
|
.Append(kv.Value != null ? kv.Value.ToString() : "null");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
sb.Append('}');
|
sb.Append('}');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,7 @@ public sealed class LogBuilder
|
|||||||
string directory,
|
string directory,
|
||||||
string filePrefix = "log",
|
string filePrefix = "log",
|
||||||
long maxFileSize = 100 * 1024 * 1024,
|
long maxFileSize = 100 * 1024 * 1024,
|
||||||
|
long maxDirectorySize = 2L * 1024 * 1024 * 1024,
|
||||||
FileRetentionPolicy fileRetentionPolicy = null,
|
FileRetentionPolicy fileRetentionPolicy = null,
|
||||||
int flushIntervalInMilliSeconds = 2000,
|
int flushIntervalInMilliSeconds = 2000,
|
||||||
bool useCategoryRouting = false,
|
bool useCategoryRouting = false,
|
||||||
@@ -110,6 +111,7 @@ public sealed class LogBuilder
|
|||||||
directory,
|
directory,
|
||||||
filePrefix,
|
filePrefix,
|
||||||
maxFileSize,
|
maxFileSize,
|
||||||
|
maxDirectorySize,
|
||||||
fileRetentionPolicy,
|
fileRetentionPolicy,
|
||||||
flushIntervalInMilliSeconds,
|
flushIntervalInMilliSeconds,
|
||||||
batchSize,
|
batchSize,
|
||||||
@@ -140,9 +142,14 @@ public sealed class LogBuilder
|
|||||||
password,
|
password,
|
||||||
filePrefix,
|
filePrefix,
|
||||||
maxFileSize,
|
maxFileSize,
|
||||||
flushIntervalInMilliSeconds,
|
maxDirectorySize: 2L * 1024 * 1024 * 1024,
|
||||||
minimumLevel,
|
retention: fileRetentionPolicy,
|
||||||
_timestampMode));
|
flushIntervalMs: flushIntervalInMilliSeconds,
|
||||||
|
batchSize: 1,
|
||||||
|
minimumLevel: minimumLevel,
|
||||||
|
useCategoryRouting: useCategoryRouting,
|
||||||
|
logLevelsForSeparateFiles: logLevelsForSeparateFiles,
|
||||||
|
tsMode: _timestampMode));
|
||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="EonaCat.Versioning" Version="1.2.8">
|
<PackageReference Include="EonaCat.Versioning" Version="1.2.9">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
</PackageReference>
|
</PackageReference>
|
||||||
|
|||||||
Reference in New Issue
Block a user