diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/MessageTemplateOutputTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/MessageTemplateOutputTokenRenderer.cs index 06dda5a..f7849a5 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/MessageTemplateOutputTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/MessageTemplateOutputTokenRenderer.cs @@ -30,7 +30,9 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken) break; case PropertyToken pt: if (logEvent.Properties.TryGetValue(pt.PropertyName, out var propertyValue)) - emitToken.Object(ObjectModelInterop.ToInteropValue(propertyValue)); + { + new PropertyTokenRenderer(pt, propertyValue).Render(logEvent, emitToken); + } break; default: throw new InvalidOperationException(); diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateTokenRenderer.cs index b50010c..ede64cf 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/OutputTemplateTokenRenderer.cs @@ -14,6 +14,7 @@ using Serilog.Events; using System.Text; +using static System.Net.Mime.MediaTypeNames; namespace Serilog.Sinks.BrowserConsole.Output; @@ -22,21 +23,35 @@ class TokenEmitter private StringBuilder _template = new(); private List _args = []; - internal void Literal(string text) + internal void Literal(string template) { - _template.Append(text.Replace("%", "%%")); + _template.Append(template.Replace("%", "%%")); } - internal void Text(string text) { + internal void Text(object @string) + { _template.Append("%s"); - _args.Add(text); + _args.Add(@string); } + internal void Text(string @string) => Text((object)@string); - internal void Object(object? value) + internal void Object(object? @object) { _template.Append("%o"); - _args.Add(value); + _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) diff --git a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertiesTokenRenderer.cs b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertiesTokenRenderer.cs index 9701156..16f336b 100644 --- a/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertiesTokenRenderer.cs +++ b/src/Serilog.Sinks.BrowserConsole/Sinks/BrowserConsole/Output/PropertiesTokenRenderer.cs @@ -36,8 +36,47 @@ public override void Render(LogEvent logEvent, TokenEmitter emitToken) foreach (var property in included) { - emitToken.Object(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/test/Serilog.Sinks.BrowserConsole.Tests/FormatterTests.cs b/test/Serilog.Sinks.BrowserConsole.Tests/FormatterTests.cs index 23a1f4d..15eed87 100644 --- a/test/Serilog.Sinks.BrowserConsole.Tests/FormatterTests.cs +++ b/test/Serilog.Sinks.BrowserConsole.Tests/FormatterTests.cs @@ -64,7 +64,7 @@ public void SupportsStylingWithProperties() }; 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%o%c", STYLE1, "", STYLE2, ((ScalarValue)PROPERTIES[0].Value).Value, ""], args); + Assert.Equal([$"%cHello%c %c%d%c", STYLE1, "", STYLE2, ((ScalarValue)PROPERTIES[0].Value).Value, ""], args); } [Fact] @@ -77,24 +77,61 @@ public void SupportsStylingWithSimpleMessage() } [Fact] - public void SupportsStylingWithMessageContainingInterpolation() + public void SupportsStylingWithinSimpleMessageContainingStyle() { - var PLACE = "my tests"; - var MESSAGE = "and welcome to {Place}"; + 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), new[]{ - new LogEventProperty("Place", new ScalarValue(PLACE)), - })); - Assert.Equal([$"%cHello%c %c{MESSAGE.Replace("{Place}", "%o")}%c", STYLE1, "", STYLE2, PLACE, ""], args); + 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 SupportsStylingWithinSimpleMessage() + public void SupportsStylingWithMessageContainingScalarDecimal() { - 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); + 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]