Initial version

This commit is contained in:
2022-09-19 09:07:51 +02:00
parent 12150155da
commit 7783dc07e1
19 changed files with 1318 additions and 1 deletions

View File

@@ -0,0 +1,53 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System;
namespace EonaCat.Logger.Extensions
{
// 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.
/// <summary>
/// Extensions for adding the <see cref="FileLoggerProvider" /> to the <see cref="ILoggingBuilder" />
/// </summary>
public static class FileLoggerFactoryExtensions
{
/// <summary>
/// Adds a file logger named 'File' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
public static ILoggingBuilder AddFile(this ILoggingBuilder builder)
{
builder.Services.AddSingleton<ILoggerProvider, FileLoggerProvider>();
return builder;
}
/// <summary>
/// Adds a file logger named 'File' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="filenamePrefix">Sets the filename prefix to use for log files</param>
public static ILoggingBuilder AddFile(this ILoggingBuilder builder, string filenamePrefix)
{
builder.AddFile(options => options.FileNamePrefix = filenamePrefix);
return builder;
}
/// <summary>
/// Adds a file logger named 'File' to the factory.
/// </summary>
/// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
/// <param name="configure">Configure an instance of the <see cref="FileLoggerOptions" /> to set logging options</param>
public static ILoggingBuilder AddFile(this ILoggingBuilder builder, Action<FileLoggerOptions> configure)
{
if (configure == null)
{
throw new ArgumentNullException(nameof(configure));
}
builder.AddFile();
builder.Services.Configure(configure);
return builder;
}
}
}

View File

@@ -0,0 +1,89 @@
using EonaCat.Logger.Internal;
using System;
using System.IO;
namespace EonaCat.Logger
{
// 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.
/// <summary>
/// Options for file logging.
/// </summary>
public class FileLoggerOptions : BatchingLoggerOptions
{
private int _fileSizeLimit = 200 * 1024 * 1024;
private int _retainedFileCountLimit = 50;
private int _maxRolloverFiles = 10;
public static string DefaultPath => AppDomain.CurrentDomain.RelativeSearchPath ?? AppDomain.CurrentDomain.BaseDirectory;
/// <summary>
/// Gets or sets a strictly positive value representing the maximum log size in bytes or null for no limit.
/// Once the log is full, no more messages will be appended.
/// Defaults to <c>200MB</c>.
/// </summary>
public int FileSizeLimit
{
get => _fileSizeLimit;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FileSizeLimit)} must be positive.");
}
_fileSizeLimit = value;
}
}
/// <summary>
/// Gets or sets a strictly positive value representing the maximum retained file count or null for no limit.
/// Defaults to <c>50</c>.
/// </summary>
public int RetainedFileCountLimit
{
get => _retainedFileCountLimit;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(RetainedFileCountLimit)} must be positive.");
}
_retainedFileCountLimit = value;
}
}
/// <summary>
/// Gets or sets a strictly positive value representing the maximum retained file rollovers or null for no limit.
/// Defaults to <c>10</c>.
/// </summary>
public int MaxRolloverFiles
{
get => _maxRolloverFiles;
set
{
if (value <= 0)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(MaxRolloverFiles)} must be positive.");
}
_maxRolloverFiles = value;
}
}
/// <summary>
/// Gets or sets the filename prefix to use for log files.
/// Defaults to <c>EonaCat_</c>.
/// </summary>
public string FileNamePrefix { get; set; } = "EonaCat";
/// <summary>
/// The directory in which log files will be written, relative to the app process.
/// Default to <c>executablePath\logs</c>
/// </summary>
/// <returns></returns>
public string LogDirectory { get; set; } = Path.Combine(DefaultPath, "logs");
}
}

View File

