diff --git a/appveyor.yml b/appveyor.yml index 397acc7..c6dd7f8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -9,7 +9,7 @@ artifacts: deploy: - provider: NuGet api_key: - secure: oemq1E4zMR+LKQyrR83ZLcugPpZtl5OMKjtpMy/mbPEwuFGS+Oe46427D9KoHYD8 + secure: dX4ewxGxhiNURkqJPuZQ8GQNjLvb8oZrHBThVotn+9GSMyQzeKXFpHkN04ykOfgc skip_symbols: true on: branch: /^(main|dev)$/ diff --git a/sample/ConsoleDemo/ConsoleDemo.csproj b/sample/ConsoleDemo/ConsoleDemo.csproj index 81b1a1f..de9024a 100644 --- a/sample/ConsoleDemo/ConsoleDemo.csproj +++ b/sample/ConsoleDemo/ConsoleDemo.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp2.1;netcoreapp2.2;netcoreapp3.0;netcoreapp3.1;net452;net462;net472;net48;net5.0 + net7.0 diff --git a/sample/SyncWritesDemo/SyncWritesDemo.csproj b/sample/SyncWritesDemo/SyncWritesDemo.csproj index a72595e..de9024a 100644 --- a/sample/SyncWritesDemo/SyncWritesDemo.csproj +++ b/sample/SyncWritesDemo/SyncWritesDemo.csproj @@ -2,7 +2,7 @@ Exe - net5.0 + net7.0 diff --git a/src/Serilog.Sinks.Console/ConsoleAuditLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.Console/ConsoleAuditLoggerConfigurationExtensions.cs new file mode 100644 index 0000000..6073c37 --- /dev/null +++ b/src/Serilog.Sinks.Console/ConsoleAuditLoggerConfigurationExtensions.cs @@ -0,0 +1,109 @@ +// Copyright 2017 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +using Serilog.Configuration; +using Serilog.Core; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Sinks.SystemConsole; +using Serilog.Sinks.SystemConsole.Output; +using Serilog.Sinks.SystemConsole.Themes; +using System; + +namespace Serilog +{ + /// + /// Adds the AuditTo.Console() extension method to . + /// + public static class ConsoleAuditLoggerConfigurationExtensions + { + /// + /// Writes log events to . + /// + /// Logger sink configuration. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// 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}". + /// An object that will be used to `lock` (sync) access to the console output. If you specify this, you + /// will have the ability to lock on this object, and guarantee that the console sink will not be about to output anything while + /// the lock is held. + /// Supplies culture-specific formatting information, or null. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Specifies the level at which events will be written to standard error. + /// The theme to apply to the styled output. If not specified, + /// uses . + /// Applies the selected or default theme even when output redirection is detected. + /// Configuration object allowing method chaining. + /// When is null + /// When is null + public static LoggerConfiguration Console( + this LoggerAuditSinkConfiguration sinkConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string outputTemplate = ConsoleLoggerConfigurationExtensions.DefaultConsoleOutputTemplate, + IFormatProvider? formatProvider = null, + LoggingLevelSwitch? levelSwitch = null, + LogEventLevel? standardErrorFromLevel = null, + ConsoleTheme? theme = null, + bool applyThemeToRedirectedOutput = false, + object? syncRoot = null) + { + if (sinkConfiguration is null) throw new ArgumentNullException(nameof(sinkConfiguration)); + if (outputTemplate is null) throw new ArgumentNullException(nameof(outputTemplate)); + + var appliedTheme = !applyThemeToRedirectedOutput && (System.Console.IsOutputRedirected || System.Console.IsErrorRedirected) ? + ConsoleTheme.None : + theme ?? SystemConsoleThemes.Literate; + + syncRoot ??= ConsoleLoggerConfigurationExtensions.DefaultSyncRoot; + + var formatter = new OutputTemplateRenderer(appliedTheme, outputTemplate, formatProvider); + return sinkConfiguration.Sink(new ConsoleSink(appliedTheme, formatter, standardErrorFromLevel, syncRoot), restrictedToMinimumLevel, levelSwitch); + } + + /// + /// Writes log events to . + /// + /// Logger sink configuration. + /// Controls the rendering of log events into text, for example to log JSON. To + /// control plain text formatting, use the overload that accepts an output template. + /// An object that will be used to `lock` (sync) access to the console output. If you specify this, you + /// will have the ability to lock on this object, and guarantee that the console sink will not be about to output anything while + /// the lock is held. + /// The minimum level for + /// events passed through the sink. Ignored when is specified. + /// A switch allowing the pass-through minimum level + /// to be changed at runtime. + /// Specifies the level at which events will be written to standard error. + /// Configuration object allowing method chaining. + /// When is null + /// When is null + public static LoggerConfiguration Console( + this LoggerAuditSinkConfiguration sinkConfiguration, + ITextFormatter formatter, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch? levelSwitch = null, + LogEventLevel? standardErrorFromLevel = null, + object? syncRoot = null) + { + if (sinkConfiguration is null) throw new ArgumentNullException(nameof(sinkConfiguration)); + if (formatter is null) throw new ArgumentNullException(nameof(formatter)); + + syncRoot ??= ConsoleLoggerConfigurationExtensions.DefaultSyncRoot; + + return sinkConfiguration.Sink(new ConsoleSink(ConsoleTheme.None, formatter, standardErrorFromLevel, syncRoot), restrictedToMinimumLevel, levelSwitch); + } + } +} diff --git a/src/Serilog.Sinks.Console/ConsoleLoggerConfigurationExtensions.cs b/src/Serilog.Sinks.Console/ConsoleLoggerConfigurationExtensions.cs index e55f7a2..cca312a 100644 --- a/src/Serilog.Sinks.Console/ConsoleLoggerConfigurationExtensions.cs +++ b/src/Serilog.Sinks.Console/ConsoleLoggerConfigurationExtensions.cs @@ -28,8 +28,8 @@ namespace Serilog /// public static class ConsoleLoggerConfigurationExtensions { - static readonly object DefaultSyncRoot = new object(); - const string DefaultConsoleOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"; + internal static readonly object DefaultSyncRoot = new object(); + internal const string DefaultConsoleOutputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"; /// /// Writes log events to . diff --git a/src/Serilog.Sinks.Console/Serilog.Sinks.Console.csproj b/src/Serilog.Sinks.Console/Serilog.Sinks.Console.csproj index 06abb29..c1aeaf1 100644 --- a/src/Serilog.Sinks.Console/Serilog.Sinks.Console.csproj +++ b/src/Serilog.Sinks.Console/Serilog.Sinks.Console.csproj @@ -1,37 +1,50 @@ + + A Serilog sink that writes log events to the console/terminal. + 5.0.0 + Serilog Contributors + net462;net471 + $(TargetFrameworks);netstandard2.1;netstandard2.0;net5.0;net6.0;net7.0 + enable + ../../assets/Serilog.snk + true + true + serilog;console;terminal + icon.png + https://github.com/serilog/serilog-sinks-console + Apache-2.0 + https://github.com/serilog/serilog-sinks-console + git + true + True + Serilog + latest + README.md + - - A Serilog sink that writes log events to the console/terminal. - 4.1.0 - Serilog Contributors - net45;netstandard1.3;netstandard2.0;net5.0 - 8.0 - enable - ../../assets/Serilog.snk - true - true - serilog;console;terminal - icon.png - https://github.com/serilog/serilog-sinks-console - Apache-2.0 - https://github.com/serilog/serilog-sinks-console - git - true - True - - Serilog - + + $(DefineConstants);RUNTIME_INFORMATION + - - $(DefineConstants);RUNTIME_INFORMATION - - - - - - + + $(DefineConstants);FEATURE_SPAN + + + + $(DefineConstants);FEATURE_SPAN + + + + $(DefineConstants);FEATURE_SPAN + + + + + + - - - + + + + diff --git a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/LevelOutputFormat.cs b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/LevelOutputFormat.cs index d703bb8..a0c996f 100644 --- a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/LevelOutputFormat.cs +++ b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/LevelOutputFormat.cs @@ -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. @@ -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 -{ - /// - /// 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). - /// - 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" }, - }; +/// +/// 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). +/// +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); } } -} \ No newline at end of file + + 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]; + } +} diff --git a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/NewLineTokenRenderer.cs b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/NewLineTokenRenderer.cs index c7174e8..4b072c3 100644 --- a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/NewLineTokenRenderer.cs +++ b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/NewLineTokenRenderer.cs @@ -37,4 +37,4 @@ public override void Render(LogEvent logEvent, TextWriter output) output.WriteLine(); } } -} \ No newline at end of file +} diff --git a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/OutputTemplateRenderer.cs b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/OutputTemplateRenderer.cs index c62ab67..f922290 100644 --- a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/OutputTemplateRenderer.cs +++ b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/OutputTemplateRenderer.cs @@ -50,6 +50,14 @@ public OutputTemplateRenderer(ConsoleTheme theme, string outputTemplate, IFormat { renderers.Add(new NewLineTokenRenderer(pt.Alignment)); } + else if (pt.PropertyName == OutputProperties.TraceIdPropertyName) + { + renderers.Add(new TraceIdTokenRenderer(theme, pt)); + } + else if (pt.PropertyName == OutputProperties.SpanIdPropertyName) + { + renderers.Add(new SpanIdTokenRenderer(theme, pt)); + } else if (pt.PropertyName == OutputProperties.ExceptionPropertyName) { renderers.Add(new ExceptionTokenRenderer(theme, pt)); @@ -62,7 +70,7 @@ public OutputTemplateRenderer(ConsoleTheme theme, string outputTemplate, IFormat { renderers.Add(new TimestampTokenRenderer(theme, pt, formatProvider)); } - else if (pt.PropertyName == "Properties") + else if (pt.PropertyName == OutputProperties.PropertiesPropertyName) { renderers.Add(new PropertiesTokenRenderer(theme, pt, template, formatProvider)); } diff --git a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/SpanIdTokenRenderer.cs b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/SpanIdTokenRenderer.cs new file mode 100644 index 0000000..394a7da --- /dev/null +++ b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/SpanIdTokenRenderer.cs @@ -0,0 +1,34 @@ +using System.IO; +using Serilog.Events; +using Serilog.Parsing; +using Serilog.Sinks.SystemConsole.Rendering; +using Serilog.Sinks.SystemConsole.Themes; + +namespace Serilog.Sinks.SystemConsole.Output; + +class SpanIdTokenRenderer : OutputTemplateTokenRenderer +{ + readonly ConsoleTheme _theme; + readonly Alignment? _alignment; + + public SpanIdTokenRenderer(ConsoleTheme theme, PropertyToken spanIdToken) + { + _theme = theme; + _alignment = spanIdToken.Alignment; + } + + public override void Render(LogEvent logEvent, TextWriter output) + { + if (logEvent.SpanId is not { } spanId) + return; + + var _ = 0; + using (_theme.Apply(output, ConsoleThemeStyle.Text, ref _)) + { + if (_alignment is {} alignment) + Padding.Apply(output, spanId.ToString(), alignment); + else + output.Write(spanId); + } + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TimestampTokenRenderer.cs b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TimestampTokenRenderer.cs index c763279..bc5f000 100644 --- a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TimestampTokenRenderer.cs +++ b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TimestampTokenRenderer.cs @@ -13,6 +13,7 @@ // limitations under the License. using System; +using System.Globalization; using System.IO; using Serilog.Events; using Serilog.Parsing; @@ -36,9 +37,7 @@ public TimestampTokenRenderer(ConsoleTheme theme, PropertyToken token, IFormatPr public override void Render(LogEvent logEvent, TextWriter output) { - // We need access to ScalarValue.Render() to avoid this alloc; just ensures - // that custom format providers are supported properly. - var sv = new ScalarValue(logEvent.Timestamp); + var sv = new DateTimeOffsetValue(logEvent.Timestamp); var _ = 0; using (_theme.Apply(output, ConsoleThemeStyle.SecondaryText, ref _)) @@ -56,5 +55,35 @@ public override void Render(LogEvent logEvent, TextWriter output) } } } + + readonly struct DateTimeOffsetValue + { + public DateTimeOffsetValue(DateTimeOffset value) + { + Value = value; + } + + public DateTimeOffset Value { get; } + + public void Render(TextWriter output, string? format = null, IFormatProvider? formatProvider = null) + { + var custom = (ICustomFormatter?)formatProvider?.GetFormat(typeof(ICustomFormatter)); + if (custom != null) + { + output.Write(custom.Format(format, Value, formatProvider)); + return; + } + +#if FEATURE_SPAN + Span buffer = stackalloc char[32]; + if (Value.TryFormat(buffer, out int written, format, formatProvider ?? CultureInfo.InvariantCulture)) + output.Write(buffer.Slice(0, written)); + else + output.Write(Value.ToString(format, formatProvider ?? CultureInfo.InvariantCulture)); +#else + output.Write(Value.ToString(format, formatProvider ?? CultureInfo.InvariantCulture)); +#endif + } + } } } diff --git a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TraceIdTokenRenderer.cs b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TraceIdTokenRenderer.cs new file mode 100644 index 0000000..c2be4d0 --- /dev/null +++ b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TraceIdTokenRenderer.cs @@ -0,0 +1,34 @@ +using System.IO; +using Serilog.Events; +using Serilog.Parsing; +using Serilog.Sinks.SystemConsole.Rendering; +using Serilog.Sinks.SystemConsole.Themes; + +namespace Serilog.Sinks.SystemConsole.Output; + +class TraceIdTokenRenderer : OutputTemplateTokenRenderer +{ + readonly ConsoleTheme _theme; + readonly Alignment? _alignment; + + public TraceIdTokenRenderer(ConsoleTheme theme, PropertyToken traceIdToken) + { + _theme = theme; + _alignment = traceIdToken.Alignment; + } + + public override void Render(LogEvent logEvent, TextWriter output) + { + if (logEvent.TraceId is not { } traceId) + return; + + var _ = 0; + using (_theme.Apply(output, ConsoleThemeStyle.Text, ref _)) + { + if (_alignment is {} alignment) + Padding.Apply(output, traceId.ToString(), alignment); + else + output.Write(traceId); + } + } +} \ No newline at end of file diff --git a/test/Serilog.Sinks.Console.Tests/Approval/ApiApprovalTests.cs b/test/Serilog.Sinks.Console.Tests/Approval/ApiApprovalTests.cs new file mode 100644 index 0000000..46b1c72 --- /dev/null +++ b/test/Serilog.Sinks.Console.Tests/Approval/ApiApprovalTests.cs @@ -0,0 +1,27 @@ +#if NET5_0 + +using PublicApiGenerator; +using Shouldly; +using Xunit; + +namespace Serilog.Sinks.Console.Tests.Approval +{ + public class ApiApprovalTests + { + [Fact] + public void PublicApi_Should_Not_Change_Unintentionally() + { + var assembly = typeof(ConsoleLoggerConfigurationExtensions).Assembly; + var publicApi = assembly.GeneratePublicApi( + new ApiGeneratorOptions() + { + IncludeAssemblyAttributes = false, + ExcludeAttributes = new[] { "System.Diagnostics.DebuggerDisplayAttribute" }, + }); + + publicApi.ShouldMatchApproved(options => options.WithFilenameGenerator((_, __, fileType, fileExtension) => $"{assembly.GetName().Name!}.{fileType}.{fileExtension}")); + } + } +} + +#endif diff --git a/test/Serilog.Sinks.Console.Tests/Approval/Serilog.Sinks.Console.approved.txt b/test/Serilog.Sinks.Console.Tests/Approval/Serilog.Sinks.Console.approved.txt new file mode 100644 index 0000000..a6239fb --- /dev/null +++ b/test/Serilog.Sinks.Console.Tests/Approval/Serilog.Sinks.Console.approved.txt @@ -0,0 +1,75 @@ +namespace Serilog +{ + public static class ConsoleAuditLoggerConfigurationExtensions + { + public static Serilog.LoggerConfiguration Console(this Serilog.Configuration.LoggerAuditSinkConfiguration sinkConfiguration, Serilog.Formatting.ITextFormatter formatter, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null, Serilog.Events.LogEventLevel? standardErrorFromLevel = default, object? syncRoot = null) { } + public static Serilog.LoggerConfiguration Console(this Serilog.Configuration.LoggerAuditSinkConfiguration sinkConfiguration, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, string outputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}", System.IFormatProvider? formatProvider = null, Serilog.Core.LoggingLevelSwitch? levelSwitch = null, Serilog.Events.LogEventLevel? standardErrorFromLevel = default, Serilog.Sinks.SystemConsole.Themes.ConsoleTheme? theme = null, bool applyThemeToRedirectedOutput = false, object? syncRoot = null) { } + } + public static class ConsoleLoggerConfigurationExtensions + { + public static Serilog.LoggerConfiguration Console(this Serilog.Configuration.LoggerSinkConfiguration sinkConfiguration, Serilog.Formatting.ITextFormatter formatter, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null, Serilog.Events.LogEventLevel? standardErrorFromLevel = default, object? syncRoot = null) { } + public static Serilog.LoggerConfiguration Console(this Serilog.Configuration.LoggerSinkConfiguration sinkConfiguration, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, string outputTemplate = "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}", System.IFormatProvider? formatProvider = null, Serilog.Core.LoggingLevelSwitch? levelSwitch = null, Serilog.Events.LogEventLevel? standardErrorFromLevel = default, Serilog.Sinks.SystemConsole.Themes.ConsoleTheme? theme = null, bool applyThemeToRedirectedOutput = false, object? syncRoot = null) { } + } +} +namespace Serilog.Sinks.SystemConsole.Themes +{ + public class AnsiConsoleTheme : Serilog.Sinks.SystemConsole.Themes.ConsoleTheme + { + public AnsiConsoleTheme(System.Collections.Generic.IReadOnlyDictionary styles) { } + public override bool CanBuffer { get; } + protected override int ResetCharCount { get; } + public static Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme Code { get; } + public static Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme Grayscale { get; } + public static Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme Literate { get; } + public static Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme Sixteen { get; } + public override void Reset(System.IO.TextWriter output) { } + public override int Set(System.IO.TextWriter output, Serilog.Sinks.SystemConsole.Themes.ConsoleThemeStyle style) { } + } + public abstract class ConsoleTheme + { + protected ConsoleTheme() { } + public abstract bool CanBuffer { get; } + protected abstract int ResetCharCount { get; } + public static Serilog.Sinks.SystemConsole.Themes.ConsoleTheme None { get; } + public abstract void Reset(System.IO.TextWriter output); + public abstract int Set(System.IO.TextWriter output, Serilog.Sinks.SystemConsole.Themes.ConsoleThemeStyle style); + } + public enum ConsoleThemeStyle + { + Text = 0, + SecondaryText = 1, + TertiaryText = 2, + Invalid = 3, + Null = 4, + Name = 5, + String = 6, + Number = 7, + Boolean = 8, + Scalar = 9, + [System.Obsolete("Use ConsoleThemeStyle.Scalar instead")] + Object = 9, + LevelVerbose = 10, + LevelDebug = 11, + LevelInformation = 12, + LevelWarning = 13, + LevelError = 14, + LevelFatal = 15, + } + public class SystemConsoleTheme : Serilog.Sinks.SystemConsole.Themes.ConsoleTheme + { + public SystemConsoleTheme(System.Collections.Generic.IReadOnlyDictionary styles) { } + public override bool CanBuffer { get; } + protected override int ResetCharCount { get; } + public System.Collections.Generic.IReadOnlyDictionary Styles { get; } + public static Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme Colored { get; } + public static Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme Grayscale { get; } + public static Serilog.Sinks.SystemConsole.Themes.SystemConsoleTheme Literate { get; } + public override void Reset(System.IO.TextWriter output) { } + public override int Set(System.IO.TextWriter output, Serilog.Sinks.SystemConsole.Themes.ConsoleThemeStyle style) { } + } + public struct SystemConsoleThemeStyle + { + public System.ConsoleColor? Background; + public System.ConsoleColor? Foreground; + } +} \ No newline at end of file diff --git a/test/Serilog.Sinks.Console.Tests/Configuration/ConsoleAuditLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.Console.Tests/Configuration/ConsoleAuditLoggerConfigurationExtensionsTests.cs new file mode 100644 index 0000000..1398128 --- /dev/null +++ b/test/Serilog.Sinks.Console.Tests/Configuration/ConsoleAuditLoggerConfigurationExtensionsTests.cs @@ -0,0 +1,64 @@ +using System; +using System.IO; +using System.Linq; +using Xunit; +using Serilog.Sinks.SystemConsole.Themes; + +namespace Serilog.Sinks.Console.Tests.Configuration +{ + [Collection("ConsoleSequentialTests")] + public class ConsoleAuditLoggerConfigurationExtensionsTests + { + [Fact] + public void OutputFormattingIsIgnored() + { + using (var stream = new MemoryStream()) + { + var sw = new StreamWriter(stream); + + System.Console.SetOut(sw); + var config = new LoggerConfiguration() + .AuditTo.Console(theme: AnsiConsoleTheme.Literate, + applyThemeToRedirectedOutput: false); + + var logger = config.CreateLogger(); + + logger.Error("test"); + stream.Position = 0; + + using (var streamReader = new StreamReader(stream)) + { + var result = streamReader.ReadToEnd(); + var controlCharacterCount = result.Count(c => Char.IsControl(c) && !Char.IsWhiteSpace(c)); + Assert.Equal(0, controlCharacterCount); + } + } + } + + [Fact] + public void OutputFormattingIsPresent() + { + using (var stream = new MemoryStream()) + { + var sw = new StreamWriter(stream); + + System.Console.SetOut(sw); + var config = new LoggerConfiguration() + .AuditTo.Console(theme: AnsiConsoleTheme.Literate, + applyThemeToRedirectedOutput: true); + + var logger = config.CreateLogger(); + + logger.Error("test"); + stream.Position = 0; + + using (var streamReader = new StreamReader(stream)) + { + var result = streamReader.ReadToEnd(); + var controlCharacterCount = result.Count(c => Char.IsControl(c) && !Char.IsWhiteSpace(c)); + Assert.NotEqual(0, controlCharacterCount); + } + } + } + } +} \ No newline at end of file diff --git a/test/Serilog.Sinks.Console.Tests/Configuration/ConsoleLoggerConfigurationExtensionsTests.cs b/test/Serilog.Sinks.Console.Tests/Configuration/ConsoleLoggerConfigurationExtensionsTests.cs index 74596c5..a66c82f 100644 --- a/test/Serilog.Sinks.Console.Tests/Configuration/ConsoleLoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Sinks.Console.Tests/Configuration/ConsoleLoggerConfigurationExtensionsTests.cs @@ -6,6 +6,7 @@ namespace Serilog.Sinks.Console.Tests.Configuration { + [Collection("ConsoleSequentialTests")] public class ConsoleLoggerConfigurationExtensionsTests { [Fact] diff --git a/test/Serilog.Sinks.Console.Tests/Output/OutputTemplateRendererTests.cs b/test/Serilog.Sinks.Console.Tests/Output/OutputTemplateRendererTests.cs index 1a71415..59bbedc 100644 --- a/test/Serilog.Sinks.Console.Tests/Output/OutputTemplateRendererTests.cs +++ b/test/Serilog.Sinks.Console.Tests/Output/OutputTemplateRendererTests.cs @@ -1,8 +1,10 @@ using System; +using System.Diagnostics; using System.Globalization; using System.IO; using System.Linq; using Serilog.Events; +using Serilog.Parsing; using Serilog.Sinks.Console.Tests.Support; using Serilog.Sinks.SystemConsole.Output; using Serilog.Sinks.SystemConsole.Themes; @@ -105,11 +107,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] @@ -131,6 +145,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() @@ -166,7 +190,7 @@ public SizeFormatter(IFormatProvider innerFormatProvider) _innerFormatProvider = innerFormatProvider; } - public object? GetFormat(Type? formatType) + public object GetFormat(Type? formatType) { return formatType == typeof(ICustomFormatter) ? this : _innerFormatProvider.GetFormat(formatType) ?? this; } @@ -352,5 +376,29 @@ public void FormatProviderWithDestructuredProperties(string format, bool shouldU Assert.Contains(expectedFormattedDate, sw.ToString()); Assert.Contains(expectedFormattedNumber, sw.ToString()); } + + [Fact] + public void TraceAndSpanAreEmptyWhenAbsent() + { + var formatter = new OutputTemplateRenderer(ConsoleTheme.None, "{TraceId}/{SpanId}", CultureInfo.InvariantCulture); + var evt = DelegatingSink.GetLogEvent(l => l.Information("Hello, world")); + var sw = new StringWriter(); + formatter.Format(evt, sw); + Assert.Equal("/", sw.ToString()); + } + + [Fact] + public void TraceAndSpanAreIncludedWhenPresent() + { + var traceId = ActivityTraceId.CreateRandom(); + var spanId = ActivitySpanId.CreateRandom(); + var formatter = new OutputTemplateRenderer(ConsoleTheme.None, "{TraceId}/{SpanId}", CultureInfo.InvariantCulture); + var evt = new LogEvent(DateTimeOffset.Now, LogEventLevel.Debug, null, + new MessageTemplate(Enumerable.Empty()), Enumerable.Empty(), + traceId, spanId); + var sw = new StringWriter(); + formatter.Format(evt, sw); + Assert.Equal($"{traceId}/{spanId}", sw.ToString()); + } } } diff --git a/test/Serilog.Sinks.Console.Tests/Serilog.Sinks.Console.Tests.csproj b/test/Serilog.Sinks.Console.Tests/Serilog.Sinks.Console.Tests.csproj index 5e141e3..472965f 100644 --- a/test/Serilog.Sinks.Console.Tests/Serilog.Sinks.Console.Tests.csproj +++ b/test/Serilog.Sinks.Console.Tests/Serilog.Sinks.Console.Tests.csproj @@ -1,12 +1,11 @@ - + - netcoreapp2.1;netcoreapp2.2;netcoreapp3.0;netcoreapp3.1;net452;net462;net472;net48;net5.0 + net7.0 true ../../assets/Serilog.snk true true - 8.0 enable @@ -15,10 +14,12 @@ - + + +