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