using System; using System.Collections.Concurrent; using System.Runtime.InteropServices; using System.Text; using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions.Internal; using Microsoft.Extensions.Logging.Console; using Microsoft.Extensions.Logging.Console.Internal; namespace BTCPayServer.Logging { public class CustomConsoleLogProvider : ILoggerProvider { ConsoleLoggerProcessor _Processor; public CustomConsoleLogProvider(ConsoleLoggerProcessor processor) { _Processor = processor; } public ILogger CreateLogger(string categoryName) { return new CustomerConsoleLogger(categoryName, (a, b) => true, null, _Processor); } public void Dispose() { } } /// /// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category /// public class CustomerConsoleLogger : 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 CustomerConsoleLogger() { var logLevelString = GetLogLevelString(LogLevel.Information); _messagePadding = new string(' ', logLevelString.Length + _loglevelPadding.Length); _newLineWithMessagePadding = Environment.NewLine + _messagePadding; } public CustomerConsoleLogger(string name, Func filter, bool includeScopes) : this(name, filter, includeScopes ? new LoggerExternalScopeProvider() : null, new ConsoleLoggerProcessor()) { } internal CustomerConsoleLogger(string name, Func filter, IExternalScopeProvider scopeProvider) : this(name, filter, scopeProvider, new ConsoleLoggerProcessor()) { } internal CustomerConsoleLogger(string name, Func filter, IExternalScopeProvider scopeProvider, ConsoleLoggerProcessor loggerProcessor) { if (name == null) { throw new ArgumentNullException(nameof(name)); } Name = name; Filter = filter ?? ((category, logLevel) => true); ScopeProvider = scopeProvider; _queueProcessor = loggerProcessor; if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { Console = new WindowsLogConsole(); } else { Console = new AnsiLogConsole(new AnsiSystemConsole()); } } public IConsole Console { get { return _queueProcessor.Console; } set { if (value == null) { throw new ArgumentNullException(nameof(value)); } _queueProcessor.Console = value; } } public Func Filter { get { return _filter; } set { if (value == null) { throw new ArgumentNullException(nameof(value)); } _filter = value; } } public string Name { get; } internal IExternalScopeProvider ScopeProvider { get; set; } public bool DisableColors { get; set; } 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 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) { if (logLevel == LogLevel.None) { return false; } return Filter(Name, logLevel); } public IDisposable BeginScope(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance; 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) { if (DisableColors) { return new ConsoleColors(null, null); } // 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 stringBuilder) { var scopeProvider = ScopeProvider; if (scopeProvider != null) { var initialLength = stringBuilder.Length; scopeProvider.ForEachScope((scope, state) => { var (builder, length) = state; var first = length == builder.Length; builder.Append(first ? "=> " : " => ").Append(scope); }, (stringBuilder, initialLength)); if (stringBuilder.Length > initialLength) { stringBuilder.Insert(initialLength, _messagePadding); stringBuilder.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, state: this, cancellationToken: default(CancellationToken), creationOptions: TaskCreationOptions.LongRunning, scheduler: TaskScheduler.Default); } 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; } }