From 80d79af9cecccd76c2ab7dc767e79f21de92565d Mon Sep 17 00:00:00 2001 From: EonaCat Date: Thu, 12 Feb 2026 20:54:24 +0100 Subject: [PATCH] Updated --- .../EonaCatCoreLogger/FileLoggerOptions.cs | 6 + .../EonaCatCoreLogger/FileLoggerProvider.cs | 677 +++++++++++++----- .../Internal/BatchingLogger.cs | 2 +- .../Internal/BatchingLoggerProvider.cs | 17 +- .../EonaCatCoreLogger/Internal/LogMessage.cs | 3 + EonaCat.Logger/Managers/LoggerSettings.cs | 2 + 6 files changed, 518 insertions(+), 189 deletions(-) diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs index 9a02ce1..08b0998 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerOptions.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.IO; using EonaCat.Logger.EonaCatCoreLogger.Internal; using EonaCat.Logger.Managers; +using Microsoft.Extensions.Logging; namespace EonaCat.Logger.EonaCatCoreLogger; // This file is part of the EonaCat project(s) which is released under the Apache License. @@ -17,7 +18,12 @@ public class FileLoggerOptions : BatchingLoggerOptions private int _maxRolloverFiles = 10; private int _retainedFileCountLimit = 50; public bool EnableCategoryRouting { get; set; } + public ELogType MinimumLogLevel { get; set; } = ELogType.INFO; public string Category { get; set; } + public byte[] EncryptionKey { get; set; } + public byte[] EncryptionIV { get; set; } + + public bool IsEncryptionEnabled => EncryptionKey != null && EncryptionIV != null; public static string DefaultPath => AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory; diff --git a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs index 87f2db7..0904adf 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/FileLoggerProvider.cs @@ -1,4 +1,5 @@ -using EonaCat.Logger.EonaCatCoreLogger; +using EonaCat.Logger; +using EonaCat.Logger.EonaCatCoreLogger; using EonaCat.Logger.EonaCatCoreLogger.Internal; using EonaCat.Logger.Managers; using Microsoft.Extensions.Logging; @@ -21,69 +22,79 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable private readonly int _maxFileSize; private readonly int _maxRetainedFiles; private readonly int _maxRolloverFiles; - private bool _disposed; + private readonly List _compressionTasks = new(); - private readonly LoggerScopedContext _context = new LoggerScopedContext(); + private readonly byte[] _encryptionKey; + private readonly byte[] _encryptionIV; + public bool IsEncryptionEnabled => _encryptionKey != null && _encryptionIV != null; + + private bool _disposed; + private int _isFlushing; + + public static TimeSpan FaultCooldown = TimeSpan.FromSeconds(60); + + private readonly LoggerScopedContext _context = new(); private readonly ConcurrentDictionary _files = new(); private readonly ConcurrentDictionary> _messageQueues = new(); - private const int BufferSize = 1024 * 1024; // 1 MB buffer for large JSON logs + private const int BufferSize = 1024 * 1024; private static readonly Encoding Utf8 = new UTF8Encoding(false); public bool IncludeCorrelationId { get; } public bool EnableCategoryRouting { get; } - public string LogFile => _files.TryGetValue(string.Empty, out var state) ? state.FilePath : null; + 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 readonly Timer _flushTimer; private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(500); + private readonly string _fallbackPath; private sealed class FileState : IDisposable { public string FilePath; public long Size; public DateTime Date; + public byte[] Buffer = ArrayPool.Shared.Rent(BufferSize); public int BufferPosition; + public FileStream Stream; public SemaphoreSlim WriteLock = new(1, 1); + public bool IsFaulted; + public DateTime LastFailureUtc; + public void Dispose() { try { if (Buffer != null) { - ArrayPool.Shared.Return(Buffer); + Array.Clear(Buffer, 0, BufferPosition); + ArrayPool.Shared.Return(Buffer, clearArray: true); Buffer = null; } Stream?.Dispose(); - } - catch - { - // Do nothing - } - - try - { WriteLock?.Dispose(); } - catch + catch { // Do nothing } } } - public FileLoggerProvider(IOptions options) : base(options) { var o = options.Value ?? throw new ArgumentNullException(nameof(options)); + _path = o.LogDirectory; _fileNamePrefix = o.FileNamePrefix; _maxFileSize = o.FileSizeLimit; @@ -91,48 +102,75 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable _maxRolloverFiles = o.MaxRolloverFiles; IncludeCorrelationId = o.IncludeCorrelationId; EnableCategoryRouting = o.EnableCategoryRouting; + MinimumLogLevel = o.MinimumLogLevel; + _encryptionKey = o.EncryptionKey; + _encryptionIV = o.EncryptionIV; - Directory.CreateDirectory(_path); + _path = EnsureWritableDirectory(o.LogDirectory); + _fallbackPath = EnsureWritableDirectory(Path.Combine(Path.GetTempPath(), "EonaCatFallbackLogs")); var defaultState = CreateFileState(DateTime.UtcNow.Date, o.Category); _files[string.Empty] = defaultState; - // Periodic flush _flushTimer = new Timer(FlushTimerCallback, null, _flushInterval, _flushInterval); } - private void FlushTimerCallback(object state) + private static string EnsureWritableDirectory(string path) { + string fallback = Path.Combine(Path.GetTempPath(), "EonaCatFallbackLogs"); + + foreach (var dir in new[] { path, fallback }) + { + try + { + Directory.CreateDirectory(dir); + + // Test write permission + string testFile = Path.Combine(dir, $"write_test_{Guid.NewGuid()}.tmp"); + File.WriteAllText(testFile, "test"); + File.Delete(testFile); + + return dir; + } + catch + { + // Do nothing + } + } + try { - PeriodicFlushAsync().GetAwaiter().GetResult(); + Directory.CreateDirectory(fallback); } - catch + catch { - // swallow - avoid timer thread crash + // Do nothing } + + return fallback; } - private void CleanupUnusedCategories() + + private void FlushTimerCallback(object state) { - var now = DateTime.UtcNow; - - foreach (var kv in _files.ToArray()) + if (_disposed) { - var state = kv.Value; + return; + } - // Remove file states older than 2 days and empty queues - if ((now - state.Date).TotalDays > 2 && - _messageQueues.TryGetValue(kv.Key, out var queue) && - queue.IsEmpty) - { - if (_files.TryRemove(kv.Key, out var removed)) - { - removed.Dispose(); - } + if (Interlocked.Exchange(ref _isFlushing, 1) == 1) + { + return; + } - _messageQueues.TryRemove(kv.Key, out _); - } + try + { + PeriodicFlushAsync().ConfigureAwait(false); + } + catch { } + finally + { + Interlocked.Exchange(ref _isFlushing, 0); } } @@ -140,13 +178,15 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable { try { + var filtered = messages.Where(m => m.Level >= MinimumLogLevel).ToList(); + if (EnableCategoryRouting) { - var grouped = messages.GroupBy(m => SanitizeCategory(m.Category)); + // Group messages by sanitized category + var grouped = filtered.GroupBy(m => SanitizeCategory(m.Category)); foreach (var group in grouped) { - var key = group.Key; - var queue = _messageQueues.GetOrAdd(key, _ => new ConcurrentQueue()); + var queue = _messageQueues.GetOrAdd(group.Key, _ => new ConcurrentQueue()); foreach (var msg in group) { queue.Enqueue(msg); @@ -156,7 +196,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable else { var queue = _messageQueues.GetOrAdd(string.Empty, _ => new ConcurrentQueue()); - foreach (var msg in messages) + foreach (var msg in filtered) { queue.Enqueue(msg); } @@ -164,7 +204,11 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable } catch (Exception ex) { - OnError?.Invoke(this, new ErrorMessage { Exception = ex, Message = ex.Message }); + OnError?.Invoke(this, new ErrorMessage + { + Exception = ex, + Message = $"Failed to enqueue messages: {ex.Message}" + }); } return Task.CompletedTask; @@ -172,6 +216,11 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable private async Task PeriodicFlushAsync() { + if (_disposed) + { + return; + } + foreach (var kv in _messageQueues) { var key = kv.Key; @@ -183,170 +232,202 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable _files[key] = state; } - if (!queue.IsEmpty) + if (!TryRecover(state)) { - await state.WriteLock.WaitAsync(); - try + // drop to prevent memory leak + while (queue.TryDequeue(out _)) { } + continue; + } + + if (queue.IsEmpty) + { + continue; + } + + state.WriteLock.Wait(); + try + { + var batch = new List(256); + + while (queue.TryDequeue(out var msg)) { - while (queue.TryDequeue(out var msg)) + batch.Add(msg); + if (batch.Count >= 256) { - WriteBatch(state, new[] { msg }, key); + break; } } - finally + + if (batch.Count > 0) { - state.WriteLock.Release(); + await WriteBatchAsync(state, batch, key).ConfigureAwait(false); } } + finally + { + state.WriteLock.Release(); + } } - DeleteOldLogFiles(); - CleanupUnusedCategories(); + CompressOldLogFiles(); + CompressOldFilesByAge(7); } - private void WriteBatch(FileState state, IEnumerable messages, string categoryKey) - { - if (!File.Exists(state.FilePath)) + private void CompressOldLogFiles() + { + if (_maxRetainedFiles <= 0) { - RecreateFile(state, categoryKey); + return; } + var files = new DirectoryInfo(_path).GetFiles($"{_fileNamePrefix}*").OrderByDescending(f => f.LastWriteTimeUtc).Skip(_maxRetainedFiles); + foreach (var currentFile in files) + { + try + { + Task.Run(() => CompressOldLogFile(currentFile.FullName)); + } + catch + { + // Do nothing + } + } + } + + private void CompressOldFilesByAge(int daysThreshold) + { + var cutoff = DateTime.UtcNow.AddDays(-daysThreshold); + + var files = new DirectoryInfo(_path) + .GetFiles($"{_fileNamePrefix}*") + .Where(f => f.LastWriteTimeUtc < cutoff && !f.Name.EndsWith(".gz")); + + foreach (var file in files) + { + var task = Task.Run(() => CompressOldLogFile(file.FullName)); + _compressionTasks.Add(task); + } + } + + private async Task WriteBatchAsync(FileState state, List messages, string categoryKey) + { foreach (var msg in messages) { var date = msg.Timestamp.UtcDateTime.Date; if (state.Date != date) { - FlushBufferAsync(state).GetAwaiter().GetResult(); + await FlushBufferAsync(state).ConfigureAwait(false); RotateByDate(state, date, categoryKey); } - WriteMessageToBuffer(state, msg); + await WriteMessageToBufferAsync(state, msg).ConfigureAwait(false); if (state.BufferPosition >= BufferSize - 1024 || state.Size >= _maxFileSize) { - FlushBufferAsync(state).GetAwaiter().GetResult(); + await FlushBufferAsync(state).ConfigureAwait(false); if (state.Size >= _maxFileSize) { - RollOver(state, categoryKey); + RollOverAndCompressOldest(state, categoryKey); } } } - FlushBufferAsync(state).GetAwaiter().GetResult(); + await FlushBufferAsync(state).ConfigureAwait(false); } - private FileState CreateFileState(DateTime date, string category) + private async Task WriteMessageToBufferAsync(FileState state, LogMessage msg) { - var path = GetFullName(date, category); - - return new FileState - { - FilePath = path, - Date = date, - Size = GetFileSize(path), - Stream = OpenFileWithRetryAsync(path).GetAwaiter().GetResult() - }; - } - - private static async Task OpenFileWithRetryAsync(string path) - { - const int retries = 3; - for (int i = 0; i < retries; i++) + try { + string text; try { - return new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan); + text = BuildMessage(msg); } - catch + catch (Exception ex) { - await Task.Delay(5); + OnError?.Invoke(this, new ErrorMessage + { + Exception = ex, + Message = $"Failed to build log message: {msg.Message}" + }); + return; } - } - throw new IOException("Unable to open log file."); - } + var data = Utf8.GetBytes(text); - private void RecreateFile(FileState state, string category) - { - FlushBufferAsync(state).GetAwaiter().GetResult(); - state.Stream?.Dispose(); - - state.FilePath = GetFullName(DateTime.UtcNow.Date, category); - state.Size = 0; - state.BufferPosition = 0; - state.Stream = OpenFileWithRetryAsync(state.FilePath).GetAwaiter().GetResult(); - } - - private void RotateByDate(FileState state, DateTime newDate, string category) - { - state.Stream?.Dispose(); - - state.Date = newDate; - state.FilePath = GetFullName(newDate, category); - state.Size = GetFileSize(state.FilePath); - state.BufferPosition = 0; - state.Stream = OpenFileWithRetryAsync(state.FilePath).GetAwaiter().GetResult(); - } - - private void RollOver(FileState state, string category) - { - FlushBufferAsync(state).GetAwaiter().GetResult(); - state.Stream?.Dispose(); - - var dir = Path.GetDirectoryName(state.FilePath); - var name = Path.GetFileNameWithoutExtension(state.FilePath); - var ext = Path.GetExtension(state.FilePath); - - for (int i = _maxRolloverFiles - 1; i >= 1; i--) - { - var src = Path.Combine(dir, $"{name}.{i}{ext}"); - var dst = Path.Combine(dir, $"{name}.{i + 1}{ext}"); - - if (File.Exists(dst)) + if (IsEncryptionEnabled) { - File.Delete(dst); + data = Encrypt(data); } - if (File.Exists(src)) + // Flush buffer if not enough space + if (state.BufferPosition + data.Length > BufferSize) { - File.Move(src, dst); + await FlushBufferAsync(state).ConfigureAwait(false); } - } - var first = Path.Combine(dir, $"{name}.1{ext}"); - if (File.Exists(state.FilePath)) + // Copy to buffer safely + if (data.Length <= BufferSize) + { + Array.Copy(data, 0, state.Buffer, state.BufferPosition, data.Length); + state.BufferPosition += data.Length; + state.Size += data.Length; + } + + // Clear temporary data + Array.Clear(data, 0, data.Length); + } + catch (Exception ex) { - File.Move(state.FilePath, first); + HandleWriteFailure(state, ex); } - - OnRollOver?.Invoke(this, state.FilePath); - - state.Size = 0; - state.BufferPosition = 0; - state.Stream = OpenFileWithRetryAsync(state.FilePath).GetAwaiter().GetResult(); } - private void WriteMessageToBuffer(FileState state, LogMessage msg) + private byte[] Encrypt(byte[] plainBytes) { - var text = BuildMessage(msg); - var byteCount = Utf8.GetByteCount(text); + if (plainBytes == null || plainBytes.Length == 0) return plainBytes; - if (state.BufferPosition + byteCount > BufferSize) - { - FlushBufferAsync(state).GetAwaiter().GetResult(); - } + using var aes = System.Security.Cryptography.Aes.Create(); + aes.Key = _encryptionKey; + aes.IV = _encryptionIV; - var written = Utf8.GetBytes(text, 0, text.Length, state.Buffer, state.BufferPosition); - state.BufferPosition += written; - state.Size += written; + using var encryptor = aes.CreateEncryptor(); + var encrypted = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length); + + // Clear plaintext bytes + Array.Clear(plainBytes, 0, plainBytes.Length); + + return encrypted; + } + + public byte[] Decrypt(byte[] encryptedData) + { + if (!IsEncryptionEnabled || encryptedData == null || encryptedData.Length == 0) + return encryptedData; + + using var aes = System.Security.Cryptography.Aes.Create(); + aes.Key = _encryptionKey; + aes.IV = _encryptionIV; + + using var decryptor = aes.CreateDecryptor(); + using var ms = new MemoryStream(encryptedData); + using var cryptoStream = new System.Security.Cryptography.CryptoStream(ms, decryptor, System.Security.Cryptography.CryptoStreamMode.Read); + using var resultStream = new MemoryStream(); + cryptoStream.CopyTo(resultStream); + + var result = resultStream.ToArray(); + + // Clear sensitive memory + Array.Clear(encryptedData, 0, encryptedData.Length); + + return result; } private string BuildMessage(LogMessage msg) { - var settings = msg.Settings ?? LoggerSettings; - if (!IncludeCorrelationId) { return msg.Message + Environment.NewLine; @@ -358,55 +439,275 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable return msg.Message + Environment.NewLine; } - var sb = new StringBuilder(256); + var sb = new StringBuilder(msg.Message.Length + 64); sb.Append(msg.Message).Append(" ["); - bool first = true; - foreach (var kv in ctx) + foreach (var (key, value) in ctx.Select(kv => (kv.Key, kv.Value))) { - if (!first) - { - sb.Append(' '); - } + sb.Append(key).Append('=').Append(value).Append(' '); + } - sb.Append(kv.Key).Append('=').Append(kv.Value); - first = false; + if (msg.Tags != null) + { + foreach (var tag in msg.Tags) + { + sb.Append("tag=").Append(tag).Append(' '); + } + } + + if (sb[sb.Length - 1] == ' ') + { + sb.Length--; // remove trailing space } sb.Append(']').AppendLine(); return sb.ToString(); } - private async Task FlushBufferAsync(FileState state) - { - if (state.BufferPosition == 0 || state.Stream == null) - { - return; - } - await state.Stream.WriteAsync(state.Buffer, 0, state.BufferPosition); - await state.Stream.FlushAsync(); - state.BufferPosition = 0; + private async Task FlushBufferAsync(FileState state, CancellationToken token = default) + { + 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 leaking sensitive info + Array.Clear(state.Buffer, 0, state.BufferPosition); + state.BufferPosition = 0; + } } - private static long GetFileSize(string path) => File.Exists(path) ? new FileInfo(path).Length : 0; - private void DeleteOldLogFiles() + + private void HandleWriteFailure(FileState state, Exception ex) { - if (_maxRetainedFiles <= 0) + state.IsFaulted = true; + state.LastFailureUtc = DateTime.UtcNow; + + // Dispose current stream + state.Stream?.Dispose(); + state.Stream = null; + + // Determine a fallback path + string originalDir = Path.GetDirectoryName(state.FilePath); + string fallbackDir = EnsureWritableDirectory(originalDir); + string fileName = Path.GetFileName(state.FilePath); + string fallbackFile = Path.Combine(fallbackDir, fileName); + + try { + // Try to reopen the stream in the fallback directory + state.FilePath = fallbackFile; + state.Stream = new FileStream( + fallbackFile, + FileMode.Append, + FileAccess.Write, + FileShare.ReadWrite | FileShare.Delete + ); + + 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}" + }); + } + catch (Exception fallbackEx) + { + OnError?.Invoke(this, new ErrorMessage + { + Exception = fallbackEx, + Message = $"Failed to recover logging using fallback path: {fallbackFile}" + }); + } + } + + + + private bool TryRecover(FileState state) + { + if (!state.IsFaulted) + { + return true; + } + + if (DateTime.UtcNow - state.LastFailureUtc < FaultCooldown) + { + return false; + } + + try + { + state.Stream = new FileStream(state.FilePath, FileMode.Append, + FileAccess.Write, FileShare.ReadWrite | FileShare.Delete); + + state.Size = GetFileSize(state.FilePath); + state.IsFaulted = false; + + return true; + } + catch + { + state.LastFailureUtc = DateTime.UtcNow; + return false; + } + } + + private FileState CreateFileState(DateTime date, string category) + { + // Get the intended log file path + var intendedPath = GetFullName(date, category); + + // Ensure directory is writable (falls back automatically if needed) + var writableDir = EnsureWritableDirectory(Path.GetDirectoryName(intendedPath)); + var path = Path.Combine(writableDir, Path.GetFileName(intendedPath)); + + try + { + return new FileState + { + FilePath = path, + Date = date, + Size = GetFileSize(path), + Stream = new FileStream( + path, + FileMode.Append, + FileAccess.Write, + FileShare.ReadWrite | FileShare.Delete + ) + }; + } + catch (Exception ex) + { + OnError?.Invoke(this, new ErrorMessage + { + Exception = ex, + Message = $"Failed to create log file: {path}" + }); + + return new FileState + { + FilePath = path, + Date = date, + IsFaulted = true + }; + } + } + + private void RotateByDate(FileState state, DateTime newDate, string category) + { + state.Stream?.Dispose(); + state.Date = newDate; + state.FilePath = GetFullName(newDate, category); + state.Size = GetFileSize(state.FilePath); + state.Stream = new FileStream(state.FilePath, FileMode.Append, + FileAccess.Write, FileShare.ReadWrite | FileShare.Delete); + } + + private void RollOverAndCompressOldest(FileState state, string category) + { + state.Stream?.Dispose(); + + var dir = Path.GetDirectoryName(state.FilePath); + var name = Path.GetFileNameWithoutExtension(state.FilePath); + var ext = Path.GetExtension(state.FilePath); + + for (int i = _maxRolloverFiles - 1; i >= 1; i--) + { + var src = Path.Combine(dir, $"{name}.{i}{ext}"); + var dst = Path.Combine(dir, $"{name}.{i + 1}{ext}"); + if (File.Exists(dst)) + { + File.Delete(dst); + } + + if (File.Exists(src)) + { + File.Move(src, dst); + } + } + + var rolledFile = Path.Combine(dir, $"{name}.1{ext}"); + if (File.Exists(state.FilePath)) + { + File.Move(state.FilePath, rolledFile); + } + + OnRollOver?.Invoke(this, rolledFile); + + state.Size = 0; + state.Stream = new FileStream(state.FilePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete); + + // Compress the oldest rolled file safely + var oldestFile = Path.Combine(dir, $"{name}.{_maxRolloverFiles}{ext}"); + if (File.Exists(oldestFile)) + { + Task.Run(() => CompressOldLogFile(oldestFile)); + } + } + + + private static long GetFileSize(string path) + => File.Exists(path) ? new FileInfo(path).Length : 0; + + private void CompressOldLogFile(string filePath, int retryCount = 3) + { + if (filePath.EndsWith(".gz", StringComparison.OrdinalIgnoreCase)) return; - } - var files = new DirectoryInfo(_path) - .GetFiles($"{_fileNamePrefix}*") - .OrderByDescending(f => f.LastWriteTimeUtc) - .Skip(_maxRetainedFiles); - - foreach (var f in files) + Task.Run(async () => { - try { f.Delete(); } catch { } - } + for (int attemptRetry = 1; attemptRetry <= retryCount; attemptRetry++) + { + try + { + string compressedFile; + int suffix = 0; + do + { + string suffixText = suffix == 0 ? "" : $"_{suffix}"; + compressedFile = filePath + suffixText + ".gz"; + suffix++; + } while (File.Exists(compressedFile)); + + using var originalFileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read); + using var compressedFileStream = new FileStream(compressedFile, FileMode.CreateNew, FileAccess.Write); + using (var compressionStream = new System.IO.Compression.GZipStream(compressedFileStream, System.IO.Compression.CompressionLevel.Optimal)) + { + await originalFileStream.CopyToAsync(compressionStream).ConfigureAwait(false); + await compressionStream.FlushAsync().ConfigureAwait(false); + } + + File.Delete(filePath); + break; + } + catch (IOException) + { + await Task.Delay(100).ConfigureAwait(false); + } + catch (Exception ex) + { + OnError?.Invoke(this, new ErrorMessage + { + Exception = ex, + Message = $"Failed to compress log file: {filePath}" + }); + break; + } + } + }); } private string GetFullName(DateTime date, string category) @@ -429,18 +730,22 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable { category = category.Replace(c, '_'); } + return category.Replace('.', '_'); } - protected override void OnShutdownFlush() + protected override async Task OnShutdownFlushAsync() { + _disposed = true; _flushTimer?.Dispose(); + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + try { - PeriodicFlushAsync().GetAwaiter().GetResult(); + await PeriodicFlushAsync().ConfigureAwait(false); } - catch + catch { // Do nothing } @@ -449,9 +754,9 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable { try { - FlushBufferAsync(state).GetAwaiter().GetResult(); + await FlushBufferAsync(state, cts.Token).ConfigureAwait(false); } - catch + catch { // Do nothing } @@ -462,6 +767,16 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable _files.Clear(); _messageQueues.Clear(); - base.OnShutdownFlush(); + try + { + if (_compressionTasks.Count > 0) + { + await Task.WhenAny(Task.WhenAll(_compressionTasks), Task.Delay(TimeSpan.FromSeconds(5))); + } + } + catch + { + // Do nothing + } } } diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs index 1f2af09..8bedac6 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLogger.cs @@ -71,7 +71,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal timestamp.DateTime, category); - var writtenMessage = _provider.AddMessage(timestamp, formatted, category); + var writtenMessage = _provider.AddMessage(timestamp, formatted, category, logLevel.FromLogLevel()); effectiveSettings?.RaiseOnLog(new EonaCatLogMessage { diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs index 6f62385..89acf7e 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/BatchingLoggerProvider.cs @@ -1,4 +1,5 @@ -using EonaCat.Logger.EonaCatCoreLogger; +using EonaCat.Logger; +using EonaCat.Logger.EonaCatCoreLogger; using EonaCat.Logger.EonaCatCoreLogger.Internal; using EonaCat.Logger.Extensions; using EonaCat.Logger.Managers; @@ -67,9 +68,9 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable IReadOnlyList messages, CancellationToken token); - internal string AddMessage(DateTimeOffset timestamp, string message, string category) + internal string AddMessage(DateTimeOffset timestamp, string message, string category, ELogType logLevel, string[] tags = null) { - var log = CreateLogMessage(message, timestamp, category); + var log = CreateLogMessage(message, timestamp, category, logLevel, tags); var size = log.EstimatedSize; var newSize = Interlocked.Add(ref _currentQueueBytes, size); @@ -88,7 +89,7 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable } - private LogMessage CreateLogMessage(string message, DateTimeOffset ts, string category, LoggerSettings? settings = null) + private LogMessage CreateLogMessage(string message, DateTimeOffset ts, string category, ELogType logLevel, string[] tags = null, LoggerSettings? settings = null) { var effectiveSettings = settings ?? LoggerSettings; @@ -103,7 +104,9 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable Message = message, Timestamp = ts, Category = category, - Settings = effectiveSettings + Settings = effectiveSettings, + Level = logLevel, + Tags = tags, }; } @@ -171,12 +174,12 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable _cts.Cancel(); _worker.Join(); - OnShutdownFlush(); + OnShutdownFlushAsync().ConfigureAwait(false); _cts.Dispose(); } - protected virtual void OnShutdownFlush() + protected virtual async Task OnShutdownFlushAsync() { // default: Do nothing } diff --git a/EonaCat.Logger/EonaCatCoreLogger/Internal/LogMessage.cs b/EonaCat.Logger/EonaCatCoreLogger/Internal/LogMessage.cs index f2e30cb..d414dc9 100644 --- a/EonaCat.Logger/EonaCatCoreLogger/Internal/LogMessage.cs +++ b/EonaCat.Logger/EonaCatCoreLogger/Internal/LogMessage.cs @@ -1,4 +1,5 @@ using EonaCat.Logger.Managers; +using Microsoft.Extensions.Logging; using System; namespace EonaCat.Logger.EonaCatCoreLogger.Internal; @@ -12,4 +13,6 @@ public struct LogMessage public string Category { get; set; } public int EstimatedSize { get; set; } public LoggerSettings? Settings { get; set; } + public ELogType Level { get; set; } + public string[] Tags { get; set; } } \ No newline at end of file diff --git a/EonaCat.Logger/Managers/LoggerSettings.cs b/EonaCat.Logger/Managers/LoggerSettings.cs index a83718c..ceb4696 100644 --- a/EonaCat.Logger/Managers/LoggerSettings.cs +++ b/EonaCat.Logger/Managers/LoggerSettings.cs @@ -177,6 +177,8 @@ public class LoggerSettings if (_fileLoggerOptions == null) { _fileLoggerOptions = CreateDefaultFileLoggerOptions(); + _fileLoggerOptions.LoggerSettings = this; + _fileLoggerOptions.MinimumLogLevel = ELogType.INFO; } return _fileLoggerOptions;