diff --git a/EonaCat.Logger.LogClient/LogCentralClient.cs b/EonaCat.Logger.LogClient/LogCentralClient.cs index 3165b39..fe49de8 100644 --- a/EonaCat.Logger.LogClient/LogCentralClient.cs +++ b/EonaCat.Logger.LogClient/LogCentralClient.cs @@ -51,8 +51,6 @@ namespace EonaCat.Logger.LogClient } } - public void Log(LogEntry entry) => LogAsync(entry).GetAwaiter().GetResult(); - public async Task LogExceptionAsync(Exception ex, string message = "", Dictionary? properties = null) { diff --git a/EonaCat.Logger.LogClient/LogCentralEonaCatAdapter.cs b/EonaCat.Logger.LogClient/LogCentralEonaCatAdapter.cs index 748a7ab..ff9cf92 100644 --- a/EonaCat.Logger.LogClient/LogCentralEonaCatAdapter.cs +++ b/EonaCat.Logger.LogClient/LogCentralEonaCatAdapter.cs @@ -37,7 +37,7 @@ namespace EonaCat.Logger.LogClient entry.StackTrace = e.Exception.StackTrace; } - _client.Log(entry); + _client.LogAsync(entry).ConfigureAwait(false); } private static LogLevel MapLogLevel(ELogType logType) diff --git a/EonaCat.Logger.SerilogTest/EonaCat.Logger.SerilogTest.csproj b/EonaCat.Logger.SerilogTest/EonaCat.Logger.SerilogTest.csproj new file mode 100644 index 0000000..af014cb --- /dev/null +++ b/EonaCat.Logger.SerilogTest/EonaCat.Logger.SerilogTest.csproj @@ -0,0 +1,18 @@ + + + + Exe + net10.0 + enable + enable + + + + + + + + + + + diff --git a/EonaCat.Logger.SerilogTest/Program.cs b/EonaCat.Logger.SerilogTest/Program.cs new file mode 100644 index 0000000..62b59c9 --- /dev/null +++ b/EonaCat.Logger.SerilogTest/Program.cs @@ -0,0 +1,197 @@ +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.DataProtection; +using Microsoft.Extensions.Hosting; +using Serilog; +using Serilog.Events; +using Serilog.Formatting.Json; +using System.Net.Sockets; +using System.Runtime.InteropServices; +using System.Text; + +var builder = WebApplication.CreateBuilder(args); + +// +// LOGGER CONFIGURATION (Equivalent to LoggerSettings) +// +Log.Logger = new LoggerConfiguration() + .MinimumLevel.Verbose() + .Enrich.WithProperty("Id", "TEST") + .Enrich.WithProperty("AppName", "[JIJ BENT EEN BRASSER!]") + .WriteTo.Async(a => a.Console()) + .WriteTo.Async(a => a.File( + path: "logs/web-.log", + rollingInterval: RollingInterval.Day, + fileSizeLimitBytes: 1_000_000, + rollOnFileSizeLimit: true, + retainedFileCountLimit: 5, + shared: true)) + .WriteTo.Async(a => a.File( + new JsonFormatter(), + path: "logs/test.json", + rollingInterval: RollingInterval.Day)) + //.WriteTo.Seq("http://localhost:5341") // central logging + .CreateLogger(); + +builder.Services.AddDataProtection() + .PersistKeysToFileSystem(new DirectoryInfo(Path.Combine(Directory.GetCurrentDirectory(), "keys"))) + .SetApplicationName("SerilogStressTest"); + +builder.Services.AddRazorPages(); + +builder.WebHost.ConfigureKestrel(options => +{ + options.ListenAnyIP(6000); +}); + +var app = builder.Build(); + +app.UseHttpsRedirection(); +app.UseStaticFiles(); +app.UseRouting(); +app.UseAuthorization(); +app.MapRazorPages(); + +// +// ============================== +// 🔥 TESTS START HERE +// ============================== +// + +_ = Task.Run(RunLoggingTestsAsync); +_ = Task.Run(RunWebLoggingTestsAsync); +_ = Task.Run(RunLoggingExceptionTests); +_ = Task.Run(RunWebLoggingExceptionTests); +//_ = Task.Run(RunMemoryLeakTest); +_ = Task.Run(RunTcpLoggerTest); + +app.Run(); + + +// ======================================================= +// 1️⃣ EXACT HIGH-SPEED FILE LOGGING LOOP +// ======================================================= +async Task RunLoggingTestsAsync() +{ + for (var i = 0; i < 9_000_000; i++) + { + Log.Information("test to file {i} INFO", i); + Log.Fatal("test to file {i} CRITICAL", i); + Log.Debug("test to file {i} DEBUG", i); + Log.Error("test to file {i} ERROR", i); + Log.Verbose("test to file {i} TRACE", i); + Log.Warning("test to file {i} WARNING", i); + + Console.WriteLine($"Logged: {i}"); + await Task.Delay(1); + } +} + + +// ======================================================= +// 2️⃣ WEB LOGGER STRESS TEST +// ======================================================= +async Task RunWebLoggingTestsAsync() +{ + int i = 0; + + while (true) + { + i++; + + Log.Information("web-test {i}", i); + Log.Debug("web-test {i}", i); + Log.Warning("web-test {i}", i); + Log.Error("web-test {i}", i); + Log.Verbose("web-test {i}", i); + + await Task.Delay(1); + } +} + + +// ======================================================= +// 3️⃣ EXCEPTION TEST (FILE LOGGER) +// ======================================================= +void RunLoggingExceptionTests() +{ + for (int i = 0; i < 10; i++) + { + try + { + throw new Exception($"Normal Exception {i}"); + } + catch (Exception ex) + { + Log.Error(ex, "Exception {Index}", i); + Console.WriteLine($"Normal ExceptionLogged: {i}"); + } + } +} + + +// ======================================================= +// 4️⃣ WEB EXCEPTION TEST +// ======================================================= +void RunWebLoggingExceptionTests() +{ + for (int i = 0; i < 10; i++) + { + try + { + throw new Exception($"WebException {i}"); + } + catch (Exception ex) + { + Log.Fatal(ex, "CRITICAL"); + Log.Debug(ex, "DEBUG"); + Log.Error(ex, "ERROR"); + Log.Verbose(ex, "TRACE"); + Log.Warning(ex, "WARNING"); + Log.Information(ex, "INFORMATION"); + + Console.WriteLine($"WebExceptionLogged: {i}"); + } + } +} + +// ======================================================= +// 6️⃣ MEMORY LEAK TEST (IDENTICAL BEHAVIOR) +// ======================================================= +async Task RunMemoryLeakTest() +{ + var managedLeak = new List(); + + while (true) + { + managedLeak.Add(new byte[5_000_000]); // 5MB + Marshal.AllocHGlobal(10_000_000); // 10MB unmanaged + + await Task.Delay(500); + } +} + + +// ======================================================= +// 7️⃣ TCP LOGGER TEST +// ======================================================= +async Task RunTcpLoggerTest() +{ + using var client = new TcpClient(); + + try + { + await client.ConnectAsync("192.168.1.1", 12345); + + int i = 0; + while (true) + { + var message = Encoding.UTF8.GetBytes($"TCP log {++i}\n"); + await client.GetStream().WriteAsync(message); + await Task.Delay(1000); + } + } + catch + { + Log.Warning("TCP server not reachable"); + } +} diff --git a/EonaCat.Logger.SerilogTest/Properties/launchSettings.json b/EonaCat.Logger.SerilogTest/Properties/launchSettings.json new file mode 100644 index 0000000..5b27d6c --- /dev/null +++ b/EonaCat.Logger.SerilogTest/Properties/launchSettings.json @@ -0,0 +1,12 @@ +{ + "profiles": { + "EonaCat.Logger.SerilogTest": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "https://localhost:56815;http://localhost:56816" + } + } +} \ No newline at end of file diff --git a/EonaCat.Logger.sln b/EonaCat.Logger.sln index c679750..dfbb194 100644 --- a/EonaCat.Logger.sln +++ b/EonaCat.Logger.sln @@ -11,6 +11,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.Logger.LogClient", EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.Logger.Server", "EonaCat.Logger.Server\EonaCat.Logger.Server.csproj", "{E2AC1343-1D08-8FBF-0194-270CB862675F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "EonaCat.Logger.SerilogTest", "EonaCat.Logger.SerilogTest\EonaCat.Logger.SerilogTest.csproj", "{1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -69,6 +71,18 @@ Global {E2AC1343-1D08-8FBF-0194-270CB862675F}.Release|x64.Build.0 = Release|Any CPU {E2AC1343-1D08-8FBF-0194-270CB862675F}.Release|x86.ActiveCfg = Release|Any CPU {E2AC1343-1D08-8FBF-0194-270CB862675F}.Release|x86.Build.0 = Release|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Debug|x64.ActiveCfg = Debug|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Debug|x64.Build.0 = Debug|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Debug|x86.ActiveCfg = Debug|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Debug|x86.Build.0 = Debug|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Release|Any CPU.Build.0 = Release|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Release|x64.ActiveCfg = Release|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Release|x64.Build.0 = Release|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Release|x86.ActiveCfg = Release|Any CPU + {1EAB9752-2FDF-49CC-A9E3-7591375B4BF6}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj index 95f005c..d564952 100644 --- a/EonaCat.Logger/EonaCat.Logger.csproj +++ b/EonaCat.Logger/EonaCat.Logger.csproj @@ -13,8 +13,8 @@ EonaCat (Jeroen Saey) EonaCat;Logger;EonaCatLogger;Log;Writer;Jeroen;Saey - 1.7.3 - 1.7.3 + 1.7.4 + 1.7.4 README.md True LICENSE @@ -25,7 +25,7 @@ - 1.7.3+{chash:10}.{c:ymd} + 1.7.4+{chash:10}.{c:ymd} true true v[0-9]* diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs index 08b0998..92d165f 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs @@ -111,44 +111,6 @@ public class FileLoggerOptions : BatchingLoggerOptions /// public string LogDirectory { get; set; } = Path.Combine(DefaultPath, "logs"); - /// - /// Determines if we need to mask certain keywords - /// - public bool UseMask { get; set; } - - /// - /// Determines the keywords to mask - /// - public List MaskedKeywords { get; set; } = new List(); - public string Mask { get; set; } = "***MASKED***"; - - /// - /// Determines that if masking is enabled we also need to use the default masking options: - /// IP addresses - /// MAC addresses - /// Emails - /// Passwords - /// Credit card numbers - /// Social security numbers (SSN) and BSN (Dutch Citizen Service Number) - /// API keys/tokens - /// Phone numbers (generic and Dutch specific) - /// Dates of birth (DOB) or other date formats - /// IBAN/Bank account numbers (generic and Dutch specific) - /// JWT tokens - /// URLs with sensitive query strings - /// License keys - /// Public and private keys (e.g., PEM format) - /// Dutch KVK number (8 or 12 digits) - /// Dutch BTW-nummer (VAT number) - /// Dutch driving license number (10-12 characters) - /// Dutch health insurance number (Zorgnummer) - /// Other Dutch Bank Account numbers (9-10 digits) - /// Dutch Passport Numbers (9 alphanumeric characters - /// Dutch Identification Document Numbers (varying formats) - /// Custom keywords specified in LoggerSettings - /// - public bool UseDefaultMasking { get; set; } = true; - /// /// Determines if we need to include the correlation ID in the log (default: false) /// diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs index 9bed913..e899156 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs @@ -10,10 +10,11 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.IO.Compression; -using System.Linq; +using System.Runtime.CompilerServices; using System.Security.Cryptography; using System.Text; using System.Threading; +using System.Threading.Channels; using System.Threading.Tasks; [ProviderAlias("EonaCatFileLogger")] @@ -29,149 +30,56 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable private readonly byte[] _encryptionIV; private readonly bool _isEncryptionEnabled; - private const int MaxQueueSize = 50_000; - private const int MaxCategories = 256; - private const int MaxStringBuilderPoolSize = 32; // Limit pool size - private const int MaxCompressionQueueSize = 100; // Limit compression queue + private const int MaxQueueSize = 500; + private const int BufferSize = 16 * 1024; + private const int MaxStringBuilderPool = 4; + private const int MaxStringBuilderCapacity = 512; + private const int InitialStringBuilderCapacity = 256; + private const int MaxSanitizedCacheSize = 256; - public bool IsEncryptionEnabled => _encryptionKey != null && _encryptionIV != null; + private static readonly Encoding Utf8 = new UTF8Encoding(false); + + private readonly LoggerScopedContext _context = new(); + + private readonly ConcurrentDictionary _files = new(1, 4, StringComparer.Ordinal); + private readonly ConcurrentQueue _compressionQueue = new(); + private readonly SemaphoreSlim _compressionSemaphore = new(1, 1); + private readonly CancellationTokenSource _compressionCts = new(); + private Task _compressionWorker; + + private static readonly object _sanitizedCacheLock = new object(); + private static readonly Dictionary _sanitizedCache = new(StringComparer.Ordinal); + private static readonly Queue _sanitizedCacheOrder = new(); + + private readonly Channel _channel; + private readonly Task _backgroundWorker; + + private readonly ConcurrentBag _sbPool = new(); + private int _sbPoolCount; + + private Timer _flushTimer; + private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(500); private int _disposed; - private int _isFlushing; + private Aes _aes; - public static TimeSpan FaultCooldown = TimeSpan.FromSeconds(60); - - private readonly ConcurrentQueue _compressionQueue = new ConcurrentQueue(); - private readonly SemaphoreSlim _compressionSemaphore = new SemaphoreSlim(1, 1); - private Task _compressionWorker; - private readonly CancellationTokenSource _compressionCts = new CancellationTokenSource(); - - private readonly LoggerScopedContext _context = new LoggerScopedContext(); - private readonly ConcurrentDictionary _files = new ConcurrentDictionary(); - - private sealed class MessageQueue - { - public readonly ConcurrentQueue Queue = new ConcurrentQueue(); - public int Count; - - public bool TryEnqueue(LogMessage msg, int maxSize) - { - // Check before incrementing - int currentCount = Volatile.Read(ref Count); - if (currentCount >= maxSize) - { - return false; - } - - Queue.Enqueue(msg); - Interlocked.Increment(ref Count); - return true; - } - - public bool TryDequeue(out LogMessage msg) - { - if (Queue.TryDequeue(out msg)) - { - Interlocked.Decrement(ref Count); - return true; - } - - msg = default(LogMessage); - return false; - } - - public void Clear() - { - while (Queue.TryDequeue(out _)) - { - Interlocked.Decrement(ref Count); - } - } - } - - private readonly ConcurrentDictionary _messageQueues = new ConcurrentDictionary(); - - private const int BufferSize = 256 * 1024; // 256 KB - private static readonly Encoding Utf8 = new UTF8Encoding(false); + private readonly ThreadLocal _encryptor = new ThreadLocal(() => null, trackAllValues: true); + private readonly object _aesLock = new object(); public bool IncludeCorrelationId { get; } public bool EnableCategoryRouting { get; } - - public string LogFile => _files.TryGetValue(string.Empty, out var s) ? s.FilePath : null; - public ELogType MinimumLogLevel { get; set; } public event EventHandler OnError; public event EventHandler OnRollOver; - private Timer _flushTimer; - private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(100); - private readonly string _fallbackPath; - private Aes _aes; - - // Bounded StringBuilder pool with size limit - private readonly ConcurrentBag _stringBuilderPool = new ConcurrentBag(); - private int _stringBuilderPoolCount; - - private sealed class FileState : IDisposable - { - public string FilePath; - public long Size; - public DateTime Date; - - public byte[] Buffer; - public int BufferPosition; - - public FileStream Stream; - public SemaphoreSlim WriteLock = new SemaphoreSlim(1, 1); - - public bool IsFaulted; - public DateTime LastFailureUtc; - private int _disposed; - - public void Dispose() - { - if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) - { - return; // Already disposed - } - - try - { - if (Buffer != null) - { - ArrayPool.Shared.Return(Buffer, clearArray: true); - Buffer = null; - } - - if (Stream != null) - { - try - { - Stream.Flush(); - } - catch { } - Stream.Dispose(); - Stream = null; - } - - if (WriteLock != null) - { - WriteLock.Dispose(); - WriteLock = null; - } - } - catch { } - } - } + public string LogFile => _files.TryGetValue(string.Empty, out var state) ? state.FilePath : null; public FileLoggerProvider(IOptions options) : base(options) { var o = options.Value ?? throw new ArgumentNullException(nameof(options)); _path = EnsureWritableDirectory(o.LogDirectory); - _fallbackPath = EnsureWritableDirectory(Path.Combine(Path.GetTempPath(), "EonaCatFallbackLogs")); - _fileNamePrefix = o.FileNamePrefix; _maxFileSize = o.FileSizeLimit; _maxRetainedFiles = o.RetainedFileCountLimit; @@ -191,18 +99,26 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable _aes.IV = _encryptionIV; } - var defaultState = CreateFileState(DateTime.UtcNow.Date, o.Category); + var defaultState = CreateFileState(DateTime.UtcNow.Date, string.Empty); _files[string.Empty] = defaultState; - _flushTimer = new Timer(FlushTimerCallback, null, _flushInterval, _flushInterval); + var channelOptions = new BoundedChannelOptions(MaxQueueSize) + { + SingleReader = true, + SingleWriter = false, + FullMode = BoundedChannelFullMode.Wait, + AllowSynchronousContinuations = false + }; + _channel = Channel.CreateBounded(channelOptions); + _backgroundWorker = Task.Run(ProcessQueueAsync); + _flushTimer = new Timer(FlushTimerCallback, null, _flushInterval, _flushInterval); _compressionWorker = Task.Run(() => CompressionWorkerAsync(_compressionCts.Token)); } private string EnsureWritableDirectory(string path) { string fallback = Path.Combine(Path.GetTempPath(), "EonaCatFallbackLogs"); - foreach (var dir in new[] { path, fallback }) { try @@ -215,7 +131,6 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable } catch { } } - Directory.CreateDirectory(fallback); return fallback; } @@ -227,562 +142,129 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable return; } - // Non-blocking check - skip if already flushing - if (Interlocked.CompareExchange(ref _isFlushing, 1, 0) != 0) + // Synchronous flush on timer to avoid task buildup + foreach (var f in _files.Values) { - return; + FlushBufferSync(f); } - - // Fire-and-forget with proper error handling - _ = Task.Run(async () => - { - try - { - await PeriodicFlushAsync().ConfigureAwait(false); - } - catch (Exception ex) - { - OnError?.Invoke(this, new ErrorMessage - { - Exception = ex, - Message = "Error during periodic flush" - }); - } - finally - { - Interlocked.Exchange(ref _isFlushing, 0); - } - }); } - internal override Task WriteMessagesAsync( - IReadOnlyList messages, - CancellationToken token) + internal override Task WriteMessagesAsync(IReadOnlyList messages, CancellationToken token) { - if (_disposed == 1) + for (int i = 0; i < messages.Count; i++) { - return Task.CompletedTask; - } - - foreach (var msg in messages) - { - if (msg.Level < MinimumLogLevel) + var msg = messages[i]; + if (msg.Level >= MinimumLogLevel) { - continue; - } - - var key = EnableCategoryRouting - ? SanitizeCategory(msg.Category) - : string.Empty; - - // Limit total categories to prevent unbounded growth - if (_messageQueues.Count >= MaxCategories && !_messageQueues.ContainsKey(key)) - { - // Use default queue if we've hit category limit - key = string.Empty; - } - - var mq = _messageQueues.GetOrAdd(key, _ => new MessageQueue()); - - // Use atomic enqueue with size check - if (!mq.TryEnqueue(msg, MaxQueueSize)) - { - // Queue full - drop oldest message - if (mq.TryDequeue(out _)) - { - mq.TryEnqueue(msg, MaxQueueSize); - } + // TryWrite waits until it can write + _channel.Writer.TryWrite(msg); } } - return Task.CompletedTask; } - private async Task PeriodicFlushAsync() - { - if (_disposed == 1) - { - return; - } - - var keysToRemove = new List(); - - foreach (var kv in _messageQueues) - { - if (_disposed == 1) - { - break; - } - - var key = kv.Key; - var mq = kv.Value; - - if (!_files.TryGetValue(key, out var state)) - { - state = CreateFileState(DateTime.UtcNow.Date, key); - _files[key] = state; - } - - if (!TryRecover(state)) - { - // Drain queue on unrecoverable failure - mq.Clear(); - continue; - } - - if (mq.Count == 0) - { - continue; - } - - // Non-blocking lock attempt with immediate bailout - if (!state.WriteLock.Wait(0)) - { - continue; - } - - try - { - await FlushMessagesBatchAsync(state, mq).ConfigureAwait(false); - - if (state.BufferPosition > 0) - { - await FlushBufferAsync(state).ConfigureAwait(false); - } - - // Mark for cleanup if empty - if (mq.Count == 0 && !string.IsNullOrEmpty(key)) - { - keysToRemove.Add(key); - } - } - catch (Exception ex) - { - OnError?.Invoke(this, new ErrorMessage - { - Exception = ex, - Message = $"Error flushing messages for category: {key}" - }); - } - finally - { - state.WriteLock.Release(); - } - } - - // Cleanup empty categories outside the iteration - foreach (var key in keysToRemove) - { - if (_messageQueues.TryRemove(key, out var removed)) - { - removed.Clear(); - } - - if (_files.TryRemove(key, out var removedState)) - { - removedState.Dispose(); - } - } - - QueueOldFilesForCompression(); - } - - private async Task FlushMessagesBatchAsync(FileState state, MessageQueue queue) - { - const int MaxBatch = 5000; - int batchCount = 0; - - // Rent buffer - int estimatedSize = 64 * 1024; // 64 KB - byte[] combined = ArrayPool.Shared.Rent(estimatedSize); - int pos = 0; - - try - { - while (queue.TryDequeue(out var msg) && batchCount < MaxBatch) - { - var msgDate = msg.Timestamp.UtcDateTime.Date; - - // Rotate if date changed - if (state.Date != msgDate) - { - if (pos > 0) - { - await WriteBufferedData(state, combined, pos).ConfigureAwait(false); - pos = 0; - } - await FlushBufferAsync(state).ConfigureAwait(false); - RotateByDate(state, msgDate, string.Empty); - } - - var messageString = BuildMessage(msg); - int requiredLength = Utf8.GetByteCount(messageString); - - // Grow buffer if needed - if (pos + requiredLength > combined.Length) - { - // Write what we have first - if (pos > 0) - { - await WriteBufferedData(state, combined, pos).ConfigureAwait(false); - pos = 0; - } - - // If single message is huge, rent bigger buffer - if (requiredLength > combined.Length) - { - ArrayPool.Shared.Return(combined, clearArray: true); - combined = ArrayPool.Shared.Rent(requiredLength + 1024); - } - } - - pos += Utf8.GetBytes(messageString, 0, messageString.Length, combined, pos); - batchCount++; - } - - if (pos > 0) - { - await WriteBufferedData(state, combined, pos).ConfigureAwait(false); - } - } - finally - { - ArrayPool.Shared.Return(combined, clearArray: true); - } - } - - private async Task WriteBufferedData(FileState state, byte[] data, int length) - { - byte[] dataToWrite = data; - int dataLength = length; - byte[] encryptedData = null; - - try - { - if (_isEncryptionEnabled) - { - // Encrypt - this creates a new array - encryptedData = Encrypt(data, length); - dataToWrite = encryptedData; - dataLength = encryptedData.Length; - } - - // Flush buffer if needed - if (state.BufferPosition + dataLength > BufferSize) - { - await FlushBufferAsync(state).ConfigureAwait(false); - } - - // Rollover check - if (_maxFileSize > 0 && state.Size + dataLength > _maxFileSize) - { - await FlushBufferAsync(state).ConfigureAwait(false); - RollOverAndCompressOldest(state, string.Empty); - } - - // Ensure buffer exists - if (state.Buffer == null) - { - state.Buffer = ArrayPool.Shared.Rent(BufferSize); - } - - // Copy to file buffer - Array.Copy(dataToWrite, 0, state.Buffer, state.BufferPosition, dataLength); - state.BufferPosition += dataLength; - state.Size += dataLength; - } - finally - { - // Clear and return encrypted data - if (encryptedData != null) - { - Array.Clear(encryptedData, 0, encryptedData.Length); - } - } - } - - private async Task FlushBufferAsync(FileState state, CancellationToken token = default(CancellationToken)) - { - if (state.IsFaulted || state.BufferPosition == 0 || state.Stream == null) - { - return; - } - - try - { - await state.Stream.WriteAsync(state.Buffer, 0, state.BufferPosition, token).ConfigureAwait(false); - await state.Stream.FlushAsync(token).ConfigureAwait(false); - } - catch (Exception ex) - { - HandleWriteFailure(state, ex); - } - finally - { - // Clear buffer to prevent sensitive data leak - if (state.Buffer != null) - { - Array.Clear(state.Buffer, 0, state.BufferPosition); - } - state.BufferPosition = 0; - } - } - - private void QueueOldFilesForCompression() - { - if (_maxRetainedFiles <= 0 || _disposed == 1) - { - return; - } - - try - { - var dirInfo = new DirectoryInfo(_path); - if (!dirInfo.Exists) - { - return; - } - - var files = dirInfo.GetFiles($"{_fileNamePrefix}*") - .Where(f => !f.Name.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(f => f.LastWriteTimeUtc) - .Skip(_maxRetainedFiles) - .ToList(); - - foreach (var f in files) - { - EnqueueCompression(f.FullName); - } - } - catch - { - // Ignore errors in cleanup - } - } - - private void EnqueueCompression(string file) - { - // Limit compression queue size to prevent unbounded growth - if (_compressionQueue.Count >= MaxCompressionQueueSize) - { - return; - } - - _compressionQueue.Enqueue(file); - } - - private async Task CompressionWorkerAsync(CancellationToken cancellationToken) + private async Task ProcessQueueAsync() { try { - while (!cancellationToken.IsCancellationRequested) + await foreach (var msg in _channel.Reader.ReadAllAsync()) { - try - { - // Wait for work with cancellation - await Task.Delay(TimeSpan.FromSeconds(1), cancellationToken).ConfigureAwait(false); - } - catch (OperationCanceledException) - { - break; - } - - // Process all queued files - while (_compressionQueue.TryDequeue(out var filePath)) - { - if (cancellationToken.IsCancellationRequested) - { - break; - } - - await _compressionSemaphore.WaitAsync(cancellationToken).ConfigureAwait(false); - try - { - await CompressOldLogFileAsync(filePath).ConfigureAwait(false); - } - catch (Exception ex) - { - OnError?.Invoke(this, new ErrorMessage - { - Exception = ex, - Message = $"Compression worker error: {filePath}" - }); - } - finally - { - _compressionSemaphore.Release(); - } - } + await ProcessSingleMessageAsync(msg); } } catch (OperationCanceledException) { - // Normal shutdown + // Expected on shutdown } } - private async Task CompressOldLogFileAsync(string filePath, int retryCount = 3) + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private async Task ProcessSingleMessageAsync(LogMessage msg) { - if (!File.Exists(filePath) || filePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) + var key = EnableCategoryRouting ? GetOrCreateSanitizedCategory(msg.Category) : string.Empty; + + if (!_files.TryGetValue(key, out var state)) + { + state = CreateFileState(DateTime.UtcNow.Date, key); + _files[key] = state; + } + + if (!TryRecover(state)) { return; } - var dir = Path.GetDirectoryName(filePath); - var name = Path.GetFileNameWithoutExtension(filePath); - var ext = Path.GetExtension(filePath); - - // Determine the next _N.log.gz name - int suffix = 1; - string compressedFile; - do + await state.WriteLock.WaitAsync(); + try { - compressedFile = Path.Combine(dir, $"{name}_{suffix}{ext}.gz"); - suffix++; - } while (File.Exists(compressedFile) && suffix < 1000); // Prevent infinite loop - - for (int attempt = 0; attempt < retryCount; attempt++) + await WriteMessageAsync(state, msg); + } + finally { - FileStream original = null; - FileStream compressed = null; - GZipStream gzip = null; + state.WriteLock.Release(); + } + } + + private async Task WriteMessageAsync(FileState state, LogMessage msg) + { + var sb = RentStringBuilder(); + + try + { + BuildMessageInto(sb, msg); + var text = sb.ToString(); + + // Size the array precisely + int maxByteCount = Utf8.GetMaxByteCount(text.Length); + byte[] rentedBytes = ArrayPool.Shared.Rent(maxByteCount); try { - original = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); - compressed = new FileStream(compressedFile, FileMode.CreateNew, FileAccess.Write); - gzip = new GZipStream(compressed, CompressionLevel.Optimal); + int actualByteCount = Utf8.GetBytes(text, 0, text.Length, rentedBytes, 0); - await original.CopyToAsync(gzip).ConfigureAwait(false); - await gzip.FlushAsync().ConfigureAwait(false); + byte[] finalBytes = rentedBytes; + int finalLength = actualByteCount; - gzip.Dispose(); - gzip = null; - compressed.Dispose(); - compressed = null; - original.Dispose(); - original = null; - - File.Delete(filePath); - return; // success - } - catch (IOException) - { - // File busy? Wait a bit and retry - await Task.Delay(200).ConfigureAwait(false); - } - catch (Exception ex) - { - OnError?.Invoke(this, new ErrorMessage + if (_isEncryptionEnabled) { - Exception = ex, - Message = $"Failed to compress log file: {filePath}" - }); - return; + byte[] encrypted = EncryptFast(rentedBytes, actualByteCount); + finalBytes = encrypted; + finalLength = encrypted.Length; + ArrayPool.Shared.Return(rentedBytes, clearArray: true); + rentedBytes = null; + } + + if (state.Buffer == null) + { + state.Buffer = ArrayPool.Shared.Rent(BufferSize); + } + + // Flush if needed + if (state.BufferPosition + finalLength > BufferSize) + { + await FlushBufferAsync(state).ConfigureAwait(false); + } + + // Handle file size limit + if (_maxFileSize > 0 && state.Size + finalLength > _maxFileSize) + { + await FlushBufferAsync(state).ConfigureAwait(false); + RollOverAndCompressOldest(state, string.Empty); + } + + // Write to buffer + Buffer.BlockCopy(finalBytes, 0, state.Buffer, state.BufferPosition, finalLength); + state.BufferPosition += finalLength; + state.Size += finalLength; } finally { - if (gzip != null) gzip.Dispose(); - if (compressed != null) compressed.Dispose(); - if (original != null) original.Dispose(); - } - } - } - - private byte[] Encrypt(byte[] plainBytes, int length) - { - if (plainBytes == null || length == 0) - { - return Array.Empty(); - } - - using (var encryptor = _aes.CreateEncryptor()) - { - return encryptor.TransformFinalBlock(plainBytes, 0, length); - } - } - - public byte[] Decrypt(byte[] encryptedData) - { - if (!IsEncryptionEnabled || encryptedData == null || encryptedData.Length == 0) - { - return encryptedData; - } - - using (var decryptor = _aes.CreateDecryptor()) - using (var ms = new MemoryStream(encryptedData)) - using (var cryptoStream = new CryptoStream(ms, decryptor, CryptoStreamMode.Read)) - using (var resultStream = new MemoryStream()) - { - cryptoStream.CopyTo(resultStream); - return resultStream.ToArray(); - } - } - - private StringBuilder RentStringBuilder() - { - if (_stringBuilderPool.TryTake(out var sb)) - { - sb.Clear(); - return sb; - } - return new StringBuilder(512); - } - - private void ReturnStringBuilder(StringBuilder sb) - { - // Limit pool size and builder capacity to prevent unbounded growth - if (sb.Capacity < 8192 && _stringBuilderPoolCount < MaxStringBuilderPoolSize) - { - if (Interlocked.Increment(ref _stringBuilderPoolCount) <= MaxStringBuilderPoolSize) - { - _stringBuilderPool.Add(sb); - } - else - { - Interlocked.Decrement(ref _stringBuilderPoolCount); - } - } - } - - private string BuildMessage(LogMessage msg) - { - if (!IncludeCorrelationId) - { - return msg.Message + Environment.NewLine; - } - - var ctx = _context.GetAll(); - if (ctx.Count == 0 && (msg.Tags == null || !msg.Tags.Any())) - { - return msg.Message + Environment.NewLine; - } - - var sb = RentStringBuilder(); - try - { - sb.Append(msg.Message).Append(" ["); - - foreach (var kv in ctx) - { - sb.Append(kv.Key).Append('=').Append(kv.Value).Append(' '); - } - - if (msg.Tags != null) - { - foreach (var tag in msg.Tags) + if (rentedBytes != null) { - sb.Append("tag=").Append(tag).Append(' '); + ArrayPool.Shared.Return(rentedBytes, clearArray: true); } } - if (sb.Length > 0 && sb[sb.Length - 1] == ' ') - { - sb.Length--; // remove trailing space - } - - sb.Append(']').AppendLine(); - return sb.ToString(); } finally { @@ -790,52 +272,162 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable } } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private byte[] EncryptFast(byte[] plainBytes, int length) + { + var encryptor = _encryptor.Value; + if (encryptor == null) + { + lock (_aesLock) + { + encryptor = _aes?.CreateEncryptor(); + _encryptor.Value = encryptor; + } + } + return encryptor.TransformFinalBlock(plainBytes, 0, length); + } + + private void FlushBufferSync(FileState state) + { + if (state.BufferPosition == 0 || state.Stream == null) + { + return; + } + + try + { + state.Stream.Write(state.Buffer, 0, state.BufferPosition); + state.Stream.Flush(); + } + catch (Exception ex) + { + HandleWriteFailure(state, ex); + } + finally + { + state.BufferPosition = 0; + } + } + + private async Task FlushBufferAsync(FileState state) + { + if (state.BufferPosition == 0 || state.Stream == null) + { + return; + } + + try + { + await state.Stream.WriteAsync(state.Buffer, 0, state.BufferPosition).ConfigureAwait(false); + await state.Stream.FlushAsync().ConfigureAwait(false); + } + catch (Exception ex) + { + HandleWriteFailure(state, ex); + } + finally + { + state.BufferPosition = 0; + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private StringBuilder RentStringBuilder() + { + if (_sbPool.TryTake(out var sb)) + { + sb.Clear(); + Interlocked.Decrement(ref _sbPoolCount); + return sb; + } + return new StringBuilder(InitialStringBuilderCapacity); + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void ReturnStringBuilder(StringBuilder sb) + { + if (sb.Capacity > MaxStringBuilderCapacity) + { + return; // Don't pool oversized builders + } + + if (_sbPoolCount < MaxStringBuilderPool) + { + _sbPool.Add(sb); + Interlocked.Increment(ref _sbPoolCount); + } + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private void BuildMessageInto(StringBuilder sb, LogMessage msg) + { + sb.Clear(); + sb.Append(msg.Message); + + if (IncludeCorrelationId) + { + var ctx = _context.GetAll(); + if (ctx.Count > 0 || (msg.Tags?.Length ?? 0) > 0) + { + sb.Append(" ["); + + foreach (var kv in ctx) + { + sb.Append(kv.Key); + sb.Append('='); + sb.Append(kv.Value); + sb.Append(' '); + } + + if (msg.Tags != null) + { + for (int i = 0; i < msg.Tags.Length; i++) + { + sb.Append("tag="); + sb.Append(msg.Tags[i]); + sb.Append(' '); + } + } + + if (sb[sb.Length - 1] == ' ') + { + sb.Length--; + } + + sb.Append(']'); + } + } + + sb.Append(Environment.NewLine); + } + private void HandleWriteFailure(FileState state, Exception ex) { state.IsFaulted = true; state.LastFailureUtc = DateTime.UtcNow; - if (state.Stream != null) - { - try - { - state.Stream.Dispose(); - } - catch { } - state.Stream = null; - } - - string originalDir = Path.GetDirectoryName(state.FilePath); - string fallbackDir = Directory.Exists(_fallbackPath) ? _fallbackPath : EnsureWritableDirectory(originalDir); - string fileName = Path.GetFileName(state.FilePath); - string fallbackFile = Path.Combine(fallbackDir, fileName); + state.Stream?.Dispose(); + state.Stream = null; try { - state.FilePath = fallbackFile; + string fallbackFile = Path.Combine(_path, Path.GetFileName(state.FilePath)); state.Stream = new FileStream( fallbackFile, FileMode.Append, FileAccess.Write, - FileShare.ReadWrite | FileShare.Delete - ); - + FileShare.ReadWrite | FileShare.Delete, + 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan); + state.FilePath = fallbackFile; state.Size = GetFileSize(fallbackFile); state.IsFaulted = false; - OnError?.Invoke(this, new ErrorMessage - { - Exception = ex, - Message = $"Logging failed for original path. Switching to fallback path: {fallbackFile}" - }); + OnError?.Invoke(this, new ErrorMessage { Exception = ex, Message = $"Switched to fallback path: {fallbackFile}" }); } catch (Exception fallbackEx) { - OnError?.Invoke(this, new ErrorMessage - { - Exception = fallbackEx, - Message = $"Failed to recover logging using fallback path: {fallbackFile}" - }); + OnError?.Invoke(this, new ErrorMessage { Exception = fallbackEx, Message = "Failed to recover logging using fallback path" }); } } @@ -846,57 +438,33 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable return true; } - if (DateTime.UtcNow - state.LastFailureUtc < FaultCooldown) + if (DateTime.UtcNow - state.LastFailureUtc < TimeSpan.FromSeconds(60)) { return false; } try { - state.Stream = new FileStream(state.FilePath, FileMode.Append, - FileAccess.Write, FileShare.ReadWrite | FileShare.Delete); - + state.Stream = new FileStream( + state.FilePath, + FileMode.Append, + FileAccess.Write, + FileShare.ReadWrite | FileShare.Delete, + 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan); state.Size = GetFileSize(state.FilePath); state.IsFaulted = false; - return true; } - catch - { - // Attempt fallback path if recovery fails - try - { - string fallbackFile = Path.Combine(_fallbackPath, Path.GetFileName(state.FilePath)); - state.Stream = new FileStream(fallbackFile, FileMode.Append, - FileAccess.Write, FileShare.ReadWrite | FileShare.Delete); - state.FilePath = fallbackFile; - state.Size = GetFileSize(fallbackFile); - state.IsFaulted = false; - - OnError?.Invoke(this, new ErrorMessage - { - Message = $"Switched to fallback path: {fallbackFile}" - }); - - return true; - } - catch - { - state.LastFailureUtc = DateTime.UtcNow; - return false; - } - } + catch { return false; } } private FileState CreateFileState(DateTime date, string category) { - var intendedPath = GetFullName(date, category); - var writableDir = EnsureWritableDirectory(Path.GetDirectoryName(intendedPath)); - var path = Path.Combine(writableDir, Path.GetFileName(intendedPath)); - + var path = GetFullName(date, category); try { - var state = new FileState + return new FileState { FilePath = path, Date = date, @@ -906,71 +474,100 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable path, FileMode.Append, FileAccess.Write, - FileShare.ReadWrite | FileShare.Delete - ) + FileShare.ReadWrite | FileShare.Delete, + 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan), + WriteLock = new SemaphoreSlim(1, 1) }; - return state; } catch (Exception ex) { - OnError?.Invoke(this, new ErrorMessage - { - Exception = ex, - Message = $"Failed to create log file: {path}" - }); - + OnError?.Invoke(this, new ErrorMessage { Exception = ex, Message = $"Failed to create log file: {path}" }); return new FileState { FilePath = path, Date = date, + Buffer = ArrayPool.Shared.Rent(BufferSize), IsFaulted = true, - Buffer = ArrayPool.Shared.Rent(BufferSize) + WriteLock = new SemaphoreSlim(1, 1) }; } } - private void RotateByDate(FileState state, DateTime newDate, string category) + private string GetFullName(DateTime date, string category) { - if (state.Stream != null) + var datePart = date.ToString("yyyyMMdd"); + var machine = Environment.MachineName; + + if (!EnableCategoryRouting || string.IsNullOrWhiteSpace(category)) { - try + return Path.Combine(_path, $"{_fileNamePrefix}_{machine}_{datePart}.log"); + } + + var safeCategory = GetOrCreateSanitizedCategory(category); + return Path.Combine(_path, $"{_fileNamePrefix}_{machine}_{safeCategory}_{datePart}.log"); + } + + private static string GetOrCreateSanitizedCategory(string category) + { + if (string.IsNullOrEmpty(category)) + { + return string.Empty; + } + + lock (_sanitizedCacheLock) + { + if (_sanitizedCache.TryGetValue(category, out var cached)) { - state.Stream.Flush(); + return cached; } - catch { } - state.Stream.Dispose(); - state.Stream = null; - } - state.Date = newDate; - state.FilePath = GetFullName(newDate, category); - state.Size = GetFileSize(state.FilePath); + var sanitized = SanitizeCategory(category); - try - { - state.Stream = new FileStream(state.FilePath, FileMode.Append, - FileAccess.Write, FileShare.ReadWrite | FileShare.Delete); - } - catch (Exception ex) - { - state.IsFaulted = true; - OnError?.Invoke(this, new ErrorMessage + // Simple LRU: evict oldest if cache exceeds max size + if (_sanitizedCache.Count >= MaxSanitizedCacheSize) { - Exception = ex, - Message = $"Failed to rotate log file: {state.FilePath}" - }); + var oldestKey = _sanitizedCacheOrder.Dequeue(); + _sanitizedCache.Remove(oldestKey); + } + + _sanitizedCache[category] = sanitized; + _sanitizedCacheOrder.Enqueue(category); + return sanitized; } } + private static string SanitizeCategory(string category) + { + if (string.IsNullOrEmpty(category)) + { + return category; + } + + var chars = category.ToCharArray(); + var invalid = Path.GetInvalidFileNameChars(); + bool modified = false; + + for (int i = 0; i < chars.Length; i++) + { + if (Array.IndexOf(invalid, chars[i]) >= 0 || chars[i] == '.') + { + chars[i] = '_'; + modified = true; + } + } + + return modified ? new string(chars) : category; + } + + [MethodImpl(MethodImplOptions.AggressiveInlining)] + private static long GetFileSize(string path) => File.Exists(path) ? new FileInfo(path).Length : 0; + private void RollOverAndCompressOldest(FileState state, string category) { if (state.Stream != null) { - try - { - state.Stream.Flush(); - } - catch { } + state.Stream.Flush(); state.Stream.Dispose(); state.Stream = null; } @@ -979,7 +576,6 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable var name = Path.GetFileNameWithoutExtension(state.FilePath); var ext = Path.GetExtension(state.FilePath); - // Shift existing rolled files up for (int i = _maxRolloverFiles - 1; i >= 1; i--) { var src = Path.Combine(dir, $"{name}_{i}{ext}"); @@ -998,11 +594,10 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable } catch { - // Continue on error + // Ignore rollover failures } } - // Move current file to _1 var rolledFile = Path.Combine(dir, $"{name}_1{ext}"); try { @@ -1015,158 +610,120 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable } catch { - // Continue on error + // Ignore rollover failures } - // Create new active log file state.Size = 0; - try - { - state.Stream = new FileStream(state.FilePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete); - } - catch (Exception ex) - { - state.IsFaulted = true; - OnError?.Invoke(this, new ErrorMessage - { - Exception = ex, - Message = $"Failed to create new log file after rollover: {state.FilePath}" - }); - } + state.Stream = new FileStream( + state.FilePath, + FileMode.Create, + FileAccess.Write, + FileShare.ReadWrite | FileShare.Delete, + 4096, + FileOptions.Asynchronous | FileOptions.SequentialScan); - // Compress the oldest file safely var oldestFile = Path.Combine(dir, $"{name}_{_maxRolloverFiles}{ext}"); if (File.Exists(oldestFile)) { - EnqueueCompression(oldestFile); + _compressionQueue.Enqueue(oldestFile); } } - private static long GetFileSize(string path) - => File.Exists(path) ? new FileInfo(path).Length : 0; - - private string GetFullName(DateTime date, string category) + private async Task CompressionWorkerAsync(CancellationToken token) { - var datePart = date.ToString("yyyyMMdd"); - var machine = Environment.MachineName; - - if (!EnableCategoryRouting || string.IsNullOrWhiteSpace(category)) + while (!token.IsCancellationRequested) { - return Path.Combine(_path, $"{_fileNamePrefix}_{machine}_{datePart}.log"); - } - - var safeCategory = SanitizeCategory(category); - return Path.Combine(_path, $"{_fileNamePrefix}_{machine}_{safeCategory}_{datePart}.log"); - } - - private static string SanitizeCategory(string category) - { - if (string.IsNullOrEmpty(category)) - { - return category; - } - - var chars = category.ToCharArray(); - var invalidChars = Path.GetInvalidFileNameChars(); - - for (int i = 0; i < chars.Length; i++) - { - if (Array.IndexOf(invalidChars, chars[i]) >= 0 || chars[i] == '.') + try { - chars[i] = '_'; + await Task.Delay(1000, token).ConfigureAwait(false); + + while (_compressionQueue.TryDequeue(out var file)) + { + await _compressionSemaphore.WaitAsync(token); + try + { + await CompressOldLogFileAsync(file); + } + catch + { + // Silently fail compression + } + finally + { + _compressionSemaphore.Release(); + } + } + } + catch (OperationCanceledException) + { + break; } } + } - return new string(chars); + private async Task CompressOldLogFileAsync(string filePath) + { + if (!File.Exists(filePath) || filePath.EndsWith(".gz")) + { + return; + } + + var compressedFile = filePath + ".gz"; + + try + { + using (var original = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 8192, FileOptions.Asynchronous | FileOptions.SequentialScan)) + using (var compressed = new FileStream(compressedFile, FileMode.Create, FileAccess.Write, FileShare.None, 8192, FileOptions.Asynchronous | FileOptions.SequentialScan)) + using (var gzip = new GZipStream(compressed, CompressionLevel.Fastest)) // Fastest compression for lower memory usage + { + await original.CopyToAsync(gzip, 8192).ConfigureAwait(false); + } + + File.Delete(filePath); + } + catch + { + // Do nothing + } } protected override async Task OnShutdownFlushAsync() { if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) { - return; // Already disposed + return; } - // Stop timer - if (_flushTimer != null) + _flushTimer?.Dispose(); + _flushTimer = null; + + try { _compressionCts.Cancel(); } catch { } + _channel.Writer.Complete(); + + try { - _flushTimer.Dispose(); - _flushTimer = null; + await _backgroundWorker.ConfigureAwait(false); } - - // Stop compression worker - _compressionCts.Cancel(); - - using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5))) + catch { - try - { - await PeriodicFlushAsync().ConfigureAwait(false); - } - catch - { - // Ignore errors during shutdown - } - - foreach (var state in _files.Values) - { - try - { - await FlushBufferAsync(state, cts.Token).ConfigureAwait(false); - } - catch - { - // Ignore errors during shutdown - } - - state.Dispose(); - } - - _files.Clear(); - - // Clear all message queues - foreach (var mq in _messageQueues.Values) - { - mq.Clear(); - } - _messageQueues.Clear(); - - // Wait for compression worker to finish with timeout - try - { - if (_compressionWorker != null && !_compressionWorker.IsCompleted) - { - await Task.WhenAny(_compressionWorker, Task.Delay(TimeSpan.FromSeconds(2))).ConfigureAwait(false); - } - } - catch - { - // Ignore errors - } - - // Dispose resources - if (_compressionSemaphore != null) - { - _compressionSemaphore.Dispose(); - } - - if (_compressionCts != null) - { - _compressionCts.Dispose(); - } - - if (_aes != null) - { - _aes.Dispose(); - _aes = null; - } - - // Clear StringBuilder pool - while (_stringBuilderPool.TryTake(out _)) - { - Interlocked.Decrement(ref _stringBuilderPoolCount); - } + // Ignore } + + foreach (var state in _files.Values) + { + try { await FlushBufferAsync(state).ConfigureAwait(false); } catch { } + try { state.Dispose(); } catch { } + } + + _files.Clear(); + + while (_sbPool.TryTake(out _)) { } + Interlocked.Exchange(ref _sbPoolCount, 0); + + _encryptor.Dispose(); + _aes?.Dispose(); + _compressionCts.Dispose(); + _compressionSemaphore.Dispose(); } public new void Dispose() @@ -1174,4 +731,32 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable OnShutdownFlushAsync().GetAwaiter().GetResult(); base.Dispose(); } -} \ No newline at end of file + + private sealed class FileState : IDisposable + { + public string FilePath; + public long Size; + public DateTime Date; + public byte[] Buffer; + public int BufferPosition; + public FileStream Stream; + public SemaphoreSlim WriteLock; + public bool IsFaulted; + public DateTime LastFailureUtc; + + public void Dispose() + { + Stream?.Dispose(); + Stream = null; + + if (Buffer != null) + { + ArrayPool.Shared.Return(Buffer, clearArray: true); + Buffer = null; + } + + WriteLock?.Dispose(); + WriteLock = null; + } + } +} diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs index 63ec768..c0ad232 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs @@ -18,7 +18,7 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable private readonly CancellationTokenSource _cts = new(); private readonly int _batchSize; - private readonly long _maxQueueBytes = 512L * 1024 * 1024; // 512MB safety + private readonly long _maxQueueBytes = 13L * 1024 * 1024; // 13MB safety - forces frequent flushes to disk private long _currentQueueBytes; private long _dropped; @@ -38,7 +38,6 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable if (o is FileLoggerOptions file) { UseLocalTime = file.UseLocalTime; - UseMask = file.UseMask; LoggerSettings = file.LoggerSettings; } @@ -51,14 +50,13 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable } protected bool UseLocalTime { get; } - protected bool UseMask { get; } protected DateTimeOffset NowOffset => UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow; protected LoggerSettings LoggerSettings { - get => _loggerSettings ??= new LoggerSettings { UseLocalTime = UseLocalTime, UseMask = UseMask }; + get => _loggerSettings ??= new LoggerSettings { UseLocalTime = UseLocalTime }; set => _loggerSettings = value; } @@ -89,16 +87,10 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable } - private LogMessage CreateLogMessage(string message, DateTimeOffset ts, string category, ELogType logLevel, string[] tags = null, LoggerSettings? settings = null) + private LogMessage CreateLogMessage(string message, DateTimeOffset ts, string category, ELogType logLevel, string[] tags = null, LoggerSettings? settings = null) { var effectiveSettings = settings ?? LoggerSettings; - if (effectiveSettings.UseMask) - { - var masker = new SensitiveDataMasker(effectiveSettings); - message = masker.MaskSensitiveInformation(message); - } - return new LogMessage { Message = message, @@ -175,6 +167,7 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable public void Dispose() { + // Make Dispose idempotent and ensure derived shutdown completes synchronously. if (_disposed) { return; @@ -182,17 +175,33 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable _disposed = true; - _cts.Cancel(); - _worker.Join(TimeSpan.FromSeconds(5)); + try + { + // Signal cancellation and wake the worker thread. + _cts.Cancel(); + _signal.Set(); - OnShutdownFlushAsync().ConfigureAwait(false); + // Wait briefly for the worker to finish; thread is background so process won't hang indefinitely. + _worker.Join(TimeSpan.FromSeconds(5)); - _cts.Dispose(); - _signal.Dispose(); + // Ensure derived classes flush synchronously before we dispose native resources. + OnShutdownFlushAsync().GetAwaiter().GetResult(); + } + catch (Exception ex) + { + // Surface errors via event if available. + try { OnError?.Invoke(this, ex); } catch { } + } + finally + { + try { _cts.Dispose(); } catch { } + try { _signal.Dispose(); } catch { } + GC.SuppressFinalize(this); + } } protected virtual async Task OnShutdownFlushAsync() { // default: Do nothing } -} +} \ No newline at end of file diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/SensitiveDataMasker.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/SensitiveDataMasker.cs deleted file mode 100644 index dc256ac..0000000 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/SensitiveDataMasker.cs +++ /dev/null @@ -1,100 +0,0 @@ -using global::EonaCat.Logger.Managers; -using System; -using System.Collections.Generic; -using System.Text; -using System.Text.RegularExpressions; - -namespace EonaCat.Logger.EonaCatCoreLogger.Internal -{ - internal class SensitiveDataMasker - { - internal LoggerSettings LoggerSettings { get; } - - internal SensitiveDataMasker(LoggerSettings settings) - { - LoggerSettings = settings; - } - - // Precompiled Regexes - private static readonly Regex IpRegex = new Regex(@"\b(?:\d{1,3}\.){3}\d{1,3}\b(?!\d)", RegexOptions.Compiled); - private static readonly Regex MacRegex = new Regex(@"\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b", RegexOptions.Compiled); - private static readonly Regex EmailRegex = new Regex(@"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex PasswordRegex = new Regex(@"(?i)(password\s*[:= ]\s*|pwd\s*[:= ]\s*)[^\s]+", RegexOptions.Compiled); - private static readonly Regex DutchPasswordRegex = new Regex(@"(?i)(wachtwoord\s*[:= ]\s*|ww\s*=\s*)[^\s]+", RegexOptions.Compiled); - private static readonly Regex CreditCardRegex = new Regex(@"\b(?:\d{4}[ -]?){3}\d{4}\b", RegexOptions.Compiled); - private static readonly Regex SsnBsnRegex = new Regex(@"\b\d{3}-\d{2}-\d{4}\b|\b\d{9}\b", RegexOptions.Compiled); - private static readonly Regex ApiKeyRegex = new Regex(@"\b[A-Za-z0-9-_]{20,}\b", RegexOptions.Compiled); - private static readonly Regex PhoneGenericRegex = new Regex(@"\b(\+?\d{1,4}[-.\s]?)?(\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}\b", RegexOptions.Compiled); - private static readonly Regex PhoneDutchRegex = new Regex(@"\b(\+31|0031|0|06)[-\s]?\d{8}\b", RegexOptions.Compiled); - private static readonly Regex PhoneBelgiumRegex = new Regex(@"\b(\+32|0032|0|06)[-\s]?\d{8}\b", RegexOptions.Compiled); - private static readonly Regex DobRegex = new Regex(@"\b\d{2}[/-]\d{2}[/-]\d{4}\b", RegexOptions.Compiled); - private static readonly Regex PostalCodeDutchRegex = new Regex(@"\b\d{4}\s?[A-Z]{2}\b", RegexOptions.Compiled); - private static readonly Regex IbanGenericRegex = new Regex(@"\b[A-Z]{2}\d{2}[A-Z0-9]{1,30}\b", RegexOptions.Compiled); - private static readonly Regex IbanDutchRegex = new Regex(@"\bNL\d{2}[A-Z]{4}\d{10}\b", RegexOptions.Compiled); - private static readonly Regex JwtRegex = new Regex(@"\b[A-Za-z0-9-_]{16,}\.[A-Za-z0-9-_]{16,}\.[A-Za-z0-9-_]{16,}\b", RegexOptions.Compiled); - private static readonly Regex UrlSensitiveRegex = new Regex(@"\bhttps?:\/\/[^\s?]+(\?[^\s]+?[\&=](password|key|token|wachtwoord|sleutel))[^&\s]*", RegexOptions.IgnoreCase | RegexOptions.Compiled); - private static readonly Regex LicenseKeyRegex = new Regex(@"\b[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}\b", RegexOptions.Compiled); - private static readonly Regex PemKeyRegex = new Regex(@"-----BEGIN [A-Z ]+KEY-----[\s\S]+?-----END [A-Z ]+KEY-----", RegexOptions.Compiled); - private static readonly Regex KvkRegex = new Regex(@"\b\d{8}|\d{12}\b", RegexOptions.Compiled); - private static readonly Regex BtwRegex = new Regex(@"\bNL\d{9}B\d{2}\b", RegexOptions.Compiled); - private static readonly Regex DutchDrivingLicenseRegex = new Regex(@"\b[A-Z0-9]{10,12}\b", RegexOptions.Compiled); - private static readonly Regex ZorgnummerRegex = new Regex(@"\b\d{9}\b", RegexOptions.Compiled); - private static readonly Regex DutchBankAccountRegex = new Regex(@"\b\d{9,10}\b", RegexOptions.Compiled); - private static readonly Regex DutchPassportRegex = new Regex(@"\b[A-Z0-9]{9}\b", RegexOptions.Compiled); - private static readonly Regex DutchIdDocRegex = new Regex(@"\b[A-Z]{2}\d{6,7}\b", RegexOptions.Compiled); - - internal virtual string MaskSensitiveInformation(string message) - { - if (string.IsNullOrEmpty(message)) - { - return message; - } - - if (LoggerSettings != null && LoggerSettings.UseDefaultMasking) - { - string mask = LoggerSettings.Mask ?? "***"; - - message = IpRegex.Replace(message, mask); - message = MacRegex.Replace(message, mask); - message = EmailRegex.Replace(message, mask); - message = PasswordRegex.Replace(message, $"password={mask}"); - message = DutchPasswordRegex.Replace(message, $"wachtwoord={mask}"); - message = CreditCardRegex.Replace(message, mask); - message = SsnBsnRegex.Replace(message, mask); - message = ApiKeyRegex.Replace(message, mask); - message = PhoneGenericRegex.Replace(message, mask); - message = PhoneDutchRegex.Replace(message, mask); - message = PhoneBelgiumRegex.Replace(message, mask); - message = DobRegex.Replace(message, mask); - message = PostalCodeDutchRegex.Replace(message, mask); - message = IbanGenericRegex.Replace(message, mask); - message = IbanDutchRegex.Replace(message, mask); - message = JwtRegex.Replace(message, mask); - message = UrlSensitiveRegex.Replace(message, mask); - message = LicenseKeyRegex.Replace(message, mask); - message = PemKeyRegex.Replace(message, mask); - message = KvkRegex.Replace(message, mask); - message = BtwRegex.Replace(message, mask); - message = DutchDrivingLicenseRegex.Replace(message, mask); - message = ZorgnummerRegex.Replace(message, mask); - message = DutchBankAccountRegex.Replace(message, mask); - message = DutchPassportRegex.Replace(message, mask); - message = DutchIdDocRegex.Replace(message, mask); - } - - // Mask custom keywords - if (LoggerSettings?.MaskedKeywords != null) - { - foreach (var keyword in LoggerSettings.MaskedKeywords) - { - if (!string.IsNullOrWhiteSpace(keyword)) - { - message = message.Replace(keyword, LoggerSettings.Mask); - } - } - } - - return message; - } - } -} diff --git a/EonaCat.Logger/LoggerConfigurator.cs b/EonaCat.Logger/LoggerConfigurator.cs index e6825af..2b5fa54 100644 --- a/EonaCat.Logger/LoggerConfigurator.cs +++ b/EonaCat.Logger/LoggerConfigurator.cs @@ -27,10 +27,6 @@ namespace EonaCat.Logger target.IsEnabled = source.IsEnabled; target.MaxRolloverFiles = source.MaxRolloverFiles; target.UseLocalTime = source.UseLocalTime; - target.UseMask = source.UseMask; - target.Mask = source.Mask; - target.UseDefaultMasking = source.UseDefaultMasking; - target.MaskedKeywords = source.MaskedKeywords; target.IncludeCorrelationId = source.IncludeCorrelationId; target.EnableCategoryRouting = source.EnableCategoryRouting; target.Category = source.Category; diff --git a/EonaCat.Logger/Managers/LoggerSettings.cs b/EonaCat.Logger/Managers/LoggerSettings.cs index ceb4696..52f912b 100644 --- a/EonaCat.Logger/Managers/LoggerSettings.cs +++ b/EonaCat.Logger/Managers/LoggerSettings.cs @@ -192,44 +192,6 @@ public class LoggerSettings /// public string LogOrigin { get; set; } - /// - /// Determines if we need to mask certain keywords - /// - public bool UseMask { get; set; } - - /// - /// Determines the keywords to mask - /// - public List MaskedKeywords { get; set; } = new List(); - public string Mask { get; set; } = "***MASKED***"; - - /// - /// Determines that if masking is enabled we also need to use the default masking options: - /// IP addresses - /// MAC addresses - /// Emails - /// Passwords - /// Credit card numbers - /// Social security numbers (SSN) and BSN (Dutch Citizen Service Number) - /// API keys/tokens - /// Phone numbers (generic and Dutch specific) - /// Dates of birth (DOB) or other date formats - /// IBAN/Bank account numbers (generic and Dutch specific) - /// JWT tokens - /// URLs with sensitive query strings - /// License keys - /// Public and private keys (e.g., PEM format) - /// Dutch KVK number (8 or 12 digits) - /// Dutch BTW-nummer (VAT number) - /// Dutch driving license number (10-12 characters) - /// Dutch health insurance number (Zorgnummer) - /// Other Dutch Bank Account numbers (9-10 digits) - /// Dutch Passport Numbers (9 alphanumeric characters - /// Dutch Identification Document Numbers (varying formats) - /// Custom keywords specified in LoggerSettings - /// - public bool UseDefaultMasking { get; set; } = true; - public event LogDelegate OnLog; public event LogDelegate OnError; diff --git a/README.md b/README.md index c53a91a..53663c0 100644 --- a/README.md +++ b/README.md @@ -260,42 +260,6 @@ namespace EonaCat.Logger.Advanced } ``` -**Code for enabling keyword masking (default: false):** - -```csharp -Logger.LoggerSettings.UseMask = true; -``` - -Sensitive information will be replaced with ****MASKED**** -You can add additional masking keywords using the MaskedKeywords property on the LoggerSettings object - -You can also override the MaskSensitiveInformation method of the batchLoggingProvider to add your own masking - - -Default maskings (when the masking is enabled): -IP addresses -MAC addresses -Emails -Passwords -Credit card numbers -Social security numbers (SSN) and BSN (Dutch Citizen Service Number) -API keys/tokens -Phone numbers (generic and Dutch specific) -Dates of birth (DOB) or other date formats -IBAN/Bank account numbers (generic and Dutch specific) -JWT tokens -URLs with sensitive query strings -License keys -Public and private keys (e.g., PEM format) -Dutch KVK number (8 or 12 digits) -Dutch BTW-nummer (VAT number) -Dutch driving license number (10-12 characters) -Dutch health insurance number (Zorgnummer) -Other Dutch Bank Account numbers (9-10 digits) -Dutch Passport Numbers (9 alphanumeric characters -Dutch Identification Document Numbers (varying formats) -Custom keywords specified in LoggerSettings - Header tokens: {date} {time} {ts} {tz} {unix} {ticks}{newline} diff --git a/Testers/EonaCat.Logger.Test.Web/EonaCat.Logger.Test.Web.csproj b/Testers/EonaCat.Logger.Test.Web/EonaCat.Logger.Test.Web.csproj index e11238b..90223a2 100644 --- a/Testers/EonaCat.Logger.Test.Web/EonaCat.Logger.Test.Web.csproj +++ b/Testers/EonaCat.Logger.Test.Web/EonaCat.Logger.Test.Web.csproj @@ -7,8 +7,6 @@ - - all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Testers/EonaCat.Logger.Test.Web/Program.cs b/Testers/EonaCat.Logger.Test.Web/Program.cs index 63c7d32..62cf0ca 100644 --- a/Testers/EonaCat.Logger.Test.Web/Program.cs +++ b/Testers/EonaCat.Logger.Test.Web/Program.cs @@ -6,7 +6,7 @@ using EonaCat.Logger.EonaCatCoreLogger.Models; using EonaCat.Logger.LogClient; using EonaCat.Logger.Managers; - using EonaCat.MemoryGuard; + //using EonaCat.MemoryGuard; using EonaCat.Versioning.Helpers; using EonaCat.Web.RateLimiter; using EonaCat.Web.RateLimiter.Endpoints.Extensions; @@ -42,22 +42,22 @@ }); Console.ReadKey(); - var _config = new MemoryGuardConfiguration - { - MonitoringInterval = TimeSpan.FromSeconds(5), - AnalysisInterval = TimeSpan.FromSeconds(10), - PredictionInterval = TimeSpan.FromSeconds(15), - LeakDetectionThreshold = TimeSpan.FromSeconds(5), - SuspiciousObjectThreshold = TimeSpan.FromSeconds(3), - BackgroundReportingInterval = TimeSpan.FromMinutes(1.5), - CaptureStackTraces = true, - EnableAutoRemediation = true, - AutoSaveReports = true, - MemoryPressureThreshold = 500 * 1024 * 1024, // 500MB - BackgroundAnalysisInterval = TimeSpan.FromMinutes(1), - OptimizationInterval = TimeSpan.FromMinutes(10), - PatternDetectionInterval = TimeSpan.FromMinutes(3) - }; + //var _config = new MemoryGuardConfiguration + //{ + // MonitoringInterval = TimeSpan.FromSeconds(5), + // AnalysisInterval = TimeSpan.FromSeconds(10), + // PredictionInterval = TimeSpan.FromSeconds(15), + // LeakDetectionThreshold = TimeSpan.FromSeconds(5), + // SuspiciousObjectThreshold = TimeSpan.FromSeconds(3), + // BackgroundReportingInterval = TimeSpan.FromMinutes(1.5), + // CaptureStackTraces = true, + // EnableAutoRemediation = true, + // AutoSaveReports = true, + // MemoryPressureThreshold = 500 * 1024 * 1024, // 500MB + // BackgroundAnalysisInterval = TimeSpan.FromMinutes(1), + // OptimizationInterval = TimeSpan.FromMinutes(10), + // PatternDetectionInterval = TimeSpan.FromMinutes(3) + //}; //MemoryGuard.Start(_config); @@ -112,7 +112,6 @@ logger.LoggerSettings.LogTrace(); logger.LoggerSettings.LogTraffic(); logger.LoggerSettings.OnLog += LoggerSettings_OnLog; - logger.LoggerSettings.UseMask = true; //LoggerSettings.CustomHeaderFormatter = ctx => //{ @@ -168,7 +167,6 @@ options.MaxRolloverFiles = 5; //options.FileSizeLimit = 1 * 1024 * 1024 / 4; options.UseLocalTime = true; - options.UseMask = true; builder.Logging.AddEonaCatFileLogger(fileLoggerOptions: options, filenamePrefix: "web"); builder.Logging.AddEonaCatConsoleLogger(); @@ -287,23 +285,22 @@ } //MemoryLeakTester.Start(logger); - _ = Task.Run(RunMemoryReportTask).ConfigureAwait(false); - _ = Task.Run(RunMaskTest).ConfigureAwait(false); + //_ = Task.Run(RunMemoryReportTask).ConfigureAwait(false); _ = Task.Run(RunWebLoggerTestsAsync).ConfigureAwait(false); _ = Task.Run(RunWebLoggingTests).ConfigureAwait(false); _ = Task.Run(RunLoggingTestsAsync).ConfigureAwait(false); _ = Task.Run(RunLoggingExceptionTests).ConfigureAwait(false); _ = Task.Run(RunWebLoggingExceptionTests).ConfigureAwait(false); - async Task RunMemoryReportTask() - { - while (true) - { - await MemoryGuard.PrintReportAsync().ConfigureAwait(false); - await Task.Delay(60000).ConfigureAwait(false); - Console.ReadKey(); - } - } + //async Task RunMemoryReportTask() + //{ + // while (true) + // { + // await MemoryGuard.PrintReportAsync().ConfigureAwait(false); + // await Task.Delay(60000).ConfigureAwait(false); + // Console.ReadKey(); + // } + //} void RunWebLoggingExceptionTests() { @@ -347,29 +344,6 @@ } } - async Task RunMaskTest() - { - if (!Directory.Exists(logger.LogFolder)) - { - Directory.CreateDirectory(logger.LogFolder); - } - - for (var i = 0; i < 9000000; i++) - { - var message = $"mask-test {i}"; - app.Logger.LogInformation("password: test"); - app.Logger.LogInformation("JWT Token: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"); - app.Logger.LogInformation("JWT Token2: eyJhbGciOiJQUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWUsImlhdCI6MTUxNjIzOTAyMn0.iOeNU4dAFFeBwNj6qdhdvm-IvDQrTa6R22lQVJVuWJxorJfeQww5Nwsra0PjaOYhAMj9jNMO5YLmud8U7iQ5gJK2zYyepeSuXhfSi8yjFZfRiSkelqSkU19I-Ja8aQBDbqXf2SAWA8mHF8VS3F08rgEaLCyv98fLLH4vSvsJGf6ueZSLKDVXz24rZRXGWtYYk_OYYTVgR1cg0BLCsuCvqZvHleImJKiWmtS0-CymMO4MMjCy_FIl6I56NqLE9C87tUVpo1mT-kbg5cHDD8I7MjCW5Iii5dethB4Vid3mZ6emKjVYgXrtkOQ-JyGMh6fnQxEFN1ft33GX2eRHluK9eg"); - - using (var file = new StreamWriter(Path.Combine(logger.LogFolder, "testmask.log"), true)) - { - await file.WriteAsync(message); - } - Console.WriteLine($"Masked: {i}"); - await Task.Delay(1); - } - } - async Task RunLoggingTestsAsync() { var loggerSettings = new LoggerSettings(); @@ -430,10 +404,10 @@ app.Run(); } - private static void Instance_LeakDetected(object? sender, EonaCat.MemoryGuard.EventArguments.MemoryLeakDetectedEventArgs e) - { - // Leak detected - } + //private static void Instance_LeakDetected(object? sender, EonaCat.MemoryGuard.EventArguments.MemoryLeakDetectedEventArgs e) + //{ + // // Leak detected + //} } static class MemoryLeakTester