Merge main
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
|
||||
namespace System
|
||||
@@ -40,5 +41,13 @@ namespace System
|
||||
/// <returns>The description or the string representation of the value.</returns>
|
||||
public static string GetDescription(this Enum value)
|
||||
=> value.GetAttribute<DescriptionAttribute>()?.Description ?? value.ToString();
|
||||
|
||||
/// <summary>
|
||||
/// Returns the name from <see cref="DisplayAttribute"/>.
|
||||
/// </summary>
|
||||
/// <param name="value">The enum value.</param>
|
||||
/// <returns>The display name or the string representation of the value.</returns>
|
||||
public static string GetDisplayName(this Enum value)
|
||||
=> value.GetAttribute<DisplayAttribute>()?.Name ?? value.ToString();
|
||||
}
|
||||
}
|
||||
|
||||
329
AMWD.Common/Logging/FileLogger.cs
Normal file
329
AMWD.Common/Logging/FileLogger.cs
Normal file
@@ -0,0 +1,329 @@
|
||||
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 implementing the <see cref="IDisposable"/> interface!
|
||||
/// <br/>
|
||||
/// Inspired by <a href="https://github.com/aspnet/Logging/blob/2d2f31968229eddb57b6ba3d34696ef366a6c71b/src/Microsoft.Extensions.Logging.Console/ConsoleLogger.cs" />
|
||||
/// </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; }
|
||||
}
|
||||
}
|
||||
}
|
||||
25
AMWD.Common/Logging/NullScope.cs
Normal file
25
AMWD.Common/Logging/NullScope.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
using System;
|
||||
|
||||
namespace AMWD.Common.Logging
|
||||
{
|
||||
/// <summary>
|
||||
/// An empty scope without any logic.
|
||||
/// </summary>
|
||||
[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage]
|
||||
internal class NullScope : IDisposable
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="NullScope"/> instance to use.
|
||||
/// </summary>
|
||||
public static NullScope Instance { get; } = new NullScope();
|
||||
|
||||
private NullScope()
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void Dispose()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user