feat!: reworked the logging commands for the tool
- Added many parameters to control formatting - Added the ability to exclude source contexts - Extracted the SpectreConsole expression loggger into a separate assembly
This commit is contained in:
parent
6c05266005
commit
770edf99a1
15
MfGames.sln
15
MfGames.sln
|
@ -83,6 +83,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{F7
|
|||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SampleTool", "examples\SampleTool\SampleTool.csproj", "{D58365E6-E98B-4A04-8447-4B9417850D85}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MfGames.Serilog.SpectreExpressions", "src\MfGames.Serilog.SpectreExpressions\MfGames.Serilog.SpectreExpressions.csproj", "{25457946-9CD0-498E-8B46-03C420CCF103}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
|
@ -540,6 +542,18 @@ Global
|
|||
{D58365E6-E98B-4A04-8447-4B9417850D85}.Release|x64.Build.0 = Release|Any CPU
|
||||
{D58365E6-E98B-4A04-8447-4B9417850D85}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{D58365E6-E98B-4A04-8447-4B9417850D85}.Release|x86.Build.0 = Release|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Release|x64.Build.0 = Release|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(NestedProjects) = preSolution
|
||||
{5253E2A6-9565-45AF-92EA-1BFD3A63AC23} = {9C845D9A-B359-43B3-AE9E-B84CE945AF21}
|
||||
|
@ -579,5 +593,6 @@ Global
|
|||
{80EC7EB3-AC5C-4A23-A1BA-BCB6AD5A36B3} = {4CE102F8-5C70-4696-B85F-93BB10034918}
|
||||
{36F37E51-E760-4368-B71A-BB2A9C82736A} = {4CE102F8-5C70-4696-B85F-93BB10034918}
|
||||
{D58365E6-E98B-4A04-8447-4B9417850D85} = {F79B6838-B175-43A3-8C52-69A414CC1386}
|
||||
{25457946-9CD0-498E-8B46-03C420CCF103} = {9C845D9A-B359-43B3-AE9E-B84CE945AF21}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
|
|
|
@ -10,77 +10,86 @@ namespace SampleTool;
|
|||
|
||||
public class LogCommand : Command, ICommandHandler
|
||||
{
|
||||
private readonly ILogger<LogCommand> extensionLogger;
|
||||
private readonly ILogger<LogCommand> extensionLogger;
|
||||
|
||||
private readonly ILogger serilogLogger;
|
||||
private readonly ILogger serilogContextLogger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public LogCommand(
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger serilogLogger)
|
||||
: base("log", "Shows various logging messages using Serilog and Microsoft")
|
||||
{
|
||||
this.serilogLogger = serilogLogger;
|
||||
this.extensionLogger = loggerFactory.CreateLogger<LogCommand>();
|
||||
this.Handler = this;
|
||||
}
|
||||
private readonly ILogger serilogLogger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Invoke(InvocationContext context)
|
||||
{
|
||||
return this.InvokeAsync(context).Result;
|
||||
}
|
||||
/// <inheritdoc />
|
||||
public LogCommand(
|
||||
ILoggerFactory loggerFactory,
|
||||
ILogger serilogLogger)
|
||||
: base(
|
||||
"log",
|
||||
"Shows various logging messages using Serilog and Microsoft")
|
||||
{
|
||||
this.serilogLogger = serilogLogger;
|
||||
this.serilogContextLogger = serilogLogger.ForContext<LogCommand>();
|
||||
this.extensionLogger = loggerFactory.CreateLogger<LogCommand>();
|
||||
this.Handler = this;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Task<int> InvokeAsync(InvocationContext context)
|
||||
{
|
||||
// Show the serilog logging.
|
||||
this.serilogLogger.Verbose("Serilog Verbose()");
|
||||
this.serilogLogger.Debug("Serilog Debug()");
|
||||
this.serilogLogger.Information("Serilog Information()");
|
||||
this.serilogLogger.Warning("Serilog Warning()");
|
||||
this.serilogLogger.Error("Serilog Error()");
|
||||
this.serilogLogger.Fatal("Serilog Fatal()");
|
||||
/// <inheritdoc />
|
||||
public int Invoke(InvocationContext context)
|
||||
{
|
||||
return this.InvokeAsync(context).Result;
|
||||
}
|
||||
|
||||
// Show the extension logging.
|
||||
this.extensionLogger.LogTrace(
|
||||
"System.Extension.Logging LogTrace");
|
||||
this.extensionLogger.LogDebug(
|
||||
"System.Extension.Logging LogDebug");
|
||||
this.extensionLogger.LogInformation(
|
||||
"System.Extension.Logging LogInformation");
|
||||
this.extensionLogger.LogWarning(
|
||||
"System.Extension.Logging LogWarning");
|
||||
this.extensionLogger.LogError(
|
||||
"System.Extension.Logging LogError");
|
||||
this.extensionLogger.LogCritical(
|
||||
"System.Extension.Logging LogCritical");
|
||||
/// <inheritdoc />
|
||||
public Task<int> InvokeAsync(InvocationContext context)
|
||||
{
|
||||
// Show the serilog logging.
|
||||
this.serilogLogger.Verbose("Serilog Verbose()");
|
||||
this.serilogLogger.Debug("Serilog Debug()");
|
||||
this.serilogLogger.Information("Serilog Information()");
|
||||
this.serilogLogger.Warning("Serilog Warning()");
|
||||
this.serilogLogger.Error("Serilog Error()");
|
||||
this.serilogLogger.Fatal("Serilog Fatal()");
|
||||
|
||||
// Show Serilog working through logging extensions.
|
||||
var hash = new
|
||||
{
|
||||
Number = 1,
|
||||
String = "String",
|
||||
Inner = new { Nested = true }
|
||||
};
|
||||
// Show serilog with context.
|
||||
this.serilogContextLogger.Information(
|
||||
"Serilog Information() with context");
|
||||
|
||||
this.serilogLogger.Information(
|
||||
"Serilog Contextual parameters {Name} and {Quotes:l}",
|
||||
"extension logger",
|
||||
"without quotes");
|
||||
this.serilogLogger.Information(
|
||||
"Serilog Contextual nested object {@Nested}",
|
||||
hash);
|
||||
// Show the extension logging.
|
||||
this.extensionLogger.LogTrace(
|
||||
"System.Extension.Logging LogTrace");
|
||||
this.extensionLogger.LogDebug(
|
||||
"System.Extension.Logging LogDebug");
|
||||
this.extensionLogger.LogInformation(
|
||||
"System.Extension.Logging LogInformation");
|
||||
this.extensionLogger.LogWarning(
|
||||
"System.Extension.Logging LogWarning");
|
||||
this.extensionLogger.LogError(
|
||||
"System.Extension.Logging LogError");
|
||||
this.extensionLogger.LogCritical(
|
||||
"System.Extension.Logging LogCritical");
|
||||
|
||||
this.extensionLogger.LogInformation(
|
||||
"System.Extension.Logging parameters {Name} and {Quotes:l}",
|
||||
"extension logger",
|
||||
"without quotes");
|
||||
this.extensionLogger.LogInformation(
|
||||
"System.Extension.Logging nested object {@Nested}",
|
||||
hash);
|
||||
// Show Serilog working through logging extensions.
|
||||
var hash = new
|
||||
{
|
||||
Number = 1,
|
||||
String = "String",
|
||||
Inner = new { Nested = true }
|
||||
};
|
||||
|
||||
// We're good.
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
this.serilogLogger.Information(
|
||||
"Serilog Contextual parameters {Name} and {Quotes:l}",
|
||||
"extension logger",
|
||||
"without quotes");
|
||||
this.serilogLogger.Information(
|
||||
"Serilog Contextual nested object {@Nested}",
|
||||
hash);
|
||||
|
||||
this.extensionLogger.LogInformation(
|
||||
"System.Extension.Logging parameters {Name} and {Quotes:l}",
|
||||
"extension logger",
|
||||
"without quotes");
|
||||
this.extensionLogger.LogInformation(
|
||||
"System.Extension.Logging nested object {@Nested}",
|
||||
hash);
|
||||
|
||||
// We're good.
|
||||
return Task.FromResult(0);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,539 @@
|
|||
using System.Text;
|
||||
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Templates;
|
||||
|
||||
using Spectre.Console;
|
||||
|
||||
namespace MfGames.Serilog.SpectreExpressions;
|
||||
|
||||
public class ExpressionSpectreConsoleSink : ILogEventSink
|
||||
{
|
||||
/// <summary>
|
||||
/// The internal lookup codes for ANSI colors to Spectre color names.
|
||||
/// https://spectreconsole.net/appendix/colors
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> AnsiToSpectre = new()
|
||||
{
|
||||
["0"] = "black",
|
||||
["1"] = "maroon",
|
||||
["2"] = "green",
|
||||
["3"] = "olive",
|
||||
["4"] = "navy",
|
||||
["5"] = "purple",
|
||||
["6"] = "teal",
|
||||
["7"] = "silver",
|
||||
["8"] = "grey",
|
||||
["9"] = "red",
|
||||
["10"] = "lime",
|
||||
["11"] = "yellow",
|
||||
["12"] = "blue",
|
||||
["13"] = "fuchsia",
|
||||
["14"] = "aqua",
|
||||
["15"] = "white",
|
||||
["16"] = "grey0",
|
||||
["17"] = "navyblue",
|
||||
["18"] = "darkblue",
|
||||
["19"] = "blue3",
|
||||
["20"] = "blue3_1",
|
||||
["21"] = "blue1",
|
||||
["22"] = "darkgreen",
|
||||
["23"] = "deepskyblue4",
|
||||
["24"] = "deepskyblue4_1",
|
||||
["25"] = "deepskyblue4_2",
|
||||
["26"] = "dodgerblue3",
|
||||
["27"] = "dodgerblue2",
|
||||
["28"] = "green4",
|
||||
["29"] = "springgreen4",
|
||||
["30"] = "turquoise4",
|
||||
["31"] = "deepskyblue3",
|
||||
["32"] = "deepskyblue3_1",
|
||||
["33"] = "dodgerblue1",
|
||||
["34"] = "green3",
|
||||
["35"] = "springgreen3",
|
||||
["36"] = "darkcyan",
|
||||
["37"] = "lightseagreen",
|
||||
["38"] = "deepskyblue2",
|
||||
["39"] = "deepskyblue1",
|
||||
["40"] = "green3_1",
|
||||
["41"] = "springgreen3_1",
|
||||
["42"] = "springgreen2",
|
||||
["43"] = "cyan3",
|
||||
["44"] = "darkturquoise",
|
||||
["45"] = "turquoise2",
|
||||
["46"] = "green1",
|
||||
["47"] = "springgreen2_1",
|
||||
["48"] = "springgreen1",
|
||||
["49"] = "mediumspringgreen",
|
||||
["50"] = "cyan2",
|
||||
["51"] = "cyan1",
|
||||
["52"] = "darkred",
|
||||
["53"] = "deeppink4",
|
||||
["54"] = "purple4",
|
||||
["55"] = "purple4_1",
|
||||
["56"] = "purple3",
|
||||
["57"] = "blueviolet",
|
||||
["58"] = "orange4",
|
||||
["59"] = "grey37",
|
||||
["60"] = "mediumpurple4",
|
||||
["61"] = "slateblue3",
|
||||
["62"] = "slateblue3_1",
|
||||
["63"] = "royalblue1",
|
||||
["64"] = "chartreuse4",
|
||||
["65"] = "darkseagreen4",
|
||||
["66"] = "paleturquoise4",
|
||||
["67"] = "steelblue",
|
||||
["68"] = "steelblue3",
|
||||
["69"] = "cornflowerblue",
|
||||
["70"] = "chartreuse3",
|
||||
["71"] = "darkseagreen4_1",
|
||||
["72"] = "cadetblue",
|
||||
["73"] = "cadetblue_1",
|
||||
["74"] = "skyblue3",
|
||||
["75"] = "steelblue1",
|
||||
["76"] = "chartreuse3_1",
|
||||
["77"] = "palegreen3",
|
||||
["78"] = "seagreen3",
|
||||
["79"] = "aquamarine3",
|
||||
["80"] = "mediumturquoise",
|
||||
["81"] = "steelblue1_1",
|
||||
["82"] = "chartreuse2",
|
||||
["83"] = "seagreen2",
|
||||
["84"] = "seagreen1",
|
||||
["85"] = "seagreen1_1",
|
||||
["86"] = "aquamarine1",
|
||||
["87"] = "darkslategray2",
|
||||
["88"] = "darkred_1",
|
||||
["89"] = "deeppink4_1",
|
||||
["90"] = "darkmagenta",
|
||||
["91"] = "darkmagenta_1",
|
||||
["92"] = "darkviolet",
|
||||
["93"] = "purple_1",
|
||||
["94"] = "orange4_1",
|
||||
["95"] = "lightpink4",
|
||||
["96"] = "plum4",
|
||||
["97"] = "mediumpurple3",
|
||||
["98"] = "mediumpurple3_1",
|
||||
["99"] = "slateblue1",
|
||||
["100"] = "yellow4",
|
||||
["101"] = "wheat4",
|
||||
["102"] = "grey53",
|
||||
["103"] = "lightslategrey",
|
||||
["104"] = "mediumpurple",
|
||||
["105"] = "lightslateblue",
|
||||
["106"] = "yellow4_1",
|
||||
["107"] = "darkolivegreen3",
|
||||
["108"] = "darkseagreen",
|
||||
["109"] = "lightskyblue3",
|
||||
["110"] = "lightskyblue3_1",
|
||||
["111"] = "skyblue2",
|
||||
["112"] = "chartreuse2_1",
|
||||
["113"] = "darkolivegreen3_1",
|
||||
["114"] = "palegreen3_1",
|
||||
["115"] = "darkseagreen3",
|
||||
["116"] = "darkslategray3",
|
||||
["117"] = "skyblue1",
|
||||
["118"] = "chartreuse1",
|
||||
["119"] = "lightgreen",
|
||||
["120"] = "lightgreen_1",
|
||||
["121"] = "palegreen1",
|
||||
["122"] = "aquamarine1_1",
|
||||
["123"] = "darkslategray1",
|
||||
["124"] = "red3",
|
||||
["125"] = "deeppink4_2",
|
||||
["126"] = "mediumvioletred",
|
||||
["127"] = "magenta3",
|
||||
["128"] = "darkviolet_1",
|
||||
["129"] = "purple_2",
|
||||
["130"] = "darkorange3",
|
||||
["131"] = "indianred",
|
||||
["132"] = "hotpink3",
|
||||
["133"] = "mediumorchid3",
|
||||
["134"] = "mediumorchid",
|
||||
["135"] = "mediumpurple2",
|
||||
["136"] = "darkgoldenrod",
|
||||
["137"] = "lightsalmon3",
|
||||
["138"] = "rosybrown",
|
||||
["139"] = "grey63",
|
||||
["140"] = "mediumpurple2_1",
|
||||
["141"] = "mediumpurple1",
|
||||
["142"] = "gold3",
|
||||
["143"] = "darkkhaki",
|
||||
["144"] = "navajowhite3",
|
||||
["145"] = "grey69",
|
||||
["146"] = "lightsteelblue3",
|
||||
["147"] = "lightsteelblue",
|
||||
["148"] = "yellow3",
|
||||
["149"] = "darkolivegreen3_2",
|
||||
["150"] = "darkseagreen3_1",
|
||||
["151"] = "darkseagreen2",
|
||||
["152"] = "lightcyan3",
|
||||
["153"] = "lightskyblue1",
|
||||
["154"] = "greenyellow",
|
||||
["155"] = "darkolivegreen2",
|
||||
["156"] = "palegreen1_1",
|
||||
["157"] = "darkseagreen2_1",
|
||||
["158"] = "darkseagreen1",
|
||||
["159"] = "paleturquoise1",
|
||||
["160"] = "red3_1",
|
||||
["161"] = "deeppink3",
|
||||
["162"] = "deeppink3_1",
|
||||
["163"] = "magenta3_1",
|
||||
["164"] = "magenta3_2",
|
||||
["165"] = "magenta2",
|
||||
["166"] = "darkorange3_1",
|
||||
["167"] = "indianred_1",
|
||||
["168"] = "hotpink3_1",
|
||||
["169"] = "hotpink2",
|
||||
["170"] = "orchid",
|
||||
["171"] = "mediumorchid1",
|
||||
["172"] = "orange3",
|
||||
["173"] = "lightsalmon3_1",
|
||||
["174"] = "lightpink3",
|
||||
["175"] = "pink3",
|
||||
["176"] = "plum3",
|
||||
["177"] = "violet",
|
||||
["178"] = "gold3_1",
|
||||
["179"] = "lightgoldenrod3",
|
||||
["180"] = "tan",
|
||||
["181"] = "mistyrose3",
|
||||
["182"] = "thistle3",
|
||||
["183"] = "plum2",
|
||||
["184"] = "yellow3_1",
|
||||
["185"] = "khaki3",
|
||||
["186"] = "lightgoldenrod2",
|
||||
["187"] = "lightyellow3",
|
||||
["188"] = "grey84",
|
||||
["189"] = "lightsteelblue1",
|
||||
["190"] = "yellow2",
|
||||
["191"] = "darkolivegreen1",
|
||||
["192"] = "darkolivegreen1_1",
|
||||
["193"] = "darkseagreen1_1",
|
||||
["194"] = "honeydew2",
|
||||
["195"] = "lightcyan1",
|
||||
["196"] = "red1",
|
||||
["197"] = "deeppink2",
|
||||
["198"] = "deeppink1",
|
||||
["199"] = "deeppink1_1",
|
||||
["200"] = "magenta2_1",
|
||||
["201"] = "magenta1",
|
||||
["202"] = "orangered1",
|
||||
["203"] = "indianred1",
|
||||
["204"] = "indianred1_1",
|
||||
["205"] = "hotpink",
|
||||
["206"] = "hotpink_1",
|
||||
["207"] = "mediumorchid1_1",
|
||||
["208"] = "darkorange",
|
||||
["209"] = "salmon1",
|
||||
["210"] = "lightcoral",
|
||||
["211"] = "palevioletred1",
|
||||
["212"] = "orchid2",
|
||||
["213"] = "orchid1",
|
||||
["214"] = "orange1",
|
||||
["215"] = "sandybrown",
|
||||
["216"] = "lightsalmon1",
|
||||
["217"] = "lightpink1",
|
||||
["218"] = "pink1",
|
||||
["219"] = "plum1",
|
||||
["220"] = "gold1",
|
||||
["221"] = "lightgoldenrod2_1",
|
||||
["222"] = "lightgoldenrod2_2",
|
||||
["223"] = "navajowhite1",
|
||||
["224"] = "mistyrose1",
|
||||
["225"] = "thistle1",
|
||||
["226"] = "yellow1",
|
||||
["227"] = "lightgoldenrod1",
|
||||
["228"] = "khaki1",
|
||||
["229"] = "wheat1",
|
||||
["230"] = "cornsilk1",
|
||||
["231"] = "grey100",
|
||||
["232"] = "grey3",
|
||||
["233"] = "grey7",
|
||||
["234"] = "grey11",
|
||||
["235"] = "grey15",
|
||||
["236"] = "grey19",
|
||||
["237"] = "grey23",
|
||||
["238"] = "grey27",
|
||||
["239"] = "grey30",
|
||||
["240"] = "grey35",
|
||||
["241"] = "grey39",
|
||||
["242"] = "grey42",
|
||||
["243"] = "grey46",
|
||||
["244"] = "grey50",
|
||||
["245"] = "grey54",
|
||||
["246"] = "grey58",
|
||||
["247"] = "grey62",
|
||||
["248"] = "grey66",
|
||||
["249"] = "grey70",
|
||||
["250"] = "grey74",
|
||||
["251"] = "grey78",
|
||||
["252"] = "grey82",
|
||||
["253"] = "grey85",
|
||||
["254"] = "grey89",
|
||||
["255"] = "grey93",
|
||||
};
|
||||
|
||||
private readonly ExpressionTemplate template;
|
||||
|
||||
public ExpressionSpectreConsoleSink(ExpressionTemplate template)
|
||||
{
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
// SpectreConsole cannot handle the ANSI codes that Serilog.Expressions
|
||||
// generates, but Serilog.Expressions provides no mechanism for swapping
|
||||
// out the ANSI for the tags that SpectreConsole uses. To get around
|
||||
// this, we use a hack that parses the expected ANSI codes from
|
||||
// Expressions and then convert each one into SpectreConsole's tags
|
||||
// which will then be turned *back* into ANSI codes by that library.
|
||||
//
|
||||
// It is frustrating when popular libraries don't give the ability to
|
||||
// override things like this.
|
||||
|
||||
// Start by having Expressions write out the ANSI-formatted code to a
|
||||
// string so we can loop through it.
|
||||
StringWriter writer = new();
|
||||
|
||||
this.template.Format(logEvent, writer);
|
||||
|
||||
string output = writer.ToString();
|
||||
|
||||
// Loop through the output and build up a new markup file that uses
|
||||
// the SpectreConsole codes instead.
|
||||
StringBuilder markup = new();
|
||||
bool inTag = false;
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
// If we aren't an escape character, escape any required fields and
|
||||
// then add it to the buffer.
|
||||
char c = output[i];
|
||||
|
||||
if (c != '\x1b')
|
||||
{
|
||||
// Add in the character, then escape the specials if needed.
|
||||
markup.Append(output[i]);
|
||||
|
||||
if (output[i] is '[' or ']')
|
||||
{
|
||||
markup.Append(output[i]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// For escape codes, we want to grab all the escape codes in a
|
||||
// series so we can do the correct foreground and background color
|
||||
// codes in a single markup.
|
||||
List<string> codes = new();
|
||||
|
||||
while (output[i] == '\x1b')
|
||||
{
|
||||
StringBuilder escape = new();
|
||||
|
||||
i += 2;
|
||||
|
||||
while (output[i] != 'm')
|
||||
{
|
||||
escape.Append(output[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
codes.Add(escape.ToString());
|
||||
}
|
||||
|
||||
// Parse the output without the trailing `m` and the escape code.
|
||||
string spectreMarkup =
|
||||
this.TranslateAnsiToSpectre(codes, ref inTag);
|
||||
|
||||
markup.Append(spectreMarkup);
|
||||
}
|
||||
|
||||
// Send the modified code to SpectreConsole.
|
||||
markup.Append(Finalize(ref inTag));
|
||||
|
||||
AnsiConsole.Markup(markup.ToString());
|
||||
}
|
||||
|
||||
private static string Finalize(ref bool inTag)
|
||||
{
|
||||
return GetMarkup("default", "", "", ref inTag);
|
||||
}
|
||||
|
||||
private static string GetColor256(string index)
|
||||
{
|
||||
return AnsiToSpectre.TryGetValue(index.TrimStart('0'), out string? name)
|
||||
? name
|
||||
: "";
|
||||
}
|
||||
|
||||
private static string GetMarkup(
|
||||
string foreground,
|
||||
string background,
|
||||
string bold,
|
||||
ref bool inTag)
|
||||
{
|
||||
StringBuilder markup = new();
|
||||
|
||||
if (inTag)
|
||||
{
|
||||
inTag = false;
|
||||
markup.Append("[/]");
|
||||
}
|
||||
|
||||
if (foreground != "default"
|
||||
|| background.Length != 0
|
||||
|| bold.Length != 0)
|
||||
{
|
||||
inTag = true;
|
||||
markup.AppendFormat("[{0}{1}{2}]", bold, foreground, background);
|
||||
}
|
||||
|
||||
return markup.ToString();
|
||||
}
|
||||
|
||||
private string TranslateAnsiToSpectre(List<string> codes, ref bool inTag)
|
||||
{
|
||||
// Build up the markup to give by going through the codes and seeing if
|
||||
// we have to combine everything together.
|
||||
StringBuilder markup = new();
|
||||
string bold = "";
|
||||
string foreground = "default";
|
||||
string background = "";
|
||||
|
||||
foreach (string code in codes)
|
||||
{
|
||||
// This will be a semicolon separated list of parameters.
|
||||
string[] args = code.Split(';');
|
||||
|
||||
switch (args[0])
|
||||
{
|
||||
case "0": // Reset
|
||||
markup.Append(
|
||||
GetMarkup(foreground, background, bold, ref inTag));
|
||||
break;
|
||||
|
||||
case "38": // Foreground Color
|
||||
switch (args[1])
|
||||
{
|
||||
case "5": // 256 color
|
||||
foreground = GetColor256(args[2]);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "48": // Background color
|
||||
switch (args[1])
|
||||
{
|
||||
case "5": // 256 color
|
||||
background = " on " + GetColor256(args[2]);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default: // Unknown
|
||||
// If we have more than one, then treat them as individual
|
||||
// codes.
|
||||
//
|
||||
// https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
|
||||
foreach (string inner in args)
|
||||
{
|
||||
switch (inner)
|
||||
{
|
||||
case "1": // Bold
|
||||
bold = "bold ";
|
||||
break;
|
||||
|
||||
case "30":
|
||||
foreground = "black";
|
||||
break;
|
||||
|
||||
case "31":
|
||||
foreground = "red";
|
||||
break;
|
||||
|
||||
case "32":
|
||||
foreground = "green";
|
||||
break;
|
||||
|
||||
case "33":
|
||||
foreground = "yellow";
|
||||
break;
|
||||
|
||||
case "34":
|
||||
foreground = "blue";
|
||||
break;
|
||||
|
||||
case "35":
|
||||
foreground = "magenta";
|
||||
break;
|
||||
|
||||
case "36":
|
||||
foreground = "cyan";
|
||||
break;
|
||||
|
||||
case "37":
|
||||
foreground = "white";
|
||||
break;
|
||||
|
||||
case "39":
|
||||
foreground = "default";
|
||||
break;
|
||||
|
||||
case "40":
|
||||
background = " on black";
|
||||
break;
|
||||
|
||||
case "41":
|
||||
background = " on red";
|
||||
break;
|
||||
|
||||
case "42":
|
||||
background = " on green";
|
||||
break;
|
||||
|
||||
case "43":
|
||||
background = " on yellow";
|
||||
break;
|
||||
|
||||
case "44":
|
||||
background = " on blue";
|
||||
break;
|
||||
|
||||
case "45":
|
||||
background = " on magenta";
|
||||
break;
|
||||
|
||||
case "46":
|
||||
background = " on cyan";
|
||||
break;
|
||||
|
||||
case "47":
|
||||
background = " on white";
|
||||
break;
|
||||
|
||||
case "49":
|
||||
background = " on default";
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Finish up the codes.
|
||||
markup.Append(GetMarkup(foreground, background, bold, ref inTag));
|
||||
}
|
||||
|
||||
// Return the resulting markup.
|
||||
return markup.ToString();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,50 @@
|
|||
using Serilog;
|
||||
using Serilog.Configuration;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Templates;
|
||||
using Serilog.Templates.Themes;
|
||||
|
||||
namespace MfGames.Serilog.SpectreExpressions;
|
||||
|
||||
public static class ExpressionSpectreConsoleSinkExtensions
|
||||
{
|
||||
private const string DefaultConsoleOutputTemplate = "[{@t:HH:mm:ss} {@l:u3}] {@m}\n{@x}";
|
||||
|
||||
/// <summary>
|
||||
/// Write log events to the console using Spectre.Console.
|
||||
/// </summary>
|
||||
/// <param name="loggerConfiguration">Logger sink configuration.</param>
|
||||
/// <param name="outputTemplate">
|
||||
/// A message template describing the format used to write to the sink.
|
||||
/// The default is "[{Timestamp:HH:mm:ss} {Level:u3}]
|
||||
/// {Message:lj}{NewLine}{Exception}".
|
||||
/// </param>
|
||||
/// <param name="restrictedToMinimumLevel">
|
||||
/// The minimum level for
|
||||
/// events passed through the sink. Ignored when <paramref name="levelSwitch" /> is
|
||||
/// specified.
|
||||
/// </param>
|
||||
/// <param name="levelSwitch">
|
||||
/// A switch allowing the pass-through minimum level
|
||||
/// to be changed at runtime.
|
||||
/// </param>
|
||||
/// <param name="theme">The Serilog theme for the output.</param>
|
||||
/// <returns>Configuration object allowing method chaining.</returns>
|
||||
public static LoggerConfiguration SpectreExpression(
|
||||
this LoggerSinkConfiguration loggerConfiguration,
|
||||
ExpressionTemplate? outputTemplate = null,
|
||||
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
|
||||
LoggingLevelSwitch? levelSwitch = null,
|
||||
TemplateTheme? theme = null)
|
||||
{
|
||||
outputTemplate ??= new ExpressionTemplate(
|
||||
DefaultConsoleOutputTemplate,
|
||||
theme: theme);
|
||||
|
||||
return loggerConfiguration.Sink(
|
||||
new ExpressionSpectreConsoleSink(outputTemplate),
|
||||
restrictedToMinimumLevel,
|
||||
levelSwitch);
|
||||
}
|
||||
}
|
13
src/MfGames.Serilog.SpectreExpressions/GitVersion.yml
Normal file
13
src/MfGames.Serilog.SpectreExpressions/GitVersion.yml
Normal file
|
@ -0,0 +1,13 @@
|
|||
mode: ContinuousDelivery
|
||||
increment: Inherit
|
||||
continuous-delivery-fallback-tag: ci
|
||||
|
||||
major-version-bump-message: "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?(!:|:.*\\n\\n((.+\\n)+\\n)?BREAKING CHANGE:\\s.+)"
|
||||
minor-version-bump-message: "^(feat)(\\([\\w\\s-]*\\))?:"
|
||||
patch-version-bump-message: "^(build|chore|ci|docs|fix|perf|refactor|revert|style|test)(\\([\\w\\s-]*\\))?:"
|
||||
|
||||
assembly-versioning-scheme: MajorMinorPatch
|
||||
assembly-file-versioning-scheme: MajorMinorPatch
|
||||
assembly-informational-format: "{InformationalVersion}"
|
||||
|
||||
tag-prefix: "MfGames.Serilog.SpectreExpressions-"
|
|
@ -0,0 +1,32 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="GitVersion.MsBuild" Version="5.12.0">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Analyzers" Version="4.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.CodeAnalysis.Analyzers" Version="4.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Roslynator.Formatting.Analyzers" Version="4.3.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
</PackageReference>
|
||||
<PackageReference Include="Serilog" Version="3.0.1"/>
|
||||
<PackageReference Include="Serilog.Expressions" Version="3.4.1"/>
|
||||
<PackageReference Include="SerilogAnalyzer" Version="0.15.0"/>
|
||||
<PackageReference Include="Spectre.Console" Version="0.47.0"/>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
|
@ -1,541 +0,0 @@
|
|||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Templates;
|
||||
|
||||
using Spectre.Console;
|
||||
|
||||
namespace MfGames.ToolBuilder.Services;
|
||||
|
||||
public class ExpressionSpectreConsoleSink : ILogEventSink
|
||||
{
|
||||
/// <summary>
|
||||
/// The internal lookup codes for ANSI colors to Spectre color names.
|
||||
/// https://spectreconsole.net/appendix/colors
|
||||
/// </summary>
|
||||
private static readonly Dictionary<string, string> AnsiToSpectre = new()
|
||||
{
|
||||
["0"] = "black",
|
||||
["1"] = "maroon",
|
||||
["2"] = "green",
|
||||
["3"] = "olive",
|
||||
["4"] = "navy",
|
||||
["5"] = "purple",
|
||||
["6"] = "teal",
|
||||
["7"] = "silver",
|
||||
["8"] = "grey",
|
||||
["9"] = "red",
|
||||
["10"] = "lime",
|
||||
["11"] = "yellow",
|
||||
["12"] = "blue",
|
||||
["13"] = "fuchsia",
|
||||
["14"] = "aqua",
|
||||
["15"] = "white",
|
||||
["16"] = "grey0",
|
||||
["17"] = "navyblue",
|
||||
["18"] = "darkblue",
|
||||
["19"] = "blue3",
|
||||
["20"] = "blue3_1",
|
||||
["21"] = "blue1",
|
||||
["22"] = "darkgreen",
|
||||
["23"] = "deepskyblue4",
|
||||
["24"] = "deepskyblue4_1",
|
||||
["25"] = "deepskyblue4_2",
|
||||
["26"] = "dodgerblue3",
|
||||
["27"] = "dodgerblue2",
|
||||
["28"] = "green4",
|
||||
["29"] = "springgreen4",
|
||||
["30"] = "turquoise4",
|
||||
["31"] = "deepskyblue3",
|
||||
["32"] = "deepskyblue3_1",
|
||||
["33"] = "dodgerblue1",
|
||||
["34"] = "green3",
|
||||
["35"] = "springgreen3",
|
||||
["36"] = "darkcyan",
|
||||
["37"] = "lightseagreen",
|
||||
["38"] = "deepskyblue2",
|
||||
["39"] = "deepskyblue1",
|
||||
["40"] = "green3_1",
|
||||
["41"] = "springgreen3_1",
|
||||
["42"] = "springgreen2",
|
||||
["43"] = "cyan3",
|
||||
["44"] = "darkturquoise",
|
||||
["45"] = "turquoise2",
|
||||
["46"] = "green1",
|
||||
["47"] = "springgreen2_1",
|
||||
["48"] = "springgreen1",
|
||||
["49"] = "mediumspringgreen",
|
||||
["50"] = "cyan2",
|
||||
["51"] = "cyan1",
|
||||
["52"] = "darkred",
|
||||
["53"] = "deeppink4",
|
||||
["54"] = "purple4",
|
||||
["55"] = "purple4_1",
|
||||
["56"] = "purple3",
|
||||
["57"] = "blueviolet",
|
||||
["58"] = "orange4",
|
||||
["59"] = "grey37",
|
||||
["60"] = "mediumpurple4",
|
||||
["61"] = "slateblue3",
|
||||
["62"] = "slateblue3_1",
|
||||
["63"] = "royalblue1",
|
||||
["64"] = "chartreuse4",
|
||||
["65"] = "darkseagreen4",
|
||||
["66"] = "paleturquoise4",
|
||||
["67"] = "steelblue",
|
||||
["68"] = "steelblue3",
|
||||
["69"] = "cornflowerblue",
|
||||
["70"] = "chartreuse3",
|
||||
["71"] = "darkseagreen4_1",
|
||||
["72"] = "cadetblue",
|
||||
["73"] = "cadetblue_1",
|
||||
["74"] = "skyblue3",
|
||||
["75"] = "steelblue1",
|
||||
["76"] = "chartreuse3_1",
|
||||
["77"] = "palegreen3",
|
||||
["78"] = "seagreen3",
|
||||
["79"] = "aquamarine3",
|
||||
["80"] = "mediumturquoise",
|
||||
["81"] = "steelblue1_1",
|
||||
["82"] = "chartreuse2",
|
||||
["83"] = "seagreen2",
|
||||
["84"] = "seagreen1",
|
||||
["85"] = "seagreen1_1",
|
||||
["86"] = "aquamarine1",
|
||||
["87"] = "darkslategray2",
|
||||
["88"] = "darkred_1",
|
||||
["89"] = "deeppink4_1",
|
||||
["90"] = "darkmagenta",
|
||||
["91"] = "darkmagenta_1",
|
||||
["92"] = "darkviolet",
|
||||
["93"] = "purple_1",
|
||||
["94"] = "orange4_1",
|
||||
["95"] = "lightpink4",
|
||||
["96"] = "plum4",
|
||||
["97"] = "mediumpurple3",
|
||||
["98"] = "mediumpurple3_1",
|
||||
["99"] = "slateblue1",
|
||||
["100"] = "yellow4",
|
||||
["101"] = "wheat4",
|
||||
["102"] = "grey53",
|
||||
["103"] = "lightslategrey",
|
||||
["104"] = "mediumpurple",
|
||||
["105"] = "lightslateblue",
|
||||
["106"] = "yellow4_1",
|
||||
["107"] = "darkolivegreen3",
|
||||
["108"] = "darkseagreen",
|
||||
["109"] = "lightskyblue3",
|
||||
["110"] = "lightskyblue3_1",
|
||||
["111"] = "skyblue2",
|
||||
["112"] = "chartreuse2_1",
|
||||
["113"] = "darkolivegreen3_1",
|
||||
["114"] = "palegreen3_1",
|
||||
["115"] = "darkseagreen3",
|
||||
["116"] = "darkslategray3",
|
||||
["117"] = "skyblue1",
|
||||
["118"] = "chartreuse1",
|
||||
["119"] = "lightgreen",
|
||||
["120"] = "lightgreen_1",
|
||||
["121"] = "palegreen1",
|
||||
["122"] = "aquamarine1_1",
|
||||
["123"] = "darkslategray1",
|
||||
["124"] = "red3",
|
||||
["125"] = "deeppink4_2",
|
||||
["126"] = "mediumvioletred",
|
||||
["127"] = "magenta3",
|
||||
["128"] = "darkviolet_1",
|
||||
["129"] = "purple_2",
|
||||
["130"] = "darkorange3",
|
||||
["131"] = "indianred",
|
||||
["132"] = "hotpink3",
|
||||
["133"] = "mediumorchid3",
|
||||
["134"] = "mediumorchid",
|
||||
["135"] = "mediumpurple2",
|
||||
["136"] = "darkgoldenrod",
|
||||
["137"] = "lightsalmon3",
|
||||
["138"] = "rosybrown",
|
||||
["139"] = "grey63",
|
||||
["140"] = "mediumpurple2_1",
|
||||
["141"] = "mediumpurple1",
|
||||
["142"] = "gold3",
|
||||
["143"] = "darkkhaki",
|
||||
["144"] = "navajowhite3",
|
||||
["145"] = "grey69",
|
||||
["146"] = "lightsteelblue3",
|
||||
["147"] = "lightsteelblue",
|
||||
["148"] = "yellow3",
|
||||
["149"] = "darkolivegreen3_2",
|
||||
["150"] = "darkseagreen3_1",
|
||||
["151"] = "darkseagreen2",
|
||||
["152"] = "lightcyan3",
|
||||
["153"] = "lightskyblue1",
|
||||
["154"] = "greenyellow",
|
||||
["155"] = "darkolivegreen2",
|
||||
["156"] = "palegreen1_1",
|
||||
["157"] = "darkseagreen2_1",
|
||||
["158"] = "darkseagreen1",
|
||||
["159"] = "paleturquoise1",
|
||||
["160"] = "red3_1",
|
||||
["161"] = "deeppink3",
|
||||
["162"] = "deeppink3_1",
|
||||
["163"] = "magenta3_1",
|
||||
["164"] = "magenta3_2",
|
||||
["165"] = "magenta2",
|
||||
["166"] = "darkorange3_1",
|
||||
["167"] = "indianred_1",
|
||||
["168"] = "hotpink3_1",
|
||||
["169"] = "hotpink2",
|
||||
["170"] = "orchid",
|
||||
["171"] = "mediumorchid1",
|
||||
["172"] = "orange3",
|
||||
["173"] = "lightsalmon3_1",
|
||||
["174"] = "lightpink3",
|
||||
["175"] = "pink3",
|
||||
["176"] = "plum3",
|
||||
["177"] = "violet",
|
||||
["178"] = "gold3_1",
|
||||
["179"] = "lightgoldenrod3",
|
||||
["180"] = "tan",
|
||||
["181"] = "mistyrose3",
|
||||
["182"] = "thistle3",
|
||||
["183"] = "plum2",
|
||||
["184"] = "yellow3_1",
|
||||
["185"] = "khaki3",
|
||||
["186"] = "lightgoldenrod2",
|
||||
["187"] = "lightyellow3",
|
||||
["188"] = "grey84",
|
||||
["189"] = "lightsteelblue1",
|
||||
["190"] = "yellow2",
|
||||
["191"] = "darkolivegreen1",
|
||||
["192"] = "darkolivegreen1_1",
|
||||
["193"] = "darkseagreen1_1",
|
||||
["194"] = "honeydew2",
|
||||
["195"] = "lightcyan1",
|
||||
["196"] = "red1",
|
||||
["197"] = "deeppink2",
|
||||
["198"] = "deeppink1",
|
||||
["199"] = "deeppink1_1",
|
||||
["200"] = "magenta2_1",
|
||||
["201"] = "magenta1",
|
||||
["202"] = "orangered1",
|
||||
["203"] = "indianred1",
|
||||
["204"] = "indianred1_1",
|
||||
["205"] = "hotpink",
|
||||
["206"] = "hotpink_1",
|
||||
["207"] = "mediumorchid1_1",
|
||||
["208"] = "darkorange",
|
||||
["209"] = "salmon1",
|
||||
["210"] = "lightcoral",
|
||||
["211"] = "palevioletred1",
|
||||
["212"] = "orchid2",
|
||||
["213"] = "orchid1",
|
||||
["214"] = "orange1",
|
||||
["215"] = "sandybrown",
|
||||
["216"] = "lightsalmon1",
|
||||
["217"] = "lightpink1",
|
||||
["218"] = "pink1",
|
||||
["219"] = "plum1",
|
||||
["220"] = "gold1",
|
||||
["221"] = "lightgoldenrod2_1",
|
||||
["222"] = "lightgoldenrod2_2",
|
||||
["223"] = "navajowhite1",
|
||||
["224"] = "mistyrose1",
|
||||
["225"] = "thistle1",
|
||||
["226"] = "yellow1",
|
||||
["227"] = "lightgoldenrod1",
|
||||
["228"] = "khaki1",
|
||||
["229"] = "wheat1",
|
||||
["230"] = "cornsilk1",
|
||||
["231"] = "grey100",
|
||||
["232"] = "grey3",
|
||||
["233"] = "grey7",
|
||||
["234"] = "grey11",
|
||||
["235"] = "grey15",
|
||||
["236"] = "grey19",
|
||||
["237"] = "grey23",
|
||||
["238"] = "grey27",
|
||||
["239"] = "grey30",
|
||||
["240"] = "grey35",
|
||||
["241"] = "grey39",
|
||||
["242"] = "grey42",
|
||||
["243"] = "grey46",
|
||||
["244"] = "grey50",
|
||||
["245"] = "grey54",
|
||||
["246"] = "grey58",
|
||||
["247"] = "grey62",
|
||||
["248"] = "grey66",
|
||||
["249"] = "grey70",
|
||||
["250"] = "grey74",
|
||||
["251"] = "grey78",
|
||||
["252"] = "grey82",
|
||||
["253"] = "grey85",
|
||||
["254"] = "grey89",
|
||||
["255"] = "grey93",
|
||||
};
|
||||
|
||||
private readonly ExpressionTemplate template;
|
||||
|
||||
public ExpressionSpectreConsoleSink(ExpressionTemplate template)
|
||||
{
|
||||
this.template = template;
|
||||
}
|
||||
|
||||
public void Emit(LogEvent logEvent)
|
||||
{
|
||||
// SpectreConsole cannot handle the ANSI codes that Serilog.Expressions
|
||||
// generates, but Serilog.Expressions provides no mechanism for swapping
|
||||
// out the ANSI for the tags that SpectreConsole uses. To get around
|
||||
// this, we use a hack that parses the expected ANSI codes from
|
||||
// Expressions and then convert each one into SpectreConsole's tags
|
||||
// which will then be turned *back* into ANSI codes by that library.
|
||||
//
|
||||
// It is frustrating when popular libraries don't give the ability to
|
||||
// override things like this.
|
||||
|
||||
// Start by having Expressions write out the ANSI-formatted code to a
|
||||
// string so we can loop through it.
|
||||
StringWriter writer = new();
|
||||
|
||||
this.template.Format(logEvent, writer);
|
||||
|
||||
string output = writer.ToString();
|
||||
|
||||
// Loop through the output and build up a new markup file that uses
|
||||
// the SpectreConsole codes instead.
|
||||
StringBuilder markup = new();
|
||||
bool inTag = false;
|
||||
|
||||
for (int i = 0; i < output.Length; i++)
|
||||
{
|
||||
// If we aren't an escape character, escape any required fields and
|
||||
// then add it to the buffer.
|
||||
char c = output[i];
|
||||
|
||||
if (c != '\x1b')
|
||||
{
|
||||
// Add in the character, then escape the specials if needed.
|
||||
markup.Append(output[i]);
|
||||
|
||||
if (output[i] is '[' or ']')
|
||||
{
|
||||
markup.Append(output[i]);
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// For escape codes, we want to grab all the escape codes in a
|
||||
// series so we can do the correct foreground and background color
|
||||
// codes in a single markup.
|
||||
List<string> codes = new();
|
||||
|
||||
while (output[i] == '\x1b')
|
||||
{
|
||||
StringBuilder escape = new();
|
||||
|
||||
i += 2;
|
||||
|
||||
while (output[i] != 'm')
|
||||
{
|
||||
escape.Append(output[i]);
|
||||
i++;
|
||||
}
|
||||
|
||||
codes.Add(escape.ToString());
|
||||
}
|
||||
|
||||
// Parse the output without the trailing `m` and the escape code.
|
||||
string spectreMarkup =
|
||||
this.TranslateAnsiToSpectre(codes, ref inTag);
|
||||
|
||||
markup.Append(spectreMarkup);
|
||||
}
|
||||
|
||||
// Send the modified code to SpectreConsole.
|
||||
markup.Append(Finalize(ref inTag));
|
||||
|
||||
AnsiConsole.Markup(markup.ToString());
|
||||
}
|
||||
|
||||
private static string Finalize(ref bool inTag)
|
||||
{
|
||||
return GetMarkup("default", "", "", ref inTag);
|
||||
}
|
||||
|
||||
private static string GetColor256(string index)
|
||||
{
|
||||
return AnsiToSpectre.TryGetValue(index.TrimStart('0'), out string? name)
|
||||
? name
|
||||
: "";
|
||||
}
|
||||
|
||||
private static string GetMarkup(
|
||||
string foreground,
|
||||
string background,
|
||||
string bold,
|
||||
ref bool inTag)
|
||||
{
|
||||
StringBuilder markup = new();
|
||||
|
||||
if (inTag)
|
||||
{
|
||||
inTag = false;
|
||||
markup.Append("[/]");
|
||||
}
|
||||
|
||||
if (foreground != "default"
|
||||
|| background.Length != 0
|
||||
|| bold.Length != 0)
|
||||
{
|
||||
inTag = true;
|
||||
markup.AppendFormat("[{0}{1}{2}]", bold, foreground, background);
|
||||
}
|
||||
|
||||
return markup.ToString();
|
||||
}
|
||||
|
||||
private string TranslateAnsiToSpectre(List<string> codes, ref bool inTag)
|
||||
{
|
||||
// Build up the markup to give by going through the codes and seeing if
|
||||
// we have to combine everything together.
|
||||
StringBuilder markup = new();
|
||||
string bold = "";
|
||||
string foreground = "default";
|
||||
string background = "";
|
||||
|
||||
foreach (string code in codes)
|
||||
{
|
||||
// This will be a semicolon separated list of parameters.
|
||||
string[] args = code.Split(';');
|
||||
|
||||
switch (args[0])
|
||||
{
|
||||
case "0": // Reset
|
||||
markup.Append(
|
||||
GetMarkup(foreground, background, bold, ref inTag));
|
||||
break;
|
||||
|
||||
case "38": // Foreground Color
|
||||
switch (args[1])
|
||||
{
|
||||
case "5": // 256 color
|
||||
foreground = GetColor256(args[2]);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case "48": // Background color
|
||||
switch (args[1])
|
||||
{
|
||||
case "5": // 256 color
|
||||
background = " on " + GetColor256(args[2]);
|
||||
break;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
default: // Unknown
|
||||
// If we have more than one, then treat them as individual
|
||||
// codes.
|
||||
//
|
||||
// https://gist.github.com/fnky/458719343aabd01cfb17a3a4f7296797
|
||||
foreach (string inner in args)
|
||||
{
|
||||
switch (inner)
|
||||
{
|
||||
case "1": // Bold
|
||||
bold = "bold ";
|
||||
break;
|
||||
|
||||
case "30":
|
||||
foreground = "black";
|
||||
break;
|
||||
|
||||
case "31":
|
||||
foreground = "red";
|
||||
break;
|
||||
|
||||
case "32":
|
||||
foreground = "green";
|
||||
break;
|
||||
|
||||
case "33":
|
||||
foreground = "yellow";
|
||||
break;
|
||||
|
||||
case "34":
|
||||
foreground = "blue";
|
||||
break;
|
||||
|
||||
case "35":
|
||||
foreground = "magenta";
|
||||
break;
|
||||
|
||||
case "36":
|
||||
foreground = "cyan";
|
||||
break;
|
||||
|
||||
case "37":
|
||||
foreground = "white";
|
||||
break;
|
||||
|
||||
case "39":
|
||||
foreground = "default";
|
||||
break;
|
||||
|
||||
case "40":
|
||||
background = " on black";
|
||||
break;
|
||||
|
||||
case "41":
|
||||
background = " on red";
|
||||
break;
|
||||
|
||||
case "42":
|
||||
background = " on green";
|
||||
break;
|
||||
|
||||
case "43":
|
||||
background = " on yellow";
|
||||
break;
|
||||
|
||||
case "44":
|
||||
background = " on blue";
|
||||
break;
|
||||
|
||||
case "45":
|
||||
background = " on magenta";
|
||||
break;
|
||||
|
||||
case "46":
|
||||
background = " on cyan";
|
||||
break;
|
||||
|
||||
case "47":
|
||||
background = " on white";
|
||||
break;
|
||||
|
||||
case "49":
|
||||
background = " on default";
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// Finish up the codes.
|
||||
markup.Append(GetMarkup(foreground, background, bold, ref inTag));
|
||||
}
|
||||
|
||||
// Return the resulting markup.
|
||||
return markup.ToString();
|
||||
}
|
||||
}
|
|
@ -1,46 +0,0 @@
|
|||
using Serilog;
|
||||
using Serilog.Configuration;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Templates;
|
||||
|
||||
namespace MfGames.ToolBuilder.Services;
|
||||
|
||||
public static class ExpressionSpectreConsoleSinkExtensions
|
||||
{
|
||||
private const string DefaultConsoleOutputTemplate =
|
||||
"[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}";
|
||||
|
||||
/// <summary>
|
||||
/// Write log events to the console using Spectre.Console.
|
||||
/// </summary>
|
||||
/// <param name="loggerConfiguration">Logger sink configuration.</param>
|
||||
/// <param name="outputTemplate">
|
||||
/// A message template describing the format used to write to the sink.
|
||||
/// The default is "[{Timestamp:HH:mm:ss} {Level:u3}]
|
||||
/// {Message:lj}{NewLine}{Exception}".
|
||||
/// </param>
|
||||
/// <param name="restrictedToMinimumLevel">
|
||||
/// The minimum level for
|
||||
/// events passed through the sink. Ignored when <paramref name="levelSwitch" /> is
|
||||
/// specified.
|
||||
/// </param>
|
||||
/// <param name="levelSwitch">
|
||||
/// A switch allowing the pass-through minimum level
|
||||
/// to be changed at runtime.
|
||||
/// </param>
|
||||
/// <returns>Configuration object allowing method chaining.</returns>
|
||||
public static LoggerConfiguration SpectreExpression(
|
||||
this LoggerSinkConfiguration loggerConfiguration,
|
||||
ExpressionTemplate? outputTemplate = null,
|
||||
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
|
||||
LoggingLevelSwitch? levelSwitch = null)
|
||||
{
|
||||
outputTemplate ??= new ExpressionTemplate(DefaultConsoleOutputTemplate);
|
||||
|
||||
return loggerConfiguration.Sink(
|
||||
new ExpressionSpectreConsoleSink(outputTemplate),
|
||||
restrictedToMinimumLevel,
|
||||
levelSwitch);
|
||||
}
|
||||
}
|
19
src/MfGames.ToolBuilder/Logging/LogContextFormat.cs
Normal file
19
src/MfGames.ToolBuilder/Logging/LogContextFormat.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace MfGames.ToolBuilder.Services;
|
||||
|
||||
public enum LogContextFormat
|
||||
{
|
||||
/// <summary>
|
||||
/// Indicates that no log context should be shown.
|
||||
/// </summary>
|
||||
None,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that only the class name should be shown.
|
||||
/// </summary>
|
||||
Class,
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the full path should be shown.
|
||||
/// </summary>
|
||||
Full,
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
using System;
|
||||
using System.CommandLine;
|
||||
|
||||
using MfGames.ToolBuilder.Extensions;
|
||||
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Exceptions;
|
||||
using Serilog.Templates;
|
||||
using Serilog.Templates.Themes;
|
||||
|
||||
namespace MfGames.ToolBuilder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A service for handling logging options.
|
||||
/// </summary>
|
||||
public class LoggingToolGlobal
|
||||
{
|
||||
public LoggingToolGlobal()
|
||||
{
|
||||
this.LogLevelOption = new Option<string>(
|
||||
"--log-level",
|
||||
() => nameof(LogEventLevel.Information),
|
||||
string.Format(
|
||||
"Controls the verbosity of the output, not case-sensitive and prefixes allowed: {0}",
|
||||
string.Join(", ", Enum.GetNames<LogEventLevel>())));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the common option for setting the log level.
|
||||
/// </summary>
|
||||
public Option<string> LogLevelOption { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the common options to the command.
|
||||
/// </summary>
|
||||
/// <param name="root"></param>
|
||||
public void Attach(Command root)
|
||||
{
|
||||
root.AddGlobalOption(this.LogLevelOption);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up logging based on the global settings.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The arguments to the command.</param>
|
||||
public void Configure(string[] arguments)
|
||||
{
|
||||
// Figure out the logging level.
|
||||
string level = GlobalOptionHelper.GetArgumentValue(
|
||||
this.LogLevelOption,
|
||||
arguments,
|
||||
"Warning");
|
||||
LogEventLevel logLevel = level.GetEnumFuzzy<LogEventLevel>(
|
||||
"log level");
|
||||
|
||||
// Figure out the expression template we want to use.
|
||||
var template = new ExpressionTemplate(
|
||||
"[{@t:HH:mm:ss} {@l:u3}] {@m}\n{@x}",
|
||||
theme: TemplateTheme.Literate);
|
||||
|
||||
// Figure out how we are going to configure the logger.
|
||||
var configuration = new LoggerConfiguration();
|
||||
Logger logger = configuration
|
||||
.Enrich.WithDemystifiedStackTraces()
|
||||
.Enrich.WithExceptionDetails()
|
||||
.MinimumLevel.Is(logLevel)
|
||||
.MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.SpectreExpression(template)
|
||||
.CreateLogger();
|
||||
|
||||
Log.Logger = logger;
|
||||
}
|
||||
}
|
204
src/MfGames.ToolBuilder/Logging/LoggingToolService.cs
Normal file
204
src/MfGames.ToolBuilder/Logging/LoggingToolService.cs
Normal file
|
@ -0,0 +1,204 @@
|
|||
using System.CommandLine;
|
||||
|
||||
using MfGames.Serilog.SpectreExpressions;
|
||||
using MfGames.ToolBuilder.Extensions;
|
||||
|
||||
using Serilog;
|
||||
using Serilog.Core;
|
||||
using Serilog.Events;
|
||||
using Serilog.Exceptions;
|
||||
using Serilog.Templates;
|
||||
using Serilog.Templates.Themes;
|
||||
|
||||
namespace MfGames.ToolBuilder.Services;
|
||||
|
||||
/// <summary>
|
||||
/// A service for handling logging options.
|
||||
/// </summary>
|
||||
public class LoggingToolService
|
||||
{
|
||||
public LoggingToolService()
|
||||
{
|
||||
this.LogLevelOption = new Option<string>(
|
||||
"--log-level",
|
||||
() => nameof(LogEventLevel.Warning),
|
||||
string.Format(
|
||||
"Controls the verbosity of the output, not case-sensitive and prefixes allowed: {0}",
|
||||
string.Join(", ", Enum.GetNames<LogEventLevel>())));
|
||||
|
||||
this.LogContextOption = new Option<string>(
|
||||
"--log-context-format",
|
||||
() => nameof(LogContextFormat.Class),
|
||||
string.Format(
|
||||
"Controls the format of the source context for log items, not case-sensitive and prefixes allowed: {0}",
|
||||
string.Join(", ", Enum.GetNames<LogContextFormat>())));
|
||||
|
||||
this.LogTimeFormat = new Option<string>(
|
||||
"--log-time-format",
|
||||
() => "HH:mm:ss",
|
||||
string.Join(
|
||||
" ",
|
||||
"Controls the format of the time in the log messages,",
|
||||
"such as 'HH:mm:ss' (default) or 'yyyy-MM-ddTHH:mm:ss'.",
|
||||
"Blank means exclude entirely."));
|
||||
|
||||
this.LogLevelOverrideOption = new Option<List<string>>(
|
||||
"--log-level-override",
|
||||
() => new List<string> { "Microsoft=Warning" },
|
||||
"Overrides log levels for certain contexts in the format of either 'Context' or 'Context=Level' (repeat for multiple)")
|
||||
{
|
||||
Arity = ArgumentArity.OneOrMore,
|
||||
};
|
||||
}
|
||||
|
||||
public Option<string> LogContextOption { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the common option for setting the log level.
|
||||
/// </summary>
|
||||
public Option<string> LogLevelOption { get; }
|
||||
|
||||
public Option<List<string>> LogLevelOverrideOption { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the log time format to use.
|
||||
/// </summary>
|
||||
public Option<string> LogTimeFormat { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Adds the options to the given command.
|
||||
/// </summary>
|
||||
/// <param name="command"></param>
|
||||
public void Attach(Command command)
|
||||
{
|
||||
command.AddOption(this.LogLevelOption);
|
||||
command.AddOption(this.LogContextOption);
|
||||
command.AddOption(this.LogTimeFormat);
|
||||
command.AddOption(this.LogLevelOverrideOption);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds the common options to the command.
|
||||
/// </summary>
|
||||
/// <param name="command"></param>
|
||||
public void AttachGlobal(Command command)
|
||||
{
|
||||
command.AddGlobalOption(this.LogLevelOption);
|
||||
command.AddGlobalOption(this.LogContextOption);
|
||||
command.AddGlobalOption(this.LogTimeFormat);
|
||||
command.AddGlobalOption(this.LogLevelOverrideOption);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets up logging based on the global settings.
|
||||
/// </summary>
|
||||
/// <param name="arguments">The arguments to the command.</param>
|
||||
public void Configure(string[] arguments)
|
||||
{
|
||||
LogEventLevel logLevel = this.GetEventLevel(arguments);
|
||||
ExpressionTemplate template = this.GetExpressionTemplate(arguments);
|
||||
Dictionary<string, LogEventLevel> levelOverrides =
|
||||
this.GetLevelOverrides(arguments);
|
||||
|
||||
LoggerConfiguration? configuration = new LoggerConfiguration()
|
||||
.Enrich.WithDemystifiedStackTraces()
|
||||
.Enrich.WithExceptionDetails()
|
||||
.MinimumLevel.Is(logLevel);
|
||||
|
||||
foreach (KeyValuePair<string, LogEventLevel> pair in levelOverrides)
|
||||
{
|
||||
configuration = configuration.MinimumLevel
|
||||
.Override(pair.Key, pair.Value);
|
||||
}
|
||||
|
||||
configuration = configuration
|
||||
.Enrich.FromLogContext()
|
||||
.WriteTo.SpectreExpression(template);
|
||||
|
||||
Logger logger = configuration.CreateLogger();
|
||||
Log.Logger = logger;
|
||||
}
|
||||
|
||||
private LogContextFormat GetContextFormat(string[] arguments)
|
||||
{
|
||||
string level = GlobalOptionHelper.GetArgumentValue(
|
||||
this.LogContextOption,
|
||||
arguments,
|
||||
nameof(LogContextFormat.Class));
|
||||
|
||||
LogContextFormat format = level.GetEnumFuzzy<LogContextFormat>(
|
||||
"log context format");
|
||||
|
||||
return format;
|
||||
}
|
||||
|
||||
private LogEventLevel GetEventLevel(string[] arguments)
|
||||
{
|
||||
string level = GlobalOptionHelper.GetArgumentValue(
|
||||
this.LogLevelOption,
|
||||
arguments,
|
||||
"Warning");
|
||||
|
||||
LogEventLevel logLevel = level.GetEnumFuzzy<LogEventLevel>(
|
||||
"log level");
|
||||
return logLevel;
|
||||
}
|
||||
|
||||
private ExpressionTemplate GetExpressionTemplate(string[] arguments)
|
||||
{
|
||||
// Pull out the formats.
|
||||
LogContextFormat contextFormat = this.GetContextFormat(arguments);
|
||||
string timeFormat = this.GetTimeFormat(arguments);
|
||||
|
||||
// Figure out the expression template we want to use.
|
||||
string timeExpression = string.IsNullOrWhiteSpace(timeFormat)
|
||||
? ""
|
||||
: $"{{@t:{timeFormat}}} ";
|
||||
|
||||
string contextExpression = contextFormat switch
|
||||
{
|
||||
LogContextFormat.None => "",
|
||||
LogContextFormat.Class =>
|
||||
"{#if SourceContext is not null} <{Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}>{#end}",
|
||||
LogContextFormat.Full =>
|
||||
"{#if SourceContext is not null} <{SourceContext}>{#end}",
|
||||
_ => throw new ArgumentOutOfRangeException(),
|
||||
};
|
||||
|
||||
var template = new ExpressionTemplate(
|
||||
string.Join(
|
||||
"",
|
||||
$"[{timeExpression}{{@l:u3}}]",
|
||||
contextExpression,
|
||||
" {@m}\n{@x}"),
|
||||
theme: TemplateTheme.Literate);
|
||||
return template;
|
||||
}
|
||||
|
||||
private Dictionary<string, LogEventLevel> GetLevelOverrides(
|
||||
string[] arguments)
|
||||
{
|
||||
List<string> levels = GlobalOptionHelper.GetArgumentValue(
|
||||
this.LogLevelOverrideOption,
|
||||
arguments,
|
||||
new List<string> { "Microsoft=Warning" });
|
||||
|
||||
return levels
|
||||
.ToDictionary(
|
||||
a => a.Split("=", 2)[0],
|
||||
a => a.Contains("=")
|
||||
? a.Split("=", 2)[1]
|
||||
.GetEnumFuzzy<LogEventLevel>(
|
||||
"log level")
|
||||
: (LogEventLevel)int.MaxValue);
|
||||
}
|
||||
|
||||
private string GetTimeFormat(string[] arguments)
|
||||
{
|
||||
string timeFormat = GlobalOptionHelper.GetArgumentValue(
|
||||
this.LogTimeFormat,
|
||||
arguments,
|
||||
"HH:mm:ss");
|
||||
return timeFormat;
|
||||
}
|
||||
}
|
|
@ -64,6 +64,7 @@
|
|||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MfGames.IO\MfGames.IO.csproj" />
|
||||
<ProjectReference Include="..\MfGames.Serilog.SpectreExpressions\MfGames.Serilog.SpectreExpressions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -32,14 +32,14 @@ namespace MfGames.ToolBuilder
|
|||
|
||||
private readonly IHostBuilder hostBuilder;
|
||||
|
||||
private readonly LoggingToolGlobal logging;
|
||||
private readonly LoggingToolService logging;
|
||||
|
||||
public ToolBoxBuilder(string configName, string[] arguments)
|
||||
{
|
||||
// Create our various services.
|
||||
this.arguments = arguments;
|
||||
this.config = new ConfigToolGlobal(configName);
|
||||
this.logging = new LoggingToolGlobal();
|
||||
this.logging = new LoggingToolService();
|
||||
|
||||
// Set up logging first so we can report the loading process. This
|
||||
// sets up the Serilog.Log.Logger which means we can use that for
|
||||
|
@ -94,14 +94,14 @@ namespace MfGames.ToolBuilder
|
|||
RootCommand rootCommand = container.Resolve<RootCommand>();
|
||||
|
||||
this.config.Attach(rootCommand);
|
||||
this.logging.Attach(rootCommand);
|
||||
this.logging.AttachGlobal(rootCommand);
|
||||
|
||||
// Finish creating the command line builder so we can make the parser.
|
||||
CommandLineBuilder builder = new CommandLineBuilder(rootCommand)
|
||||
.UseDefaults()
|
||||
.UseHost();
|
||||
|
||||
// Finish building the parser, wrap it in a tool box, and return it.
|
||||
// Finish building the parser, wrap it in a tool box, and return it.
|
||||
Parser parser = builder.Build();
|
||||
var toolBox = new ToolBox(this.arguments, parser);
|
||||
|
||||
|
|
|
@ -15,7 +15,7 @@ namespace MfGames.ToolBuilder
|
|||
builder
|
||||
.RegisterAssemblyTypes(this.GetType().Assembly)
|
||||
.Except<ConfigToolGlobal>()
|
||||
.Except<LoggingToolGlobal>()
|
||||
.Except<LoggingToolService>()
|
||||
.AsSelf()
|
||||
.AsImplementedInterfaces();
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue