mirror of
https://github.com/btcpayserver/btcpayserver.git
synced 2025-02-23 06:35:13 +01:00
* Enable NETAnalyzers for whole project - remove obsolete analyzers so that the .NET Core SDK NETAnalyzers can be used - enable NETAnalyzers for all projects so that developers can use them by defining the AnalysisMode on individual projects This is because if we set AnalysisMode to minimal, recommended or all it would spam with warning. The idea is to be able to turn them on during development to fix recommended stuff without polluting the build output. Following commits will implement some of the Code Analysis findings * Performance hints for using char overloads for single characters (CA1834 and CA1847) CA1834: Use StringBuilder.Append(char) for single character strings CA1847: Use string.Contains(char) instead of string.Contains(string) with single characters
428 lines
14 KiB
C#
428 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.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
|
|
{
|
|
readonly 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;
|
|
}
|
|
|
|
}
|