diff --git a/EonaCat.Logger/EonaCat.Logger.csproj b/EonaCat.Logger/EonaCat.Logger.csproj index e8d9dad..2ff60db 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.6.1 - 1.6.1 + 1.6.2 + 1.6.2 README.md True LICENSE @@ -25,7 +25,7 @@ - 1.6.1+{chash:10}.{c:ymd} + 1.6.2+{chash:10}.{c:ymd} true true v[0-9]* diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs index ece1b20..617e440 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs @@ -13,9 +13,6 @@ using Microsoft.Extensions.Options; namespace EonaCat.Logger.EonaCatCoreLogger; -// 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. - [ProviderAlias("EonaCatFileLogger")] public sealed class FileLoggerProvider : BatchingLoggerProvider { @@ -25,19 +22,20 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider private readonly int _maxRetainedFiles; private readonly int _maxRolloverFiles; - private readonly ConcurrentDictionary _buffer = new(); - private readonly SemaphoreSlim _writeLock = new(1, 1); - private readonly SemaphoreSlim _rolloverLock = new(1, 1); - private readonly LoggerScopedContext _context = new(); - private string _logFile; private long _currentFileSize; - private int _isWriting; + + private readonly ConcurrentDictionary _buffers = new(); + private readonly ConcurrentDictionary _fileSizes = new(); + private readonly SemaphoreSlim _writeLock = new(1, 1); + private readonly SemaphoreSlim _rolloverLock = new(1, 1); + + private readonly LoggerScopedContext _context = new(); + + public string LogFile => _logFile ?? string.Empty; public event EventHandler OnError; - public string LogFile => _logFile; - public FileLoggerProvider(IOptions options) : base(options) { var o = options.Value; @@ -47,68 +45,56 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider _maxRetainedFiles = o.RetainedFileCountLimit; _maxRolloverFiles = o.MaxRolloverFiles; IncludeCorrelationId = o.IncludeCorrelationId; + + Directory.CreateDirectory(_path); + InitializeCurrentFile(); } public bool IncludeCorrelationId { get; } - protected override async Task WriteMessagesAsync( + internal override async Task WriteMessagesAsync( IReadOnlyList messages, CancellationToken token) { - if (Interlocked.Exchange(ref _isWriting, 1) == 1) + if (messages.Count == 0) { return; } - try + Directory.CreateDirectory(_path); + + // Group messages by date + foreach (var group in messages.GroupBy(m => (m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day))) { - Directory.CreateDirectory(_path); + var file = GetFullName(group.Key); + InitializeFile(file); - foreach (var group in messages.GroupBy(GetGrouping)) + var sb = _buffers.GetOrAdd(file, _ => new StringBuilder(4096)); + + lock (sb) { - var file = GetFullName(group.Key); - InitializeFile(file); - - var stringBuilder = _buffer.GetOrAdd(file, _ => new StringBuilder(4096)); - lock (stringBuilder) + foreach (var message in group) { - foreach (var message in group) - { - AppendMessage(stringBuilder, message); - } + AppendMessage(sb, message); } - - await FlushAsync(file, stringBuilder, token).ConfigureAwait(false); - DeleteOldLogFiles(); } - } - catch (Exception ex) - { - OnError?.Invoke(this, new ErrorMessage - { - Exception = ex, - Message = "Failed to write log file" - }); - } - finally - { - Interlocked.Exchange(ref _isWriting, 0); + + await FlushAsync(file, sb, token).ConfigureAwait(false); + DeleteOldLogFiles(); } } private void AppendMessage(StringBuilder sb, LogMessage msg) { - // Ensure correlation id exists (once per scope) + // Ensure correlation id exists (once per async context) if (IncludeCorrelationId) { var cid = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString(); _context.Set("CorrelationId", cid); } - // 1. Append the already-formatted message FIRST sb.Append(msg.Message); - // 2. Append context AFTER the message var ctx = _context.GetAll(); if (ctx.Count > 0) { @@ -121,19 +107,26 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider sb.Append(' '); } - sb.Append(kv.Key) - .Append('=') - .Append(kv.Value); - + sb.Append(kv.Key).Append('=').Append(kv.Value); first = false; } sb.Append(']'); } - // 3. End the line sb.AppendLine(); } + public void InitializeCurrentFile() + { + if (!string.IsNullOrEmpty(_logFile)) + { + return; + } + + _logFile = GetFullName((DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day)); + _currentFileSize = File.Exists(_logFile) ? new FileInfo(_logFile).Length : 0; + } + private async Task FlushAsync(string file, StringBuilder sb, CancellationToken token) { if (sb.Length == 0) @@ -152,14 +145,16 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider 64 * 1024, useAsync: true); - using var writer = new StreamWriter(fs); + using var writer = new StreamWriter(fs, Encoding.UTF8); var text = sb.ToString(); await writer.WriteAsync(text).ConfigureAwait(false); - _currentFileSize += Encoding.UTF8.GetByteCount(text); + _fileSizes.AddOrUpdate(file, Encoding.UTF8.GetByteCount(text), (_, old) => old + Encoding.UTF8.GetByteCount(text)); + _currentFileSize = _fileSizes[file]; + sb.Clear(); - if (_currentFileSize >= _maxFileSize) + if (_fileSizes[file] >= _maxFileSize) { await RollOverAsync(file).ConfigureAwait(false); } @@ -195,8 +190,13 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider } } - File.Move(file, Path.Combine(dir, $"{name}.1{ext}")); - _currentFileSize = 0; + var firstRoll = Path.Combine(dir, $"{name}.1{ext}"); + if (File.Exists(file)) + { + File.Move(file, firstRoll); + } + + _fileSizes[file] = 0; } finally { @@ -206,15 +206,8 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider private void InitializeFile(string file) { - if (_logFile == file) - { - return; - } - - _logFile = file; - _currentFileSize = File.Exists(file) - ? new FileInfo(file).Length - : 0; + _fileSizes.TryAdd(file, File.Exists(file) ? new FileInfo(file).Length : 0); + _buffers.TryAdd(file, new StringBuilder(4096)); } private (int Year, int Month, int Day) GetGrouping(LogMessage m) => @@ -225,7 +218,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider ? Path.Combine(_path, $"{g.Year:0000}{g.Month:00}{g.Day:00}.log") : Path.Combine(_path, $"{_fileNamePrefix}_{g.Year:0000}{g.Month:00}{g.Day:00}.log"); - protected void DeleteOldLogFiles() + private void DeleteOldLogFiles() { if (_maxRetainedFiles <= 0) { @@ -234,13 +227,24 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider var files = new DirectoryInfo(_path) .GetFiles($"{_fileNamePrefix}*") - .Where(f => !f.FullName.Equals(_logFile, StringComparison.OrdinalIgnoreCase)) - .OrderByDescending(f => f.CreationTimeUtc) + .OrderByDescending(f => + { + // Parse date from filename instead of CreationTimeUtc + var name = Path.GetFileNameWithoutExtension(f.Name); + var parts = name.Split('_'); + var datePart = parts.Length > 1 ? parts[1] : parts[0]; + if (DateTime.TryParseExact(datePart, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out var dt)) + { + return dt; + } + + return DateTime.MinValue; + }) .Skip(_maxRetainedFiles); foreach (var f in files) { - f.Delete(); + try { f.Delete(); } catch { } } } } diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs index f946a71..6ef69a7 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs @@ -1,8 +1,11 @@ -using System; -using EonaCat.Logger.EonaCatCoreLogger.Models; +using EonaCat.Logger.EonaCatCoreLogger.Models; using EonaCat.Logger.Extensions; using EonaCat.Logger.Managers; using Microsoft.Extensions.Logging; +using System; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; namespace EonaCat.Logger.EonaCatCoreLogger.Internal { @@ -26,7 +29,6 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal { get { - // Avoid DateTimeOffset.Now if UseLocalTime is false return _settings.UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow; } } @@ -63,36 +65,52 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal } var timestamp = Now; - LogInternal(timestamp, logLevel, rawMessage, exception); + LogInternalAsync(timestamp, logLevel, rawMessage, exception).ConfigureAwait(false); } - private void LogInternal( - DateTimeOffset timestamp, - LogLevel logLevel, - string message, - Exception exception) + private async Task LogInternalAsync( + DateTimeOffset timestamp, + LogLevel logLevel, + string message, + Exception exception) { - string formatted = LogHelper.FormatMessageWithHeader( - _settings, - logLevel.FromLogLevel(), - message, - timestamp.DateTime, - _category); - - var writtenMessage = _provider.AddMessage(timestamp, formatted, _category); - var onLogEvent = _settings.OnLogEvent; - - if (onLogEvent != null) + try { - onLogEvent(new EonaCatLogMessage + string formatted = LogHelper.FormatMessageWithHeader( + _settings, + logLevel.FromLogLevel(), + message, + timestamp.DateTime, + _category); + + var writtenMessage = _provider.AddMessage(timestamp, formatted, _category); + var onLogEvent = _settings.OnLogEvent; + + if (onLogEvent != null) { - DateTime = timestamp.DateTime, - Message = writtenMessage, - LogType = logLevel.FromLogLevel(), - Category = _category, - Exception = exception, - Origin = string.IsNullOrWhiteSpace(_settings.LogOrigin) ? "BatchingLogger" : _settings.LogOrigin - }); + onLogEvent(new EonaCatLogMessage + { + DateTime = timestamp.DateTime, + Message = writtenMessage, + LogType = logLevel.FromLogLevel(), + Category = _category, + Exception = exception, + Origin = string.IsNullOrWhiteSpace(_settings.LogOrigin) ? "BatchingLogger" : _settings.LogOrigin + }); + } + + await _provider.WriteMessagesAsync(new List + { + new LogMessage + { + Timestamp = timestamp, + Message = formatted + } + }, CancellationToken.None).ConfigureAwait(false); + } + catch (Exception ex) + { + Console.WriteLine($"Logging error: {ex.Message}"); } } diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs index ed8f48c..39c4f0b 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs @@ -22,17 +22,17 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable protected BatchingLoggerProvider(IOptions options) { - var o = options.Value ?? throw new ArgumentNullException(nameof(options)); + var currentOptions = options.Value ?? throw new ArgumentNullException(nameof(options)); - if (o.FlushPeriod <= TimeSpan.Zero) + if (currentOptions.FlushPeriod <= TimeSpan.Zero) { - throw new ArgumentOutOfRangeException(nameof(o.FlushPeriod)); + throw new ArgumentOutOfRangeException(nameof(currentOptions.FlushPeriod)); } - _batchSize = o.BatchSize > 0 ? o.BatchSize : 100; + _batchSize = currentOptions.BatchSize > 0 ? currentOptions.BatchSize : 100; _queue = new BlockingCollection(new ConcurrentQueue()); - if (o is FileLoggerOptions file) + if (currentOptions is FileLoggerOptions file) { UseLocalTime = file.UseLocalTime; UseMask = file.UseMask; @@ -72,7 +72,7 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable public ILogger CreateLogger(string categoryName) => new BatchingLogger(this, categoryName, LoggerSettings); - protected abstract Task WriteMessagesAsync( + internal abstract Task WriteMessagesAsync( IReadOnlyList messages, CancellationToken token); @@ -99,7 +99,7 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable }; } - private async void ProcessLoop() + private async Task ProcessLoop() { var batch = new List(_batchSize); @@ -109,32 +109,45 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable { batch.Add(item); - if (batch.Count < _batchSize) + if (batch.Count >= _batchSize) { - continue; + await FlushBatchAsync(batch); } - - await WriteMessagesAsync(batch, _cts.Token).ConfigureAwait(false); - batch.Clear(); } if (batch.Count > 0) { - await WriteMessagesAsync(batch, _cts.Token).ConfigureAwait(false); + await FlushBatchAsync(batch); } } catch (OperationCanceledException) { - // normal shutdown + if (batch.Count > 0) + { + await FlushBatchAsync(batch); + } } catch (Exception ex) { - // last-resort logging Console.Error.WriteLine(ex); } } - + private async Task FlushBatchAsync(List batch) + { + try + { + await WriteMessagesAsync(batch, _cts.Token).ConfigureAwait(false); + } + catch (Exception ex) + { + Console.WriteLine("An error occurred while processing log batches.", ex); + } + finally + { + batch.Clear(); + } + } public void Dispose() { diff --git a/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs index 05e5ec8..ddd754b 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/TelegramLogger.cs @@ -48,7 +48,10 @@ namespace EonaCat.Logger.EonaCatCoreLogger public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) { - if (!IsEnabled(logLevel) || formatter == null) return; + if (!IsEnabled(logLevel) || formatter == null) + { + return; + } try { diff --git a/EonaCat.Logger/Managers/LogManager.cs b/EonaCat.Logger/Managers/LogManager.cs index 9ef12f7..69568bc 100644 --- a/EonaCat.Logger/Managers/LogManager.cs +++ b/EonaCat.Logger/Managers/LogManager.cs @@ -55,9 +55,23 @@ namespace EonaCat.Logger.Managers public ILogger Logger { get; private set; } public bool IsRunning { get; private set; } - public string CurrentLogFile => LoggerProvider is FileLoggerProvider fileLoggerProvider - ? fileLoggerProvider.LogFile - : string.Empty; + public string CurrentLogFile + { + get + { + if (LoggerProvider is FileLoggerProvider fileLoggerProvider) + { + // Ensure log file is initialized + if (string.IsNullOrEmpty(fileLoggerProvider.LogFile)) + { + fileLoggerProvider.InitializeCurrentFile(); + } + return fileLoggerProvider.LogFile; + } + + return string.Empty; + } + } private DateTime CurrentDateTime => Settings.UseLocalTime ? DateTime.Now : DateTime.UtcNow; @@ -104,11 +118,17 @@ namespace EonaCat.Logger.Managers return; } + if (!IsRunning) + { + await StartNewLogAsync().ConfigureAwait(false); + } + await InternalWriteAsync(CurrentDateTime, message, logType, writeToConsole, customSplunkSourceType, grayLogFacility, grayLogSource, grayLogVersion, disableSplunkSSL) .ConfigureAwait(false); } + public async Task StartNewLogAsync() { if (_isDisposing || _tokenSource.IsCancellationRequested) @@ -121,10 +141,17 @@ namespace EonaCat.Logger.Managers await StopLoggingAsync().ConfigureAwait(false); } - IsRunning = true; CreateLogger(); + + // Ensure log file exists Directory.CreateDirectory(Settings.FileLoggerOptions.LogDirectory); + if (LoggerProvider is FileLoggerProvider fileProvider) + { + fileProvider.InitializeCurrentFile(); + } + _logDate = CurrentDateTime; + IsRunning = true; } @@ -291,8 +318,8 @@ namespace EonaCat.Logger.Managers _tokenSource?.Dispose(); _tokenSource = null; - LoggerProvider?.Dispose(); - LoggerFactory?.Dispose(); + try { LoggerProvider?.Dispose(); } catch { } + try { LoggerFactory?.Dispose(); } catch { } } } diff --git a/Testers/EonaCat.Logger.Test.Web/Program.cs b/Testers/EonaCat.Logger.Test.Web/Program.cs index c9c4036..9d2170f 100644 --- a/Testers/EonaCat.Logger.Test.Web/Program.cs +++ b/Testers/EonaCat.Logger.Test.Web/Program.cs @@ -33,7 +33,7 @@ PatternDetectionInterval = TimeSpan.FromMinutes(3) }; - MemoryGuard.Start(_config); + //MemoryGuard.Start(_config); var builder = WebApplication.CreateBuilder(args); int onLogCounter = 0; @@ -94,6 +94,7 @@ // Create the adapter var adapter = new LogCentralEonaCatAdapter(logger.LoggerSettings, logClient); + await LogManager.Instance.WriteAsync("LogCentral adapter initialized", ELogType.INFO).ConfigureAwait(false); // Now all EonaCat.Logger logs will be sent to LogCentral automatically await logger.LogAsync("This is a test log message sent to LogCentral!", ELogType.INFO).ConfigureAwait(false);