This commit is contained in:
2026-02-16 06:21:59 +01:00
parent e13f7217ab
commit bb767e89f7
3 changed files with 181 additions and 756 deletions

View File

@@ -13,8 +13,8 @@
<Copyright>EonaCat (Jeroen Saey)</Copyright>
<PackageTags>EonaCat;Logger;EonaCatLogger;Log;Writer;Jeroen;Saey</PackageTags>
<PackageIconUrl />
<Version>1.7.4</Version>
<FileVersion>1.7.4</FileVersion>
<Version>1.7.5</Version>
<FileVersion>1.7.5</FileVersion>
<PackageReadmeFile>README.md</PackageReadmeFile>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<PackageLicenseFile>LICENSE</PackageLicenseFile>
@@ -25,7 +25,7 @@
</PropertyGroup>
<PropertyGroup>
<EVRevisionFormat>1.7.4+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVRevisionFormat>1.7.5+{chash:10}.{c:ymd}</EVRevisionFormat>
<EVDefault>true</EVDefault>
<EVInfo>true</EVInfo>
<EVTagMatch>v[0-9]*</EVTagMatch>

View File

@@ -1,12 +1,10 @@
using EonaCat.Logger;
using EonaCat.Logger.EonaCatCoreLogger;
using EonaCat.Logger.EonaCatCoreLogger.Internal;
using EonaCat.Logger.Managers;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Buffers;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
@@ -20,349 +18,149 @@ using System.Threading.Tasks;
[ProviderAlias("EonaCatFileLogger")]
public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
{
private readonly string _path;
private readonly string _fileNamePrefix;
private readonly int _maxFileSize;
private readonly int _maxRetainedFiles;
private readonly int _maxRolloverFiles;
private const int BufferSize = 64 * 1024;
private const int ChannelCapacity = 8192;
private const int FlushThreshold = 48 * 1024;
private readonly byte[] _encryptionKey;
private readonly byte[] _encryptionIV;
private readonly bool _isEncryptionEnabled;
private const int MaxQueueSize = 500;
private const int BufferSize = 16 * 1024;
private const int MaxStringBuilderPool = 4;
private const int MaxStringBuilderCapacity = 512;
private const int InitialStringBuilderCapacity = 256;
private const int MaxSanitizedCacheSize = 256;
private static readonly Encoding Utf8 = new UTF8Encoding(false);
private readonly LoggerScopedContext _context = new();
private readonly ConcurrentDictionary<string, FileState> _files = new(1, 4, StringComparer.Ordinal);
private readonly ConcurrentQueue<string> _compressionQueue = new();
private readonly SemaphoreSlim _compressionSemaphore = new(1, 1);
private readonly CancellationTokenSource _compressionCts = new();
private Task _compressionWorker;
private static readonly object _sanitizedCacheLock = new object();
private static readonly Dictionary<string, string> _sanitizedCache = new(StringComparer.Ordinal);
private static readonly Queue<string> _sanitizedCacheOrder = new();
private static readonly UTF8Encoding Utf8 = new(false);
private readonly Channel<LogMessage> _channel;
private readonly Task _backgroundWorker;
private readonly Thread _writerThread;
private readonly ConcurrentBag<StringBuilder> _sbPool = new();
private int _sbPoolCount;
private readonly string _filePath;
private readonly int _maxFileSize;
private readonly bool _encryptionEnabled;
private Timer _flushTimer;
private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(500);
private int _disposed;
private Aes _aes;
private readonly ThreadLocal<ICryptoTransform> _encryptor = new ThreadLocal<ICryptoTransform>(() => null, trackAllValues: true);
private readonly object _aesLock = new object();
private readonly Aes _aes;
private readonly ICryptoTransform _encryptor;
public bool IncludeCorrelationId { get; }
public bool EnableCategoryRouting { get; }
private FileStream _stream;
private byte[] _buffer;
private int _position;
private long _size;
public string LogFile => _filePath;
private volatile bool _running = true;
private readonly int _maxRolloverFiles;
public ELogType MinimumLogLevel { get; set; }
public event EventHandler<ErrorMessage> OnError;
public event EventHandler<string> OnRollOver;
public string LogFile => _files.TryGetValue(string.Empty, out var state) ? state.FilePath : null;
private readonly LoggerScopedContext _context = new();
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
{
var o = options.Value ?? throw new ArgumentNullException(nameof(options));
var o = options.Value;
_filePath = Path.Combine(o.LogDirectory,
$"{o.FileNamePrefix}_{Environment.MachineName}_{DateTime.UtcNow:yyyyMMdd}.log");
Directory.CreateDirectory(o.LogDirectory);
_path = EnsureWritableDirectory(o.LogDirectory);
_fileNamePrefix = o.FileNamePrefix;
_maxFileSize = o.FileSizeLimit;
_maxRetainedFiles = o.RetainedFileCountLimit;
_maxRolloverFiles = o.MaxRolloverFiles;
IncludeCorrelationId = o.IncludeCorrelationId;
EnableCategoryRouting = o.EnableCategoryRouting;
MinimumLogLevel = o.MinimumLogLevel;
_encryptionKey = o.EncryptionKey;
_encryptionIV = o.EncryptionIV;
_isEncryptionEnabled = _encryptionKey != null && _encryptionIV != null;
_encryptionEnabled = o.EncryptionKey != null && o.EncryptionIV != null;
if (_isEncryptionEnabled)
if (_encryptionEnabled)
{
_aes = Aes.Create();
_aes.Key = _encryptionKey;
_aes.IV = _encryptionIV;
_aes.Key = o.EncryptionKey;
_aes.IV = o.EncryptionIV;
_encryptor = _aes.CreateEncryptor();
}
var defaultState = CreateFileState(DateTime.UtcNow.Date, string.Empty);
_files[string.Empty] = defaultState;
IncludeCorrelationId = o.IncludeCorrelationId;
EnableCategoryRouting = o.EnableCategoryRouting;
var channelOptions = new BoundedChannelOptions(MaxQueueSize)
_stream = new FileStream(_filePath, FileMode.Append, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 1, FileOptions.WriteThrough | FileOptions.SequentialScan);
_size = _stream.Length;
_buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
_channel = Channel.CreateBounded<LogMessage>(
new BoundedChannelOptions(ChannelCapacity)
{
SingleReader = true,
SingleWriter = false,
FullMode = BoundedChannelFullMode.Wait,
AllowSynchronousContinuations = false
FullMode = BoundedChannelFullMode.Wait
});
_writerThread = new Thread(WriterLoop)
{
IsBackground = true,
Priority = ThreadPriority.AboveNormal
};
_channel = Channel.CreateBounded<LogMessage>(channelOptions);
_backgroundWorker = Task.Run(ProcessQueueAsync);
_flushTimer = new Timer(FlushTimerCallback, null, _flushInterval, _flushInterval);
_compressionWorker = Task.Run(() => CompressionWorkerAsync(_compressionCts.Token));
}
private string EnsureWritableDirectory(string path)
{
string fallback = Path.Combine(Path.GetTempPath(), "EonaCatFallbackLogs");
foreach (var dir in new[] { path, fallback })
{
try
{
Directory.CreateDirectory(dir);
string testFile = Path.Combine(dir, $"write_test_{Guid.NewGuid()}.tmp");
File.WriteAllText(testFile, "test");
File.Delete(testFile);
return dir;
}
catch { }
}
Directory.CreateDirectory(fallback);
return fallback;
}
private void FlushTimerCallback(object state)
{
if (_disposed == 1)
{
return;
}
// Synchronous flush on timer to avoid task buildup
foreach (var f in _files.Values)
{
FlushBufferSync(f);
}
_writerThread.Start();
}
internal override Task WriteMessagesAsync(IReadOnlyList<LogMessage> messages, CancellationToken token)
{
for (int i = 0; i < messages.Count; i++)
{
var msg = messages[i];
if (msg.Level >= MinimumLogLevel)
var message = messages[i];
if (message.Level >= MinimumLogLevel)
{
// TryWrite waits until it can write
_channel.Writer.TryWrite(msg);
while (!_channel.Writer.TryWrite(message))
{
Thread.SpinWait(1);
}
}
}
return Task.CompletedTask;
}
private async Task ProcessQueueAsync()
private void WriterLoop()
{
try
var reader = _channel.Reader;
var spin = new SpinWait();
while (_running || reader.Count > 0)
{
await foreach (var msg in _channel.Reader.ReadAllAsync())
while (reader.TryRead(out var message))
{
await ProcessSingleMessageAsync(msg);
WriteMessage(message);
}
FlushIfNeeded();
spin.SpinOnce();
}
catch (OperationCanceledException)
{
// Expected on shutdown
}
FlushFinal();
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private async Task ProcessSingleMessageAsync(LogMessage msg)
private void FlushIfNeeded()
{
var key = EnableCategoryRouting ? GetOrCreateSanitizedCategory(msg.Category) : string.Empty;
if (!_files.TryGetValue(key, out var state))
if (_position >= FlushThreshold)
{
state = CreateFileState(DateTime.UtcNow.Date, key);
_files[key] = state;
FlushInternal();
}
}
if (!TryRecover(state))
private void FlushInternal()
{
if (_position == 0)
{
return;
}
await state.WriteLock.WaitAsync();
try
{
await WriteMessageAsync(state, msg);
}
finally
{
state.WriteLock.Release();
}
_stream.Write(_buffer, 0, _position);
_position = 0;
}
private async Task WriteMessageAsync(FileState state, LogMessage msg)
private void FlushFinal()
{
var sb = RentStringBuilder();
try
{
BuildMessageInto(sb, msg);
var text = sb.ToString();
// Size the array precisely
int maxByteCount = Utf8.GetMaxByteCount(text.Length);
byte[] rentedBytes = ArrayPool<byte>.Shared.Rent(maxByteCount);
try
{
int actualByteCount = Utf8.GetBytes(text, 0, text.Length, rentedBytes, 0);
byte[] finalBytes = rentedBytes;
int finalLength = actualByteCount;
if (_isEncryptionEnabled)
{
byte[] encrypted = EncryptFast(rentedBytes, actualByteCount);
finalBytes = encrypted;
finalLength = encrypted.Length;
ArrayPool<byte>.Shared.Return(rentedBytes, clearArray: true);
rentedBytes = null;
}
if (state.Buffer == null)
{
state.Buffer = ArrayPool<byte>.Shared.Rent(BufferSize);
}
// Flush if needed
if (state.BufferPosition + finalLength > BufferSize)
{
await FlushBufferAsync(state).ConfigureAwait(false);
}
// Handle file size limit
if (_maxFileSize > 0 && state.Size + finalLength > _maxFileSize)
{
await FlushBufferAsync(state).ConfigureAwait(false);
RollOverAndCompressOldest(state, string.Empty);
}
// Write to buffer
Buffer.BlockCopy(finalBytes, 0, state.Buffer, state.BufferPosition, finalLength);
state.BufferPosition += finalLength;
state.Size += finalLength;
}
finally
{
if (rentedBytes != null)
{
ArrayPool<byte>.Shared.Return(rentedBytes, clearArray: true);
}
}
}
finally
{
ReturnStringBuilder(sb);
}
FlushInternal();
_stream.Flush(true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private byte[] EncryptFast(byte[] plainBytes, int length)
private void WriteMessage(LogMessage msg)
{
var encryptor = _encryptor.Value;
if (encryptor == null)
{
lock (_aesLock)
{
encryptor = _aes?.CreateEncryptor();
_encryptor.Value = encryptor;
}
}
return encryptor.TransformFinalBlock(plainBytes, 0, length);
}
private void FlushBufferSync(FileState state)
{
if (state.BufferPosition == 0 || state.Stream == null)
{
return;
}
try
{
state.Stream.Write(state.Buffer, 0, state.BufferPosition);
state.Stream.Flush();
}
catch (Exception ex)
{
HandleWriteFailure(state, ex);
}
finally
{
state.BufferPosition = 0;
}
}
private async Task FlushBufferAsync(FileState state)
{
if (state.BufferPosition == 0 || state.Stream == null)
{
return;
}
try
{
await state.Stream.WriteAsync(state.Buffer, 0, state.BufferPosition).ConfigureAwait(false);
await state.Stream.FlushAsync().ConfigureAwait(false);
}
catch (Exception ex)
{
HandleWriteFailure(state, ex);
}
finally
{
state.BufferPosition = 0;
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private StringBuilder RentStringBuilder()
{
if (_sbPool.TryTake(out var sb))
{
sb.Clear();
Interlocked.Decrement(ref _sbPoolCount);
return sb;
}
return new StringBuilder(InitialStringBuilderCapacity);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void ReturnStringBuilder(StringBuilder sb)
{
if (sb.Capacity > MaxStringBuilderCapacity)
{
return; // Don't pool oversized builders
}
if (_sbPoolCount < MaxStringBuilderPool)
{
_sbPool.Add(sb);
Interlocked.Increment(ref _sbPoolCount);
}
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void BuildMessageInto(StringBuilder sb, LogMessage msg)
{
sb.Clear();
sb.Append(msg.Message);
StringBuilder sb = new StringBuilder();
if (IncludeCorrelationId)
{
@@ -398,332 +196,140 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
}
}
sb.Append(Environment.NewLine);
string correlationId = null;
if (IncludeCorrelationId)
{
correlationId = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
_context.Set("CorrelationId", correlationId);
}
private void HandleWriteFailure(FileState state, Exception ex)
string text = sb.ToString() + ' ' + msg.Message + Environment.NewLine;
int max = Utf8.GetMaxByteCount(text.Length);
byte[] temp = ArrayPool<byte>.Shared.Rent(max);
int bytes = Utf8.GetBytes(text, 0, text.Length, temp, 0);
byte[] final = temp;
int length = bytes;
if (_encryptionEnabled)
{
state.IsFaulted = true;
state.LastFailureUtc = DateTime.UtcNow;
state.Stream?.Dispose();
state.Stream = null;
try
{
string fallbackFile = Path.Combine(_path, Path.GetFileName(state.FilePath));
state.Stream = new FileStream(
fallbackFile,
FileMode.Append,
FileAccess.Write,
FileShare.ReadWrite | FileShare.Delete,
4096,
FileOptions.Asynchronous | FileOptions.SequentialScan);
state.FilePath = fallbackFile;
state.Size = GetFileSize(fallbackFile);
state.IsFaulted = false;
OnError?.Invoke(this, new ErrorMessage { Exception = ex, Message = $"Switched to fallback path: {fallbackFile}" });
final = _encryptor.TransformFinalBlock(temp, 0, bytes);
length = final.Length;
ArrayPool<byte>.Shared.Return(temp, true);
temp = null;
}
catch (Exception fallbackEx)
if (_position + length > BufferSize)
{
OnError?.Invoke(this, new ErrorMessage { Exception = fallbackEx, Message = "Failed to recover logging using fallback path" });
FlushInternal();
}
Buffer.BlockCopy(final, 0, _buffer, _position, length);
_position += length;
_size += length;
if (_maxFileSize > 0 && _size >= _maxFileSize)
{
RollFile();
}
if (temp != null)
{
ArrayPool<byte>.Shared.Return(temp, true);
}
}
private bool TryRecover(FileState state)
private readonly object _rollLock = new();
private void RollFile()
{
if (!state.IsFaulted)
lock (_rollLock)
{
return true;
FlushInternal();
_stream?.Dispose();
_stream = null;
RotateCompressedFiles();
string tempArchive = _filePath + ".rolling";
File.Move(_filePath, tempArchive);
CompressToIndex(tempArchive, 1);
_stream = new FileStream(_filePath, FileMode.Create, FileAccess.Write, FileShare.ReadWrite | FileShare.Delete, 4096, FileOptions.SequentialScan);
_size = 0;
}
}
private void RotateCompressedFiles()
{
int maxFiles = _maxRolloverFiles;
string directory = Path.GetDirectoryName(_filePath)!;
string nameWithoutExt = Path.GetFileNameWithoutExtension(_filePath);
string extension = Path.GetExtension(_filePath); // .log
for (int i = maxFiles; i >= 1; i--)
{
string current = Path.Combine(directory, $"{nameWithoutExt}_{i}{extension}.gz");
if (!File.Exists(current))
{
continue;
}
if (DateTime.UtcNow - state.LastFailureUtc < TimeSpan.FromSeconds(60))
if (i == maxFiles)
{
return false;
File.Delete(current);
}
else
{
string next = Path.Combine(directory, $"{nameWithoutExt}_{i + 1}{extension}.gz");
if (File.Exists(next))
{
File.Delete(next);
}
try
{
state.Stream = new FileStream(
state.FilePath,
FileMode.Append,
FileAccess.Write,
FileShare.ReadWrite | FileShare.Delete,
4096,
FileOptions.Asynchronous | FileOptions.SequentialScan);
state.Size = GetFileSize(state.FilePath);
state.IsFaulted = false;
return true;
}
catch { return false; }
}
private FileState CreateFileState(DateTime date, string category)
{
var path = GetFullName(date, category);
try
{
return new FileState
{
FilePath = path,
Date = date,
Size = GetFileSize(path),
Buffer = ArrayPool<byte>.Shared.Rent(BufferSize),
Stream = new FileStream(
path,
FileMode.Append,
FileAccess.Write,
FileShare.ReadWrite | FileShare.Delete,
4096,
FileOptions.Asynchronous | FileOptions.SequentialScan),
WriteLock = new SemaphoreSlim(1, 1)
};
}
catch (Exception ex)
{
OnError?.Invoke(this, new ErrorMessage { Exception = ex, Message = $"Failed to create log file: {path}" });
return new FileState
{
FilePath = path,
Date = date,
Buffer = ArrayPool<byte>.Shared.Rent(BufferSize),
IsFaulted = true,
WriteLock = new SemaphoreSlim(1, 1)
};
}
}
private string GetFullName(DateTime date, string category)
{
var datePart = date.ToString("yyyyMMdd");
var machine = Environment.MachineName;
if (!EnableCategoryRouting || string.IsNullOrWhiteSpace(category))
{
return Path.Combine(_path, $"{_fileNamePrefix}_{machine}_{datePart}.log");
}
var safeCategory = GetOrCreateSanitizedCategory(category);
return Path.Combine(_path, $"{_fileNamePrefix}_{machine}_{safeCategory}_{datePart}.log");
}
private static string GetOrCreateSanitizedCategory(string category)
{
if (string.IsNullOrEmpty(category))
{
return string.Empty;
}
lock (_sanitizedCacheLock)
{
if (_sanitizedCache.TryGetValue(category, out var cached))
{
return cached;
}
var sanitized = SanitizeCategory(category);
// Simple LRU: evict oldest if cache exceeds max size
if (_sanitizedCache.Count >= MaxSanitizedCacheSize)
{
var oldestKey = _sanitizedCacheOrder.Dequeue();
_sanitizedCache.Remove(oldestKey);
}
_sanitizedCache[category] = sanitized;
_sanitizedCacheOrder.Enqueue(category);
return sanitized;
}
}
private static string SanitizeCategory(string category)
{
if (string.IsNullOrEmpty(category))
{
return category;
}
var chars = category.ToCharArray();
var invalid = Path.GetInvalidFileNameChars();
bool modified = false;
for (int i = 0; i < chars.Length; i++)
{
if (Array.IndexOf(invalid, chars[i]) >= 0 || chars[i] == '.')
{
chars[i] = '_';
modified = true;
}
}
return modified ? new string(chars) : category;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static long GetFileSize(string path) => File.Exists(path) ? new FileInfo(path).Length : 0;
private void RollOverAndCompressOldest(FileState state, string category)
{
if (state.Stream != null)
{
state.Stream.Flush();
state.Stream.Dispose();
state.Stream = null;
}
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}");
try
{
if (File.Exists(dst))
{
File.Delete(dst);
}
if (File.Exists(src))
{
File.Move(src, dst);
}
}
catch
{
// Ignore rollover failures
}
}
var rolledFile = Path.Combine(dir, $"{name}_1{ext}");
try
{
if (File.Exists(state.FilePath))
{
File.Move(state.FilePath, rolledFile);
}
OnRollOver?.Invoke(this, rolledFile);
}
catch
{
// Ignore rollover failures
}
state.Size = 0;
state.Stream = new FileStream(
state.FilePath,
FileMode.Create,
FileAccess.Write,
FileShare.ReadWrite | FileShare.Delete,
4096,
FileOptions.Asynchronous | FileOptions.SequentialScan);
var oldestFile = Path.Combine(dir, $"{name}_{_maxRolloverFiles}{ext}");
if (File.Exists(oldestFile))
{
_compressionQueue.Enqueue(oldestFile);
}
}
private async Task CompressionWorkerAsync(CancellationToken token)
{
while (!token.IsCancellationRequested)
{
try
{
await Task.Delay(1000, token).ConfigureAwait(false);
while (_compressionQueue.TryDequeue(out var file))
{
await _compressionSemaphore.WaitAsync(token);
try
{
await CompressOldLogFileAsync(file);
}
catch
{
// Silently fail compression
}
finally
{
_compressionSemaphore.Release();
}
}
}
catch (OperationCanceledException)
{
break;
File.Move(current, next);
}
}
}
private async Task CompressOldLogFileAsync(string filePath)
private void CompressToIndex(string sourceFile, int index)
{
if (!File.Exists(filePath) || filePath.EndsWith(".gz"))
string directory = Path.GetDirectoryName(_filePath)!;
string nameWithoutExtension = Path.GetFileNameWithoutExtension(_filePath);
string extension = Path.GetExtension(_filePath); // .log
// destination uses the same "_index" before .log + add .gz
string destination = Path.Combine(directory, $"{nameWithoutExtension}_{index}{extension}.gz");
using (var input = new FileStream(sourceFile, FileMode.Open, FileAccess.Read, FileShare.Read))
using (var output = new FileStream(destination, FileMode.Create, FileAccess.Write, FileShare.None))
using (var gzip = new GZipStream(output, CompressionLevel.Fastest))
{
return;
input.CopyTo(gzip);
}
var compressedFile = filePath + ".gz";
try
{
using (var original = new FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 8192, FileOptions.Asynchronous | FileOptions.SequentialScan))
using (var compressed = new FileStream(compressedFile, FileMode.Create, FileAccess.Write, FileShare.None, 8192, FileOptions.Asynchronous | FileOptions.SequentialScan))
using (var gzip = new GZipStream(compressed, CompressionLevel.Fastest)) // Fastest compression for lower memory usage
{
await original.CopyToAsync(gzip, 8192).ConfigureAwait(false);
File.Delete(sourceFile);
}
File.Delete(filePath);
}
catch
{
// Do nothing
}
}
protected override async Task OnShutdownFlushAsync()
protected override Task OnShutdownFlushAsync()
{
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
{
return;
}
_flushTimer?.Dispose();
_flushTimer = null;
try { _compressionCts.Cancel(); } catch { }
_running = false;
_channel.Writer.Complete();
_writerThread.Join();
try
{
await _backgroundWorker.ConfigureAwait(false);
}
catch
{
// Ignore
}
foreach (var state in _files.Values)
{
try { await FlushBufferAsync(state).ConfigureAwait(false); } catch { }
try { state.Dispose(); } catch { }
}
_files.Clear();
while (_sbPool.TryTake(out _)) { }
Interlocked.Exchange(ref _sbPoolCount, 0);
_encryptor.Dispose();
ArrayPool<byte>.Shared.Return(_buffer, true);
_stream.Dispose();
_aes?.Dispose();
_compressionCts.Dispose();
_compressionSemaphore.Dispose();
return Task.CompletedTask;
}
public new void Dispose()
@@ -731,32 +337,4 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
OnShutdownFlushAsync().GetAwaiter().GetResult();
base.Dispose();
}
private sealed class FileState : IDisposable
{
public string FilePath;
public long Size;
public DateTime Date;
public byte[] Buffer;
public int BufferPosition;
public FileStream Stream;
public SemaphoreSlim WriteLock;
public bool IsFaulted;
public DateTime LastFailureUtc;
public void Dispose()
{
Stream?.Dispose();
Stream = null;
if (Buffer != null)
{
ArrayPool<byte>.Shared.Return(Buffer, clearArray: true);
Buffer = null;
}
WriteLock?.Dispose();
WriteLock = null;
}
}
}

View File

@@ -1,153 +0,0 @@
using System.IO.Compression;
using EonaCat.Logger.EonaCatCoreLogger;
using EonaCat.Logger.Extensions;
using EonaCat.Logger.Managers;
namespace EonaCat.Logger.Test.Web;
public class Logger
{
private LogManager _logManager;
public LoggerSettings LoggerSettings { get; }
public bool UseLocalTime { get; set; }
public string LogFolder { get; set; } = Path.Combine(FileLoggerOptions.DefaultPath, "logs");
public string CurrentLogFile => _logManager.CurrentLogFile;
public bool IsDisabled { get; set; }
/// <summary>
/// Logger
/// </summary>
/// <param name="name"></param>
/// <param name="typesToLog"></param>
/// <param name="useLocalTime"></param>
/// <param name="maxFileSize"></param>
public Logger(string name = "EonaCatTestLogger", List<ELogType> typesToLog = null, bool useLocalTime = false, int maxFileSize = 20_000_000)
{
UseLocalTime = useLocalTime;
LoggerSettings = new LoggerSettings
{
Id = name,
TypesToLog = typesToLog,
UseLocalTime = UseLocalTime,
FileLoggerOptions =
{
LogDirectory = LogFolder,
FileSizeLimit = maxFileSize,
UseLocalTime = UseLocalTime,
},
};
}
public void DeleteCurrentLogFile()
{
if (IsDisabled)
{
return;
}
_logManager.DeleteCurrentLogFile();
}
private string ConvertToAbsolutePath(string path)
{
return Path.IsPathRooted(path) ? path : Path.Combine(LogFolder, path);
}
public async Task DownloadLogAsync(HttpContext context, string logName, long limit)
{
var logFileName = logName + ".log";
await using var fileStream = new FileStream(Path.Combine(ConvertToAbsolutePath(LogFolder), logFileName), FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete, 64 * 1024, true);
var response = context.Response;
response.ContentType = "text/plain";
response.Headers.ContentDisposition = "attachment;filename=" + logFileName;
if (limit > fileStream.Length || limit < 1)
{
limit = fileStream.Length;
}
var oFS = new OffsetStream(fileStream, 0, limit);
var request = context.Request;
Stream stream;
string acceptEncoding = request.Headers["Accept-Encoding"];
if (string.IsNullOrEmpty(acceptEncoding))
{
stream = response.Body;
}
else
{
var acceptEncodingParts = acceptEncoding.Split(new[] { ',' },
StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
if (acceptEncodingParts.Contains("br"))
{
response.Headers.ContentEncoding = "br";
stream = new BrotliStream(response.Body, CompressionMode.Compress);
}
else if (acceptEncodingParts.Contains("gzip"))
{
response.Headers.ContentEncoding = "gzip";
stream = new GZipStream(response.Body, CompressionMode.Compress);
}
else if (acceptEncodingParts.Contains("deflate"))
{
response.Headers.ContentEncoding = "deflate";
stream = new DeflateStream(response.Body, CompressionMode.Compress);
}
else
{
stream = response.Body;
}
}
await using (stream)
{
await oFS.CopyToAsync(stream).ConfigureAwait(false);
if (fileStream.Length > limit)
{
await stream.WriteAsync("\r\n####___TRUNCATED___####"u8.ToArray()).ConfigureAwait(false);
}
}
}
public async Task LogAsync(string message, ELogType logType = ELogType.INFO, bool writeToConsole = true)
{
if (IsDisabled)
{
return;
}
InitLogger();
await _logManager.WriteAsync(message, logType, writeToConsole).ConfigureAwait(false);
}
private void InitLogger()
{
if (_logManager == null)
{
// Initialize LogManager
_logManager = new LogManager(LoggerSettings);
_logManager.Settings.TypesToLog.Clear();
}
}
public async Task LogAsync(Exception exception, string message = "", bool writeToConsole = true)
{
if (IsDisabled)
{
return;
}
InitLogger();
if (LoggerSettings.TypesToLog.Contains(ELogType.ERROR))
{
await _logManager.WriteAsync(exception, message, writeToConsole: writeToConsole).ConfigureAwait(false);
}
}
}