Updated
This commit is contained in:
@@ -13,8 +13,8 @@
|
|||||||
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
<Copyright>EonaCat (Jeroen Saey)</Copyright>
|
||||||
<PackageTags>EonaCat;Logger;EonaCatLogger;Log;Writer;Jeroen;Saey</PackageTags>
|
<PackageTags>EonaCat;Logger;EonaCatLogger;Log;Writer;Jeroen;Saey</PackageTags>
|
||||||
<PackageIconUrl />
|
<PackageIconUrl />
|
||||||
<Version>1.7.4</Version>
|
<Version>1.7.5</Version>
|
||||||
<FileVersion>1.7.4</FileVersion>
|
<FileVersion>1.7.5</FileVersion>
|
||||||
<PackageReadmeFile>README.md</PackageReadmeFile>
|
<PackageReadmeFile>README.md</PackageReadmeFile>
|
||||||
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
<GenerateDocumentationFile>True</GenerateDocumentationFile>
|
||||||
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
<PackageLicenseFile>LICENSE</PackageLicenseFile>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<EVRevisionFormat>1.7.4+{chash:10}.{c:ymd}</EVRevisionFormat>
|
<EVRevisionFormat>1.7.5+{chash:10}.{c:ymd}</EVRevisionFormat>
|
||||||
<EVDefault>true</EVDefault>
|
<EVDefault>true</EVDefault>
|
||||||
<EVInfo>true</EVInfo>
|
<EVInfo>true</EVInfo>
|
||||||
<EVTagMatch>v[0-9]*</EVTagMatch>
|
<EVTagMatch>v[0-9]*</EVTagMatch>
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
using EonaCat.Logger;
|
using EonaCat.Logger;
|
||||||
using EonaCat.Logger.EonaCatCoreLogger;
|
using EonaCat.Logger.EonaCatCoreLogger;
|
||||||
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
||||||
using EonaCat.Logger.Managers;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using System;
|
using System;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
@@ -20,349 +18,149 @@ using System.Threading.Tasks;
|
|||||||
[ProviderAlias("EonaCatFileLogger")]
|
[ProviderAlias("EonaCatFileLogger")]
|
||||||
public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
||||||
{
|
{
|
||||||
private readonly string _path;
|
private const int BufferSize = 64 * 1024;
|
||||||
private readonly string _fileNamePrefix;
|
private const int ChannelCapacity = 8192;
|
||||||
private readonly int _maxFileSize;
|
private const int FlushThreshold = 48 * 1024;
|
||||||
private readonly int _maxRetainedFiles;
|
|
||||||
private readonly int _maxRolloverFiles;
|
|
||||||
|
|
||||||
private readonly byte[] _encryptionKey;
|
private static readonly UTF8Encoding Utf8 = new(false);
|
||||||
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 readonly Channel<LogMessage> _channel;
|
private readonly Channel<LogMessage> _channel;
|
||||||
private readonly Task _backgroundWorker;
|
private readonly Thread _writerThread;
|
||||||
|
|
||||||
private readonly ConcurrentBag<StringBuilder> _sbPool = new();
|
private readonly string _filePath;
|
||||||
private int _sbPoolCount;
|
private readonly int _maxFileSize;
|
||||||
|
private readonly bool _encryptionEnabled;
|
||||||
|
|
||||||
private Timer _flushTimer;
|
private readonly Aes _aes;
|
||||||
private readonly TimeSpan _flushInterval = TimeSpan.FromMilliseconds(500);
|
private readonly ICryptoTransform _encryptor;
|
||||||
|
|
||||||
private int _disposed;
|
|
||||||
private Aes _aes;
|
|
||||||
|
|
||||||
private readonly ThreadLocal<ICryptoTransform> _encryptor = new ThreadLocal<ICryptoTransform>(() => null, trackAllValues: true);
|
|
||||||
private readonly object _aesLock = new object();
|
|
||||||
|
|
||||||
public bool IncludeCorrelationId { get; }
|
public bool IncludeCorrelationId { get; }
|
||||||
public bool EnableCategoryRouting { 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 ELogType MinimumLogLevel { get; set; }
|
||||||
|
private readonly LoggerScopedContext _context = new();
|
||||||
public event EventHandler<ErrorMessage> OnError;
|
|
||||||
public event EventHandler<string> OnRollOver;
|
|
||||||
|
|
||||||
public string LogFile => _files.TryGetValue(string.Empty, out var state) ? state.FilePath : null;
|
|
||||||
|
|
||||||
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;
|
||||||
|
|
||||||
|
_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;
|
_maxFileSize = o.FileSizeLimit;
|
||||||
_maxRetainedFiles = o.RetainedFileCountLimit;
|
|
||||||
_maxRolloverFiles = o.MaxRolloverFiles;
|
_maxRolloverFiles = o.MaxRolloverFiles;
|
||||||
IncludeCorrelationId = o.IncludeCorrelationId;
|
|
||||||
EnableCategoryRouting = o.EnableCategoryRouting;
|
|
||||||
MinimumLogLevel = o.MinimumLogLevel;
|
|
||||||
|
|
||||||
_encryptionKey = o.EncryptionKey;
|
_encryptionEnabled = o.EncryptionKey != null && o.EncryptionIV != null;
|
||||||
_encryptionIV = o.EncryptionIV;
|
|
||||||
_isEncryptionEnabled = _encryptionKey != null && _encryptionIV != null;
|
|
||||||
|
|
||||||
if (_isEncryptionEnabled)
|
if (_encryptionEnabled)
|
||||||
{
|
{
|
||||||
_aes = Aes.Create();
|
_aes = Aes.Create();
|
||||||
_aes.Key = _encryptionKey;
|
_aes.Key = o.EncryptionKey;
|
||||||
_aes.IV = _encryptionIV;
|
_aes.IV = o.EncryptionIV;
|
||||||
|
_encryptor = _aes.CreateEncryptor();
|
||||||
}
|
}
|
||||||
|
|
||||||
var defaultState = CreateFileState(DateTime.UtcNow.Date, string.Empty);
|
IncludeCorrelationId = o.IncludeCorrelationId;
|
||||||
_files[string.Empty] = defaultState;
|
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,
|
SingleReader = true,
|
||||||
SingleWriter = false,
|
SingleWriter = false,
|
||||||
FullMode = BoundedChannelFullMode.Wait,
|
FullMode = BoundedChannelFullMode.Wait
|
||||||
AllowSynchronousContinuations = false
|
});
|
||||||
|
|
||||||
|
_writerThread = new Thread(WriterLoop)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Priority = ThreadPriority.AboveNormal
|
||||||
};
|
};
|
||||||
_channel = Channel.CreateBounded<LogMessage>(channelOptions);
|
|
||||||
|
|
||||||
_backgroundWorker = Task.Run(ProcessQueueAsync);
|
_writerThread.Start();
|
||||||
_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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
internal override Task WriteMessagesAsync(IReadOnlyList<LogMessage> messages, CancellationToken token)
|
internal override Task WriteMessagesAsync(IReadOnlyList<LogMessage> messages, CancellationToken token)
|
||||||
{
|
{
|
||||||
for (int i = 0; i < messages.Count; i++)
|
for (int i = 0; i < messages.Count; i++)
|
||||||
{
|
{
|
||||||
var msg = messages[i];
|
var message = messages[i];
|
||||||
if (msg.Level >= MinimumLogLevel)
|
if (message.Level >= MinimumLogLevel)
|
||||||
{
|
{
|
||||||
// TryWrite waits until it can write
|
while (!_channel.Writer.TryWrite(message))
|
||||||
_channel.Writer.TryWrite(msg);
|
{
|
||||||
|
Thread.SpinWait(1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Task.CompletedTask;
|
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)
|
|
||||||
{
|
FlushFinal();
|
||||||
// Expected on shutdown
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private async Task ProcessSingleMessageAsync(LogMessage msg)
|
private void FlushIfNeeded()
|
||||||
{
|
{
|
||||||
var key = EnableCategoryRouting ? GetOrCreateSanitizedCategory(msg.Category) : string.Empty;
|
if (_position >= FlushThreshold)
|
||||||
|
|
||||||
if (!_files.TryGetValue(key, out var state))
|
|
||||||
{
|
{
|
||||||
state = CreateFileState(DateTime.UtcNow.Date, key);
|
FlushInternal();
|
||||||
_files[key] = state;
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryRecover(state))
|
private void FlushInternal()
|
||||||
|
{
|
||||||
|
if (_position == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
await state.WriteLock.WaitAsync();
|
_stream.Write(_buffer, 0, _position);
|
||||||
try
|
_position = 0;
|
||||||
{
|
|
||||||
await WriteMessageAsync(state, msg);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
state.WriteLock.Release();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task WriteMessageAsync(FileState state, LogMessage msg)
|
private void FlushFinal()
|
||||||
{
|
{
|
||||||
var sb = RentStringBuilder();
|
FlushInternal();
|
||||||
|
_stream.Flush(true);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||||
private byte[] EncryptFast(byte[] plainBytes, int length)
|
private void WriteMessage(LogMessage msg)
|
||||||
{
|
{
|
||||||
var encryptor = _encryptor.Value;
|
StringBuilder sb = new StringBuilder();
|
||||||
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);
|
|
||||||
|
|
||||||
if (IncludeCorrelationId)
|
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;
|
final = _encryptor.TransformFinalBlock(temp, 0, bytes);
|
||||||
state.LastFailureUtc = DateTime.UtcNow;
|
length = final.Length;
|
||||||
|
ArrayPool<byte>.Shared.Return(temp, true);
|
||||||
state.Stream?.Dispose();
|
temp = null;
|
||||||
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}" });
|
|
||||||
}
|
}
|
||||||
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
|
File.Move(current, next);
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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";
|
File.Delete(sourceFile);
|
||||||
|
|
||||||
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(filePath);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
// Do nothing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
protected override async Task OnShutdownFlushAsync()
|
protected override Task OnShutdownFlushAsync()
|
||||||
{
|
{
|
||||||
if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0)
|
_running = false;
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_flushTimer?.Dispose();
|
|
||||||
_flushTimer = null;
|
|
||||||
|
|
||||||
try { _compressionCts.Cancel(); } catch { }
|
|
||||||
_channel.Writer.Complete();
|
_channel.Writer.Complete();
|
||||||
|
_writerThread.Join();
|
||||||
|
|
||||||
try
|
ArrayPool<byte>.Shared.Return(_buffer, true);
|
||||||
{
|
_stream.Dispose();
|
||||||
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();
|
|
||||||
_aes?.Dispose();
|
_aes?.Dispose();
|
||||||
_compressionCts.Dispose();
|
return Task.CompletedTask;
|
||||||
_compressionSemaphore.Dispose();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public new void Dispose()
|
public new void Dispose()
|
||||||
@@ -731,32 +337,4 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider, IDisposable
|
|||||||
OnShutdownFlushAsync().GetAwaiter().GetResult();
|
OnShutdownFlushAsync().GetAwaiter().GetResult();
|
||||||
base.Dispose();
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user