@@ -0,0 +1,138 @@
using EonaCat.Logger.Internal;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
namespace EonaCat.Logger
{
// 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.
/// <summary>
/// An <see cref="ILoggerProvider" /> that writes logs to a file
/// </summary>
[ProviderAlias("File")]
public class FileLoggerProvider : BatchingLoggerProvider
{
private readonly string _path;
private readonly string _fileNamePrefix;
private readonly int _maxFileSize;
private readonly int _maxRetainedFiles;
private readonly int _maxRolloverFiles;
private int _rollOverCount = 0;
public string LogFile { get; private set; }
/// <summary>
/// Creates an instance of the <see cref="FileLoggerProvider" />
/// </summary>
/// <param name="options">The options object controlling the logger</param>
public FileLoggerProvider(IOptions<FileLoggerOptions> options) : base(options)
{
FileLoggerOptions loggerOptions = options.Value;
_path = loggerOptions.LogDirectory;
_fileNamePrefix = loggerOptions.FileNamePrefix;
_maxFileSize = loggerOptions.FileSizeLimit;
_maxRetainedFiles = loggerOptions.RetainedFileCountLimit;
_maxRolloverFiles = loggerOptions.MaxRolloverFiles;
}
/// <inheritdoc />
protected override async Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken cancellationToken)
{
Directory.CreateDirectory(_path);
foreach (IGrouping<(int Year, int Month, int Day), LogMessage> group in messages.GroupBy(GetGrouping))
{
LogFile = GetFullName(group.Key);
FileInfo fileInfo = new FileInfo(LogFile);
if (_maxFileSize > 0 && fileInfo.Exists && fileInfo.Length > _maxFileSize)
{
if (_maxRolloverFiles > 0 && _rollOverCount >= 0)
{
if (_rollOverCount < _maxRolloverFiles)
{
fileInfo.CopyTo(LogFile.Replace(".log", $"_{++_rollOverCount}.log"));
File.WriteAllText(LogFile, string.Empty);
}
else
{
bool areFilesDeleted = false;
for (int i = 0; i < _rollOverCount; i++)
{
File.Delete(LogFile.Replace(".log", $"_{i}.log"));
areFilesDeleted = true;
}
if (areFilesDeleted)
{
File.Move(LogFile.Replace(".log", $"_{_rollOverCount}.log"), LogFile.Replace(".log", $"_1.log"));
_rollOverCount = 0;
}
}
}
}
using (StreamWriter streamWriter = File.AppendText(LogFile))
{
foreach (LogMessage item in group)
{
await streamWriter.WriteAsync(item.Message);
}
}
}
DeleteOldLogFiles();
}
private string GetFullName((int Year, int Month, int Day) group)
{
bool hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix);
if (hasPrefix)
{
return Path.Combine(_path, $"{_fileNamePrefix}_{group.Year:0000}{group.Month:00}{group.Day:00}.log");
}
else
{
return Path.Combine(_path, $"{group.Year:0000}{group.Month:00}{group.Day:00}.log");
}
}
private (int Year, int Month, int Day) GetGrouping(LogMessage message)
{
return (message.Timestamp.Year, message.Timestamp.Month, message.Timestamp.Day);
}
/// <summary>
/// Deletes old log files, keeping a number of files defined by <see cref="FileLoggerOptions.RetainedFileCountLimit" />
/// </summary>
protected void DeleteOldLogFiles()
{
if (_maxRetainedFiles > 0)
{
bool hasPrefix = !string.IsNullOrWhiteSpace(_fileNamePrefix);
IEnumerable<FileInfo> files = null;
if (hasPrefix)
{
files = new DirectoryInfo(_path).GetFiles(_fileNamePrefix + "*");
}
else
{
files = new DirectoryInfo(_path).GetFiles("*");
}
files = files.OrderByDescending(file => file.Name).Skip(_maxRetainedFiles);
foreach (FileInfo item in files)
{
item.Delete();
}
}
}
}
}

View File

