254 lines
7.4 KiB
C#
254 lines
7.4 KiB
C#
using System;
|
|
using System.Collections.Concurrent;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading;
|
|
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")]
|
|
public sealed class FileLoggerProvider : BatchingLoggerProvider
|
|
{
|
|
private readonly string _path;
|
|
private readonly string _fileNamePrefix;
|
|
private readonly int _maxFileSize;
|
|
private readonly int _maxRetainedFiles;
|
|
private readonly int _maxRolloverFiles;
|
|
|
|
private string _logFile;
|
|
private long _currentFileSize;
|
|
|
|
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();
|
|
|
|
public string LogFile => _logFile ?? string.Empty;
|
|
|
|
public event EventHandler<ErrorMessage> OnError;
|
|
|
|
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
|
|
{
|
|
var o = options.Value;
|
|
_path = o.LogDirectory;
|
|
_fileNamePrefix = o.FileNamePrefix;
|
|
_maxFileSize = o.FileSizeLimit;
|
|
_maxRetainedFiles = o.RetainedFileCountLimit;
|
|
_maxRolloverFiles = o.MaxRolloverFiles;
|
|
IncludeCorrelationId = o.IncludeCorrelationId;
|
|
|
|
Directory.CreateDirectory(_path);
|
|
InitializeCurrentFile();
|
|
}
|
|
|
|
public bool IncludeCorrelationId { get; }
|
|
|
|
internal override async Task WriteMessagesAsync(
|
|
IReadOnlyList<LogMessage> messages,
|
|
CancellationToken token)
|
|
{
|
|
if (messages.Count == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Directory.CreateDirectory(_path);
|
|
|
|
// Group messages by date
|
|
foreach (var group in messages.GroupBy(m => (m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day)))
|
|
{
|
|
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)
|
|
{
|
|
// Ensure correlation id exists (once per async context)
|
|
if (IncludeCorrelationId)
|
|
{
|
|
var cid = _context.Get("CorrelationId") ?? Guid.NewGuid().ToString();
|
|
_context.Set("CorrelationId", cid);
|
|
}
|
|
|
|
sb.Append(msg.Message);
|
|
|
|
var ctx = _context.GetAll();
|
|
if (ctx.Count > 0)
|
|
{
|
|
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(']');
|
|
}
|
|
|
|
sb.AppendLine();
|
|
}
|
|
|
|
public void InitializeCurrentFile()
|
|
{
|
|
if (!string.IsNullOrEmpty(_logFile))
|
|
{
|
|
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
|
|
{
|
|
var text = stringBuilder.ToString();
|
|
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);
|
|
}
|
|
|
|
// Update current file size correctly
|
|
_fileSizes.AddOrUpdate(file, bytesToWrite.Length, (_, old) => old + bytesToWrite.Length);
|
|
_currentFileSize = _fileSizes[file];
|
|
|
|
stringBuilder.Clear();
|
|
|
|
if (_fileSizes[file] >= _maxFileSize)
|
|
{
|
|
await RollOverAsync(file).ConfigureAwait(false);
|
|
}
|
|
}
|
|
finally
|
|
{
|
|
_writeLock.Release();
|
|
}
|
|
}
|
|
|
|
private async Task RollOverAsync(string file)
|
|
{
|
|
await _rolloverLock.WaitAsync().ConfigureAwait(false);
|
|
try
|
|
{
|
|
var dir = Path.GetDirectoryName(file);
|
|
var name = Path.GetFileNameWithoutExtension(file);
|
|
var ext = Path.GetExtension(file);
|
|
|
|
for (int i = _maxRolloverFiles - 1; i >= 1; i--)
|
|
{
|
|
var src = Path.Combine(dir, $"{name}.{i}{ext}");
|
|
var dst = Path.Combine(dir, $"{name}.{i + 1}{ext}");
|
|
|
|
if (File.Exists(dst))
|
|
{
|
|
File.Delete(dst);
|
|
}
|
|
|
|
if (File.Exists(src))
|
|
{
|
|
File.Move(src, dst);
|
|
}
|
|
}
|
|
|
|
var firstRoll = Path.Combine(dir, $"{name}.1{ext}");
|
|
if (File.Exists(file))
|
|
{
|
|
File.Move(file, firstRoll);
|
|
}
|
|
|
|
_fileSizes[file] = 0;
|
|
}
|
|
finally
|
|
{
|
|
_rolloverLock.Release();
|
|
}
|
|
}
|
|
|
|
private void InitializeFile(string file)
|
|
{
|
|
_fileSizes.TryAdd(file, File.Exists(file) ? new FileInfo(file).Length : 0);
|
|
_buffers.TryAdd(file, new StringBuilder(4096));
|
|
}
|
|
|
|
private (int Year, int Month, int Day) GetGrouping(LogMessage m) =>
|
|
(m.Timestamp.Year, m.Timestamp.Month, m.Timestamp.Day);
|
|
|
|
private string GetFullName((int Year, int Month, int Day) g) =>
|
|
string.IsNullOrWhiteSpace(_fileNamePrefix)
|
|
? Path.Combine(_path, $"{g.Year:0000}{g.Month:00}{g.Day:00}.log")
|
|
: Path.Combine(_path, $"{_fileNamePrefix}_{g.Year:0000}{g.Month:00}{g.Day:00}.log");
|
|
|
|
private void DeleteOldLogFiles()
|
|
{
|
|
if (_maxRetainedFiles <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var files = new DirectoryInfo(_path)
|
|
.GetFiles($"{_fileNamePrefix}*")
|
|
.OrderByDescending(f =>
|
|
{
|
|
// Parse date from filename instead of CreationTimeUtc
|
|
var name = Path.GetFileNameWithoutExtension(f.Name);
|
|
var parts = name.Split('_');
|
|
var datePart = parts.Length > 1 ? parts[1] : parts[0];
|
|
if (DateTime.TryParseExact(datePart, "yyyyMMdd", null, System.Globalization.DateTimeStyles.None, out var dt))
|
|
{
|
|
return dt;
|
|
}
|
|
|
|
return DateTime.MinValue;
|
|
})
|
|
.Skip(_maxRetainedFiles);
|
|
|
|
foreach (var f in files)
|
|
{
|
|
try { f.Delete(); } catch { }
|
|
}
|
|
}
|
|
}
|