mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-19 18:11:36 +01:00
430 lines
14 KiB
C#
430 lines
14 KiB
C#
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()
|
|
{
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
|
|
/// </summary>
|
|
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<string, LogLevel, bool> _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<string, LogLevel, bool> filter, bool includeScopes)
|
|
: this(name, filter, includeScopes ? new LoggerExternalScopeProvider() : null, new ConsoleLoggerProcessor())
|
|
{
|
|
}
|
|
|
|
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> filter, IExternalScopeProvider scopeProvider)
|
|
: this(name, filter, scopeProvider, new ConsoleLoggerProcessor())
|
|
{
|
|
}
|
|
|
|
internal CustomerConsoleLogger(string name, Func<string, LogLevel, bool> 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<string, LogLevel, bool> 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<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> 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>(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<LogMessageEntry> _messageQueue = new BlockingCollection<LogMessageEntry>(_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;
|
|
}
|
|
|
|
}
|