using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Logging.Console.Internal;
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading.Tasks;
namespace BTCPayServer.Logging
{
public class CustomConsoleLogProvider : ILoggerProvider
{
ConsoleLoggerProcessor _Processor = new ConsoleLoggerProcessor();
public ILogger CreateLogger(string categoryName)
{
return new CustomConsoleLogger(categoryName, (a,b) => true, false, _Processor);
}
public void Dispose()
{
}
}
///
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
///
public class CustomConsoleLogger : ILogger
{
private static readonly string _loglevelPadding = ": ";
private static readonly string _messagePadding;
private static readonly string _newLineWithMessagePadding;
// ConsoleColor does not have a value to specify the 'Default' color
private readonly ConsoleColor? DefaultConsoleColor = null;
private readonly ConsoleLoggerProcessor _queueProcessor;
private Func _filter;
[ThreadStatic]
private static StringBuilder _logBuilder;
static CustomConsoleLogger()
{
var logLevelString = GetLogLevelString(LogLevel.Information);
_messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length);
_newLineWithMessagePadding = Environment.NewLine + _messagePadding;
}
public CustomConsoleLogger(string name, Func filter, bool includeScopes, ConsoleLoggerProcessor loggerProcessor)
{
Name = name ?? throw new ArgumentNullException(nameof(name));
Filter = filter ?? ((category, logLevel) => true);
IncludeScopes = includeScopes;
_queueProcessor = loggerProcessor;
if(RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Console = new WindowsLogConsole();
}
else
{
Console = new AnsiLogConsole(new AnsiSystemConsole());
}
}
public IConsole Console
{
get
{
return _queueProcessor.Console;
}
set
{
_queueProcessor.Console = value ?? throw new ArgumentNullException(nameof(value));
}
}
public Func Filter
{
get
{
return _filter;
}
set
{
_filter = value ?? throw new ArgumentNullException(nameof(value));
}
}
public bool IncludeScopes
{
get; set;
}
public string Name
{
get;
}
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
{
if(!IsEnabled(logLevel))
{
return;
}
if(formatter == null)
{
throw new ArgumentNullException(nameof(formatter));
}
var message = formatter(state, exception);
if(!string.IsNullOrEmpty(message) || exception != null)
{
WriteMessage(logLevel, Name, eventId.Id, message, exception);
}
}
public virtual void WriteMessage(LogLevel logLevel, string logName, int eventId, string message, Exception exception)
{
var logBuilder = _logBuilder;
_logBuilder = null;
if(logBuilder == null)
{
logBuilder = new StringBuilder();
}
var logLevelColors = default(ConsoleColors);
var logLevelString = string.Empty;
// Example:
// INFO: ConsoleApp.Program[10]
// Request received
logLevelColors = GetLogLevelConsoleColors(logLevel);
logLevelString = GetLogLevelString(logLevel);
// category and event id
var lenBefore = logBuilder.ToString().Length;
logBuilder.Append(_loglevelPadding);
logBuilder.Append(logName);
logBuilder.Append(": ");
var lenAfter = logBuilder.ToString().Length;
while(lenAfter++ < 18)
logBuilder.Append(" ");
// scope information
if(IncludeScopes)
{
GetScopeInformation(logBuilder);
}
if(!string.IsNullOrEmpty(message))
{
// message
//logBuilder.Append(_messagePadding);
var len = logBuilder.Length;
logBuilder.AppendLine(message);
logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length);
}
// Example:
// System.InvalidOperationException
// at Namespace.Class.Function() in File:line X
if(exception != null)
{
// exception message
logBuilder.AppendLine(exception.ToString());
}
if(logBuilder.Length > 0)
{
var hasLevel = !string.IsNullOrEmpty(logLevelString);
// Queue log message
_queueProcessor.EnqueueMessage(new LogMessageEntry()
{
Message = logBuilder.ToString(),
MessageColor = DefaultConsoleColor,
LevelString = hasLevel ? logLevelString : null,
LevelBackground = hasLevel ? logLevelColors.Background : null,
LevelForeground = hasLevel ? logLevelColors.Foreground : null
});
}
logBuilder.Clear();
if(logBuilder.Capacity > 1024)
{
logBuilder.Capacity = 1024;
}
_logBuilder = logBuilder;
}
public bool IsEnabled(LogLevel logLevel)
{
return Filter(Name, logLevel);
}
public IDisposable BeginScope(TState state)
{
if(state == null)
{
throw new ArgumentNullException(nameof(state));
}
return ConsoleLogScope.Push(Name, state);
}
private static string GetLogLevelString(LogLevel logLevel)
{
switch(logLevel)
{
case LogLevel.Trace:
return "trce";
case LogLevel.Debug:
return "dbug";
case LogLevel.Information:
return "info";
case LogLevel.Warning:
return "warn";
case LogLevel.Error:
return "fail";
case LogLevel.Critical:
return "crit";
default:
throw new ArgumentOutOfRangeException(nameof(logLevel));
}
}
private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
{
// We must explicitly set the background color if we are setting the foreground color,
// since just setting one can look bad on the users console.
switch(logLevel)
{
case LogLevel.Critical:
return new ConsoleColors(ConsoleColor.White, ConsoleColor.Red);
case LogLevel.Error:
return new ConsoleColors(ConsoleColor.Black, ConsoleColor.Red);
case LogLevel.Warning:
return new ConsoleColors(ConsoleColor.Yellow, ConsoleColor.Black);
case LogLevel.Information:
return new ConsoleColors(ConsoleColor.DarkGreen, ConsoleColor.Black);
case LogLevel.Debug:
return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black);
case LogLevel.Trace:
return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black);
default:
return new ConsoleColors(DefaultConsoleColor, DefaultConsoleColor);
}
}
private void GetScopeInformation(StringBuilder builder)
{
var current = ConsoleLogScope.Current;
string scopeLog = string.Empty;
var length = builder.Length;
while(current != null)
{
if(length == builder.Length)
{
scopeLog = $"=> {current}";
}
else
{
scopeLog = $"=> {current} ";
}
builder.Insert(length, scopeLog);
current = current.Parent;
}
if(builder.Length > length)
{
builder.Insert(length, _messagePadding);
builder.AppendLine();
}
}
private struct ConsoleColors
{
public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background)
{
Foreground = foreground;
Background = background;
}
public ConsoleColor? Foreground
{
get;
}
public ConsoleColor? Background
{
get;
}
}
private class AnsiSystemConsole : IAnsiSystemConsole
{
public void Write(string message)
{
System.Console.Write(message);
}
public void WriteLine(string message)
{
System.Console.WriteLine(message);
}
}
}
public class ConsoleLoggerProcessor : IDisposable
{
private const int _maxQueuedMessages = 1024;
private readonly BlockingCollection _messageQueue = new BlockingCollection(_maxQueuedMessages);
private readonly Task _outputTask;
public IConsole Console;
public ConsoleLoggerProcessor()
{
// Start Console message queue processor
_outputTask = Task.Factory.StartNew(
ProcessLogQueue,
this,
TaskCreationOptions.LongRunning);
}
public virtual void EnqueueMessage(LogMessageEntry message)
{
if(!_messageQueue.IsAddingCompleted)
{
try
{
_messageQueue.Add(message);
return;
}
catch(InvalidOperationException) { }
}
// Adding is completed so just log the message
WriteMessage(message);
}
// for testing
internal virtual void WriteMessage(LogMessageEntry message)
{
if(message.LevelString != null)
{
Console.Write(message.LevelString, message.LevelBackground, message.LevelForeground);
}
Console.Write(message.Message, message.MessageColor, message.MessageColor);
Console.Flush();
}
private void ProcessLogQueue()
{
foreach(var message in _messageQueue.GetConsumingEnumerable())
{
WriteMessage(message);
}
}
private static void ProcessLogQueue(object state)
{
var consoleLogger = (ConsoleLoggerProcessor)state;
consoleLogger.ProcessLogQueue();
}
public void Dispose()
{
_messageQueue.CompleteAdding();
try
{
_outputTask.Wait(1500); // with timeout in-case Console is locked by user input
}
catch(TaskCanceledException) { }
catch(AggregateException ex) when(ex.InnerExceptions.Count == 1 && ex.InnerExceptions[0] is TaskCanceledException) { }
}
}
public struct LogMessageEntry
{
public string LevelString;
public ConsoleColor? LevelBackground;
public ConsoleColor? LevelForeground;
public ConsoleColor? MessageColor;
public string Message;
}
}