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.6.4</Version>
|
<Version>1.6.5</Version>
|
||||||
<FileVersion>1.6.4</FileVersion>
|
<FileVersion>1.6.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.6.4+{chash:10}.{c:ymd}</EVRevisionFormat>
|
<EVRevisionFormat>1.6.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>
|
||||||
|
|||||||
@@ -43,26 +43,11 @@ public static class FileLoggerFactoryExtensions
|
|||||||
fileLoggerOptions.FileNamePrefix = filenamePrefix;
|
fileLoggerOptions.FileNamePrefix = filenamePrefix;
|
||||||
}
|
}
|
||||||
|
|
||||||
builder.AddEonaCatFileLogger(options =>
|
builder = builder.AddEonaCatFileLogger(options =>
|
||||||
{
|
{
|
||||||
options.FileNamePrefix = fileLoggerOptions.FileNamePrefix;
|
LoggerConfigurator.ApplyFileLoggerSettings(options, fileLoggerOptions);
|
||||||
options.FlushPeriod = fileLoggerOptions.FlushPeriod;
|
});
|
||||||
options.RetainedFileCountLimit = fileLoggerOptions.RetainedFileCountLimit;
|
|
||||||
options.MaxWriteTries = fileLoggerOptions.MaxWriteTries;
|
|
||||||
options.FileSizeLimit = fileLoggerOptions.FileSizeLimit;
|
|
||||||
options.LogDirectory = fileLoggerOptions.LogDirectory;
|
|
||||||
options.BatchSize = fileLoggerOptions.BatchSize;
|
|
||||||
options.FileSizeLimit = fileLoggerOptions.FileSizeLimit;
|
|
||||||
options.IsEnabled = fileLoggerOptions.IsEnabled;
|
|
||||||
options.MaxRolloverFiles = fileLoggerOptions.MaxRolloverFiles;
|
|
||||||
options.UseLocalTime = fileLoggerOptions.UseLocalTime;
|
|
||||||
options.UseMask = fileLoggerOptions.UseMask;
|
|
||||||
options.Mask = fileLoggerOptions.Mask;
|
|
||||||
options.UseDefaultMasking = fileLoggerOptions.UseDefaultMasking;
|
|
||||||
options.MaskedKeywords = fileLoggerOptions.MaskedKeywords;
|
|
||||||
options.IncludeCorrelationId = fileLoggerOptions.IncludeCorrelationId;
|
|
||||||
}
|
|
||||||
);
|
|
||||||
return builder;
|
return builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,8 @@ public class FileLoggerOptions : BatchingLoggerOptions
|
|||||||
private int _fileSizeLimit = 200 * 1024 * 1024;
|
private int _fileSizeLimit = 200 * 1024 * 1024;
|
||||||
private int _maxRolloverFiles = 10;
|
private int _maxRolloverFiles = 10;
|
||||||
private int _retainedFileCountLimit = 50;
|
private int _retainedFileCountLimit = 50;
|
||||||
|
public bool EnableCategoryRouting { get; set; }
|
||||||
|
public string Category { get; set; }
|
||||||
|
|
||||||
public static string DefaultPath =>
|
public static string DefaultPath =>
|
||||||
AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory;
|
AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory;
|
||||||
|
|||||||
@@ -1,4 +1,9 @@
|
|||||||
using System;
|
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.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
@@ -6,12 +11,6 @@ using System.Linq;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
|
||||||
using EonaCat.Logger.Managers;
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
using Microsoft.Extensions.Options;
|
|
||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger;
|
|
||||||
|
|
||||||
[ProviderAlias("EonaCatFileLogger")]
|
[ProviderAlias("EonaCatFileLogger")]
|
||||||
public sealed class FileLoggerProvider : BatchingLoggerProvider
|
public sealed class FileLoggerProvider : BatchingLoggerProvider
|
||||||
@@ -22,212 +21,371 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
|||||||
private readonly int _maxRetainedFiles;
|
private readonly int _maxRetainedFiles;
|
||||||
private readonly int _maxRolloverFiles;
|
private readonly int _maxRolloverFiles;
|
||||||
|
|
||||||
private string _logFile;
|
private readonly LoggerScopedContext _context = new LoggerScopedContext();
|
||||||
private long _currentFileSize;
|
private readonly Dictionary<string, FileState> _files = new Dictionary<string, FileState>();
|
||||||
private Timer _flushTimer;
|
private static readonly ConcurrentDictionary<string, SemaphoreSlim> _fileLocks = new ConcurrentDictionary<string, SemaphoreSlim>();
|
||||||
private readonly ConcurrentDictionary<string, StringBuilder> _buffers = new();
|
|
||||||
private readonly ConcurrentDictionary<string, long> _fileSizes = new();
|
|
||||||
private readonly SemaphoreSlim _writeLock = new(1, 1);
|
|
||||||
private readonly SemaphoreSlim _rolloverLock = new(1, 1);
|
|
||||||
|
|
||||||
private readonly LoggerScopedContext _context = new();
|
// High-performance buffer pool
|
||||||
|
private static readonly ConcurrentBag<byte[]> _bufferPool = new ConcurrentBag<byte[]>();
|
||||||
|
private const int BufferSize = 256 * 1024; // 256KB buffers
|
||||||
|
private const int MaxPoolSize = 100;
|
||||||
|
|
||||||
public string LogFile => _logFile ?? string.Empty;
|
// Pre-allocated StringBuilder pool for low memory
|
||||||
|
private static readonly ConcurrentBag<StringBuilder> _stringBuilderPool = new ConcurrentBag<StringBuilder>();
|
||||||
|
private const int MaxStringBuilderPoolSize = 50;
|
||||||
|
|
||||||
|
public bool IncludeCorrelationId { get; }
|
||||||
|
public bool EnableCategoryRouting { get; }
|
||||||
|
|
||||||
public event EventHandler<ErrorMessage> OnError;
|
public event EventHandler<ErrorMessage> OnError;
|
||||||
public event EventHandler<string> OnRollOver;
|
public event EventHandler<string> OnRollOver;
|
||||||
|
|
||||||
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
|
public string LogFile
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
FileState state;
|
||||||
|
return _files.TryGetValue(string.Empty, out state) ? state.FilePath : string.Empty;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class FileState
|
||||||
|
{
|
||||||
|
public string FilePath;
|
||||||
|
public long Size;
|
||||||
|
public DateTime Date;
|
||||||
|
public readonly SemaphoreSlim WriteLock = new SemaphoreSlim(1, 1);
|
||||||
|
|
||||||
|
// Buffered writing for high throughput
|
||||||
|
public byte[] Buffer;
|
||||||
|
public int BufferPosition;
|
||||||
|
public FileStream Stream;
|
||||||
|
}
|
||||||
|
|
||||||
|
public FileLoggerProvider(IOptions<FileLoggerOptions> options)
|
||||||
|
: base(options)
|
||||||
{
|
{
|
||||||
var o = options.Value;
|
var o = options.Value;
|
||||||
|
if (o == null)
|
||||||
|
{
|
||||||
|
throw new ArgumentNullException("options");
|
||||||
|
}
|
||||||
|
|
||||||
_path = o.LogDirectory;
|
_path = o.LogDirectory;
|
||||||
_fileNamePrefix = o.FileNamePrefix;
|
_fileNamePrefix = o.FileNamePrefix;
|
||||||
_maxFileSize = o.FileSizeLimit;
|
_maxFileSize = o.FileSizeLimit;
|
||||||
_maxRetainedFiles = o.RetainedFileCountLimit;
|
_maxRetainedFiles = o.RetainedFileCountLimit;
|
||||||
_maxRolloverFiles = o.MaxRolloverFiles;
|
_maxRolloverFiles = o.MaxRolloverFiles;
|
||||||
IncludeCorrelationId = o.IncludeCorrelationId;
|
IncludeCorrelationId = o.IncludeCorrelationId;
|
||||||
|
EnableCategoryRouting = o.EnableCategoryRouting;
|
||||||
Directory.CreateDirectory(_path);
|
|
||||||
InitializeCurrentFile();
|
|
||||||
_flushTimer = new Timer(_ => FlushAllAsync().GetAwaiter().GetResult(), null, 5000, 5000);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task FlushAllAsync()
|
|
||||||
{
|
|
||||||
foreach (var kv in _buffers)
|
|
||||||
{
|
|
||||||
await FlushAsync(kv.Key, kv.Value, CancellationToken.None);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool IncludeCorrelationId { get; }
|
|
||||||
|
|
||||||
internal override async Task WriteMessagesAsync(
|
|
||||||
IReadOnlyList<LogMessage> messages,
|
|
||||||
CancellationToken token)
|
|
||||||
{
|
|
||||||
if (messages.Count == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Directory.CreateDirectory(_path);
|
Directory.CreateDirectory(_path);
|
||||||
|
|
||||||
// Group messages by date
|
// Initialize
|
||||||
foreach (var group in messages.GroupBy(m => (m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day)))
|
var defaultState = CreateFileState(DateTime.UtcNow.Date, options.Value.Category);
|
||||||
{
|
_files[string.Empty] = defaultState;
|
||||||
var file = GetFullName(group.Key);
|
|
||||||
InitializeFile(file);
|
|
||||||
|
|
||||||
var stringBuilder = _buffers.GetOrAdd(file, _ => new StringBuilder(4096));
|
|
||||||
|
|
||||||
lock (stringBuilder)
|
|
||||||
{
|
|
||||||
foreach (var message in group)
|
|
||||||
{
|
|
||||||
AppendMessage(stringBuilder, message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await FlushAsync(file, stringBuilder, token).ConfigureAwait(false);
|
|
||||||
DeleteOldLogFiles();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void AppendMessage(StringBuilder sb, LogMessage msg)
|
internal override async Task WriteMessagesAsync(IReadOnlyList<LogMessage> messages, CancellationToken token)
|
||||||
{
|
{
|
||||||
// Ensure correlation id exists (once per async context)
|
// Group messages by category for batch processing
|
||||||
if (IncludeCorrelationId)
|
if (EnableCategoryRouting)
|
||||||
{
|
{
|
||||||
var cid = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
var grouped = messages.GroupBy(m => SanitizeCategory(m.Category));
|
||||||
_context.Set("CorrelationId", cid);
|
foreach (var group in grouped)
|
||||||
}
|
|
||||||
|
|
||||||
sb.Append(msg.Message);
|
|
||||||
|
|
||||||
var ctx = _context.GetAll();
|
|
||||||
if (ctx.Count > 0)
|
|
||||||
{
|
|
||||||
sb.Append(" [");
|
|
||||||
bool first = true;
|
|
||||||
foreach (var kv in ctx)
|
|
||||||
{
|
{
|
||||||
if (!first)
|
var categoryKey = group.Key;
|
||||||
|
|
||||||
|
FileState state;
|
||||||
|
if (!_files.TryGetValue(categoryKey, out state))
|
||||||
{
|
{
|
||||||
sb.Append(' ');
|
var date = DateTime.UtcNow.Date;
|
||||||
|
state = CreateFileState(date, categoryKey);
|
||||||
|
_files[categoryKey] = state;
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.Append(kv.Key).Append('=').Append(kv.Value);
|
await WriteBatchAsync(state, group, categoryKey, token);
|
||||||
first = false;
|
|
||||||
}
|
}
|
||||||
sb.Append(']');
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var categoryKey = string.Empty;
|
||||||
|
|
||||||
|
FileState state;
|
||||||
|
if (!_files.TryGetValue(categoryKey, out state))
|
||||||
|
{
|
||||||
|
var date = DateTime.UtcNow.Date;
|
||||||
|
state = CreateFileState(date, categoryKey);
|
||||||
|
_files[categoryKey] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
await WriteBatchAsync(state, messages, categoryKey, token);
|
||||||
}
|
}
|
||||||
|
|
||||||
sb.AppendLine();
|
DeleteOldLogFiles();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void InitializeCurrentFile()
|
private async Task WriteBatchAsync(FileState state, IEnumerable<LogMessage> messages, string categoryKey, CancellationToken token)
|
||||||
{
|
{
|
||||||
if (!string.IsNullOrEmpty(_logFile))
|
await state.WriteLock.WaitAsync(token);
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_logFile = GetFullName((DateTime.Now.Year, DateTime.Now.Month, DateTime.Now.Day));
|
|
||||||
_currentFileSize = File.Exists(_logFile) ? new FileInfo(_logFile).Length : 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task FlushAsync(string file, StringBuilder stringBuilder, CancellationToken token)
|
|
||||||
{
|
|
||||||
if (stringBuilder.Length == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await _writeLock.WaitAsync(token).ConfigureAwait(false);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var text = stringBuilder.ToString();
|
foreach (var msg in messages)
|
||||||
byte[] bytesToWrite = Encoding.UTF8.GetBytes(text);
|
|
||||||
|
|
||||||
using (var fileStream = new FileStream(
|
|
||||||
file,
|
|
||||||
FileMode.Append,
|
|
||||||
FileAccess.Write,
|
|
||||||
FileShare.Read,
|
|
||||||
64 * 1024,
|
|
||||||
useAsync: true))
|
|
||||||
{
|
{
|
||||||
await fileStream.WriteAsync(bytesToWrite, 0, bytesToWrite.Length, token);
|
if (token.IsCancellationRequested)
|
||||||
|
break;
|
||||||
|
|
||||||
|
var date = msg.Timestamp.UtcDateTime.Date;
|
||||||
|
|
||||||
|
// Rotate file by date
|
||||||
|
if (state.Date != date)
|
||||||
|
{
|
||||||
|
await FlushBufferAsync(state);
|
||||||
|
RotateByDate(state, date, categoryKey);
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteMessageToBuffer(state, msg);
|
||||||
|
|
||||||
|
// Flush buffer if nearly full or file size limit reached
|
||||||
|
if (state.BufferPosition >= BufferSize - 1024 || state.Size >= _maxFileSize)
|
||||||
|
{
|
||||||
|
await FlushBufferAsync(state);
|
||||||
|
|
||||||
|
if (state.Size >= _maxFileSize)
|
||||||
|
{
|
||||||
|
RollOver(state, categoryKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update current file size correctly
|
// Final flush for this batch
|
||||||
_fileSizes.AddOrUpdate(file, bytesToWrite.Length, (_, old) => old + bytesToWrite.Length);
|
await FlushBufferAsync(state);
|
||||||
_currentFileSize = _fileSizes[file];
|
}
|
||||||
|
catch (Exception ex)
|
||||||
stringBuilder.Clear();
|
{
|
||||||
|
if (OnError != null)
|
||||||
if (_fileSizes[file] >= _maxFileSize)
|
|
||||||
{
|
{
|
||||||
await RollOverAsync(file).ConfigureAwait(false);
|
OnError.Invoke(this, new ErrorMessage { Exception = ex, Message = ex.Message });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_writeLock.Release();
|
state.WriteLock.Release();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task RollOverAsync(string file)
|
private FileState CreateFileState(DateTime date, string category)
|
||||||
|
{
|
||||||
|
var path = GetFullName(date, category);
|
||||||
|
|
||||||
|
var buffer = GetBuffer();
|
||||||
|
var stream = new FileStream(
|
||||||
|
path,
|
||||||
|
FileMode.Append,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.ReadWrite,
|
||||||
|
BufferSize,
|
||||||
|
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||||
|
|
||||||
|
var state = new FileState
|
||||||
|
{
|
||||||
|
Stream = stream,
|
||||||
|
FilePath = path,
|
||||||
|
Size = GetFileSize(path),
|
||||||
|
Date = date,
|
||||||
|
Buffer = buffer,
|
||||||
|
BufferPosition = 0
|
||||||
|
};
|
||||||
|
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] GetBuffer()
|
||||||
|
{
|
||||||
|
byte[] buffer;
|
||||||
|
if (!_bufferPool.TryTake(out buffer))
|
||||||
|
{
|
||||||
|
buffer = new byte[BufferSize];
|
||||||
|
}
|
||||||
|
return buffer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReturnBuffer(byte[] buffer)
|
||||||
|
{
|
||||||
|
if (_bufferPool.Count < MaxPoolSize)
|
||||||
|
{
|
||||||
|
Array.Clear(buffer, 0, buffer.Length);
|
||||||
|
_bufferPool.Add(buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static StringBuilder GetStringBuilder()
|
||||||
|
{
|
||||||
|
StringBuilder sb;
|
||||||
|
if (!_stringBuilderPool.TryTake(out sb))
|
||||||
|
{
|
||||||
|
sb = new StringBuilder(512);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
sb.Clear();
|
||||||
|
}
|
||||||
|
return sb;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReturnStringBuilder(StringBuilder sb)
|
||||||
|
{
|
||||||
|
if (_stringBuilderPool.Count < MaxStringBuilderPoolSize && sb.Capacity <= 2048)
|
||||||
|
{
|
||||||
|
sb.Clear();
|
||||||
|
_stringBuilderPool.Add(sb);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long GetFileSize(string path)
|
||||||
{
|
{
|
||||||
await _rolloverLock.WaitAsync().ConfigureAwait(false);
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var dir = Path.GetDirectoryName(file);
|
return File.Exists(path) ? new FileInfo(path).Length : 0;
|
||||||
var name = Path.GetFileNameWithoutExtension(file);
|
}
|
||||||
var ext = Path.GetExtension(file);
|
catch
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for (int i = _maxRolloverFiles - 1; i >= 1; i--)
|
private void RotateByDate(FileState state, DateTime newDate, string category)
|
||||||
|
{
|
||||||
|
if (state.Stream != null)
|
||||||
|
{
|
||||||
|
state.Stream.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Date = newDate;
|
||||||
|
state.FilePath = GetFullName(newDate, category);
|
||||||
|
state.Size = GetFileSize(state.FilePath);
|
||||||
|
state.BufferPosition = 0;
|
||||||
|
|
||||||
|
state.Stream = new FileStream(
|
||||||
|
state.FilePath,
|
||||||
|
FileMode.Append,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.ReadWrite,
|
||||||
|
BufferSize,
|
||||||
|
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RollOver(FileState state, string category)
|
||||||
|
{
|
||||||
|
if (state.Stream != null)
|
||||||
|
{
|
||||||
|
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, string.Format("{0}.{1}{2}", name, i, ext));
|
||||||
|
var dst = Path.Combine(dir, string.Format("{0}.{1}{2}", name, i + 1, ext));
|
||||||
|
|
||||||
|
if (File.Exists(dst))
|
||||||
{
|
{
|
||||||
var source = Path.Combine(dir, $"{name}.{i}{ext}");
|
File.Delete(dst);
|
||||||
var destination = Path.Combine(dir, $"{name}.{i + 1}{ext}");
|
}
|
||||||
|
|
||||||
if (File.Exists(destination))
|
if (File.Exists(src))
|
||||||
{
|
{
|
||||||
File.Delete(destination);
|
File.Move(src, dst);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (File.Exists(source))
|
var first = Path.Combine(dir, string.Format("{0}.1{1}", name, ext));
|
||||||
|
if (File.Exists(state.FilePath))
|
||||||
|
{
|
||||||
|
File.Move(state.FilePath, first);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (OnRollOver != null)
|
||||||
|
{
|
||||||
|
OnRollOver.Invoke(this, state.FilePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Size = 0;
|
||||||
|
state.BufferPosition = 0;
|
||||||
|
|
||||||
|
state.Stream = new FileStream(
|
||||||
|
state.FilePath,
|
||||||
|
FileMode.Append,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.ReadWrite,
|
||||||
|
BufferSize,
|
||||||
|
FileOptions.Asynchronous | FileOptions.SequentialScan);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WriteMessageToBuffer(FileState state, LogMessage msg)
|
||||||
|
{
|
||||||
|
var sb = GetStringBuilder();
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
sb.Append(msg.Message);
|
||||||
|
|
||||||
|
if (IncludeCorrelationId)
|
||||||
|
{
|
||||||
|
var ctx = _context.GetAll();
|
||||||
|
if (ctx.Count > 0)
|
||||||
{
|
{
|
||||||
File.Move(source, destination);
|
sb.Append(" [");
|
||||||
|
bool first = true;
|
||||||
|
foreach (var kv in ctx)
|
||||||
|
{
|
||||||
|
if (!first)
|
||||||
|
{
|
||||||
|
sb.Append(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
sb.Append(kv.Key).Append('=').Append(kv.Value);
|
||||||
|
first = false;
|
||||||
|
}
|
||||||
|
sb.Append(']');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var firstRoll = Path.Combine(dir, $"{name}.1{ext}");
|
sb.AppendLine();
|
||||||
if (File.Exists(file))
|
|
||||||
|
var text = sb.ToString();
|
||||||
|
var byteCount = Encoding.UTF8.GetByteCount(text);
|
||||||
|
|
||||||
|
// If message won't fit in buffer, flush first
|
||||||
|
if (state.BufferPosition + byteCount > BufferSize)
|
||||||
{
|
{
|
||||||
File.Move(file, firstRoll);
|
FlushBufferAsync(state).Wait();
|
||||||
}
|
}
|
||||||
|
|
||||||
_fileSizes[file] = 0;
|
// Write to buffer
|
||||||
|
var written = Encoding.UTF8.GetBytes(text, 0, text.Length, state.Buffer, state.BufferPosition);
|
||||||
|
state.BufferPosition += written;
|
||||||
|
state.Size += written;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_rolloverLock.Release();
|
ReturnStringBuilder(sb);
|
||||||
OnRollOver?.Invoke(this, file);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InitializeFile(string file)
|
private async Task FlushBufferAsync(FileState state)
|
||||||
{
|
{
|
||||||
_fileSizes.TryAdd(file, File.Exists(file) ? new FileInfo(file).Length : 0);
|
if (state.BufferPosition == 0 || state.Stream == null)
|
||||||
_buffers.TryAdd(file, new StringBuilder(4096));
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
private string GetFullName((int Year, int Month, int Day) g) =>
|
await state.Stream.WriteAsync(state.Buffer, 0, state.BufferPosition);
|
||||||
string.IsNullOrWhiteSpace(_fileNamePrefix)
|
await state.Stream.FlushAsync();
|
||||||
? Path.Combine(_path, $"{Environment.MachineName}_{g.Year:0000}{g.Month:00}{g.Day:00}.log")
|
state.BufferPosition = 0;
|
||||||
: Path.Combine(_path, $"{_fileNamePrefix}_{Environment.MachineName}_{g.Year:0000}{g.Month:00}{g.Day:00}.log");
|
}
|
||||||
|
|
||||||
private void DeleteOldLogFiles()
|
private void DeleteOldLogFiles()
|
||||||
{
|
{
|
||||||
@@ -237,19 +395,17 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
|||||||
}
|
}
|
||||||
|
|
||||||
var files = new DirectoryInfo(_path)
|
var files = new DirectoryInfo(_path)
|
||||||
.GetFiles($"{_fileNamePrefix}*")
|
.GetFiles(string.Format("{0}*", _fileNamePrefix))
|
||||||
.OrderByDescending(f =>
|
.OrderByDescending(f =>
|
||||||
{
|
{
|
||||||
// Parse date from filename instead of CreationTimeUtc
|
|
||||||
var name = Path.GetFileNameWithoutExtension(f.Name);
|
var name = Path.GetFileNameWithoutExtension(f.Name);
|
||||||
var parts = name.Split('_');
|
var parts = name.Split('_');
|
||||||
var datePart = parts.Length > 1 ? parts[1] : parts[0];
|
var datePart = parts.LastOrDefault();
|
||||||
if (DateTime.TryParseExact(datePart, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out var dt))
|
DateTime dt;
|
||||||
{
|
return DateTime.TryParseExact(datePart, "yyyyMMdd", null,
|
||||||
return dt;
|
System.Globalization.DateTimeStyles.None, out dt)
|
||||||
}
|
? dt
|
||||||
|
: DateTime.MinValue;
|
||||||
return DateTime.MinValue;
|
|
||||||
})
|
})
|
||||||
.Skip(_maxRetainedFiles);
|
.Skip(_maxRetainedFiles);
|
||||||
|
|
||||||
@@ -258,4 +414,61 @@ public sealed class FileLoggerProvider : BatchingLoggerProvider
|
|||||||
try { f.Delete(); } catch { }
|
try { f.Delete(); } catch { }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
private string GetFullName(DateTime date, string category)
|
||||||
|
{
|
||||||
|
var datePart = date.ToString("yyyyMMdd");
|
||||||
|
var machine = Environment.MachineName;
|
||||||
|
|
||||||
|
if (!EnableCategoryRouting || string.IsNullOrWhiteSpace(category))
|
||||||
|
{
|
||||||
|
return string.IsNullOrWhiteSpace(_fileNamePrefix)
|
||||||
|
? Path.Combine(_path, string.Format("{0}_{1}.log", machine, datePart))
|
||||||
|
: Path.Combine(_path, string.Format("{0}_{1}_{2}.log", _fileNamePrefix, machine, datePart));
|
||||||
|
}
|
||||||
|
|
||||||
|
var safeCategory = SanitizeCategory(category);
|
||||||
|
|
||||||
|
return string.IsNullOrWhiteSpace(_fileNamePrefix)
|
||||||
|
? Path.Combine(_path, string.Format("{0}_{1}_{2}.log", machine, safeCategory, datePart))
|
||||||
|
: Path.Combine(_path, string.Format("{0}_{1}_{2}_{3}.log", _fileNamePrefix, machine, safeCategory, datePart));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizeCategory(string category)
|
||||||
|
{
|
||||||
|
foreach (var c in Path.GetInvalidFileNameChars())
|
||||||
|
{
|
||||||
|
category = category.Replace(c, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
return category.Replace('.', '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnShutdownFlush()
|
||||||
|
{
|
||||||
|
foreach (var kvp in _files)
|
||||||
|
{
|
||||||
|
var state = kvp.Value;
|
||||||
|
if (state != null)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
FlushBufferAsync(state).Wait();
|
||||||
|
if (state.Stream != null)
|
||||||
|
{
|
||||||
|
state.Stream.Dispose();
|
||||||
|
}
|
||||||
|
if (state.Buffer != null)
|
||||||
|
{
|
||||||
|
ReturnBuffer(state.Buffer);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Ignore errors during shutdown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_files.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -63,7 +63,7 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal
|
|||||||
|
|
||||||
var writtenMessage = _provider.AddMessage(timestamp, formatted, _category);
|
var writtenMessage = _provider.AddMessage(timestamp, formatted, _category);
|
||||||
|
|
||||||
_settings.RaiseOnLog(new EonaCatLogMessage
|
_settings?.RaiseOnLog(new EonaCatLogMessage
|
||||||
{
|
{
|
||||||
DateTime = timestamp.DateTime,
|
DateTime = timestamp.DateTime,
|
||||||
Message = writtenMessage,
|
Message = writtenMessage,
|
||||||
@@ -79,7 +79,17 @@ namespace EonaCat.Logger.EonaCatCoreLogger.Internal
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.WriteLine($"Logging error: {ex.Message}");
|
_settings?.RaiseOnLogError(new EonaCatLogMessage
|
||||||
|
{
|
||||||
|
DateTime = Now.DateTime,
|
||||||
|
Message = $"Failed to log message: {ex.Message}",
|
||||||
|
LogType = ELogType.ERROR,
|
||||||
|
Category = _category,
|
||||||
|
Exception = ex,
|
||||||
|
Origin = string.IsNullOrWhiteSpace(_settings.LogOrigin)
|
||||||
|
? "BatchingLogger"
|
||||||
|
: _settings.LogOrigin
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,76 +1,67 @@
|
|||||||
using EonaCat.Logger.Managers;
|
using EonaCat.Logger.EonaCatCoreLogger;
|
||||||
|
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
||||||
|
using EonaCat.Logger.Extensions;
|
||||||
|
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.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace EonaCat.Logger.EonaCatCoreLogger.Internal;
|
|
||||||
|
|
||||||
public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
||||||
{
|
{
|
||||||
private readonly int _batchSize;
|
private readonly ConcurrentQueue<LogMessage> _queue = new();
|
||||||
private readonly BlockingCollection<LogMessage> _queue;
|
private readonly Thread _worker;
|
||||||
private readonly CancellationTokenSource _cts = new();
|
private readonly CancellationTokenSource _cts = new();
|
||||||
private readonly Task _worker;
|
|
||||||
|
private readonly int _batchSize;
|
||||||
|
private readonly long _maxQueueBytes = 512L * 1024 * 1024; // 512MB safety
|
||||||
|
private long _currentQueueBytes;
|
||||||
|
private long _dropped;
|
||||||
|
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private LoggerSettings _loggerSettings;
|
private LoggerSettings _loggerSettings;
|
||||||
|
|
||||||
|
private readonly AutoResetEvent _signal = new(false);
|
||||||
|
|
||||||
|
public event EventHandler<long>? OnLogDropped;
|
||||||
|
public event EventHandler<Exception>? OnError;
|
||||||
|
|
||||||
protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options)
|
protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options)
|
||||||
{
|
{
|
||||||
var currentOptions = options.Value ?? throw new ArgumentNullException(nameof(options));
|
var o = options.Value ?? throw new ArgumentNullException(nameof(options));
|
||||||
|
_batchSize = o.BatchSize > 0 ? o.BatchSize : 100;
|
||||||
|
|
||||||
if (currentOptions.FlushPeriod <= TimeSpan.Zero)
|
if (o is FileLoggerOptions file)
|
||||||
{
|
|
||||||
throw new ArgumentOutOfRangeException(nameof(currentOptions.FlushPeriod));
|
|
||||||
}
|
|
||||||
|
|
||||||
_batchSize = currentOptions.BatchSize > 0 ? currentOptions.BatchSize : 100;
|
|
||||||
_queue = new BlockingCollection<LogMessage>(Math.Max(1, _batchSize * 2));
|
|
||||||
|
|
||||||
if (currentOptions is FileLoggerOptions file)
|
|
||||||
{
|
{
|
||||||
UseLocalTime = file.UseLocalTime;
|
UseLocalTime = file.UseLocalTime;
|
||||||
UseMask = file.UseMask;
|
UseMask = file.UseMask;
|
||||||
LoggerSettings = file.LoggerSettings;
|
LoggerSettings = file.LoggerSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
_worker = Task.Factory.StartNew(
|
_worker = new Thread(ProcessLoop)
|
||||||
ProcessLoop,
|
{
|
||||||
_cts.Token,
|
IsBackground = true,
|
||||||
TaskCreationOptions.LongRunning,
|
Name = "EonaCat-LoggerWriter"
|
||||||
TaskScheduler.Default);
|
};
|
||||||
|
_worker.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected bool UseLocalTime { get; }
|
protected bool UseLocalTime { get; }
|
||||||
public bool UseMask { get; }
|
protected bool UseMask { get; }
|
||||||
|
|
||||||
protected DateTimeOffset NowOffset =>
|
protected DateTimeOffset NowOffset =>
|
||||||
UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
|
UseLocalTime ? DateTimeOffset.Now : DateTimeOffset.UtcNow;
|
||||||
|
|
||||||
protected LoggerSettings LoggerSettings
|
protected LoggerSettings LoggerSettings
|
||||||
{
|
{
|
||||||
get
|
get => _loggerSettings ??= new LoggerSettings { UseLocalTime = UseLocalTime, UseMask = UseMask };
|
||||||
{
|
|
||||||
if (_loggerSettings == null)
|
|
||||||
{
|
|
||||||
_loggerSettings = new LoggerSettings
|
|
||||||
{
|
|
||||||
UseLocalTime = UseLocalTime,
|
|
||||||
UseMask = UseMask
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return _loggerSettings;
|
|
||||||
}
|
|
||||||
set => _loggerSettings = value;
|
set => _loggerSettings = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ILogger CreateLogger(string categoryName)
|
public ILogger CreateLogger(string categoryName) => new BatchingLogger(this, categoryName, LoggerSettings);
|
||||||
=> new BatchingLogger(this, categoryName, LoggerSettings);
|
|
||||||
|
|
||||||
internal abstract Task WriteMessagesAsync(
|
internal abstract Task WriteMessagesAsync(
|
||||||
IReadOnlyList<LogMessage> messages,
|
IReadOnlyList<LogMessage> messages,
|
||||||
@@ -79,16 +70,30 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
|||||||
internal string AddMessage(DateTimeOffset timestamp, string message, string category)
|
internal string AddMessage(DateTimeOffset timestamp, string message, string category)
|
||||||
{
|
{
|
||||||
var log = CreateLogMessage(message, timestamp, category);
|
var log = CreateLogMessage(message, timestamp, category);
|
||||||
_queue.Add(log);
|
var size = log.EstimatedSize;
|
||||||
|
|
||||||
|
var newSize = Interlocked.Add(ref _currentQueueBytes, size);
|
||||||
|
if (newSize > _maxQueueBytes)
|
||||||
|
{
|
||||||
|
Interlocked.Add(ref _currentQueueBytes, -size);
|
||||||
|
var dropped = Interlocked.Increment(ref _dropped);
|
||||||
|
OnLogDropped?.Invoke(this, dropped);
|
||||||
|
return log.Message;
|
||||||
|
}
|
||||||
|
|
||||||
|
_queue.Enqueue(log);
|
||||||
|
_signal.Set();
|
||||||
|
|
||||||
return log.Message;
|
return log.Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
private LogMessage CreateLogMessage(string message, DateTimeOffset ts, string category)
|
private LogMessage CreateLogMessage(string message, DateTimeOffset ts, string category)
|
||||||
{
|
{
|
||||||
if (LoggerSettings.UseMask)
|
if (LoggerSettings.UseMask)
|
||||||
{
|
{
|
||||||
SensitiveDataMasker sensitiveDataMasker = new SensitiveDataMasker(LoggerSettings);
|
var masker = new SensitiveDataMasker(LoggerSettings);
|
||||||
message = sensitiveDataMasker.MaskSensitiveInformation(message);
|
message = masker.MaskSensitiveInformation(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new LogMessage
|
return new LogMessage
|
||||||
@@ -99,53 +104,58 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ProcessLoop()
|
private void ProcessLoop()
|
||||||
{
|
{
|
||||||
var batch = new List<LogMessage>(_batchSize);
|
var batch = new List<LogMessage>(_batchSize);
|
||||||
var flushInterval = LoggerSettings.FileLoggerOptions.FlushPeriod;
|
|
||||||
var timeoutMs = (int)Math.Min(flushInterval.TotalMilliseconds, int.MaxValue);
|
|
||||||
|
|
||||||
try
|
while (!_cts.IsCancellationRequested)
|
||||||
{
|
|
||||||
while (!_cts.Token.IsCancellationRequested)
|
|
||||||
{
|
|
||||||
if (_queue.TryTake(out var item, timeoutMs, _cts.Token))
|
|
||||||
{
|
|
||||||
batch.Add(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (batch.Count >= _batchSize ||
|
|
||||||
(batch.Count > 0 && !_queue.TryTake(out _, 0)))
|
|
||||||
{
|
|
||||||
await FlushBatchAsync(batch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (OperationCanceledException)
|
|
||||||
{
|
|
||||||
if (batch.Count > 0)
|
|
||||||
{
|
|
||||||
await FlushBatchAsync(batch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task FlushBatchAsync(List<LogMessage> batch)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await WriteMessagesAsync(batch, _cts.Token).ConfigureAwait(false);
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Console.WriteLine($"An error occurred while processing log batches. {ex.Message}");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
{
|
||||||
batch.Clear();
|
batch.Clear();
|
||||||
|
|
||||||
|
// Dequeue up to _batchSize messages
|
||||||
|
while (batch.Count < _batchSize && _queue.TryDequeue(out var msg))
|
||||||
|
{
|
||||||
|
batch.Add(msg);
|
||||||
|
Interlocked.Add(ref _currentQueueBytes, -msg.EstimatedSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batch.Count == 0)
|
||||||
|
{
|
||||||
|
// Wait until a new log arrives or cancellation is requested
|
||||||
|
WaitHandle.WaitAny(new WaitHandle[] { _signal, _cts.Token.WaitHandle });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
WriteMessagesAsync(batch, _cts.Token).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
OnError?.Invoke(this, ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process any remaining messages before exit
|
||||||
|
batch.Clear();
|
||||||
|
while (_queue.TryDequeue(out var msg))
|
||||||
|
{
|
||||||
|
batch.Add(msg);
|
||||||
|
Interlocked.Add(ref _currentQueueBytes, -msg.EstimatedSize);
|
||||||
|
if (batch.Count >= _batchSize)
|
||||||
|
{
|
||||||
|
WriteMessagesAsync(batch, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
|
batch.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (batch.Count > 0)
|
||||||
|
{
|
||||||
|
WriteMessagesAsync(batch, CancellationToken.None).GetAwaiter().GetResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
@@ -156,10 +166,15 @@ public abstract class BatchingLoggerProvider : ILoggerProvider, IDisposable
|
|||||||
_disposed = true;
|
_disposed = true;
|
||||||
|
|
||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
_queue.CompleteAdding();
|
_worker.Join();
|
||||||
|
|
||||||
|
OnShutdownFlush();
|
||||||
|
|
||||||
try { _worker.Wait(); } catch { }
|
|
||||||
_cts.Dispose();
|
_cts.Dispose();
|
||||||
_queue.Dispose();
|
}
|
||||||
|
|
||||||
|
protected virtual void OnShutdownFlush()
|
||||||
|
{
|
||||||
|
// default: Do nothing
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,4 +9,5 @@ public struct LogMessage
|
|||||||
public DateTimeOffset Timestamp { get; set; }
|
public DateTimeOffset Timestamp { get; set; }
|
||||||
public string Message { get; set; }
|
public string Message { get; set; }
|
||||||
public string Category { get; set; }
|
public string Category { get; set; }
|
||||||
|
public int EstimatedSize { get; set; }
|
||||||
}
|
}
|
||||||
@@ -54,7 +54,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
Exception exception, Func<TState, Exception, string> formatter)
|
Exception exception, Func<TState, Exception, string> formatter)
|
||||||
{
|
{
|
||||||
if (!IsEnabled(logLevel) || formatter == null)
|
if (!IsEnabled(logLevel) || formatter == null)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -77,14 +79,20 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
{
|
{
|
||||||
sb.Append(" | Context: ");
|
sb.Append(" | Context: ");
|
||||||
foreach (var kvp in contextData)
|
foreach (var kvp in contextData)
|
||||||
|
{
|
||||||
sb.Append(kvp.Key).Append("=").Append(kvp.Value).Append("; ");
|
sb.Append(kvp.Key).Append("=").Append(kvp.Value).Append("; ");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (exception != null)
|
if (exception != null)
|
||||||
|
{
|
||||||
sb.Append(" | Exception: ").Append(exception);
|
sb.Append(" | Exception: ").Append(exception);
|
||||||
|
}
|
||||||
|
|
||||||
if (!_logChannel.Writer.TryWrite(sb.ToString()))
|
if (!_logChannel.Writer.TryWrite(sb.ToString()))
|
||||||
|
{
|
||||||
OnLogDropped?.Invoke(this, sb.ToString()); // notify if log is dropped
|
OnLogDropped?.Invoke(this, sb.ToString()); // notify if log is dropped
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -136,7 +144,9 @@ namespace EonaCat.Logger.EonaCatCoreLogger
|
|||||||
|
|
||||||
// Flush any remaining logs
|
// Flush any remaining logs
|
||||||
if (batch.Count > 0)
|
if (batch.Count > 0)
|
||||||
|
{
|
||||||
await SafeWriteBatch(writer, batch, client, token);
|
await SafeWriteBatch(writer, batch, client, token);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
|||||||
38
EonaCat.Logger/LoggerConfigurator.cs
Normal file
38
EonaCat.Logger/LoggerConfigurator.cs
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
using EonaCat.Logger.EonaCatCoreLogger;
|
||||||
|
|
||||||
|
namespace EonaCat.Logger
|
||||||
|
{
|
||||||
|
public static class LoggerConfigurator
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Copies file logger settings from the specified source options to the target options instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <remarks>Only properties defined in FileLoggerOptions are copied. The method does not perform
|
||||||
|
/// a deep clone; reference-type properties are assigned directly. If either parameter is null, the method
|
||||||
|
/// returns without making changes.</remarks>
|
||||||
|
/// <param name="target">The FileLoggerOptions instance to which settings will be applied. If null, no changes are made.</param>
|
||||||
|
/// <param name="source">The FileLoggerOptions instance from which settings are copied. If null, no changes are made.</param>
|
||||||
|
public static void ApplyFileLoggerSettings(FileLoggerOptions target, FileLoggerOptions source)
|
||||||
|
{
|
||||||
|
if (target == null || source == null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
target.FileNamePrefix = source.FileNamePrefix;
|
||||||
|
target.FlushPeriod = source.FlushPeriod;
|
||||||
|
target.RetainedFileCountLimit = source.RetainedFileCountLimit;
|
||||||
|
target.MaxWriteTries = source.MaxWriteTries;
|
||||||
|
target.FileSizeLimit = source.FileSizeLimit;
|
||||||
|
target.IsEnabled = source.IsEnabled;
|
||||||
|
target.MaxRolloverFiles = source.MaxRolloverFiles;
|
||||||
|
target.UseLocalTime = source.UseLocalTime;
|
||||||
|
target.UseMask = source.UseMask;
|
||||||
|
target.Mask = source.Mask;
|
||||||
|
target.UseDefaultMasking = source.UseDefaultMasking;
|
||||||
|
target.MaskedKeywords = source.MaskedKeywords;
|
||||||
|
target.IncludeCorrelationId = source.IncludeCorrelationId;
|
||||||
|
target.EnableCategoryRouting = source.EnableCategoryRouting;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
using EonaCat.Json;
|
using EonaCat.Json;
|
||||||
using EonaCat.Logger.EonaCatCoreLogger;
|
|
||||||
using EonaCat.Logger.EonaCatCoreLogger.Internal;
|
|
||||||
using EonaCat.Logger.Extensions;
|
using EonaCat.Logger.Extensions;
|
||||||
using EonaCat.Logger.Servers.GrayLog;
|
using EonaCat.Logger.Servers.GrayLog;
|
||||||
using EonaCat.Logger.Servers.Splunk.Models;
|
using EonaCat.Logger.Servers.Splunk.Models;
|
||||||
@@ -8,15 +6,11 @@ using Microsoft.Extensions.Logging;
|
|||||||
using System;
|
using System;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Net;
|
using System.Net;
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using static EonaCat.Logger.Managers.LogHelper;
|
|
||||||
|
|
||||||
// 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.
|
||||||
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
// See the LICENSE file or go to https://EonaCat.com/License for full license details.
|
||||||
|
|||||||
@@ -31,15 +31,19 @@ namespace EonaCat.Logger.Managers
|
|||||||
private bool _isDisposing;
|
private bool _isDisposing;
|
||||||
private string _category;
|
private string _category;
|
||||||
|
|
||||||
public LogManager(LoggerSettings settings)
|
public LogManager(LoggerSettings settings, string category = null)
|
||||||
{
|
{
|
||||||
Settings = settings ?? CreateDefaultSettings();
|
Settings = settings ?? CreateDefaultSettings();
|
||||||
SetupLogManager();
|
_category = string.IsNullOrWhiteSpace(category) ? null : category;
|
||||||
}
|
_category = settings.FileLoggerOptions.Category;
|
||||||
|
|
||||||
public LogManager(LoggerSettings settings, string category = null) : this(settings)
|
if (string.IsNullOrEmpty(_category))
|
||||||
{
|
{
|
||||||
_category = string.IsNullOrWhiteSpace(category) ? "General" : category;
|
_category = category;
|
||||||
|
settings.FileLoggerOptions.Category = _category;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetupLogManager();
|
||||||
SetupFileLogger(settings);
|
SetupFileLogger(settings);
|
||||||
|
|
||||||
// Subscribe to static events
|
// Subscribe to static events
|
||||||
@@ -61,14 +65,8 @@ namespace EonaCat.Logger.Managers
|
|||||||
{
|
{
|
||||||
if (LoggerProvider is FileLoggerProvider fileLoggerProvider)
|
if (LoggerProvider is FileLoggerProvider fileLoggerProvider)
|
||||||
{
|
{
|
||||||
// Ensure log file is initialized
|
|
||||||
if (string.IsNullOrEmpty(fileLoggerProvider.LogFile))
|
|
||||||
{
|
|
||||||
fileLoggerProvider.InitializeCurrentFile();
|
|
||||||
}
|
|
||||||
return fileLoggerProvider.LogFile;
|
return fileLoggerProvider.LogFile;
|
||||||
}
|
}
|
||||||
|
|
||||||
return string.Empty;
|
return string.Empty;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,7 +126,9 @@ namespace EonaCat.Logger.Managers
|
|||||||
public async Task StartNewLogAsync()
|
public async Task StartNewLogAsync()
|
||||||
{
|
{
|
||||||
if (_isDisposing || _tokenSource.IsCancellationRequested)
|
if (_isDisposing || _tokenSource.IsCancellationRequested)
|
||||||
|
{
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await _startLock.WaitAsync().ConfigureAwait(false);
|
await _startLock.WaitAsync().ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
@@ -140,13 +140,8 @@ namespace EonaCat.Logger.Managers
|
|||||||
|
|
||||||
if (!IsRunning)
|
if (!IsRunning)
|
||||||
{
|
{
|
||||||
CreateLogger();
|
CreateLogger(_category);
|
||||||
Directory.CreateDirectory(Settings.FileLoggerOptions.LogDirectory);
|
Directory.CreateDirectory(Settings.FileLoggerOptions.LogDirectory);
|
||||||
if (LoggerProvider is FileLoggerProvider fileProvider)
|
|
||||||
{
|
|
||||||
fileProvider.InitializeCurrentFile();
|
|
||||||
}
|
|
||||||
|
|
||||||
_logDate = CurrentDateTime;
|
_logDate = CurrentDateTime;
|
||||||
IsRunning = true;
|
IsRunning = true;
|
||||||
}
|
}
|
||||||
@@ -175,7 +170,7 @@ namespace EonaCat.Logger.Managers
|
|||||||
LogHelper.SendToFile(Logger, Settings, ELogType.INFO, stopMessage);
|
LogHelper.SendToFile(Logger, Settings, ELogType.INFO, stopMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateLogger()
|
private void CreateLogger(string categoryName = null)
|
||||||
{
|
{
|
||||||
if (Logger != null)
|
if (Logger != null)
|
||||||
{
|
{
|
||||||
@@ -204,31 +199,15 @@ namespace EonaCat.Logger.Managers
|
|||||||
|
|
||||||
serviceCollection.AddLogging(builder =>
|
serviceCollection.AddLogging(builder =>
|
||||||
builder.SetMinimumLevel(Settings.TypesToLog.Max().ToLogLevel())
|
builder.SetMinimumLevel(Settings.TypesToLog.Max().ToLogLevel())
|
||||||
.AddEonaCatFileLogger(config =>
|
.AddEonaCatFileLogger(options =>
|
||||||
{
|
{
|
||||||
var options = Settings.FileLoggerOptions;
|
LoggerConfigurator.ApplyFileLoggerSettings(options, Settings.FileLoggerOptions);
|
||||||
config.LoggerSettings = Settings;
|
}));
|
||||||
config.MaxWriteTries = options.MaxWriteTries;
|
|
||||||
config.RetainedFileCountLimit = options.RetainedFileCountLimit;
|
|
||||||
config.FlushPeriod = options.FlushPeriod;
|
|
||||||
config.IsEnabled = options.IsEnabled;
|
|
||||||
config.BatchSize = options.BatchSize;
|
|
||||||
config.FileSizeLimit = options.FileSizeLimit;
|
|
||||||
config.LogDirectory = options.LogDirectory;
|
|
||||||
config.FileNamePrefix = options.FileNamePrefix;
|
|
||||||
config.MaxRolloverFiles = options.MaxRolloverFiles;
|
|
||||||
config.UseLocalTime = Settings.UseLocalTime;
|
|
||||||
config.UseMask = Settings.UseMask;
|
|
||||||
config.Mask = options.Mask;
|
|
||||||
config.UseDefaultMasking = Settings.UseDefaultMasking;
|
|
||||||
config.MaskedKeywords = options.MaskedKeywords;
|
|
||||||
config.Settings = Settings;
|
|
||||||
}));
|
|
||||||
|
|
||||||
var serviceProvider = serviceCollection.BuildServiceProvider();
|
var serviceProvider = serviceCollection.BuildServiceProvider();
|
||||||
LoggerProvider = serviceProvider.GetService<ILoggerProvider>();
|
LoggerProvider = serviceProvider.GetService<ILoggerProvider>();
|
||||||
LoggerFactory = serviceProvider.GetService<ILoggerFactory>();
|
LoggerFactory = serviceProvider.GetService<ILoggerFactory>();
|
||||||
Logger = LoggerFactory.CreateLogger(Settings.Id);
|
Logger = LoggerFactory.CreateLogger(_category ?? Settings.Id);
|
||||||
LogHelper.SendToFile(Logger, Settings, ELogType.INFO, LogHelper.GetStartupMessage());
|
LogHelper.SendToFile(Logger, Settings, ELogType.INFO, LogHelper.GetStartupMessage());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -229,6 +229,7 @@ public class LoggerSettings
|
|||||||
public bool UseDefaultMasking { get; set; } = true;
|
public bool UseDefaultMasking { get; set; } = true;
|
||||||
|
|
||||||
public event LogDelegate OnLog;
|
public event LogDelegate OnLog;
|
||||||
|
public event LogDelegate OnError;
|
||||||
|
|
||||||
private static FileLoggerOptions CreateDefaultFileLoggerOptions()
|
private static FileLoggerOptions CreateDefaultFileLoggerOptions()
|
||||||
{
|
{
|
||||||
@@ -360,4 +361,14 @@ public class LoggerSettings
|
|||||||
{
|
{
|
||||||
OnLogEvent(eonaCatLogMessage);
|
OnLogEvent(eonaCatLogMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal void RaiseOnLogError(EonaCatLogMessage eonaCatLogMessage)
|
||||||
|
{
|
||||||
|
OnLogErrorEvent(eonaCatLogMessage);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnLogErrorEvent(EonaCatLogMessage eonaCatLogMessage)
|
||||||
|
{
|
||||||
|
OnError?.Invoke(eonaCatLogMessage);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -16,6 +16,32 @@
|
|||||||
{
|
{
|
||||||
public static async Task Main(string[] args)
|
public static async Task Main(string[] args)
|
||||||
{
|
{
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var loggerSettings = new LoggerSettings();
|
||||||
|
loggerSettings.Id = "SPEEDTEST";
|
||||||
|
loggerSettings.UseLocalTime = true;
|
||||||
|
loggerSettings.FileLoggerOptions.UseLocalTime = true;
|
||||||
|
loggerSettings.EnableConsole = false;
|
||||||
|
loggerSettings.FileLoggerOptions.Category = "SpeedTests";
|
||||||
|
loggerSettings.FileLoggerOptions.EnableCategoryRouting = true;
|
||||||
|
loggerSettings.AllowAllLogTypes();
|
||||||
|
var logger = new LogManager(loggerSettings);
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
await logger.WriteAsync($"test to file {i} INFO").ConfigureAwait(false);
|
||||||
|
await logger.WriteAsync($"test to file {i} CRITICAL", ELogType.CRITICAL).ConfigureAwait(false);
|
||||||
|
await logger.WriteAsync($"test to file {i} DEBUG", ELogType.DEBUG).ConfigureAwait(false);
|
||||||
|
await logger.WriteAsync($"test to file {i} ERROR", ELogType.ERROR).ConfigureAwait(false);
|
||||||
|
await logger.WriteAsync($"test to file {i} TRACE", ELogType.TRACE).ConfigureAwait(false);
|
||||||
|
await Task.Delay(1).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Console.ReadKey();
|
||||||
|
|
||||||
var _config = new MemoryGuardConfiguration
|
var _config = new MemoryGuardConfiguration
|
||||||
{
|
{
|
||||||
MonitoringInterval = TimeSpan.FromSeconds(5),
|
MonitoringInterval = TimeSpan.FromSeconds(5),
|
||||||
@@ -35,6 +61,30 @@
|
|||||||
|
|
||||||
//MemoryGuard.Start(_config);
|
//MemoryGuard.Start(_config);
|
||||||
|
|
||||||
|
_ = Task.Run(async () =>
|
||||||
|
{
|
||||||
|
var loggerSettings = new LoggerSettings();
|
||||||
|
loggerSettings.Id = "SPEEDTEST";
|
||||||
|
loggerSettings.UseLocalTime = true;
|
||||||
|
loggerSettings.FileLoggerOptions.UseLocalTime = true;
|
||||||
|
loggerSettings.FileLoggerOptions.Category = "SpeedTests";
|
||||||
|
loggerSettings.FileLoggerOptions.EnableCategoryRouting = true;
|
||||||
|
loggerSettings.AllowAllLogTypes();
|
||||||
|
var logger = new LogManager(loggerSettings);
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
i++;
|
||||||
|
await logger.WriteAsync($"test to file {i} INFO").ConfigureAwait(false);
|
||||||
|
await logger.WriteAsync($"test to file {i} CRITICAL", ELogType.CRITICAL).ConfigureAwait(false);
|
||||||
|
await logger.WriteAsync($"test to file {i} DEBUG", ELogType.DEBUG).ConfigureAwait(false);
|
||||||
|
await logger.WriteAsync($"test to file {i} ERROR", ELogType.ERROR).ConfigureAwait(false);
|
||||||
|
await logger.WriteAsync($"test to file {i} TRACE", ELogType.TRACE).ConfigureAwait(false);
|
||||||
|
await Task.Delay(1).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
var builder = WebApplication.CreateBuilder(args);
|
var builder = WebApplication.CreateBuilder(args);
|
||||||
int onLogCounter = 0;
|
int onLogCounter = 0;
|
||||||
var defaultColor = Console.ForegroundColor;
|
var defaultColor = Console.ForegroundColor;
|
||||||
@@ -215,6 +265,8 @@
|
|||||||
loggerSettings.FileLoggerOptions.UseLocalTime = true;
|
loggerSettings.FileLoggerOptions.UseLocalTime = true;
|
||||||
loggerSettings.UseLocalTime = true;
|
loggerSettings.UseLocalTime = true;
|
||||||
loggerSettings.Id = "TEST";
|
loggerSettings.Id = "TEST";
|
||||||
|
loggerSettings.FileLoggerOptions.Category = "ExceptionTests";
|
||||||
|
loggerSettings.FileLoggerOptions.EnableCategoryRouting = true;
|
||||||
loggerSettings.TypesToLog.Add(ELogType.INFO);
|
loggerSettings.TypesToLog.Add(ELogType.INFO);
|
||||||
|
|
||||||
var logger = new LogManager(loggerSettings);
|
var logger = new LogManager(loggerSettings);
|
||||||
@@ -234,7 +286,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MemoryLeakTester.Start(logger);
|
//MemoryLeakTester.Start(logger);
|
||||||
_ = Task.Run(RunMemoryReportTask).ConfigureAwait(false);
|
_ = Task.Run(RunMemoryReportTask).ConfigureAwait(false);
|
||||||
_ = Task.Run(RunMaskTest).ConfigureAwait(false);
|
_ = Task.Run(RunMaskTest).ConfigureAwait(false);
|
||||||
_ = Task.Run(RunWebLoggerTestsAsync).ConfigureAwait(false);
|
_ = Task.Run(RunWebLoggerTestsAsync).ConfigureAwait(false);
|
||||||
|
|||||||
Reference in New Issue
Block a user