diff --git a/README.md b/README.md index 1c1bef2..75490a3 100644 --- a/README.md +++ b/README.md @@ -25,3 +25,15 @@ Log.Information("Hello, browser!"); ``` A more detailed example is available [in this repository](https://github.com/serilog/serilog-sinks-browserconsole/tree/dev/example/ExampleClient). + +### Styling your stuff + +In your sink's `outputTemplate` parameter, you can leverage [`console.log` styling capabilities](https://developer.mozilla.org/en-US/docs/Web/API/console#styling_console_output) by using the `<<...>>` placeholder. Use `<<_>>` to reset styles to defaults. + +Example: + +``` +<>{Level}<<_>>: {Message} +``` + +You can also define styles for tokens via the `tokenStyles` dictionary. diff --git a/src/Serilog.Sinks.BrowserConsole/LoggerConfigurationBrowserConsoleExtensions.cs b/src/Serilog.Sinks.BrowserConsole/LoggerConfigurationBrowserConsoleExtensions.cs index a1b766b..ad79c1f 100644 --- a/src/Serilog.Sinks.BrowserConsole/LoggerConfigurationBrowserConsoleExtensions.cs +++ b/src/Serilog.Sinks.BrowserConsole/LoggerConfigurationBrowserConsoleExtensions.cs @@ -27,10 +27,10 @@ namespace Serilog; public static class LoggerConfigurationBrowserConsoleExtensions { const string SerilogToken = - "%cserilog{_}color:white;background:#8c7574;border-radius:3px;padding:1px 2px;font-weight:600;"; - - const string DefaultConsoleOutputTemplate = SerilogToken + "{Message}{NewLine}{Exception}"; - + "<>serilog<<_>>"; + + const string DefaultConsoleOutputTemplate = SerilogToken + "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}"; + /// /// Writes log events to the browser console. /// @@ -38,8 +38,9 @@ public static class LoggerConfigurationBrowserConsoleExtensions /// 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}". + /// The default is "(serilog) [{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj}{NewLine}{Exception}". /// Supplies culture-specific formatting information, or null. + /// A dictionary of styles to apply to tokens. See MDN about console styling /// A switch allowing the pass-through minimum level /// to be changed at runtime. /// An instance of to interact with the browser. @@ -49,11 +50,12 @@ public static LoggerConfiguration BrowserConsole( LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, string outputTemplate = DefaultConsoleOutputTemplate, IFormatProvider? formatProvider = null, + IReadOnlyDictionary? tokenStyles = null, LoggingLevelSwitch? levelSwitch = null, IJSRuntime? jsRuntime = null) { ArgumentNullException.ThrowIfNull(sinkConfiguration); - var formatter = new OutputTemplateRenderer(outputTemplate, formatProvider); + var formatter = new OutputTemplateRenderer(outputTemplate, formatProvider, tokenStyles); return sinkConfiguration.Sink(new BrowserConsoleSink(jsRuntime, formatter), restrictedToMinimumLevel, levelSwitch); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/EventPropertyTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/EventPropertyTokenRenderer.cs index 20fda6b..17e43cd 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/EventPropertyTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/EventPropertyTokenRenderer.cs @@ -35,7 +35,7 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken) if (!logEvent.Properties.TryGetValue(_token.PropertyName, out var propertyValue)) { if (_token.Alignment is not null) - emitToken(Padding.Apply(string.Empty, _token.Alignment)); + emitToken.Literal(Padding.Apply(string.Empty, _token.Alignment)); return; } @@ -55,8 +55,8 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken) var str = writer.ToString(); if (_token.Alignment is not null) - emitToken(Padding.Apply(str, _token.Alignment)); + emitToken.Text(Padding.Apply(str, _token.Alignment)); else - emitToken(str); + emitToken.Text(str); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/ExceptionTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/ExceptionTokenRenderer.cs index bf0f9df..7bd9da3 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/ExceptionTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/ExceptionTokenRenderer.cs @@ -21,6 +21,6 @@ class ExceptionTokenRenderer : OutputTemplateTokenRenderer public override void Render(LogEvent logEvent, TokenEmitter emitToken) { if (logEvent.Exception is not null) - emitToken(logEvent.Exception.ToString()); + emitToken.Text(logEvent.Exception.ToString()); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/LevelTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/LevelTokenRenderer.cs index fff372d..6f71106 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/LevelTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/LevelTokenRenderer.cs @@ -31,6 +31,6 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken) { var moniker = LevelOutputFormat.GetLevelMoniker(logEvent.Level, _levelToken.Format); var alignedOutput = Padding.Apply(moniker, _levelToken.Alignment); - emitToken(alignedOutput); + emitToken.Text(alignedOutput); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/MessageTemplateOutputTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/MessageTemplateOutputTokenRenderer.cs index 4447165..f7849a5 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/MessageTemplateOutputTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/MessageTemplateOutputTokenRenderer.cs @@ -26,15 +26,17 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken) switch (token) { case TextToken tt: - emitToken(tt.Text); + new TextTokenRenderer(tt.Text).Render(logEvent, emitToken); break; case PropertyToken pt: if (logEvent.Properties.TryGetValue(pt.PropertyName, out var propertyValue)) - emitToken(ObjectModelInterop.ToInteropValue(propertyValue)); + { + new PropertyTokenRenderer(pt, propertyValue).Render(logEvent, emitToken); + } break; default: throw new InvalidOperationException(); } } } -} \ No newline at end of file +} diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/NewLineTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/NewLineTokenRenderer.cs index 307367d..d367daa 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/NewLineTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/NewLineTokenRenderer.cs @@ -30,8 +30,8 @@ public NewLineTokenRenderer(Alignment? alignment) public override void Render(LogEvent logEvent, TokenEmitter emitToken) { if (_alignment is not null) - emitToken(Padding.Apply(Environment.NewLine, _alignment.Value.Widen(Environment.NewLine.Length))); + emitToken.Literal(Padding.Apply(Environment.NewLine, _alignment.Value.Widen(Environment.NewLine.Length))); else - emitToken(Environment.NewLine); + emitToken.Literal(Environment.NewLine); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateRenderer.cs index 7e205ee..4ff3edc 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateRenderer.cs @@ -1,46 +1,65 @@ using Serilog.Events; using Serilog.Formatting.Display; using Serilog.Parsing; +using System; namespace Serilog.Sinks.BrowserConsole.Output; class OutputTemplateRenderer { readonly OutputTemplateTokenRenderer[] _renderers; + private readonly IFormatProvider? _formatProvider; + private readonly IReadOnlyDictionary? _tokenStyles; + private readonly MessageTemplate _template; - public OutputTemplateRenderer(string outputTemplate, IFormatProvider? formatProvider) + public OutputTemplateRenderer(string outputTemplate, IFormatProvider? formatProvider, IReadOnlyDictionary? tokenStyles = default) { ArgumentNullException.ThrowIfNull(outputTemplate); - var template = new MessageTemplateParser().Parse(outputTemplate); - - _renderers = template.Tokens - .Select(token => token switch + _formatProvider = formatProvider; + _tokenStyles = tokenStyles; + _template = new MessageTemplateParser().Parse(outputTemplate); + + _renderers = _template.Tokens + .SelectMany(token => token switch { - TextToken tt => new TextTokenRenderer(tt.Text), - PropertyToken pt => pt.PropertyName switch - { - OutputProperties.LevelPropertyName => new LevelTokenRenderer(pt) as OutputTemplateTokenRenderer, - OutputProperties.NewLinePropertyName => new NewLineTokenRenderer(pt.Alignment), - OutputProperties.ExceptionPropertyName => new ExceptionTokenRenderer(), - OutputProperties.MessagePropertyName => new MessageTemplateOutputTokenRenderer(), - OutputProperties.TimestampPropertyName => new TimestampTokenRenderer(pt, formatProvider), - OutputProperties.PropertiesPropertyName => new PropertiesTokenRenderer(pt, template), - _ => new EventPropertyTokenRenderer(pt, formatProvider) - }, + TextToken tt => [new TextTokenRenderer(tt.Text)], + PropertyToken pt => WrapStyle(pt), _ => throw new InvalidOperationException() }) .ToArray(); } + private OutputTemplateTokenRenderer[] WrapStyle(PropertyToken token) + { + OutputTemplateTokenRenderer renderer = token.PropertyName switch + { + OutputProperties.LevelPropertyName => new LevelTokenRenderer(token), + OutputProperties.NewLinePropertyName => new NewLineTokenRenderer(token.Alignment), + OutputProperties.ExceptionPropertyName => new ExceptionTokenRenderer(), + OutputProperties.MessagePropertyName => new MessageTemplateOutputTokenRenderer(), + OutputProperties.TimestampPropertyName => new TimestampTokenRenderer(token, _formatProvider), + OutputProperties.PropertiesPropertyName => new PropertiesTokenRenderer(token, _template), + _ => new EventPropertyTokenRenderer(token, _formatProvider) + }; + if (_tokenStyles?.TryGetValue(token.PropertyName, out var style) ?? false) + { + return [new StyleTokenRenderer(style), renderer, StyleTokenRenderer.Reset]; + } + else + { + return [renderer]; + } + } + public object?[] Format(LogEvent logEvent) { ArgumentNullException.ThrowIfNull(logEvent); - var buffer = new List(_renderers.Length * 2); + var tokenEmitter = new TokenEmitter(); foreach (var renderer in _renderers) { - renderer.Render(logEvent, buffer.Add); + renderer.Render(logEvent, tokenEmitter); } - return buffer.ToArray(); + return tokenEmitter.YieldArgs(); } } \ No newline at end of file diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateTokenRenderer.cs index b0776fc..ede64cf 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateTokenRenderer.cs @@ -13,10 +13,53 @@ // limitations under the License. using Serilog.Events; +using System.Text; +using static System.Net.Mime.MediaTypeNames; namespace Serilog.Sinks.BrowserConsole.Output; -delegate void TokenEmitter(object? token); +class TokenEmitter +{ + private StringBuilder _template = new(); + private List _args = []; + + internal void Literal(string template) + { + _template.Append(template.Replace("%", "%%")); + } + + internal void Text(object @string) + { + _template.Append("%s"); + _args.Add(@string); + } + internal void Text(string @string) => Text((object)@string); + + internal void Object(object? @object) + { + _template.Append("%o"); + _args.Add(@object); + } + + internal void Integer(object @int) + { + _template.Append("%d"); + _args.Add(@int); + } + + internal void Float(object @float) { + _template.Append("%f"); + _args.Add(@float); + } + + internal object?[] YieldArgs() => [_template.ToString(), .. _args]; + + internal void Style(string styleContent) + { + _template.Append("%c"); + _args.Add(styleContent); + } +} abstract class OutputTemplateTokenRenderer { diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertiesTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertiesTokenRenderer.cs index ce29847..16f336b 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertiesTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertiesTokenRenderer.cs @@ -36,9 +36,48 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken) foreach (var property in included) { - emitToken(ObjectModelInterop.ToInteropValue(property.Value, _token.Format)); + new PropertyTokenRenderer(_token, property.Value).Render(logEvent, emitToken); } } + private void HandleProperty(LogEventProperty property, TokenEmitter emitToken) + { + if (property.Value is ScalarValue sv) + { + if(sv.Value is null) + { + emitToken.Object(ObjectModelInterop.ToInteropValue(property.Value, _token.Format)); + return; + } + switch (Type.GetTypeCode(sv.Value.GetType())) + { + // See https://stackoverflow.com/a/1750024 + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + emitToken.Integer(sv.Value); + break; + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Single: + emitToken.Float(sv.Value); + break; + case TypeCode.String: + case TypeCode.Char: + emitToken.Text(sv.Value); + break; + default: + emitToken.Object(ObjectModelInterop.ToInteropValue(property.Value, _token.Format)); + break; + } + } + else + emitToken.Object(ObjectModelInterop.ToInteropValue(property.Value, _token.Format)); + } static bool TemplateContainsPropertyName(MessageTemplate template, string propertyName) { diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertyTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertyTokenRenderer.cs new file mode 100644 index 0000000..0a97b10 --- /dev/null +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertyTokenRenderer.cs @@ -0,0 +1,72 @@ +// 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 System; +using System.Linq; +using Serilog.Events; +using Serilog.Parsing; + +namespace Serilog.Sinks.BrowserConsole.Output +{ + class PropertyTokenRenderer : OutputTemplateTokenRenderer + { + readonly PropertyToken _token; + readonly LogEventPropertyValue _propertyValue; + public PropertyTokenRenderer(PropertyToken token, LogEventPropertyValue propertyValue) + { + _token = token; + _propertyValue = propertyValue; + } + + public override void Render(LogEvent logEvent, TokenEmitter emitToken) + { + if (_propertyValue is ScalarValue sv) + { + if (sv.Value is null) + { + emitToken.Object(ObjectModelInterop.ToInteropValue(sv)); + return; + } + switch (Type.GetTypeCode(sv.Value.GetType())) + { + // See https://stackoverflow.com/a/1750024 + case TypeCode.Byte: + case TypeCode.SByte: + case TypeCode.UInt16: + case TypeCode.UInt32: + case TypeCode.UInt64: + case TypeCode.Int16: + case TypeCode.Int32: + case TypeCode.Int64: + emitToken.Integer(sv.Value); + break; + case TypeCode.Decimal: + case TypeCode.Double: + case TypeCode.Single: + emitToken.Float(sv.Value); + break; + case TypeCode.String: + case TypeCode.Char: + emitToken.Text(sv.Value); + break; + default: + emitToken.Object(ObjectModelInterop.ToInteropValue(sv)); + break; + } + } + else + emitToken.Object(ObjectModelInterop.ToInteropValue(_propertyValue)); + } + } +} diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/StyleTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/StyleTokenRenderer.cs new file mode 100644 index 0000000..1c3f22e --- /dev/null +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/StyleTokenRenderer.cs @@ -0,0 +1,34 @@ +// 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.Events; + +namespace Serilog.Sinks.BrowserConsole.Output; + +class StyleTokenRenderer : OutputTemplateTokenRenderer +{ + public static readonly StyleTokenRenderer Reset = new(""); + + private readonly string _style; + + public StyleTokenRenderer(string style) + { + _style = style; + } + + public override void Render(LogEvent logEvent, TokenEmitter emitToken) + { + emitToken.Style(_style); + } +} \ No newline at end of file diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/TextTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/TextTokenRenderer.cs index c3c009d..fd80058 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/TextTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/TextTokenRenderer.cs @@ -27,6 +27,31 @@ public TextTokenRenderer(string text) public override void Render(LogEvent logEvent, TokenEmitter emitToken) { - emitToken(_text); + var textIter = _text; + while (!string.IsNullOrEmpty(textIter)) + { + var openTagIndex = textIter.IndexOf("<<"); + if (openTagIndex == -1) // If no open tag, add full text & exit loop + { + emitToken.Literal(textIter); + return; + } + var displayText = textIter[..openTagIndex]; + emitToken.Literal(displayText); + + var closeTagIndex = textIter.IndexOf(">>", openTagIndex); + if (closeTagIndex == -1) + { + throw new FormatException("Open tag found without close tag"); + } + var styleContent = textIter[(openTagIndex + 2)..closeTagIndex]; + if (styleContent.Trim() == "_") + { + styleContent = ""; + } + emitToken.Style(styleContent); + + textIter = textIter[(closeTagIndex + 2)..]; + } } } \ No newline at end of file diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/TimestampTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/TimestampTokenRenderer.cs index 75c3aec..ffd4eb8 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/TimestampTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/TimestampTokenRenderer.cs @@ -41,8 +41,8 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken) var str = buffer.ToString(); if (_token.Alignment is not null) - emitToken(Padding.Apply(str, _token.Alignment)); + emitToken.Text(Padding.Apply(str, _token.Alignment)); else - emitToken(str); + emitToken.Text(str); } } \ No newline at end of file diff --git a/test/Serilog.Sinks.BrowserConsole.Tests/FormatterTests.cs b/test/Serilog.Sinks.BrowserConsole.Tests/FormatterTests.cs new file mode 100644 index 0000000..15eed87 --- /dev/null +++ b/test/Serilog.Sinks.BrowserConsole.Tests/FormatterTests.cs @@ -0,0 +1,162 @@ +using Serilog.Events; +using Serilog.Formatting.Display; +using Serilog.Parsing; +using Serilog.Sinks.BrowserConsole.Output; +using System; +using System.Collections.Generic; +using Xunit; + +namespace Serilog.Sinks.BrowserConsole.Tests +{ + public class FormatterTests + { + const string STYLE1 = "color: red;"; + const string STYLE2 = "color: blue;"; + const string STYLE3 = "color: purple;"; + [Fact] + public void SupportsStylingSimple() + { + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Hello<<_>> <<{STYLE2}>>World<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LogEventLevel.Verbose, null, MessageTemplate.Empty, Array.Empty())); + Assert.Equal(["%cHello%c %cWorld%c", STYLE1, "", STYLE2, ""], args); + } + + [Fact] + public void SupportsStylingWithTimestamp() + { + var date = DateTimeOffset.Now; + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Hello<<_>> <<{STYLE2}>>{{{OutputProperties.TimestampPropertyName}:HH:mm}}<<_>>", default); + var args = formatter.Format(new LogEvent(date, LogEventLevel.Verbose, null, MessageTemplate.Empty, Array.Empty())); + Assert.Equal([$"%cHello%c %c%s%c", STYLE1, "", STYLE2, date.ToString("HH:mm"), ""], args); + } + + [Fact] + public void SupportsStylingWithNewLine() + { + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Hello<<_>> <<{STYLE2}>>{{{OutputProperties.NewLinePropertyName}}}<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LogEventLevel.Verbose, null, MessageTemplate.Empty, Array.Empty())); + Assert.Equal([$"%cHello%c %c{Environment.NewLine}%c", STYLE1, "", STYLE2, ""], args); + } + + [Fact] + public void SupportsStylingWithLevel() + { + var LEVEL = LogEventLevel.Verbose; + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Hello<<_>> <<{STYLE2}>>{{{OutputProperties.LevelPropertyName}}}<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LEVEL, null, MessageTemplate.Empty, Array.Empty())); + Assert.Equal([$"%cHello%c %c%s%c", STYLE1, "", STYLE2, LEVEL.ToString(), ""], args); + } + + [Fact] + public void SupportsStylingWithException() + { + var EXCEPTION = new Exception("Foo did bar"); + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Hello<<_>> <<{STYLE2}>>{{{OutputProperties.ExceptionPropertyName}}}<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LogEventLevel.Verbose, EXCEPTION, MessageTemplate.Empty, Array.Empty())); + Assert.Equal([$"%cHello%c %c%s%c", STYLE1, "", STYLE2, EXCEPTION.ToString(), ""], args); + } + + [Fact] + public void SupportsStylingWithProperties() + { + var PROPERTIES = new[] { + new LogEventProperty("Foo", new ScalarValue(42)), + }; + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Hello<<_>> <<{STYLE2}>>{{{OutputProperties.PropertiesPropertyName}}}<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LogEventLevel.Verbose, null, MessageTemplate.Empty, PROPERTIES)); + Assert.Equal([$"%cHello%c %c%d%c", STYLE1, "", STYLE2, ((ScalarValue)PROPERTIES[0].Value).Value, ""], args); + } + + [Fact] + public void SupportsStylingWithSimpleMessage() + { + var MESSAGE = "and welcome"; + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Hello<<_>> <<{STYLE2}>>{{{OutputProperties.MessagePropertyName}}}<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LogEventLevel.Verbose, null, new MessageTemplateParser().Parse(MESSAGE), Array.Empty())); + Assert.Equal([$"%cHello%c %c{MESSAGE}%c", STYLE1, "", STYLE2, ""], args); + } + + [Fact] + public void SupportsStylingWithinSimpleMessageContainingStyle() + { + var MESSAGE = $"and <<{STYLE3}>>welcome"; + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Hello<<_>> <<{STYLE2}>>{{{OutputProperties.MessagePropertyName}}}<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LogEventLevel.Verbose, null, new MessageTemplateParser().Parse(MESSAGE), Array.Empty())); + Assert.Equal([$"%cHello%c %c{MESSAGE.Replace($"<<{STYLE3}>>", "%c")}%c", STYLE1, "", STYLE2, STYLE3, ""], args); + } + + [Theory] + [InlineData("short", "%d", (short)42)] + [InlineData("int", "%d", (int)42)] + [InlineData("long", "%d", (long)42)] + [InlineData("ushort", "%d", (ushort)42)] + [InlineData("uint", "%d", (uint)42)] + [InlineData("ulong", "%d", (ulong)42)] + [InlineData("byte", "%d", (byte)42)] + [InlineData("sbyte", "%d", (sbyte)42)] + // [InlineData("decimal", "%f", (decimal)42m)] // Unsupported + [InlineData("double", "%f", (double)42m)] + [InlineData("float", "%f", (float)42m)] + [InlineData("string", "%s", (string)"foo")] + [InlineData("char", "%s", (char)'f')] + public void SupportsStylingWithMessageContainingScalarStandardValues(string propertyName, string template, object value) + { + var MESSAGE = $"where the prop is {{{propertyName}}}"; + var PROPERTIES = new[] { + new LogEventProperty(propertyName, new ScalarValue(value)), + }; + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Test {{{OutputProperties.MessagePropertyName}}} End<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LogEventLevel.Verbose, null, new MessageTemplateParser().Parse(MESSAGE), PROPERTIES)); + Assert.Equal([$"%cTest where the prop is {template} End%c", STYLE1, ((ScalarValue)PROPERTIES[0].Value).Value, ""], args); + } + + [Fact] + public void SupportsStylingWithMessageContainingScalarDecimal() + { + var MESSAGE = $"where the prop is {{decimal}}"; + var PROPERTIES = new[] { + new LogEventProperty("decimal", new ScalarValue((decimal)42m)), + }; + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Test {{{OutputProperties.MessagePropertyName}}} End<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LogEventLevel.Verbose, null, new MessageTemplateParser().Parse(MESSAGE), PROPERTIES)); + Assert.Equal([$"%cTest where the prop is %f End%c", STYLE1, ((ScalarValue)PROPERTIES[0].Value).Value, ""], args); + } + + [Fact] + public void SupportsStylingWithMessageContainingScalarObject() + { + var MESSAGE = $"where the prop is {{object}}"; + var PROPERTIES = new[] { + new LogEventProperty("object", new ScalarValue(new { Hello = "world"})), + }; + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>Test {{{OutputProperties.MessagePropertyName}}} End<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LogEventLevel.Verbose, null, new MessageTemplateParser().Parse(MESSAGE), PROPERTIES)); + Assert.Equal([$"%cTest where the prop is %c%o%c End%c", STYLE1, "", ((ScalarValue)PROPERTIES[0].Value).Value, STYLE1, ""], args); + } + + [Fact] + public void EscapePercentFromMessages() + { + var MESSAGE = $"and another % sign"; + var formatter = new OutputTemplateRenderer($"<<{STYLE1}>>A first<<_>> % sign <<{STYLE2}>>{{{OutputProperties.MessagePropertyName}}}<<_>>", default); + var args = formatter.Format(new LogEvent(DateTimeOffset.Now, LogEventLevel.Verbose, null, new MessageTemplateParser().Parse(MESSAGE), Array.Empty())); + Assert.Equal([$"%cA first%c %% sign %c{MESSAGE.Replace($"%", "%%")}%c", STYLE1, "", STYLE2, ""], args); + } + + [Fact] + public void SupportsTokenStyling() + { + var MESSAGE = $"Test"; + var LEVEL = LogEventLevel.Verbose; + var NOW = DateTimeOffset.Now; + var formatter = new OutputTemplateRenderer($"{{{OutputProperties.LevelPropertyName}}}@{{{OutputProperties.TimestampPropertyName}:HH:mm}}: {{{OutputProperties.MessagePropertyName}}}", default, new Dictionary + { + {OutputProperties.LevelPropertyName, STYLE1 }, + {OutputProperties.TimestampPropertyName, STYLE2 }, + {OutputProperties.MessagePropertyName, STYLE3 } + }); + var args = formatter.Format(new LogEvent(NOW, LEVEL, null, new MessageTemplateParser().Parse(MESSAGE), Array.Empty())); + Assert.Equal([$"%c%s%c@%c%s%c: %c{MESSAGE}%c", STYLE1, LEVEL.ToString(), "", STYLE2, NOW.ToString("HH:mm"), "", STYLE3, ""], args); + } + } +}