330 lines
9.6 KiB
C#
330 lines
9.6 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Text;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
[assembly: System.Runtime.CompilerServices.InternalsVisibleTo("UnitTests")]
|
|
|
|
namespace AMWD.Common.Logging
|
|
{
|
|
/// <summary>
|
|
/// Implements a file logging based on the <see cref="ILogger"/> interface.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This implementation is also using the <see cref="IDisposable"/> interface!
|
|
/// <br/>
|
|
/// Inspired by <see href="https://github.com/aspnet/Logging/blob/2d2f31968229eddb57b6ba3d34696ef366a6c71b/src/Microsoft.Extensions.Logging.Console/ConsoleLogger.cs">ConsoleLogger.cs</see>
|
|
/// </remarks>
|
|
/// <seealso cref="ILogger" />
|
|
/// <seealso cref="IDisposable" />
|
|
public class FileLogger : ILogger, IDisposable
|
|
{
|
|
#region Fields
|
|
|
|
private bool isDisposed = false;
|
|
private readonly CancellationTokenSource cancellationTokenSource = new();
|
|
|
|
private readonly StreamWriter fileWriter;
|
|
private readonly Task writeTask;
|
|
|
|
private readonly AsyncQueue<QueueItem> queue = new();
|
|
|
|
#endregion Fields
|
|
|
|
#region Constructors
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="FileLogger"/> class.
|
|
/// </summary>
|
|
/// <param name="file">The log file.</param>
|
|
/// <param name="append">A value indicating whether to append lines to an existing file (Default: <c>false</c>).</param>
|
|
/// <param name="encoding">The file encoding (Default: <see cref="Encoding.UTF8"/>).</param>
|
|
public FileLogger(string file, bool append = false, Encoding encoding = null)
|
|
{
|
|
FileName = file;
|
|
fileWriter = new StreamWriter(FileName, append, encoding ?? Encoding.UTF8);
|
|
writeTask = Task.Run(() => WriteFileAsync(cancellationTokenSource.Token));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new named instance of the <see cref="FileLogger"/> class.
|
|
/// </summary>
|
|
/// <param name="file">The log file.</param>
|
|
/// <param name="name">The logger name.</param>
|
|
/// <param name="append">A value indicating whether to append lines to an existing file.</param>
|
|
/// <param name="encoding">The file encoding.</param>
|
|
public FileLogger(string file, string name, bool append = false, Encoding encoding = null)
|
|
: this(file, append, encoding)
|
|
{
|
|
Name = name;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="FileLogger"/> class.
|
|
/// </summary>
|
|
/// <param name="file">The log file.</param>
|
|
/// <param name="name">The logger name.</param>
|
|
/// <param name="parentLogger">The parent logger.</param>
|
|
/// <param name="append">A value indicating whether to append lines to an existing file.</param>
|
|
/// <param name="encoding">The file encoding.</param>
|
|
public FileLogger(string file, string name, FileLogger parentLogger, bool append = false, Encoding encoding = null)
|
|
: this(file, name, append, encoding)
|
|
{
|
|
ParentLogger = parentLogger;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="FileLogger"/> class.
|
|
/// </summary>
|
|
/// <param name="file">The log file.</param>
|
|
/// <param name="name">The logger name.</param>
|
|
/// <param name="scopeProvider">The scope provider.</param>
|
|
/// <param name="append">A value indicating whether to append lines to an existing file.</param>
|
|
/// <param name="encoding">The file encoding.</param>
|
|
public FileLogger(string file, string name, IExternalScopeProvider scopeProvider, bool append = false, Encoding encoding = null)
|
|
: this(file, name, append, encoding)
|
|
{
|
|
ScopeProvider = scopeProvider;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Initializes a new instance of the <see cref="FileLogger"/> class.
|
|
/// </summary>
|
|
/// <param name="file">The log file.</param>
|
|
/// <param name="name">The logger name.</param>
|
|
/// <param name="parentLogger">The parent logger.</param>
|
|
/// <param name="scopeProvider">The scope provider.</param>
|
|
/// <param name="append">A value indicating whether to append lines to an existing file.</param>
|
|
/// <param name="encoding">The file encoding.</param>
|
|
public FileLogger(string file, string name, FileLogger parentLogger, IExternalScopeProvider scopeProvider, bool append = false, Encoding encoding = null)
|
|
: this(file, name, parentLogger, append, encoding)
|
|
{
|
|
ScopeProvider = scopeProvider;
|
|
}
|
|
|
|
#endregion Constructors
|
|
|
|
#region Properties
|
|
|
|
/// <summary>
|
|
/// Gets the log file.
|
|
/// </summary>
|
|
public string FileName { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the name of the logger.
|
|
/// </summary>
|
|
public string Name { get; }
|
|
|
|
/// <summary>
|
|
/// Gets the parent logger.
|
|
/// </summary>
|
|
public FileLogger ParentLogger { get; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the timestamp format.
|
|
/// </summary>
|
|
public string TimestampFormat { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the timestamp is in UTC.
|
|
/// </summary>
|
|
public bool UseUtcTimestamp { get; set; }
|
|
|
|
/// <summary>
|
|
/// Gets or sets the minimum level to log.
|
|
/// </summary>
|
|
public LogLevel MinLevel { get; set; }
|
|
|
|
internal IExternalScopeProvider ScopeProvider { get; }
|
|
|
|
#endregion Properties
|
|
|
|
#region ILogger implementation
|
|
|
|
/// <inheritdoc cref="ILogger.BeginScope{TState}(TState)" />
|
|
public IDisposable BeginScope<TState>(TState state)
|
|
{
|
|
if (isDisposed)
|
|
throw new ObjectDisposedException(GetType().FullName);
|
|
|
|
return ScopeProvider?.Push(state) ?? NullScope.Instance;
|
|
}
|
|
|
|
/// <inheritdoc cref="ILogger.IsEnabled(LogLevel)" />
|
|
public bool IsEnabled(LogLevel logLevel)
|
|
{
|
|
if (isDisposed)
|
|
throw new ObjectDisposedException(GetType().FullName);
|
|
|
|
return logLevel >= MinLevel;
|
|
}
|
|
|
|
/// <inheritdoc cref="ILogger.Log{TState}(LogLevel, EventId, TState, Exception, Func{TState, Exception, string})" />
|
|
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
|
|
{
|
|
if (isDisposed)
|
|
throw new ObjectDisposedException(GetType().FullName);
|
|
|
|
if (!IsEnabled(logLevel))
|
|
return;
|
|
|
|
if (formatter == null)
|
|
throw new ArgumentNullException(nameof(formatter));
|
|
|
|
string message = formatter(state, exception);
|
|
|
|
if (!string.IsNullOrEmpty(message) || exception != null)
|
|
{
|
|
if (ParentLogger == null)
|
|
{
|
|
WriteMessage(Name, logLevel, eventId.Id, message, exception);
|
|
}
|
|
else
|
|
{
|
|
ParentLogger.WriteMessage(Name, logLevel, eventId.Id, message, exception);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion ILogger implementation
|
|
|
|
#region IDisposable implementation
|
|
|
|
/// <inheritdoc cref="IDisposable.Dispose" />
|
|
public void Dispose()
|
|
{
|
|
if (!isDisposed)
|
|
{
|
|
isDisposed = true;
|
|
|
|
cancellationTokenSource.Cancel();
|
|
writeTask.GetAwaiter().GetResult();
|
|
fileWriter.Dispose();
|
|
}
|
|
}
|
|
|
|
#endregion IDisposable implementation
|
|
|
|
#region Private methods
|
|
|
|
private void WriteMessage(string name, LogLevel logLevel, int eventId, string message, Exception exception)
|
|
{
|
|
queue.Enqueue(new QueueItem
|
|
{
|
|
Timestamp = UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now,
|
|
Name = name,
|
|
LogLevel = logLevel,
|
|
EventId = eventId,
|
|
Message = message,
|
|
Exception = exception
|
|
});
|
|
}
|
|
|
|
private async Task WriteFileAsync(CancellationToken token)
|
|
{
|
|
string timestampPadding = "";
|
|
string logLevelPadding = new(' ', 7);
|
|
|
|
var sb = new StringBuilder();
|
|
|
|
while (!token.IsCancellationRequested)
|
|
{
|
|
QueueItem[] items;
|
|
try
|
|
{
|
|
items = await queue.DequeueAvailableAsync(cancellationToken: token);
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (var item in items)
|
|
{
|
|
sb.Clear();
|
|
|
|
string timestamp = "";
|
|
string message = item.Message;
|
|
|
|
if (!string.IsNullOrEmpty(TimestampFormat))
|
|
{
|
|
timestamp = item.Timestamp.ToString(TimestampFormat) + " | ";
|
|
sb.Append(timestamp);
|
|
timestampPadding = new string(' ', timestamp.Length);
|
|
}
|
|
|
|
string logLevel = item.LogLevel switch
|
|
{
|
|
LogLevel.Trace => "TRCE | ",
|
|
LogLevel.Debug => "DBUG | ",
|
|
LogLevel.Information => "INFO | ",
|
|
LogLevel.Warning => "WARN | ",
|
|
LogLevel.Error => "FAIL | ",
|
|
LogLevel.Critical => "CRIT | ",
|
|
_ => " | ",
|
|
};
|
|
sb.Append(logLevel);
|
|
logLevelPadding = new string(' ', logLevel.Length);
|
|
|
|
if (ScopeProvider != null)
|
|
{
|
|
int initLength = sb.Length;
|
|
|
|
ScopeProvider.ForEachScope((scope, state) =>
|
|
{
|
|
var (builder, length) = state;
|
|
bool first = length == builder.Length;
|
|
builder.Append(first ? "=>" : " => ").Append(scope);
|
|
}, (sb, initLength));
|
|
|
|
if (sb.Length > initLength)
|
|
sb.Insert(initLength, timestampPadding + logLevelPadding);
|
|
}
|
|
|
|
if (item.Exception != null)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(message))
|
|
{
|
|
message += Environment.NewLine + item.Exception.ToString();
|
|
}
|
|
else
|
|
{
|
|
message = item.Exception.ToString();
|
|
}
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(item.Name))
|
|
sb.Append($"[{item.Name}] ");
|
|
|
|
sb.Append(message.Replace("\n", "\n" + timestampPadding + logLevelPadding));
|
|
|
|
await fileWriter.WriteLineAsync(sb.ToString());
|
|
}
|
|
|
|
await fileWriter.FlushAsync();
|
|
}
|
|
}
|
|
|
|
#endregion Private methods
|
|
|
|
private class QueueItem
|
|
{
|
|
public DateTime Timestamp { get; set; }
|
|
|
|
public string Name { get; set; }
|
|
|
|
public LogLevel LogLevel { get; set; }
|
|
|
|
public EventId EventId { get; set; }
|
|
|
|
public string Message { get; set; }
|
|
|
|
public Exception Exception { get; set; }
|
|
}
|
|
}
|
|
}
|