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
{
///
/// Implements a file logging based on the interface.
///
///
/// This implementation is also implementing the interface!
///
/// Inspired by
///
///
///
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 queue = new();
#endregion Fields
#region Constructors
///
/// Initializes a new instance of the class.
///
/// The log file.
/// A value indicating whether to append lines to an existing file (Default: false).
/// The file encoding (Default: ).
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));
}
///
/// Initializes a new named instance of the class.
///
/// The log file.
/// The logger name.
/// A value indicating whether to append lines to an existing file.
/// The file encoding.
public FileLogger(string file, string name, bool append = false, Encoding encoding = null)
: this(file, append, encoding)
{
Name = name;
}
///
/// Initializes a new instance of the class.
///
/// The log file.
/// The logger name.
/// The parent logger.
/// A value indicating whether to append lines to an existing file.
/// The file encoding.
public FileLogger(string file, string name, FileLogger parentLogger, bool append = false, Encoding encoding = null)
: this(file, name, append, encoding)
{
ParentLogger = parentLogger;
}
///
/// Initializes a new instance of the class.
///
/// The log file.
/// The logger name.
/// The scope provider.
/// A value indicating whether to append lines to an existing file.
/// The file encoding.
public FileLogger(string file, string name, IExternalScopeProvider scopeProvider, bool append = false, Encoding encoding = null)
: this(file, name, append, encoding)
{
ScopeProvider = scopeProvider;
}
///
/// Initializes a new instance of the class.
///
/// The log file.
/// The logger name.
/// The parent logger.
/// The scope provider.
/// A value indicating whether to append lines to an existing file.
/// The file encoding.
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
///
/// Gets the log file.
///
public string FileName { get; }
///
/// Gets the name of the logger.
///
public string Name { get; }
///
/// Gets the parent logger.
///
public FileLogger ParentLogger { get; }
///
/// Gets or sets the timestamp format.
///
public string TimestampFormat { get; set; }
///
/// Gets or sets a value indicating whether the timestamp is in UTC.
///
public bool UseUtcTimestamp { get; set; }
///
/// Gets or sets the minimum level to log.
///
public LogLevel MinLevel { get; set; }
internal IExternalScopeProvider ScopeProvider { get; }
#endregion Properties
#region ILogger implementation
///
public IDisposable BeginScope(TState state)
{
if (isDisposed)
throw new ObjectDisposedException(GetType().FullName);
return ScopeProvider?.Push(state) ?? NullScope.Instance;
}
///
public bool IsEnabled(LogLevel logLevel)
{
if (isDisposed)
throw new ObjectDisposedException(GetType().FullName);
return logLevel >= MinLevel;
}
///
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func 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
///
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; }
}
}
}