Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support upper and lowercase level names for all widths #137

Merged
merged 3 commits into from
Feb 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/Serilog.Sinks.Console/Serilog.Sinks.Console.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
<TreatWarningsAsErrors>True</TreatWarningsAsErrors>
<TreatSpecificWarningsAsErrors />
<RootNamespace>Serilog</RootNamespace>
<LangVersion>latest</LangVersion>
</PropertyGroup>

<PropertyGroup Condition=" '$(TargetFramework)' != 'net45' ">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright 2017 Serilog Contributors
// Copyright 2023 Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand All @@ -12,89 +12,90 @@
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using Serilog.Events;
using Serilog.Sinks.SystemConsole.Rendering;

namespace Serilog.Sinks.SystemConsole.Output
{
/// <summary>
/// Implements the {Level} element.
/// can now have a fixed width applied to it, as well as casing rules.
/// Width is set through formats like "u3" (uppercase three chars),
/// "w1" (one lowercase char), or "t4" (title case four chars).
/// </summary>
static class LevelOutputFormat
{
static readonly string[][] TitleCaseLevelMap =
{
new[] { "V", "Vb", "Vrb", "Verb" },
new[] { "D", "De", "Dbg", "Dbug" },
new[] { "I", "In", "Inf", "Info" },
new[] { "W", "Wn", "Wrn", "Warn" },
new[] { "E", "Er", "Err", "Eror" },
new[] { "F", "Fa", "Ftl", "Fatl" },
};
namespace Serilog.Sinks.SystemConsole.Output;

static readonly string[][] LowercaseLevelMap =
{
new[] { "v", "vb", "vrb", "verb" },
new[] { "d", "de", "dbg", "dbug" },
new[] { "i", "in", "inf", "info" },
new[] { "w", "wn", "wrn", "warn" },
new[] { "e", "er", "err", "eror" },
new[] { "f", "fa", "ftl", "fatl" },
};
/// <summary>
/// Implements the {Level} element.
/// can now have a fixed width applied to it, as well as casing rules.
/// Width is set through formats like "u3" (uppercase three chars),
/// "w1" (one lowercase char), or "t4" (title case four chars).
/// </summary>
static class LevelOutputFormat
{
static readonly string[][] TitleCaseLevelMap = {
new []{ "V", "Vb", "Vrb", "Verb", "Verbo", "Verbos", "Verbose" },
new []{ "D", "De", "Dbg", "Dbug", "Debug" },
new []{ "I", "In", "Inf", "Info", "Infor", "Inform", "Informa", "Informat", "Informati", "Informatio", "Information" },
new []{ "W", "Wn", "Wrn", "Warn", "Warni", "Warnin", "Warning" },
new []{ "E", "Er", "Err", "Eror", "Error" },
new []{ "F", "Fa", "Ftl", "Fatl", "Fatal" }
};

static readonly string[][] UppercaseLevelMap =
{
new[] { "V", "VB", "VRB", "VERB" },
new[] { "D", "DE", "DBG", "DBUG" },
new[] { "I", "IN", "INF", "INFO" },
new[] { "W", "WN", "WRN", "WARN" },
new[] { "E", "ER", "ERR", "EROR" },
new[] { "F", "FA", "FTL", "FATL" },
};
static readonly string[][] LowerCaseLevelMap = {
new []{ "v", "vb", "vrb", "verb", "verbo", "verbos", "verbose" },
new []{ "d", "de", "dbg", "dbug", "debug" },
new []{ "i", "in", "inf", "info", "infor", "inform", "informa", "informat", "informati", "informatio", "information" },
new []{ "w", "wn", "wrn", "warn", "warni", "warnin", "warning" },
new []{ "e", "er", "err", "eror", "error" },
new []{ "f", "fa", "ftl", "fatl", "fatal" }
};

public static string GetLevelMoniker(LogEventLevel value, string? format = null)
{
if (format is null || format.Length != 2 && format.Length != 3)
return Casing.Format(value.ToString(), format);
static readonly string[][] UpperCaseLevelMap = {
new []{ "V", "VB", "VRB", "VERB", "VERBO", "VERBOS", "VERBOSE" },
new []{ "D", "DE", "DBG", "DBUG", "DEBUG" },
new []{ "I", "IN", "INF", "INFO", "INFOR", "INFORM", "INFORMA", "INFORMAT", "INFORMATI", "INFORMATIO", "INFORMATION" },
new []{ "W", "WN", "WRN", "WARN", "WARNI", "WARNIN", "WARNING" },
new []{ "E", "ER", "ERR", "EROR", "ERROR" },
new []{ "F", "FA", "FTL", "FATL", "FATAL" }
};

// Using int.Parse() here requires allocating a string to exclude the first character prefix.
// Junk like "wxy" will be accepted but produce benign results.
var width = format[1] - '0';
if (format.Length == 3)
{
width *= 10;
width += format[2] - '0';
}
public static string GetLevelMoniker(LogEventLevel value, string? format = null)
{
var index = (int)value;
if (index is < 0 or > (int)LogEventLevel.Fatal)
return Casing.Format(value.ToString(), format);

if (width < 1)
return string.Empty;
if (format == null || format.Length != 2 && format.Length != 3)
return Casing.Format(GetLevelMoniker(TitleCaseLevelMap, index), format);

if (width > 4)
{
var stringValue = value.ToString();
if (stringValue.Length > width)
stringValue = stringValue.Substring(0, width);
return Casing.Format(stringValue);
}
// Using int.Parse() here requires allocating a string to exclude the first character prefix.
// Junk like "wxy" will be accepted but produce benign results.
var width = format[1] - '0';
if (format.Length == 3)
{
width *= 10;
width += format[2] - '0';
}

var index = (int)value;
if (index >= 0 && index <= (int)LogEventLevel.Fatal)
{
switch (format[0])
{
case 'w':
return LowercaseLevelMap[index][width - 1];
case 'u':
return UppercaseLevelMap[index][width - 1];
case 't':
return TitleCaseLevelMap[index][width - 1];
}
}
if (width < 1)
return string.Empty;

return Casing.Format(value.ToString(), format);
switch (format[0])
{
case 'w':
return GetLevelMoniker(LowerCaseLevelMap, index, width);
case 'u':
return GetLevelMoniker(UpperCaseLevelMap, index, width);
case 't':
return GetLevelMoniker(TitleCaseLevelMap, index, width);
default:
return Casing.Format(GetLevelMoniker(TitleCaseLevelMap, index), format);
}
}
}

static string GetLevelMoniker(string[][] caseLevelMap, int index, int width)
{
var caseLevel = caseLevelMap[index];
return caseLevel[Math.Min(width, caseLevel.Length) - 1];
}

static string GetLevelMoniker(string[][] caseLevelMap, int index)
{
var caseLevel = caseLevelMap[index];
return caseLevel[caseLevel.Length - 1];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,23 @@ public void FixedLengthLevelIsSupported(
int width,
string expected)
{
var formatter = new OutputTemplateRenderer(ConsoleTheme.None, $"{{Level:t{width}}}", CultureInfo.InvariantCulture);
var evt = DelegatingSink.GetLogEvent(l => l.Write(level, "Hello"));
var sw = new StringWriter();
formatter.Format(evt, sw);
Assert.Equal(expected, sw.ToString());
var formatter1 = new OutputTemplateRenderer(ConsoleTheme.None, $"{{Level:t{width}}}", CultureInfo.InvariantCulture);
var evt1 = DelegatingSink.GetLogEvent(l => l.Write(level, "Hello"));
var sw1 = new StringWriter();
formatter1.Format(evt1, sw1);
Assert.Equal(expected, sw1.ToString());

var formatter2 = new OutputTemplateRenderer(ConsoleTheme.None, $"{{Level:u{width}}}", CultureInfo.InvariantCulture);
var evt2 = DelegatingSink.GetLogEvent(l => l.Write(level, "Hello"));
var sw2 = new StringWriter();
formatter2.Format(evt2, sw2);
Assert.Equal(expected.ToUpper(), sw2.ToString());

var formatter3 = new OutputTemplateRenderer(ConsoleTheme.None, $"{{Level:w{width}}}", CultureInfo.InvariantCulture);
var evt3 = DelegatingSink.GetLogEvent(l => l.Write(level, "Hello"));
var sw3 = new StringWriter();
formatter3.Format(evt3, sw3);
Assert.Equal(expected.ToLower(), sw3.ToString());
}

[Fact]
Expand All @@ -131,6 +143,16 @@ public void FixedLengthLevelSupportsLowerCasing()
formatter.Format(evt, sw);
Assert.Equal("inf", sw.ToString());
}

[Fact]
public void FixedLengthLevelSupportsCasingForWideNames()
{
var formatter = new OutputTemplateRenderer(ConsoleTheme.None, "{Level:w6}", CultureInfo.InvariantCulture);
var evt = DelegatingSink.GetLogEvent(l => l.Information("Hello"));
var sw = new StringWriter();
formatter.Format(evt, sw);
Assert.Equal("inform", sw.ToString());
}

[Fact]
public void DefaultLevelLengthIsFullText()
Expand Down