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 using the interface! ///
/// Inspired by ConsoleLogger.cs ///
/// /// 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).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; } } } }