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; }
}
}
}