1
0

Solution restructured to use multiple test projects

This commit is contained in:
2024-07-04 18:22:26 +02:00
parent 508379d704
commit df6763b99b
144 changed files with 387 additions and 1693 deletions

View File

@@ -0,0 +1,327 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Logging;
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).ConfigureAwait(false);
}
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()).ConfigureAwait(false);
}
await _fileWriter.FlushAsync().ConfigureAwait(false);
}
}
#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; }
}
}
}