// Copyright Epic Games, Inc. All Rights Reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using Microsoft.Extensions.Logging; namespace EpicGames.Core { /// /// Simple implementation of which formats output to the console similar to Serilog's 'Code' style. /// public class DefaultConsoleLogger : ILogger { sealed class Disposable : IDisposable { public void Dispose() { } } const string AnsiReset = "\x1b[0m"; const string AnsiTime = "\x1b[38;5;0246m"; const string AnsiTrace = "\x1b[37m"; const string AnsiDebug = "\x1b[37m"; const string AnsiInformation = "\x1b[37;1m"; const string AnsiWarning = "\x1b[38;5;0229m"; const string AnsiError = "\x1b[38;5;0197m\x1b[48;5;0238m"; const string AnsiNull = "\x1b[38;5;0038m"; const string AnsiBool = "\x1b[38;5;0038m"; const string AnsiNumber = "\x1b[38;5;151m"; const string AnsiString = "\x1b[38;5;0216m"; readonly LogLevel _minLevel; /// /// Constructor /// /// Minimum level for messages to be output public DefaultConsoleLogger(LogLevel minLevel = LogLevel.Debug) { _minLevel = minLevel; } /// public bool IsEnabled(LogLevel logLevel) => logLevel >= _minLevel; /// public IDisposable? BeginScope(TState state) where TState : notnull => new Disposable(); /// public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { using StringWriter writer = new StringWriter(); Write(logLevel, state, exception, formatter, writer); Console.WriteLine(writer.ToString()); } static void Write(LogLevel logLevel, TState state, Exception? exception, Func formatter, TextWriter textWriter) { DateTime time = DateTime.Now; textWriter.Write(AnsiTime); textWriter.Write($"[{time:HH:mm:ss} "); switch (logLevel) { case LogLevel.Trace: textWriter.Write(AnsiTrace); textWriter.Write("trc"); break; case LogLevel.Debug: textWriter.Write(AnsiDebug); textWriter.Write("dbg"); break; case LogLevel.Information: textWriter.Write(AnsiInformation); textWriter.Write("inf" + AnsiTime); break; case LogLevel.Warning: textWriter.Write(AnsiWarning); textWriter.Write("wrn" + AnsiTime); break; case LogLevel.Error: textWriter.Write(AnsiError); textWriter.Write("err"); break; case LogLevel.Critical: textWriter.Write(AnsiError); textWriter.Write("ctl"); break; } textWriter.Write(AnsiReset); textWriter.Write(AnsiTime); textWriter.Write("] "); textWriter.Write(AnsiReset); const string OriginalFormatKeyName = "{OriginalFormat}"; if (state is IEnumerable> enumerable) { string? originalFormat = enumerable.FirstOrDefault(x => x.Key.Equals(OriginalFormatKeyName, StringComparison.OrdinalIgnoreCase)).Value as string; if (originalFormat != null) { ReadOnlySpan fmt = originalFormat.AsSpan(); while (fmt.Length > 0) { fmt = WriteToken(fmt, enumerable, textWriter); } return; } } textWriter.Write(formatter(state, exception)); } static ReadOnlySpan WriteToken(ReadOnlySpan fmt, IEnumerable> properties, TextWriter writer) { int idx = fmt.IndexOf('{'); if (idx == -1 || idx + 1 == fmt.Length) { writer.Write(fmt); return []; } if (fmt[idx + 1] == '{') { writer.Write(fmt.Slice(0, idx + 1)); return fmt.Slice(idx + 2); } writer.Write(fmt.Slice(0, idx)); fmt = fmt.Slice(idx); int endIdx = fmt.IndexOf('}'); if (endIdx == -1) { writer.Write(fmt); return []; } ReadOnlySpan name = fmt.Slice(1, endIdx - 1); ReadOnlySpan spec = []; int specIdx = name.IndexOfAny(':', ','); if (specIdx != -1) { spec = name.Slice(specIdx); name = name.Slice(0, specIdx); } object? obj = GetPropertyValue(properties, name); string? color = obj switch { null => AnsiNull, bool _ => AnsiBool, byte _ => AnsiNumber, sbyte _ => AnsiNumber, short _ => AnsiNumber, ushort _ => AnsiNumber, int _ => AnsiNumber, ulong _ => AnsiNumber, float _ => AnsiNumber, double _ => AnsiNumber, string _ => AnsiString, _ => null }; if (color != null) { writer.Write(color); } if (spec.Length > 0) { writer.Write($"{{0{spec}}}", obj); } else { writer.Write(obj); } if (color != null) { writer.Write(AnsiReset); } return fmt.Slice(endIdx + 1); } static object? GetPropertyValue(IEnumerable> enumerable, ReadOnlySpan name) { foreach (KeyValuePair item in enumerable) { if (name.Equals(item.Key, StringComparison.Ordinal)) { return item.Value; } } return null; } } }