Updated
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
||||||
using EonaCat.Logger.Managers;
|
using EonaCat.Logger.Managers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger;
|
namespace EonaCat.Logger.EonaCatCoreLogger;
|
||||||
// This file is part of the EonaCat project(s) which is released under the Apache License.
|
// 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 _maxRolloverFiles = 10;
|
||||||
private int _retainedFileCountLimit = 50;
|
private int _retainedFileCountLimit = 50;
|
||||||
public bool EnableCategoryRouting { get; set; }
|
public bool EnableCategoryRouting { get; set; }
|
||||||
|
public ELogType MinimumLogLevel { get; set; } = ELogType.INFO;
|
||||||
public string Category { get; set; }
|
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 =>
|
public static string DefaultPath =>
|
||||||
AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory;
|
AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using EonaCat.Logger.EonaCatCoreLogger;
|
using EonaCat.Logger;
|
||||||
|
using EonaCat.Logger.EonaCatCoreLogger;
|
||||||
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
||||||
using EonaCat.Logger.Managers;
|
using EonaCat.Logger.Managers;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
@@ -21,55 +22,65 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
private readonly int _maxFileSize;
|
private readonly int _maxFileSize;
|
||||||
private readonly int _maxRetainedFiles;
|
private readonly int _maxRetainedFiles;
|
||||||
private readonly int _maxRolloverFiles;
|
private readonly int _maxRolloverFiles;
|
||||||
private bool _disposed;
|
private readonly List<Task> _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<string, FileState> _files = new();
|
private readonly ConcurrentDictionary<string, FileState> _files = new();
|
||||||
private readonly ConcurrentDictionary<string, ConcurrentQueue<LogMessage>> _messageQueues = new();
|
private readonly ConcurrentDictionary<string, ConcurrentQueue<LogMessage>> _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);
|
private static readonly Encoding Utf8 = new UTF8Encoding(false);
|
||||||
|
|
||||||
public bool IncludeCorrelationId { get; }
|
public bool IncludeCorrelationId { get; }
|
||||||
public bool EnableCategoryRouting { 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<ErrorMessage> OnError;
|
public event EventHandler<ErrorMessage> OnError;
|
||||||
public event EventHandler<string> OnRollOver;
|
public event EventHandler<string> OnRollOver;
|
||||||
|
|
||||||
private readonly Timer _flushTimer;
|
private readonly Timer _flushTimer;
|
||||||
private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(500);
|
private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(500);
|
||||||
|
private readonly string _fallbackPath;
|
||||||
|
|
||||||
private sealed class FileState : IDisposable
|
private sealed class FileState : IDisposable
|
||||||
{
|
{
|
||||||
public string FilePath;
|
public string FilePath;
|
||||||
public long Size;
|
public long Size;
|
||||||
public DateTime Date;
|
public DateTime Date;
|
||||||
|
|
||||||
public byte[] Buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
|
public byte[] Buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
|
||||||
public int BufferPosition;
|
public int BufferPosition;
|
||||||
|
|
||||||
public FileStream Stream;
|
public FileStream Stream;
|
||||||
public SemaphoreSlim WriteLock = new(1, 1);
|
public SemaphoreSlim WriteLock = new(1, 1);
|
||||||
|
|
||||||
|
public bool IsFaulted;
|
||||||
|
public DateTime LastFailureUtc;
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (Buffer != null)
|
if (Buffer != null)
|
||||||
{
|
{
|
||||||
ArrayPool<byte>.Shared.Return(Buffer);
|
Array.Clear(Buffer, 0, BufferPosition);
|
||||||
|
ArrayPool<byte>.Shared.Return(Buffer, clearArray: true);
|
||||||
Buffer = null;
|
Buffer = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Stream?.Dispose();
|
Stream?.Dispose();
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
WriteLock?.Dispose();
|
WriteLock?.Dispose();
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -79,11 +90,11 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
|
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
|
||||||
{
|
{
|
||||||
var o = options.Value ?? throw new ArgumentNullException(nameof(options));
|
var o = options.Value ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
|
||||||
|
|
||||||
_path = o.LogDirectory;
|
_path = o.LogDirectory;
|
||||||
_fileNamePrefix = o.FileNamePrefix;
|
_fileNamePrefix = o.FileNamePrefix;
|
||||||
_maxFileSize = o.FileSizeLimit;
|
_maxFileSize = o.FileSizeLimit;
|
||||||
@@ -91,48 +102,75 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
_maxRolloverFiles = o.MaxRolloverFiles;
|
_maxRolloverFiles = o.MaxRolloverFiles;
|
||||||
IncludeCorrelationId = o.IncludeCorrelationId;
|
IncludeCorrelationId = o.IncludeCorrelationId;
|
||||||
EnableCategoryRouting = o.EnableCategoryRouting;
|
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);
|
var defaultState = CreateFileState(DateTime.UtcNow.Date, o.Category);
|
||||||
_files[string.Empty] = defaultState;
|
_files[string.Empty] = defaultState;
|
||||||
|
|
||||||
// Periodic flush
|
|
||||||
_flushTimer = new Timer(FlushTimerCallback, null, _flushInterval, _flushInterval);
|
_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
|
try
|
||||||
{
|
{
|
||||||
PeriodicFlushAsync().GetAwaiter().GetResult();
|
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
|
catch
|
||||||
{
|
{
|
||||||
// swallow - avoid timer thread crash
|
// Do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CleanupUnusedCategories()
|
try
|
||||||
{
|
{
|
||||||
var now = DateTime.UtcNow;
|
Directory.CreateDirectory(fallback);
|
||||||
|
}
|
||||||
foreach (var kv in _files.ToArray())
|
catch
|
||||||
{
|
{
|
||||||
var state = kv.Value;
|
// Do nothing
|
||||||
|
|
||||||
// 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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_messageQueues.TryRemove(kv.Key, out _);
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private void FlushTimerCallback(object state)
|
||||||
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Interlocked.Exchange(ref _isFlushing, 1) == 1)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
PeriodicFlushAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _isFlushing, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,13 +178,15 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var filtered = messages.Where(m => m.Level >= MinimumLogLevel).ToList();
|
||||||
|
|
||||||
if (EnableCategoryRouting)
|
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)
|
foreach (var group in grouped)
|
||||||
{
|
{
|
||||||
var key = group.Key;
|
var queue = _messageQueues.GetOrAdd(group.Key, _ => new ConcurrentQueue<LogMessage>());
|
||||||
var queue = _messageQueues.GetOrAdd(key, _ => new ConcurrentQueue<LogMessage>());
|
|
||||||
foreach (var msg in group)
|
foreach (var msg in group)
|
||||||
{
|
{
|
||||||
queue.Enqueue(msg);
|
queue.Enqueue(msg);
|
||||||
@@ -156,7 +196,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
else
|
else
|
||||||
{
|
{
|
||||||
var queue = _messageQueues.GetOrAdd(string.Empty, _ => new ConcurrentQueue<LogMessage>());
|
var queue = _messageQueues.GetOrAdd(string.Empty, _ => new ConcurrentQueue<LogMessage>());
|
||||||
foreach (var msg in messages)
|
foreach (var msg in filtered)
|
||||||
{
|
{
|
||||||
queue.Enqueue(msg);
|
queue.Enqueue(msg);
|
||||||
}
|
}
|
||||||
@@ -164,7 +204,11 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
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;
|
return Task.CompletedTask;
|
||||||
@@ -172,6 +216,11 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
|
|
||||||
private async Task PeriodicFlushAsync()
|
private async Task PeriodicFlushAsync()
|
||||||
{
|
{
|
||||||
|
if (_disposed)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
foreach (var kv in _messageQueues)
|
foreach (var kv in _messageQueues)
|
||||||
{
|
{
|
||||||
var key = kv.Key;
|
var key = kv.Key;
|
||||||
@@ -183,14 +232,35 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
_files[key] = state;
|
_files[key] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!queue.IsEmpty)
|
if (!TryRecover(state))
|
||||||
{
|
{
|
||||||
await state.WriteLock.WaitAsync();
|
// drop to prevent memory leak
|
||||||
|
while (queue.TryDequeue(out _)) { }
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queue.IsEmpty)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WriteLock.Wait();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var batch = new List<LogMessage>(256);
|
||||||
|
|
||||||
while (queue.TryDequeue(out var msg))
|
while (queue.TryDequeue(out var msg))
|
||||||
{
|
{
|
||||||
WriteBatch(state, new[] { msg }, key);
|
batch.Add(msg);
|
||||||
|
if (batch.Count >= 256)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batch.Count > 0)
|
||||||
|
{
|
||||||
|
await WriteBatchAsync(state, batch, key).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -198,155 +268,166 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
state.WriteLock.Release();
|
state.WriteLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CompressOldLogFiles();
|
||||||
|
CompressOldFilesByAge(7);
|
||||||
}
|
}
|
||||||
|
|
||||||
DeleteOldLogFiles();
|
private void CompressOldLogFiles()
|
||||||
CleanupUnusedCategories();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteBatch(FileState state, IEnumerable<LogMessage> messages, string categoryKey)
|
|
||||||
{
|
{
|
||||||
if (!File.Exists(state.FilePath))
|
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<LogMessage> messages, string categoryKey)
|
||||||
|
{
|
||||||
foreach (var msg in messages)
|
foreach (var msg in messages)
|
||||||
{
|
{
|
||||||
var date = msg.Timestamp.UtcDateTime.Date;
|
var date = msg.Timestamp.UtcDateTime.Date;
|
||||||
|
|
||||||
if (state.Date != date)
|
if (state.Date != date)
|
||||||
{
|
{
|
||||||
FlushBufferAsync(state).GetAwaiter().GetResult();
|
await FlushBufferAsync(state).ConfigureAwait(false);
|
||||||
RotateByDate(state, date, categoryKey);
|
RotateByDate(state, date, categoryKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
WriteMessageToBuffer(state, msg);
|
await WriteMessageToBufferAsync(state, msg).ConfigureAwait(false);
|
||||||
|
|
||||||
if (state.BufferPosition >= BufferSize - 1024 || state.Size >= _maxFileSize)
|
if (state.BufferPosition >= BufferSize - 1024 || state.Size >= _maxFileSize)
|
||||||
{
|
{
|
||||||
FlushBufferAsync(state).GetAwaiter().GetResult();
|
await FlushBufferAsync(state).ConfigureAwait(false);
|
||||||
|
|
||||||
if (state.Size >= _maxFileSize)
|
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<FileStream> OpenFileWithRetryAsync(string path)
|
|
||||||
{
|
|
||||||
const int retries = 3;
|
|
||||||
for (int i = 0; i < retries; i++)
|
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.Asynchronous | FileOptions.SequentialScan);
|
string text;
|
||||||
}
|
try
|
||||||
catch
|
|
||||||
{
|
{
|
||||||
await Task.Delay(5);
|
text = BuildMessage(msg);
|
||||||
}
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
|
|
||||||
throw new IOException("Unable to open log file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RecreateFile(FileState state, string category)
|
|
||||||
{
|
{
|
||||||
FlushBufferAsync(state).GetAwaiter().GetResult();
|
OnError?.Invoke(this, new ErrorMessage
|
||||||
state.Stream?.Dispose();
|
{
|
||||||
|
Exception = ex,
|
||||||
state.FilePath = GetFullName(DateTime.UtcNow.Date, category);
|
Message = $"Failed to build log message: {msg.Message}"
|
||||||
state.Size = 0;
|
});
|
||||||
state.BufferPosition = 0;
|
return;
|
||||||
state.Stream = OpenFileWithRetryAsync(state.FilePath).GetAwaiter().GetResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RotateByDate(FileState state, DateTime newDate, string category)
|
var data = Utf8.GetBytes(text);
|
||||||
{
|
|
||||||
state.Stream?.Dispose();
|
|
||||||
|
|
||||||
state.Date = newDate;
|
if (IsEncryptionEnabled)
|
||||||
state.FilePath = GetFullName(newDate, category);
|
{
|
||||||
state.Size = GetFileSize(state.FilePath);
|
data = Encrypt(data);
|
||||||
state.BufferPosition = 0;
|
|
||||||
state.Stream = OpenFileWithRetryAsync(state.FilePath).GetAwaiter().GetResult();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void RollOver(FileState state, string category)
|
// Flush buffer if not enough space
|
||||||
|
if (state.BufferPosition + data.Length > BufferSize)
|
||||||
{
|
{
|
||||||
FlushBufferAsync(state).GetAwaiter().GetResult();
|
await FlushBufferAsync(state).ConfigureAwait(false);
|
||||||
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))
|
// Copy to buffer safely
|
||||||
|
if (data.Length <= BufferSize)
|
||||||
{
|
{
|
||||||
File.Move(src, dst);
|
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)
|
||||||
|
{
|
||||||
|
HandleWriteFailure(state, ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var first = Path.Combine(dir, $"{name}.1{ext}");
|
private byte[] Encrypt(byte[] plainBytes)
|
||||||
if (File.Exists(state.FilePath))
|
|
||||||
{
|
{
|
||||||
File.Move(state.FilePath, first);
|
if (plainBytes == null || plainBytes.Length == 0) return plainBytes;
|
||||||
|
|
||||||
|
using var aes = System.Security.Cryptography.Aes.Create();
|
||||||
|
aes.Key = _encryptionKey;
|
||||||
|
aes.IV = _encryptionIV;
|
||||||
|
|
||||||
|
using var encryptor = aes.CreateEncryptor();
|
||||||
|
var encrypted = encryptor.TransformFinalBlock(plainBytes, 0, plainBytes.Length);
|
||||||
|
|
||||||
|
// Clear plaintext bytes
|
||||||
|
Array.Clear(plainBytes, 0, plainBytes.Length);
|
||||||
|
|
||||||
|
return encrypted;
|
||||||
}
|
}
|
||||||
|
|
||||||
OnRollOver?.Invoke(this, state.FilePath);
|
public byte[] Decrypt(byte[] encryptedData)
|
||||||
|
|
||||||
state.Size = 0;
|
|
||||||
state.BufferPosition = 0;
|
|
||||||
state.Stream = OpenFileWithRetryAsync(state.FilePath).GetAwaiter().GetResult();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void WriteMessageToBuffer(FileState state, LogMessage msg)
|
|
||||||
{
|
{
|
||||||
var text = BuildMessage(msg);
|
if (!IsEncryptionEnabled || encryptedData == null || encryptedData.Length == 0)
|
||||||
var byteCount = Utf8.GetByteCount(text);
|
return encryptedData;
|
||||||
|
|
||||||
if (state.BufferPosition + byteCount > BufferSize)
|
using var aes = System.Security.Cryptography.Aes.Create();
|
||||||
{
|
aes.Key = _encryptionKey;
|
||||||
FlushBufferAsync(state).GetAwaiter().GetResult();
|
aes.IV = _encryptionIV;
|
||||||
}
|
|
||||||
|
|
||||||
var written = Utf8.GetBytes(text, 0, text.Length, state.Buffer, state.BufferPosition);
|
using var decryptor = aes.CreateDecryptor();
|
||||||
state.BufferPosition += written;
|
using var ms = new MemoryStream(encryptedData);
|
||||||
state.Size += written;
|
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)
|
private string BuildMessage(LogMessage msg)
|
||||||
{
|
{
|
||||||
var settings = msg.Settings ?? LoggerSettings;
|
|
||||||
|
|
||||||
if (!IncludeCorrelationId)
|
if (!IncludeCorrelationId)
|
||||||
{
|
{
|
||||||
return msg.Message + Environment.NewLine;
|
return msg.Message + Environment.NewLine;
|
||||||
@@ -358,55 +439,275 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
return msg.Message + Environment.NewLine;
|
return msg.Message + Environment.NewLine;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb = new StringBuilder(256);
|
var sb = new StringBuilder(msg.Message.Length + 64);
|
||||||
sb.Append(msg.Message).Append(" [");
|
sb.Append(msg.Message).Append(" [");
|
||||||
|
|
||||||
bool first = true;
|
foreach (var (key, value) in ctx.Select(kv => (kv.Key, kv.Value)))
|
||||||
foreach (var kv in ctx)
|
|
||||||
{
|
{
|
||||||
if (!first)
|
sb.Append(key).Append('=').Append(value).Append(' ');
|
||||||
{
|
|
||||||
sb.Append(' ');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.Append(kv.Key).Append('=').Append(kv.Value);
|
if (msg.Tags != null)
|
||||||
first = false;
|
{
|
||||||
|
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();
|
sb.Append(']').AppendLine();
|
||||||
return sb.ToString();
|
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);
|
private async Task FlushBufferAsync(FileState state, CancellationToken token = default)
|
||||||
await state.Stream.FlushAsync();
|
{
|
||||||
|
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;
|
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;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
var files = new DirectoryInfo(_path)
|
Task.Run(async () =>
|
||||||
.GetFiles($"{_fileNamePrefix}*")
|
|
||||||
.OrderByDescending(f => f.LastWriteTimeUtc)
|
|
||||||
.Skip(_maxRetainedFiles);
|
|
||||||
|
|
||||||
foreach (var f in files)
|
|
||||||
{
|
{
|
||||||
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)
|
private string GetFullName(DateTime date, string category)
|
||||||
@@ -429,16 +730,20 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
{
|
{
|
||||||
category = category.Replace(c, '_');
|
category = category.Replace(c, '_');
|
||||||
}
|
}
|
||||||
|
|
||||||
return category.Replace('.', '_');
|
return category.Replace('.', '_');
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override void OnShutdownFlush()
|
protected override async Task OnShutdownFlushAsync()
|
||||||
{
|
{
|
||||||
|
_disposed = true;
|
||||||
_flushTimer?.Dispose();
|
_flushTimer?.Dispose();
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
PeriodicFlushAsync().GetAwaiter().GetResult();
|
await PeriodicFlushAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -449,7 +754,7 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
FlushBufferAsync(state).GetAwaiter().GetResult();
|
await FlushBufferAsync(state, cts.Token).ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -462,6 +767,16 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
_files.Clear();
|
_files.Clear();
|
||||||
_messageQueues.Clear();
|
_messageQueues.Clear();
|
||||||
|
|
||||||
base.OnShutdownFlush();
|
try
|
||||||
|
{
|
||||||
|
if (_compressionTasks.Count > 0)
|
||||||
|
{
|
||||||
|
await Task.WhenAny(Task.WhenAll(_compressionTasks), Task.Delay(TimeSpan.FromSeconds(5)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Do nothing
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal
|
|||||||
timestamp.DateTime,
|
timestamp.DateTime,
|
||||||
category);
|
category);
|
||||||
|
|
||||||
var writtenMessage = _provider.AddMessage(timestamp, formatted, category);
|
var writtenMessage = _provider.AddMessage(timestamp, formatted, category, logLevel.FromLogLevel());
|
||||||
|
|
||||||
effectiveSettings?.RaiseOnLog(new EonaCatLogMessage
|
effectiveSettings?.RaiseOnLog(new EonaCatLogMessage
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using EonaCat.Logger.EonaCatCoreLogger;
|
using EonaCat.Logger;
|
||||||
|
using EonaCat.Logger.EonaCatCoreLogger;
|
||||||
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
||||||
using EonaCat.Logger.Extensions;
|
using EonaCat.Logger.Extensions;
|
||||||
using EonaCat.Logger.Managers;
|
using EonaCat.Logger.Managers;
|
||||||
@@ -67,9 +68,9 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
|||||||
IReadOnlyList<LogMessage> messages,
|
IReadOnlyList<LogMessage> messages,
|
||||||
CancellationToken token);
|
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 size = log.EstimatedSize;
|
||||||
|
|
||||||
var newSize = Interlocked.Add(ref _currentQueueBytes, size);
|
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;
|
var effectiveSettings = settings ?? LoggerSettings;
|
||||||
|
|
||||||
@@ -103,7 +104,9 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
|||||||
Message = message,
|
Message = message,
|
||||||
Timestamp = ts,
|
Timestamp = ts,
|
||||||
Category = category,
|
Category = category,
|
||||||
Settings = effectiveSettings
|
Settings = effectiveSettings,
|
||||||
|
Level = logLevel,
|
||||||
|
Tags = tags,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -171,12 +174,12 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
|||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
_worker.Join();
|
_worker.Join();
|
||||||
|
|
||||||
OnShutdownFlush();
|
OnShutdownFlushAsync().ConfigureAwait(false);
|
||||||
|
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected virtual void OnShutdownFlush()
|
protected virtual async Task OnShutdownFlushAsync()
|
||||||
{
|
{
|
||||||
// default: Do nothing
|
// default: Do nothing
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using EonaCat.Logger.Managers;
|
using EonaCat.Logger.Managers;
|
||||||
|
using Microsoft.Extensions.Logging;
|
||||||
using System;
|
using System;
|
||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Internal;
|
namespace EonaCat.Logger.EonaCatCoreLogger.Internal;
|
||||||
@@ -12,4 +13,6 @@ public struct LogMessage
|
|||||||
public string Category { get; set; }
|
public string Category { get; set; }
|
||||||
public int EstimatedSize { get; set; }
|
public int EstimatedSize { get; set; }
|
||||||
public LoggerSettings? Settings { get; set; }
|
public LoggerSettings? Settings { get; set; }
|
||||||
|
public ELogType Level { get; set; }
|
||||||
|
public string[] Tags { get; set; }
|
||||||
}
|
}
|
||||||
@@ -177,6 +177,8 @@ public class LoggerSettings
|
|||||||
if (_fileLoggerOptions == null)
|
if (_fileLoggerOptions == null)
|
||||||
{
|
{
|
||||||
_fileLoggerOptions = CreateDefaultFileLoggerOptions();
|
_fileLoggerOptions = CreateDefaultFileLoggerOptions();
|
||||||
|
_fileLoggerOptions.LoggerSettings = this;
|
||||||
|
_fileLoggerOptions.MinimumLogLevel = ELogType.INFO;
|
||||||
}
|
}
|
||||||
|
|
||||||
return _fileLoggerOptions;
|
return _fileLoggerOptions;
|
||||||
|
|||||||
Reference in New Issue
Block a user