From bdb2fe39fb0cc206c023b30d41b7524664930428 Mon Sep 17 00:00:00 2001 From: Laura Hausmann Date: Sun, 4 Feb 2024 01:45:07 +0100 Subject: [PATCH] [backend] Add custom console logger --- .../Extensions/ConsoleLoggerExtensions.cs | 219 ++++++++++++++++++ Iceshrimp.Backend/Startup.cs | 3 +- 2 files changed, 220 insertions(+), 2 deletions(-) create mode 100644 Iceshrimp.Backend/Core/Extensions/ConsoleLoggerExtensions.cs diff --git a/Iceshrimp.Backend/Core/Extensions/ConsoleLoggerExtensions.cs b/Iceshrimp.Backend/Core/Extensions/ConsoleLoggerExtensions.cs new file mode 100644 index 00000000..9dc1f2e5 --- /dev/null +++ b/Iceshrimp.Backend/Core/Extensions/ConsoleLoggerExtensions.cs @@ -0,0 +1,219 @@ +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) => + builder.AddConsole(options => options.FormatterName = "custom") + .AddConsoleFormatter(); +}; + +/* + * 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 { + public static void WriteColoredMessage(this TextWriter textWriter, string message, ConsoleColor? background, + ConsoleColor? foreground) { + 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 const string DefaultForegroundColor = "\x1B[39m\x1B[22m"; // reset to default foreground color + private const string DefaultBackgroundColor = "\x1B[49m"; // reset to the background color + + 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("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") { + private const string LoglevelPadding = ": "; + + private static readonly string MessagePadding = + new(' ', GetLogLevelString(LogLevel.Information).Length + LoglevelPadding.Length); + + private static readonly string NewLineWithMessagePadding = Environment.NewLine + MessagePadding; + + public override void Write( + in LogEntry 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 logLevelColors = GetLogLevelConsoleColors(logLevel); + var logLevelString = GetLogLevelString(logLevel); + + textWriter.WriteColoredMessage(logLevelString, logLevelColors.Background, logLevelColors.Foreground); + + CreateDefaultLogMessage(textWriter, logEntry, message); + } + + + private static void CreateDefaultLogMessage(TextWriter textWriter, in LogEntry logEntry, + string message) { + var singleLine = !message.Contains('\n'); + var eventId = logEntry.EventId.Id; + var exception = logEntry.Exception; + + textWriter.Write(LoglevelPadding); + textWriter.Write(logEntry.Category); + textWriter.Write('['); + + Span span = stackalloc char[10]; + if (eventId.TryFormat(span, out var charsWritten)) + textWriter.Write(span[..charsWritten]); + else + textWriter.Write(eventId.ToString()); + + textWriter.Write(']'); + if (!singleLine) { + 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; + } +} + +#endregion \ No newline at end of file diff --git a/Iceshrimp.Backend/Startup.cs b/Iceshrimp.Backend/Startup.cs index ecb9a5e7..888b7b6b 100644 --- a/Iceshrimp.Backend/Startup.cs +++ b/Iceshrimp.Backend/Startup.cs @@ -29,8 +29,7 @@ builder.Services.AddViteServices(options => { options.Server.AutoRun = false; //TODO: Fix script generation on macOS options.Server.UseFullDevUrl = true; }); -//TODO: single line only if there's no \n in the log msg (otherwise stacktraces don't work) -builder.Services.AddLogging(logging => logging.AddSimpleConsole(options => { options.SingleLine = false; })); +builder.Services.AddLogging(logging => logging.AddCustomConsoleFormatter()); builder.Services.AddDatabaseContext(builder.Configuration); //TODO: maybe use a dbcontext factory? builder.Services.AddRedis(builder.Configuration); builder.Services.AddSlidingWindowRateLimiter();