From 04a807df4feb2c22881b67c675c363685bc23d95 Mon Sep 17 00:00:00 2001 From: EonaCat Date: Fri, 30 Jan 2026 20:40:41 +0100 Subject: [PATCH] Updated (added more header options) --- .../EonaCat.Logger.LogClient.csproj | 2 +- EonaCat.Logger/EonaCat.Logger.csproj | 6 +- .../EonaCatCoreLogger/DiscordLogger.cs | 12 + .../Internal/BatchingLogger.cs | 3 +- .../Internal/BatchingLoggerProvider.cs | 727 +++++++++--------- .../Extensions/ExceptionExtensions.cs | 2 +- EonaCat.Logger/Managers/LogHelper.cs | 123 ++- EonaCat.Logger/Managers/LoggerSettings.cs | 15 +- Testers/EonaCat.Logger.Test.Web/Logger.cs | 12 +- 9 files changed, 512 insertions(+), 390 deletions(-) diff --git a/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj b/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj index 3a539d1..5c2640f 100644 --- a/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj +++ b/EonaCat.Logger.LogClient/EonaCat.Logger.LogClient.csproj @@ -25,7 +25,7 @@ - + diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj index e8f6e9d..c905100 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.5.7 - 1.5.7 + 1.5.8 + 1.5.8 README.md True LICENSE @@ -25,7 +25,7 @@ - 1.5.7+{chash:10}.{c:ymd} + 1.5.8+{chash:10}.{c:ymd} true true v[0-9]* diff --git a/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs index 9a5f29d..8cac15c 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/DiscordLogger.cs @@ -53,7 +53,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger Exception exception, Func formatter) { if (!IsEnabled(logLevel) || formatter == null) + { return; + } if (IncludeCorrelationId) { @@ -71,16 +73,24 @@ namespace EonaCat.Logger.EonaCatCoreLogger }; foreach (var kvp in _context.GetAll()) + { logParts.Add($"`{kvp.Key}`: {kvp.Value}"); + } if (exception != null) + { logParts.Add($"Exception: {exception}"); + } // Limit queue size to prevent memory growth if (_messageQueue.Count < 1000) + { _messageQueue.Enqueue(string.Join("\n", logParts)); + } else + { OnException?.Invoke(this, new Exception("DiscordLogger queue overflow")); + } } private async Task FlushLoopAsync(CancellationToken token) @@ -107,7 +117,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger private async Task FlushBufferAsync(CancellationToken token) { if (!await _flushLock.WaitAsync(0, token).ConfigureAwait(false)) + { return; + } try { diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs index 4d4bfff..5bd70a9 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs @@ -54,8 +54,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal ? exception.FormatExceptionToMessage() + Environment.NewLine : formatter(state, exception); - message = LogHelper.FormatMessageWithHeader(_loggerSettings, logLevel.FromLogLevel(), message, timestamp.DateTime, category) - + Environment.NewLine; + message = LogHelper.FormatMessageWithHeader(_loggerSettings, logLevel.FromLogLevel(), message, timestamp.DateTime, category) + Environment.NewLine; var currentMessage = new EonaCatLogMessage { diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs index 79fd59d..e370c90 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs @@ -1,365 +1,394 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.SymbolStore; -using System.Threading; -using System.Threading.Tasks; -using EonaCat.Logger.Managers; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -namespace EonaCat.Logger.EonaCatCoreLogger.Internal; -// This file is part of the EonaCat project(s) which is released under the Apache License. -// See the LICENSE file or go to https://EonaCat.com/License for full license details. - -public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable -{ - private readonly int _batchSize; - - private readonly List _currentBatch = new(); - private CancellationTokenSource _cancellationTokenSource; - private LoggerSettings _loggerSettings; - - private ConcurrentQueue _messageQueue; - private Task _outputTask; - private object _writeLock = new object(); - private bool _isDisposing; - protected string Category; - - protected BatchingLoggerProvider(IOptions options) - { - var loggerOptions = options.Value; - - if (loggerOptions.FlushPeriod <= TimeSpan.Zero) - { - throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), - $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero."); - } - - if (options.Value is FileLoggerOptions fileLoggerOptions) - { - UseLocalTime = fileLoggerOptions.UseLocalTime; - UseMask = fileLoggerOptions.UseMask; - } - - _batchSize = loggerOptions.BatchSize; - - StartAsync().ConfigureAwait(false); - } - - protected DateTimeOffset CurrentDateTimeOffset => UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow; - protected DateTime CurrentDateTme => UseLocalTime ? DateTime.Now : DateTime.UtcNow; - - protected bool UseLocalTime { get; set; } - - protected LoggerSettings LoggerSettings - { - get - { - if (_loggerSettings != null) - { - return _loggerSettings; - } - - _loggerSettings = new LoggerSettings(); - _loggerSettings.UseLocalTime = UseLocalTime; - _loggerSettings.UseMask = UseMask; - return _loggerSettings; - } - - set => _loggerSettings = value; +using EonaCat.Logger.Managers; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Channels; +using System.Threading.Tasks; + +namespace EonaCat.Logger.EonaCatCoreLogger.Internal; +// This file is part of the EonaCat project(s) which is released under the Apache License. +// See the LICENSE file or go to https://EonaCat.com/License for full license details. + +public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable +{ + private int _batchSize; + + private readonly Channel _channel = + Channel.CreateUnbounded(new UnboundedChannelOptions + { + SingleReader = true, + SingleWriter = false + }); + + private CancellationTokenSource _cancellationTokenSource; + private Task _outputTask; + private bool _isDisposed; + + protected BatchingLoggerProvider(IOptions options) + { + var loggerOptions = options.Value ?? throw new ArgumentNullException(nameof(options)); + + if (loggerOptions.FlushPeriod <= TimeSpan.Zero) + { + throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), + $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero."); + } + + if (options.Value is FileLoggerOptions fileLoggerOptions) + { + UseLocalTime = fileLoggerOptions.UseLocalTime; + UseMask = fileLoggerOptions.UseMask; + } + + _batchSize = loggerOptions.BatchSize > 0 ? loggerOptions.BatchSize : 100; + StartAsync(); } - private SemaphoreSlim _writeSemaphore = new SemaphoreSlim(1, 1); - - public bool IsStarted { get; set; } - public bool UseMask { get; set; } - - public async void Dispose() - { - while (!_messageQueue.IsEmpty) - { - // Do nothing, just wait for the queue to be empty - await Task.Delay(1000); - } - - _isDisposing = true; - await StopAsync().ConfigureAwait(false); - GC.SuppressFinalize(this); - } - - public ILogger CreateLogger(string categoryName) - { - Category = categoryName; - return new BatchingLogger(this, categoryName, LoggerSettings); - } - - protected abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token); - private async Task ProcessLogQueueAsync(object state) + protected DateTimeOffset CurrentDateTimeOffset => UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow; + protected DateTime CurrentDateTme => UseLocalTime ? DateTime.Now : DateTime.UtcNow; + + protected bool UseLocalTime { get; set; } + + protected LoggerSettings LoggerSettings { - while (!_cancellationTokenSource.IsCancellationRequested) + get { - var limit = _batchSize <= 0 ? int.MaxValue : _batchSize; - while (limit > 0 && _messageQueue.TryDequeue(out var message)) + if (_loggerSettings != null) { - _currentBatch.Add(message); - limit--; + return _loggerSettings; } - if (_currentBatch.Count > 0) - { - try - { - if (_isDisposing) - { - return; - } + _loggerSettings = new LoggerSettings(); + _loggerSettings.UseLocalTime = UseLocalTime; + _loggerSettings.UseMask = UseMask; + return _loggerSettings; + } - await _writeSemaphore.WaitAsync(); - await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token).ConfigureAwait(false); - _currentBatch.Clear(); - } - catch + set => _loggerSettings = value; + } + + public bool IsStarted { get; set; } + public bool UseMask { get; set; } + + private LoggerSettings _loggerSettings; + + public ILogger CreateLogger(string categoryName) + { + return new BatchingLogger(this, categoryName, LoggerSettings); + } + + protected abstract Task WriteMessagesAsync(IEnumerable messages, CancellationToken token); + + private async Task ProcessLogQueueAsync() + { + var batchSize = _batchSize > 0 ? _batchSize : 100; + var batch = new List(batchSize); + + try + { + var token = _cancellationTokenSource.Token; + while (await _channel.Reader.WaitToReadAsync(token).ConfigureAwait(false)) + { + batch.Clear(); + + while (batch.Count < batchSize && _channel.Reader.TryRead(out var message)) { - // ignored + batch.Add(message); } - finally + + if (batch.Count > 0) { - _writeSemaphore.Release(); + await WriteMessagesAsync(batch, token).ConfigureAwait(false); } } - await Task.Delay(500); + } + catch (OperationCanceledException) + { + // normal shutdown + } + catch (Exception) + { + throw; + } + } + + protected async Task WriteStartMessage() + { + var message = LogHelper.GetStartupMessage(); + var token = _cancellationTokenSource?.Token ?? CancellationToken.None; + await WriteMessagesAsync(new List { CreateLoggerMessage(message, CurrentDateTimeOffset) }, token).ConfigureAwait(false); + } + + private LogMessage CreateLoggerMessage(string message, DateTimeOffset currentDateTimeOffset) + { + var result = new LogMessage() { Message = message, Timestamp = currentDateTimeOffset }; + + if (LoggerSettings != null && LoggerSettings.UseMask) + { + // Masking sensitive information + result.Message = MaskSensitiveInformation(result.Message); + } + + return result; + } + + /// + /// Masks sensitive information within the provided message string. + /// This method is virtual and can be overridden to customize masking behavior. + /// + /// The log message potentially containing sensitive information. + /// The masked log message. + protected virtual string MaskSensitiveInformation(string message) + { + if (string.IsNullOrEmpty(message)) + { + return message; + } + + if (LoggerSettings != null && LoggerSettings.UseDefaultMasking) + { + // Mask IP addresses + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b(?:\d{1,3}\.){3}\d{1,3}\b(?!\d)", + LoggerSettings.Mask); + + // Mask MAC addresses + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b", + LoggerSettings.Mask); + + // Mask emails + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", + LoggerSettings.Mask, + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + // Mask passwords + message = System.Text.RegularExpressions.Regex.Replace(message, + @"(?i)(password\s*[:= ]\s*|pwd\s*[:= ]\s*)[^\s]+", + $"password={LoggerSettings.Mask}"); + + // Mask credit card numbers + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b(?:\d{4}[ -]?){3}\d{4}\b", + LoggerSettings.Mask); + + // Mask social security numbers (SSN) and BSN (Dutch Citizen Service Number) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b\d{3}-\d{2}-\d{4}\b|\b\d{9}\b", + LoggerSettings.Mask); // SSN (USA) + + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b\d{9}\b", + LoggerSettings.Mask); // BSN (Dutch) + + // Mask passwords (Dutch) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"(?i)(wachtwoord\s*[:= ]\s*|ww\s*=\s*)[^\s]+", + $"wachtwoord={LoggerSettings.Mask}"); + + // Mask API keys/tokens + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b[A-Za-z0-9-_]{20,}\b", + LoggerSettings.Mask); + + // Mask phone numbers (generic and Dutch specific) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b(\+?\d{1,4}[-.\s]?)?(\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}\b", + LoggerSettings.Mask); // Generic + + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b(\+31|0031|0|06)[-\s]?\d{8}\b", + LoggerSettings.Mask); // Dutch + + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b(\+32|0032|0|06)[-\s]?\d{8}\b", + LoggerSettings.Mask); // Belgium + + // Mask dates of birth (DOB) or other date formats + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b\d{2}[/-]\d{2}[/-]\d{4}\b", + LoggerSettings.Mask); + + // Mask Dutch postal codes + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b\d{4}\s?[A-Z]{2}\b", + LoggerSettings.Mask); + + // Mask IBAN/Bank account numbers (generic and Dutch specific) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b[A-Z]{2}\d{2}[A-Z0-9]{1,30}\b", + LoggerSettings.Mask); // Generic for EU and other country IBANs + + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\bNL\d{2}[A-Z]{4}\d{10}\b", + LoggerSettings.Mask); // Dutch IBAN + + // Mask JWT Tokens + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b[A-Za-z0-9-_]{16,}\.[A-Za-z0-9-_]{16,}\.[A-Za-z0-9-_]{16,}\b", + LoggerSettings.Mask); + + // Mask URLs with sensitive query strings + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\bhttps?:\/\/[^\s?]+(\?[^\s]+?[\&=](password|key|token|wachtwoord|sleutel))[^&\s]*", + LoggerSettings.Mask, + System.Text.RegularExpressions.RegexOptions.IgnoreCase); + + // Mask license keys + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}\b", + LoggerSettings.Mask); + + // Mask public and private keys (e.g., PEM format) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"-----BEGIN [A-Z ]+KEY-----[\s\S]+?-----END [A-Z ]+KEY-----", + LoggerSettings.Mask); + + // Mask Dutch KVK number (8 or 12 digits) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b\d{8}|\d{12}\b", + LoggerSettings.Mask); + + // Mask Dutch BTW-nummer (VAT number) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\bNL\d{9}B\d{2}\b", + LoggerSettings.Mask); + + // Mask Dutch driving license number (10-12 characters) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b[A-Z0-9]{10,12}\b", + LoggerSettings.Mask); + + // Mask Dutch health insurance number (Zorgnummer) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b\d{9}\b", + LoggerSettings.Mask); + + // Mask other Dutch Bank Account numbers (9-10 digits) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b\d{9,10}\b", + LoggerSettings.Mask); + + // Mask Dutch Passport Numbers (9 alphanumeric characters) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b[A-Z0-9]{9}\b", + LoggerSettings.Mask); + + // Mask Dutch Identification Document Numbers (varying formats) + message = System.Text.RegularExpressions.Regex.Replace(message, + @"\b[A-Z]{2}\d{6,7}\b", + LoggerSettings.Mask); + } + + // Mask custom keywords specified in LoggerSettings + if (LoggerSettings?.MaskedKeywords != null) + { + foreach (var keyword in LoggerSettings.MaskedKeywords) + { + if (string.IsNullOrWhiteSpace(keyword)) + { + continue; + } + message = message.Replace(keyword, LoggerSettings.Mask); + } + } + + return message; + } + + internal string AddMessage(DateTimeOffset timestamp, string message) + { + var result = CreateLoggerMessage(message, timestamp); + if (!_channel.Writer.TryWrite(result)) + { + _ = _channel.Writer.WriteAsync(result); + } + + return result.Message; + } + + private void StartAsync() + { + if (_cancellationTokenSource != null) + { + return; + } + + _cancellationTokenSource = new CancellationTokenSource(); + _outputTask = Task.Run(ProcessLogQueueAsync); + IsStarted = true; + } + + private async Task StopAsync() + { + if (_cancellationTokenSource == null) + { + return; + } + + try + { + _cancellationTokenSource.Cancel(); + _channel.Writer.Complete(); + + if (_outputTask != null) + { + await _outputTask.ConfigureAwait(false); + } + } + catch (OperationCanceledException) + { + // expected on shutdown + } + catch (AggregateException exception) when (exception.InnerExceptions.Count == 1 && exception.InnerExceptions[0] is TaskCanceledException) + { + // Do nothing + } + finally + { + try + { + _cancellationTokenSource.Dispose(); + } + catch + { + // Do nothing + } + + _cancellationTokenSource = null; + _outputTask = null; + IsStarted = false; + } + } + + public void Dispose() + { + Dispose(true); + GC.SuppressFinalize(this); + } + + protected virtual void Dispose(bool disposing) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; + + if (disposing) + { + try + { + StopAsync().GetAwaiter().GetResult(); + } + catch + { + // Do nothing + } } } - - - protected async Task WriteStartMessage() - { - var message = LogHelper.GetStartupMessage(); - await WriteMessagesAsync(new List { CreateLoggerMessage(message, CurrentDateTimeOffset) }, _cancellationTokenSource.Token).ConfigureAwait(false); - } - - private LogMessage CreateLoggerMessage(string message, DateTimeOffset currentDateTimeOffset) - { - var result = new LogMessage() { Message = message, Timestamp = currentDateTimeOffset }; - - if (LoggerSettings != null && LoggerSettings.UseMask) - { - // Masking sensitive information - result.Message = MaskSensitiveInformation(result.Message); - } - - return result; - } - - /// - /// Masks sensitive information within the provided message string. - /// This method is virtual and can be overridden to customize masking behavior. - /// - /// The log message potentially containing sensitive information. - /// The masked log message. - protected virtual string MaskSensitiveInformation(string message) - { - if (string.IsNullOrEmpty(message)) - { - return message; - } - - if (LoggerSettings != null && LoggerSettings.UseDefaultMasking) - { - // Mask IP addresses - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b(?:\d{1,3}\.){3}\d{1,3}\b(?!\d)", - LoggerSettings.Mask); - - // Mask MAC addresses - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b(?:[0-9A-Fa-f]{2}[:-]){5}[0-9A-Fa-f]{2}\b", - LoggerSettings.Mask); - - // Mask emails - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b", - LoggerSettings.Mask, - System.Text.RegularExpressions.RegexOptions.IgnoreCase); - - // Mask passwords - message = System.Text.RegularExpressions.Regex.Replace(message, - @"(?i)(password\s*[:= ]\s*|pwd\s*[:= ]\s*)[^\s]+", - $"password={LoggerSettings.Mask}"); - - // Mask credit card numbers - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b(?:\d{4}[ -]?){3}\d{4}\b", - LoggerSettings.Mask); - - // Mask social security numbers (SSN) and BSN (Dutch Citizen Service Number) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b\d{3}-\d{2}-\d{4}\b|\b\d{9}\b", - LoggerSettings.Mask); // SSN (USA) - - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b\d{9}\b", - LoggerSettings.Mask); // BSN (Dutch) - - // Mask passwords (Dutch) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"(?i)(wachtwoord\s*[:= ]\s*|ww\s*=\s*)[^\s]+", - $"wachtwoord={LoggerSettings.Mask}"); - - // Mask API keys/tokens - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b[A-Za-z0-9-_]{20,}\b", - LoggerSettings.Mask); - - // Mask phone numbers (generic and Dutch specific) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b(\+?\d{1,4}[-.\s]?)?(\(?\d{3}\)?[-.\s]?)?\d{3}[-.\s]?\d{4}\b", - LoggerSettings.Mask); // Generic - - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b(\+31|0031|0|06)[-\s]?\d{8}\b", - LoggerSettings.Mask); // Dutch - - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b(\+32|0032|0|06)[-\s]?\d{8}\b", - LoggerSettings.Mask); // Belgium - - // Mask dates of birth (DOB) or other date formats - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b\d{2}[/-]\d{2}[/-]\d{4}\b", - LoggerSettings.Mask); - - // Mask Dutch postal codes - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b\d{4}\s?[A-Z]{2}\b", - LoggerSettings.Mask); - - // Mask IBAN/Bank account numbers (generic and Dutch specific) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b[A-Z]{2}\d{2}[A-Z0-9]{1,30}\b", - LoggerSettings.Mask); // Generic for EU and other country IBANs - - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\bNL\d{2}[A-Z]{4}\d{10}\b", - LoggerSettings.Mask); // Dutch IBAN - - // Mask JWT Tokens - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b[A-Za-z0-9-_]{16,}\.[A-Za-z0-9-_]{16,}\.[A-Za-z0-9-_]{16,}\b", - LoggerSettings.Mask); - - // Mask URLs with sensitive query strings - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\bhttps?:\/\/[^\s?]+(\?[^\s]+?[\&=](password|key|token|wachtwoord|sleutel))[^&\s]*", - LoggerSettings.Mask, - System.Text.RegularExpressions.RegexOptions.IgnoreCase); - - // Mask license keys - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}-[A-Z0-9]{4}\b", - LoggerSettings.Mask); - - // Mask public and private keys (e.g., PEM format) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"-----BEGIN [A-Z ]+KEY-----[\s\S]+?-----END [A-Z ]+KEY-----", - LoggerSettings.Mask); - - // Mask Dutch KVK number (8 or 12 digits) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b\d{8}|\d{12}\b", - LoggerSettings.Mask); - - // Mask Dutch BTW-nummer (VAT number) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\bNL\d{9}B\d{2}\b", - LoggerSettings.Mask); - - // Mask Dutch driving license number (10-12 characters) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b[A-Z0-9]{10,12}\b", - LoggerSettings.Mask); - - // Mask Dutch health insurance number (Zorgnummer) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b\d{9}\b", - LoggerSettings.Mask); - - // Mask other Dutch Bank Account numbers (9-10 digits) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b\d{9,10}\b", - LoggerSettings.Mask); - - // Mask Dutch Passport Numbers (9 alphanumeric characters) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b[A-Z0-9]{9}\b", - LoggerSettings.Mask); - - // Mask Dutch Identification Document Numbers (varying formats) - message = System.Text.RegularExpressions.Regex.Replace(message, - @"\b[A-Z]{2}\d{6,7}\b", - LoggerSettings.Mask); - } - - // Mask custom keywords specified in LoggerSettings - if (LoggerSettings?.MaskedKeywords != null) - { - foreach (var keyword in LoggerSettings.MaskedKeywords) - { - if (string.IsNullOrWhiteSpace(keyword)) - { - continue; - } - message = message.Replace(keyword, LoggerSettings.Mask); - } - } - - return message; - } - - internal string AddMessage(DateTimeOffset timestamp, string message) - { - var result = CreateLoggerMessage(message, timestamp); - _messageQueue.Enqueue(result); - return result.Message; - } - - private Task StartAsync() - { - IsStarted = true; - _messageQueue = new ConcurrentQueue(); - _cancellationTokenSource = new CancellationTokenSource(); - - _outputTask = Task.Factory.StartNew( - ProcessLogQueueAsync, - null, - TaskCreationOptions.LongRunning); - return Task.CompletedTask; - } - - private async Task StopAsync() - { - _cancellationTokenSource.Cancel(); - - try - { - while (_outputTask.Status != TaskStatus.RanToCompletion && _outputTask.Status != TaskStatus.Canceled) - { - await _outputTask.ConfigureAwait(false); - await Task.Delay(100); - } - } - catch (TaskCanceledException) - { - } - catch (AggregateException exception) when (exception.InnerExceptions.Count == 1 && - exception.InnerExceptions[0] is TaskCanceledException) - { - } - } - - ~BatchingLoggerProvider() - { - Dispose(); - } } \ No newline at end of file diff --git a/EonaCat.Logger/Extensions/ExceptionExtensions.cs b/EonaCat.Logger/Extensions/ExceptionExtensions.cs index 5d67c3f..5364285 100644 --- a/EonaCat.Logger/Extensions/ExceptionExtensions.cs +++ b/EonaCat.Logger/Extensions/ExceptionExtensions.cs @@ -32,7 +32,7 @@ public static class ExceptionExtensions var sb = new StringBuilderChill(); sb.AppendLine(); - sb.AppendLine($"--- Exception details provided by {DllInfo.ApplicationName} ---"); + sb.AppendLine($"--- Exception details provided by {DllInfo.ApplicationName} on {Environment.MachineName} ---"); if (!string.IsNullOrEmpty(module)) { sb.AppendLine(" Module : " + module); diff --git a/EonaCat.Logger/Managers/LogHelper.cs b/EonaCat.Logger/Managers/LogHelper.cs index f775502..98e2103 100644 --- a/EonaCat.Logger/Managers/LogHelper.cs +++ b/EonaCat.Logger/Managers/LogHelper.cs @@ -1,14 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading.Tasks; -using EonaCat.Json; +using EonaCat.Json; using EonaCat.Logger.Extensions; using EonaCat.Logger.Servers.GrayLog; using EonaCat.Logger.Servers.Splunk.Models; using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Net; +using System.Text; +using System.Text.RegularExpressions; +using System.Threading.Tasks; // This file is part of the EonaCat project(s) which is released under the Apache License. // See the LICENSE file or go to https://EonaCat.com/License for full license details. @@ -23,6 +25,34 @@ public class ErrorMessage public static class LogHelper { + public sealed class HeaderContext + { + public DateTime Timestamp { get; set; } + public string TimestampFormat { get; set; } + public string HostName { get; set; } + public string Category { get; set; } + public ELogType LogType { get; set; } + public LoggerSettings Settings { get; set; } + public string EnvironmentName { get; set; } + } + + internal static Dictionary> TokenResolvers = + new(StringComparer.OrdinalIgnoreCase) + { + ["ts"] = ctx => ctx.Timestamp.ToString(ctx.TimestampFormat), + ["host"] = ctx => ctx.HostName, + ["machine"] = ctx => Environment.MachineName, + ["category"] = ctx => ctx.Category, + ["thread"] = ctx => Environment.CurrentManagedThreadId.ToString(), + ["process"] = ctx => Process.GetCurrentProcess().ProcessName, + ["pid"] = ctx => Process.GetCurrentProcess().Id.ToString(), + ["sev"] = ctx => ctx.LogType.ToString(), + ["user"] = ctx => Environment.UserName, + ["env"] = ctx => ctx.EnvironmentName, + ["newline"] = _ => Environment.NewLine + }; + + internal static event EventHandler OnException; internal static event EventHandler OnLogLevelDisabled; @@ -73,34 +103,71 @@ public static class LogHelper return syslogMessage; } - - internal static string FormatMessageWithHeader(LoggerSettings settings, ELogType logType, string currentMessage, - DateTime dateTime, string category = null) + private static string ResolveHeader(string format, HeaderContext ctx) { - if (string.IsNullOrWhiteSpace(currentMessage)) + if (string.IsNullOrWhiteSpace(format)) { - return currentMessage; + return string.Empty; } - category ??= "General"; - var sb = new StringBuilder(settings?.HeaderFormat); - var timestamp = dateTime.ToString(settings?.TimestampFormat ?? "yyyy-MM-dd HH:mm:ss"); - var timeLabel = settings?.UseLocalTime ?? false ? "[LOCAL]" : "[UTC]"; - - sb.Replace("{ts}", $"{timestamp} {timeLabel}") - .Replace("{host}", $"[Host:{HostName}]") - .Replace("{category}", $"[Category:{category}]") - .Replace("{thread}", $"[ThreadId:{Environment.CurrentManagedThreadId}]") - .Replace("{sev}", $"[{logType}]"); - - if (sb.Length > 0) + return Regex.Replace(format, @"\{([^}]+)\}", match => { - sb.Append(" "); - } - sb.Append(currentMessage); - return sb.ToString(); + var token = match.Groups[1].Value; + var parts = token.Split(new[] { ':' }, 2); + var key = parts[0]; + + if (!TokenResolvers.TryGetValue(key, out var resolver)) + { + return match.Value; + } + + if (key.Equals("ts", StringComparison.OrdinalIgnoreCase) && parts.Length == 2) + { + ctx.TimestampFormat = parts[1]; + } + + return resolver(ctx); + }); } + + + internal static string FormatMessageWithHeader( + LoggerSettings settings, + ELogType logType, + string message, + DateTime dateTime, + string category = null) + { + if (string.IsNullOrWhiteSpace(message)) + { + return message; + } + + var ctx = new HeaderContext + { + Timestamp = dateTime, + TimestampFormat = settings?.TimestampFormat ?? "yyyy-MM-dd HH:mm:ss", + HostName = HostName, + Category = category ?? "General", + LogType = logType, + Settings = settings, + EnvironmentName = settings?.EnvironmentName + }; + + string header = settings?.CustomHeaderFormatter != null + ? settings.CustomHeaderFormatter(ctx) + : ResolveHeader(settings?.HeaderFormat, ctx); + + if (!string.IsNullOrEmpty(header)) + { + header += " "; + } + + return header + message; + } + + internal static void SendToConsole(LoggerSettings settings, ELogType logType, string message) { if (settings == null || string.IsNullOrWhiteSpace(message)) diff --git a/EonaCat.Logger/Managers/LoggerSettings.cs b/EonaCat.Logger/Managers/LoggerSettings.cs index a27532d..ab8bfee 100644 --- a/EonaCat.Logger/Managers/LoggerSettings.cs +++ b/EonaCat.Logger/Managers/LoggerSettings.cs @@ -1,11 +1,12 @@ -using System; -using System.Collections.Generic; -using System.Net.Sockets; -using System.Text.RegularExpressions; -using EonaCat.Json.Linq; +using EonaCat.Json.Linq; using EonaCat.Logger.EonaCatCoreLogger; using EonaCat.Logger.EonaCatCoreLogger.Models; using EonaCat.Logger.Servers.GrayLog; +using System; +using System.Collections.Generic; +using System.Net.Sockets; +using System.Text.RegularExpressions; +using static EonaCat.Logger.Managers.LogHelper; namespace EonaCat.Logger.Managers; // This file is part of the EonaCat project(s) which is released under the Apache License. @@ -60,6 +61,10 @@ public class LoggerSettings } } + public Func CustomHeaderFormatter { get; set; } + + public string EnvironmentName { get; set; } + /// /// Timestamp format. /// diff --git a/Testers/EonaCat.Logger.Test.Web/Logger.cs b/Testers/EonaCat.Logger.Test.Web/Logger.cs index b68be30..e95da27 100644 --- a/Testers/EonaCat.Logger.Test.Web/Logger.cs +++ b/Testers/EonaCat.Logger.Test.Web/Logger.cs @@ -37,7 +37,17 @@ public class Logger UseLocalTime = UseLocalTime, }, }; - + + LoggerSettings.CustomHeaderFormatter = ctx => + { + if (ctx.LogType == ELogType.Error) + { + return $"{ctx.Timestamp:HH:mm:ss} [{ctx.LogType}]"; + } + + return $"{ctx.Timestamp:HH:mm:ss} [{ctx.LogType}]"; + }; + _logManager = new LogManager(LoggerSettings); _logManager.Settings.TypesToLog.Clear(); _logManager.Settings.LogInfo();