mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2024-11-20 02:28:31 +01:00
401 lines
9.5 KiB
C#
401 lines
9.5 KiB
C#
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()
|
|
{
|
|
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// A variant of ASP.NET Core ConsoleLogger which does not make new line for the category
|
|
/// </summary>
|
|
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<string, LogLevel, bool> _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<string, LogLevel, bool> 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<string, LogLevel, bool> Filter
|
|
{
|
|
get
|
|
{
|
|
return _filter;
|
|
}
|
|
set
|
|
{
|
|
_filter = value ?? throw new ArgumentNullException(nameof(value));
|
|
}
|
|
}
|
|
|
|
public bool IncludeScopes
|
|
{
|
|
get; set;
|
|
}
|
|
|
|
public string Name
|
|
{
|
|
get;
|
|
}
|
|
|
|
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
|
|
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>(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<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,
|
|
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;
|
|
}
|
|
|
|
}
|