@@ -0,0 +1,64 @@
using Microsoft.Extensions.Logging;
using System;
using System.Text;
namespace EonaCat.Logger.Internal
{
// 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.
public class BatchingLogger : ILogger
{
private readonly BatchingLoggerProvider _provider;
private readonly string _category;
public BatchingLogger(BatchingLoggerProvider loggerProvider, string categoryName)
{
_provider = loggerProvider;
_category = categoryName;
}
public IDisposable BeginScope<TState>(TState state)
{
return null;
}
public bool IsEnabled(LogLevel logLevel)
{
if (logLevel == LogLevel.None)
{
return false;
}
return true;
}
public void Log<TState>(DateTimeOffset timestamp, LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
if (!IsEnabled(logLevel))
{
return;
}
StringBuilder builder = new StringBuilder();
builder.Append(timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff zzz"));
builder.Append(" [");
builder.Append(logLevel.ToString());
builder.Append("] ");
builder.Append(_category);
builder.Append(": ");
builder.AppendLine(formatter(state, exception));
if (exception != null)
{
builder.AppendLine(exception.ToString());
}
_provider.AddMessage(timestamp, builder.ToString());
}
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
{
Log(DateTimeOffset.Now, logLevel, eventId, state, exception, formatter);
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
namespace EonaCat.Logger.Internal
{
// 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.
public class BatchingLoggerOptions
{
private int _batchSize = 32;
private int _backgroundQueueSize;
private TimeSpan _flushPeriod = TimeSpan.FromSeconds(1);
/// <summary>
/// Gets or sets the period after which logs will be flushed to the store.
/// </summary>
public TimeSpan FlushPeriod
{
get => _flushPeriod;
set
{
if (value <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(value), $"{nameof(FlushPeriod)} must be positive.");
}
_flushPeriod = value;
}
}
/// <summary>
/// Gets or sets the maximum size of the background log message queue or less than 1 for no limit.
/// After maximum queue size is reached log event sink would start blocking.
/// Defaults to <c>0</c>.
/// </summary>
public int BackgroundQueueSize
{
get => _backgroundQueueSize;
set
{
_backgroundQueueSize = value;
}
}
/// <summary>
/// Gets or sets a maximum number of events to include in a single batch or less than 1 for no limit.
/// </summary>
public int BatchSize
{
get => _batchSize;
set
{
_batchSize = value;
}
}
/// <summary>
/// Gets or sets value indicating if logger accepts and queues writes.
/// </summary>
public bool IsEnabled { get; set; }
}
}

View File

@@ -0,0 +1,138 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
namespace EonaCat.Logger.Internal
{
// 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.
public abstract class BatchingLoggerProvider : ILoggerProvider
{
private readonly List<LogMessage> _currentBatch = new List<LogMessage>();
private readonly TimeSpan _interval;
private readonly int _queueSize;
private readonly int _batchSize;
private BlockingCollection<LogMessage> _messageQueue;
private Task _outputTask;
private CancellationTokenSource _cancellationTokenSource;
protected BatchingLoggerProvider(IOptions<BatchingLoggerOptions> options)
{
BatchingLoggerOptions loggerOptions = options.Value;
if (loggerOptions.BatchSize <= 0)
{
throw new ArgumentOutOfRangeException(nameof(loggerOptions.BatchSize), $"{nameof(loggerOptions.BatchSize)} must be a positive number.");
}
if (loggerOptions.FlushPeriod <= TimeSpan.Zero)
{
throw new ArgumentOutOfRangeException(nameof(loggerOptions.FlushPeriod), $"{nameof(loggerOptions.FlushPeriod)} must be longer than zero.");
}
_interval = loggerOptions.FlushPeriod;
_batchSize = loggerOptions.BatchSize;
_queueSize = loggerOptions.BackgroundQueueSize;
Start();
}
protected abstract Task WriteMessagesAsync(IEnumerable<LogMessage> messages, CancellationToken token);
private async Task ProcessLogQueue(object state)
{
while (!_cancellationTokenSource.IsCancellationRequested)
{
int limit = _batchSize <= 0 ? int.MaxValue : _batchSize;
while (limit > 0 && _messageQueue.TryTake(out LogMessage message))
{
_currentBatch.Add(message);
limit--;
}
if (_currentBatch.Count > 0)
{
try
{
await WriteMessagesAsync(_currentBatch, _cancellationTokenSource.Token);
}
catch
{
// ignored
}
_currentBatch.Clear();
}
await IntervalAsync(_interval, _cancellationTokenSource.Token);
}
}
protected virtual Task IntervalAsync(TimeSpan interval, CancellationToken cancellationToken)
{
return Task.Delay(interval, cancellationToken);
}
internal void AddMessage(DateTimeOffset timestamp, string message)
{
if (!_messageQueue.IsAddingCompleted)
{
try
{
_messageQueue.Add(new LogMessage { Message = message, Timestamp = timestamp }, _cancellationTokenSource.Token);
}
catch
{
//cancellation token canceled or CompleteAdding called
}
}
}
private void Start()
{
_messageQueue = _queueSize == 0 ?
new BlockingCollection<LogMessage>(new ConcurrentQueue<LogMessage>()) :
new BlockingCollection<LogMessage>(new ConcurrentQueue<LogMessage>(), _queueSize);
_cancellationTokenSource = new CancellationTokenSource();
_outputTask = Task.Factory.StartNew(
ProcessLogQueue,
null,
TaskCreationOptions.LongRunning);
}
private void Stop()
{
_cancellationTokenSource.Cancel();
_messageQueue.CompleteAdding();
try
{
_outputTask.Wait(_interval);
}
catch (TaskCanceledException)
{
}
catch (AggregateException exception) when (exception.InnerExceptions.Count == 1 && exception.InnerExceptions[0] is TaskCanceledException)
{
}
}
public void Dispose()
{
Stop();
}
public ILogger CreateLogger(string categoryName)
{
return new BatchingLogger(this, categoryName);
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
namespace EonaCat.Logger.Internal
{
// 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.
public struct LogMessage
{
public DateTimeOffset Timestamp { get; set; }
public string Message { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
using EonaCat.Logger;
using EonaCat.Logger.Helpers;
using System;
namespace EonaCatLogger.EonaCatCoreLogger.Models
{
// 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.
public class EonaCatLogMessage
{
public DateTime DateTime { get; internal set; }
public string Message { get; internal set; }
public ELogType LogType { get; internal set; }
public override string ToString()
{
return $"[{EnumHelper<ELogType>.ToString(LogType)}] [{DateTime.ToString(Constants.DateTimeFormats.LOGGING)}] {Message}";
}
}
}