Iceshrimp.NET/Iceshrimp.Backend/Core/Extensions/ConsoleLoggerExtensions.cs

405 lines
No EOL
13 KiB
C#

using System.Diagnostics;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
namespace Iceshrimp.Backend.Core.Extensions;
public static class ConsoleLoggerExtensions
{
public static ILoggingBuilder AddCustomConsoleFormatter(this ILoggingBuilder builder)
{
if (Environment.GetEnvironmentVariable("INVOCATION_ID") is not null)
{
builder.AddConsole(options => options.FormatterName = "systemd-custom")
.AddConsoleFormatter<CustomSystemdConsoleFormatter, ConsoleFormatterOptions>();
}
else
{
builder.AddConsole(options => options.FormatterName = "custom")
.AddConsoleFormatter<CustomFormatter, ConsoleFormatterOptions>();
}
return builder;
}
}
/*
* This is a slightly modified version of Microsoft's SimpleConsoleFormatter.
* Changes mainly concern handling of line breaks.
* Adapted under MIT from https://github.com/dotnet/runtime/blob/main/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs
*/
#region Logger implementation
file static class TextWriterExtensions
{
private const string DefaultForegroundColor = "\x1B[39m\x1B[22m"; // reset to default foreground color
private const string DefaultBackgroundColor = "\x1B[49m"; // reset to the background color
public static void WriteColoredMessage(
this TextWriter textWriter, string message, ConsoleColor? background,
ConsoleColor? foreground
)
{
if (!ConsoleUtils.EmitAnsiColorCodes)
{
background = null;
foreground = null;
}
if (background.HasValue)
textWriter.Write(GetBackgroundColorEscapeCode(background.Value));
if (foreground.HasValue)
textWriter.Write(GetForegroundColorEscapeCode(foreground.Value));
textWriter.Write(message);
if (foreground.HasValue)
textWriter.Write(DefaultForegroundColor);
if (background.HasValue)
textWriter.Write(DefaultBackgroundColor);
}
private static string GetForegroundColorEscapeCode(ConsoleColor color)
{
return color switch
{
ConsoleColor.Black => "\x1B[30m",
ConsoleColor.DarkRed => "\x1B[31m",
ConsoleColor.DarkGreen => "\x1B[32m",
ConsoleColor.DarkYellow => "\x1B[33m",
ConsoleColor.DarkBlue => "\x1B[34m",
ConsoleColor.DarkMagenta => "\x1B[35m",
ConsoleColor.DarkCyan => "\x1B[36m",
ConsoleColor.Gray => "\x1B[37m",
ConsoleColor.Red => "\x1B[1m\x1B[31m",
ConsoleColor.Green => "\x1B[1m\x1B[32m",
ConsoleColor.Yellow => "\x1B[1m\x1B[33m",
ConsoleColor.Blue => "\x1B[1m\x1B[34m",
ConsoleColor.Magenta => "\x1B[1m\x1B[35m",
ConsoleColor.Cyan => "\x1B[1m\x1B[36m",
ConsoleColor.White => "\x1B[1m\x1B[37m",
_ => DefaultForegroundColor // default foreground color
};
}
private static string GetBackgroundColorEscapeCode(ConsoleColor color)
{
return color switch
{
ConsoleColor.Black => "\x1B[40m",
ConsoleColor.DarkRed => "\x1B[41m",
ConsoleColor.DarkGreen => "\x1B[42m",
ConsoleColor.DarkYellow => "\x1B[43m",
ConsoleColor.DarkBlue => "\x1B[44m",
ConsoleColor.DarkMagenta => "\x1B[45m",
ConsoleColor.DarkCyan => "\x1B[46m",
ConsoleColor.Gray => "\x1B[47m",
_ => DefaultBackgroundColor // Use default background color
};
}
}
file static class ConsoleUtils
{
private static volatile int _sEmitAnsiColorCodes = -1;
public static bool EmitAnsiColorCodes
{
get
{
var emitAnsiColorCodes = _sEmitAnsiColorCodes;
if (emitAnsiColorCodes != -1)
{
return Convert.ToBoolean(emitAnsiColorCodes);
}
var enabled = !Console.IsOutputRedirected;
if (enabled)
{
enabled = Environment.GetEnvironmentVariable("NO_COLOR") is null;
}
else
{
var envVar = Environment.GetEnvironmentVariable("FORCE_COLOR") ??
Environment.GetEnvironmentVariable("DOTNET_SYSTEM_CONSOLE_ALLOW_ANSI_COLOR_REDIRECTION");
enabled = envVar is not null &&
(envVar == "1" || envVar.Equals("true", StringComparison.OrdinalIgnoreCase));
}
_sEmitAnsiColorCodes = Convert.ToInt32(enabled);
return enabled;
}
}
}
file sealed class CustomFormatter() : ConsoleFormatter("custom"), ISupportExternalScope
{
private const string LoglevelPadding = ": ";
private static readonly string MessagePadding =
new(' ', GetLogLevelString(LogLevel.Information).Length + LoglevelPadding.Length);
private static readonly string NewLineWithMessagePadding = Environment.NewLine + MessagePadding;
private IExternalScopeProvider? _scopeProvider;
public override void Write<TState>(
in LogEntry<TState> logEntry,
IExternalScopeProvider? scopeProvider,
TextWriter textWriter
)
{
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (logEntry.Exception == null && message == null) return;
scopeProvider = _scopeProvider ?? scopeProvider;
Dictionary<string, string> scopes = [];
scopeProvider?.ForEachScope((p, _) =>
{
if (p is KeyValuePair<string, string> kvp)
scopes.Add(kvp.Key, kvp.Value);
else if (p is Tuple<string, string> tuple)
scopes.Add(tuple.Item1, tuple.Item2);
else if (p is (string key, string value))
scopes.Add(key, value);
else if (p is IEnumerable<KeyValuePair<string, object>> @enum)
foreach (var item in @enum.Where(e => e.Value is string))
scopes.Add(item.Key, (string)item.Value);
},
null as object);
var scope = scopes.GetValueOrDefault("JobId") ??
scopes.GetValueOrDefault("RequestId");
var logLevel = logEntry.LogLevel;
var logLevelColors = GetLogLevelConsoleColors(logLevel);
var logLevelString = GetLogLevelString(logLevel);
textWriter.WriteColoredMessage(logLevelString, logLevelColors.Background, logLevelColors.Foreground);
CreateDefaultLogMessage(textWriter, logEntry, message, scope);
}
private static void CreateDefaultLogMessage<TState>(
TextWriter textWriter, in LogEntry<TState> logEntry,
string message, string? scope
)
{
var exception = logEntry.Exception;
var singleLine = !message.Contains('\n') && exception == null;
textWriter.Write(LoglevelPadding);
textWriter.WriteColoredMessage("[", null, ConsoleColor.Gray);
textWriter.WriteColoredMessage(scope ?? "core", null, ConsoleColor.Cyan);
textWriter.WriteColoredMessage("] ", null, ConsoleColor.Gray);
textWriter.WriteColoredMessage(logEntry.Category, null, ConsoleColor.Blue);
if (singleLine) textWriter.WriteColoredMessage(" >", null, ConsoleColor.Gray);
else
{
textWriter.WriteColoredMessage(":", null, ConsoleColor.Gray);
textWriter.Write(Environment.NewLine);
}
WriteMessage(textWriter, message, singleLine);
if (exception != null)
{
WriteMessage(textWriter, exception.ToString(), singleLine);
}
if (singleLine)
{
textWriter.Write(Environment.NewLine);
}
}
private static void WriteMessage(TextWriter textWriter, string message, bool singleLine)
{
if (string.IsNullOrEmpty(message)) return;
if (singleLine)
{
textWriter.Write(' ');
WriteReplacing(textWriter, Environment.NewLine, " ", message);
}
else
{
textWriter.Write(MessagePadding);
WriteReplacing(textWriter, Environment.NewLine, NewLineWithMessagePadding, message);
textWriter.Write(Environment.NewLine);
}
return;
static void WriteReplacing(TextWriter writer, string oldValue, string newValue, string message)
{
var newMessage = message.Replace(oldValue, newValue);
writer.Write(newMessage);
}
}
private static string GetLogLevelString(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Trace => "trce",
LogLevel.Debug => "dbug",
LogLevel.Information => "info",
LogLevel.Warning => "warn",
LogLevel.Error => "fail",
LogLevel.Critical => "crit",
LogLevel.None => throw new ArgumentOutOfRangeException(nameof(logLevel)),
_ => throw new ArgumentOutOfRangeException(nameof(logLevel))
};
}
private static ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
{
if (!ConsoleUtils.EmitAnsiColorCodes)
{
return new ConsoleColors(null, null);
}
return logLevel switch
{
LogLevel.Trace => new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black),
LogLevel.Debug => new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black),
LogLevel.Information => new ConsoleColors(ConsoleColor.DarkGreen, ConsoleColor.Black),
LogLevel.Warning => new ConsoleColors(ConsoleColor.Yellow, ConsoleColor.Black),
LogLevel.Error => new ConsoleColors(ConsoleColor.Black, ConsoleColor.DarkRed),
LogLevel.Critical => new ConsoleColors(ConsoleColor.White, ConsoleColor.DarkRed),
_ => new ConsoleColors(null, null)
};
}
private readonly struct ConsoleColors(ConsoleColor? foreground, ConsoleColor? background)
{
public ConsoleColor? Foreground { get; } = foreground;
public ConsoleColor? Background { get; } = background;
}
public void SetScopeProvider(IExternalScopeProvider scopeProvider)
{
_scopeProvider = scopeProvider;
}
}
file sealed class CustomSystemdConsoleFormatter() : ConsoleFormatter("systemd-custom"), ISupportExternalScope
{
private static readonly string MessagePadding = new(' ', 6);
private IExternalScopeProvider? _scopeProvider;
public override void Write<TState>(
in LogEntry<TState> logEntry,
IExternalScopeProvider? scopeProvider,
TextWriter textWriter
)
{
var message = logEntry.Formatter(logEntry.State, logEntry.Exception);
// ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract
if (logEntry.Exception == null && message == null) return;
var logLevel = logEntry.LogLevel;
var category = logEntry.Category + " " + Activity.Current?.TraceId;
var exception = logEntry.Exception;
var singleLine = !message.Contains('\n') && exception == null;
var syslogSeverityString = GetSyslogSeverityString(logLevel);
scopeProvider = _scopeProvider ?? scopeProvider;
Dictionary<string, string> scopes = [];
scopeProvider?.ForEachScope((p, _) =>
{
if (p is KeyValuePair<string, string> kvp)
scopes.Add(kvp.Key, kvp.Value);
else if (p is Tuple<string, string> tuple)
scopes.Add(tuple.Item1, tuple.Item2);
else if (p is (string key, string value))
scopes.Add(key, value);
else if (p is IEnumerable<KeyValuePair<string, object>> @enum)
foreach (var item in @enum.Where(e => e.Value is string))
scopes.Add(item.Key, (string)item.Value);
},
null as object);
var scope = scopes.GetValueOrDefault("JobId") ??
scopes.GetValueOrDefault("RequestId");
textWriter.Write(syslogSeverityString);
textWriter.Write('[');
textWriter.Write(scope ?? "core");
textWriter.Write(']');
textWriter.Write(' ');
textWriter.Write(category);
textWriter.Write(singleLine ? " >" : ":");
if (!string.IsNullOrEmpty(message))
WriteMessage(textWriter, message, logLevel, singleLine);
if (exception != null)
WriteMessage(textWriter, exception.ToString(), logLevel, singleLine);
}
private static void WriteMessage(TextWriter textWriter, string message, LogLevel logLevel, bool singleLine)
{
if (string.IsNullOrEmpty(message)) return;
if (singleLine)
{
textWriter.Write(' ');
WriteReplacing(textWriter, Environment.NewLine, " ", message);
}
else
{
var sev = GetSyslogSeverityIndicatorString(logLevel);
var prefix = Environment.NewLine + sev + MessagePadding;
textWriter.Write(prefix);
WriteReplacing(textWriter, Environment.NewLine, prefix, message);
}
textWriter.Write(Environment.NewLine);
return;
static void WriteReplacing(TextWriter writer, string oldValue, string newValue, string message)
{
var newMessage = message.Replace(oldValue, newValue);
writer.Write(newMessage);
}
}
private static string GetSyslogSeverityString(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Trace => "<7>trce: ",
LogLevel.Debug => "<7>dbug: ",
LogLevel.Information => "<6>info: ",
LogLevel.Warning => "<4>warn: ",
LogLevel.Error => "<3>fail: ",
LogLevel.Critical => "<2>crit: ",
_ => throw new ArgumentOutOfRangeException(nameof(logLevel))
};
}
private static string GetSyslogSeverityIndicatorString(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Trace => "<7>",
LogLevel.Debug => "<7>",
LogLevel.Information => "<6>",
LogLevel.Warning => "<4>",
LogLevel.Error => "<3>",
LogLevel.Critical => "<2>",
_ => throw new ArgumentOutOfRangeException(nameof(logLevel))
};
}
public void SetScopeProvider(IExternalScopeProvider scopeProvider)
{
_scopeProvider = scopeProvider;
}
}
#endregion