using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions.Internal; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Text; using System.Threading; using System.Threading.Tasks; namespace AMWD.Logging { /// /// Implements a file logging based on the interface. /// /// /// This implementation is also implementing the interface! ///
/// Inspired by https://github.com/aspnet/Logging/blob/master/src/Microsoft.Extensions.Logging.Console/ConsoleLogger.cs ///
public class FileLogger : ILogger, IDisposable { #region Fields private bool isDisposed = false; private readonly CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); private readonly StreamWriter fileWriter; private readonly Task writeTask; private readonly ConcurrentQueue queue = new ConcurrentQueue(); #endregion Fields #region Constructors /// /// Initializes a new instance of the class. /// /// The log file. public FileLogger(string file) { FileName = file; fileWriter = new StreamWriter(FileName); writeTask = Task.Run(() => WriteFileAsync(cancellationTokenSource.Token)); } /// /// Initializes a new named instance of the class. /// /// The log file. /// The logger name. public FileLogger(string file, string name) : this(file) { Name = name; } /// /// Initializes a new instance of the class. /// /// The log file. /// The logger name. /// The parent logger. public FileLogger(string file, string name, FileLogger parentLogger) : this(file, name) { ParentLogger = parentLogger; } /// /// Initializes a new instance of the class. /// /// The log file. /// The logger name. /// The scope provider. public FileLogger(string file, string name, IExternalScopeProvider scopeProvider) : this(file, name) { ScopeProvider = scopeProvider; } /// /// Initializes a new instance of the class. /// /// The log file. /// The logger name. /// The parent logger. /// /// The scope provider. public FileLogger(string file, string name, FileLogger parentLogger, IExternalScopeProvider scopeProvider) : this(file, name, parentLogger) { 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 the minimum level to log. /// public LogLevel MinLevel { get; set; } internal IExternalScopeProvider ScopeProvider { get; } #endregion Properties #region ILogger implementation /// /// Begins a logical operation scope. /// /// The type of the parameter. /// The identifier for the scope. /// An IDisposable that ends the logical operation scope on dispose. public IDisposable BeginScope(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance; /// /// Checks if the given logLevel is enabled. /// /// level to be checked. /// true if enabled. public bool IsEnabled(LogLevel logLevel) { if (isDisposed) { throw new ObjectDisposedException(GetType().FullName); } return logLevel >= MinLevel; } /// /// Writes a log entry. /// /// The type of the parameter. /// Entry will be written on this level. /// Id of the event. /// The entry to be written. Can be also an object. /// The exception related to this entry. /// Function to create a string message of the state and exception. 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 /// /// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. /// 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 = DateTime.Now, Name = name, LogLevel = logLevel, EventId = eventId, Message = message, Exception = exception }); } private async Task WriteFileAsync(CancellationToken token) { string timestampPadding = ""; string logLevelPadding = new string(' ', 7); var sb = new StringBuilder(); while (!token.IsCancellationRequested) { SpinWait.SpinUntil(() => !queue.IsEmpty || token.IsCancellationRequested); if (token.IsCancellationRequested) return; var items = new List(); do { if (queue.TryDequeue(out var item)) { items.Add(item); } } while (!queue.IsEmpty); foreach (var item in items) { sb.Clear(); string timestamp = ""; string logLevel = ""; string message = item.Message; if (!string.IsNullOrEmpty(TimestampFormat)) { timestamp = item.Timestamp.ToString(TimestampFormat) + " | "; sb.Append(timestamp); timestampPadding = new string(' ', timestamp.Length); } switch (item.LogLevel) { case LogLevel.Trace: logLevel = "TRCE | "; break; case LogLevel.Debug: logLevel = "DBUG | "; break; case LogLevel.Information: logLevel = "INFO | "; break; case LogLevel.Warning: logLevel = "WARN | "; break; case LogLevel.Error: logLevel = "FAIL | "; break; case LogLevel.Critical: logLevel = "CRIT | "; break; default: logLevel = " | "; break; } 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()); } } } #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; } } } }