// 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;
}
}
}