diff --git a/README.md b/README.md
index ab5f3e8..70ed47d 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@ A Serilog sink that writes events to the [Seq](https://datalust.co/seq) structur
[](https://nuget.org/packages/serilog.sinks.seq)
> [!TIP]
-> If you would like to see timing and dependency information in Seq, [SerilogTracing](https://github.com/serilog-tracing/serilog-tracing) is a Serilog extension that supports both logs and traces.
+> If you would like to see timing and dependency information in Seq, [SerilogTracing](https://github.com/serilog-tracing/serilog-tracing) is a Serilog extension that can send both logs and traces through this sink.
### Getting started
diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/Formatting/CleanMessageTemplateFormatter.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/Formatting/CleanMessageTemplateFormatter.cs
deleted file mode 100644
index d082b35..0000000
--- a/src/Serilog.Sinks.Seq/Sinks/Seq/Formatting/CleanMessageTemplateFormatter.cs
+++ /dev/null
@@ -1,146 +0,0 @@
-// Copyright © 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.Collections.Generic;
-using System.IO;
-using Serilog.Events;
-using Serilog.Formatting.Json;
-using Serilog.Parsing;
-
-namespace Serilog.Sinks.Seq.Formatting;
-
-///
-/// Matches the `:lj` clean formatting style now employed by Serilog.Expressions, Serilog.Sinks.Console, and elsewhere.
-/// In this mode, strings embedded in message templates are unquoted, and structured data is rendered as JSON.
-///
-/// This implementation is derived from the Serilog.Expressions one, sans theming support, and avoiding the
-/// extra dependency. In time there should be core Serilog support for this.
-static class CleanMessageTemplateFormatter
-{
- static readonly JsonValueFormatter SharedJsonValueFormatter = new("$type");
-
- public static string Format(MessageTemplate messageTemplate, IReadOnlyDictionary properties, IFormatProvider? formatProvider)
- {
- var output = new StringWriter();
-
- foreach (var token in messageTemplate.Tokens)
- {
- switch (token)
- {
- case TextToken tt:
- {
- output.Write(tt.Text);
- break;
- }
- case PropertyToken pt:
- {
- RenderPropertyToken(properties, pt, output, formatProvider);
- break;
- }
- default:
- {
- output.Write(token);
- break;
- }
- }
- }
-
- return output.ToString();
- }
-
- static void RenderPropertyToken(IReadOnlyDictionary properties, PropertyToken pt, TextWriter output, IFormatProvider? formatProvider)
- {
- if (!properties.TryGetValue(pt.PropertyName, out var value))
- {
- output.Write(pt.ToString());
- return;
- }
-
- if (pt.Alignment is null)
- {
- RenderPropertyValueUnaligned(value, output, pt.Format, formatProvider);
- return;
- }
-
- var buffer = new StringWriter();
-
- RenderPropertyValueUnaligned(value, buffer, pt.Format, formatProvider);
-
- var result = buffer.ToString();
-
- if (result.Length >= pt.Alignment.Value.Width)
- output.Write(result);
- else
- Padding.Apply(output, result, pt.Alignment.Value);
- }
-
- static void RenderPropertyValueUnaligned(LogEventPropertyValue propertyValue, TextWriter output, string? format, IFormatProvider? formatProvider)
- {
- if (propertyValue is not ScalarValue scalar)
- {
- SharedJsonValueFormatter.Format(propertyValue, output);
- return;
- }
-
- var value = scalar.Value;
-
- if (value == null)
- {
- output.Write("null");
- return;
- }
-
- if (value is string str)
- {
- output.Write(str);
- return;
- }
-
- if (value is ValueType)
- {
- if (value is int or uint or long or ulong or decimal or byte or sbyte or short or ushort)
- {
- output.Write(((IFormattable)value).ToString(format, formatProvider));
- return;
- }
-
- if (value is double d)
- {
- output.Write(d.ToString(format, formatProvider));
- return;
- }
-
- if (value is float f)
- {
- output.Write(f.ToString(format, formatProvider));
- return;
- }
-
- if (value is bool b)
- {
- output.Write(b);
- return;
- }
- }
-
- if (value is IFormattable formattable)
- {
- output.Write(formattable.ToString(format, formatProvider));
- return;
- }
-
- output.Write(value);
- }
-}
\ No newline at end of file
diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/Formatting/Padding.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/Formatting/Padding.cs
deleted file mode 100644
index c0db3b1..0000000
--- a/src/Serilog.Sinks.Seq/Sinks/Seq/Formatting/Padding.cs
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright © 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.IO;
-using System.Linq;
-using Serilog.Parsing;
-
-namespace Serilog.Sinks.Seq.Formatting
-{
- static class Padding
- {
- static readonly char[] PaddingChars = Enumerable.Repeat(' ', 80).ToArray();
-
- ///
- /// Writes the provided value to the output, applying direction-based padding when is provided.
- ///
- public static void Apply(TextWriter output, string value, Alignment alignment)
- {
- if (value.Length >= alignment.Width)
- {
- output.Write(value);
- return;
- }
-
- var pad = alignment.Width - value.Length;
-
- if (alignment.Direction == AlignmentDirection.Left)
- output.Write(value);
-
- if (pad <= PaddingChars.Length)
- {
- output.Write(PaddingChars, 0, pad);
- }
- else
- {
- output.Write(new string(' ', pad));
- }
-
- if (alignment.Direction == AlignmentDirection.Right)
- output.Write(value);
- }
- }
-}
\ No newline at end of file
diff --git a/src/Serilog.Sinks.Seq/Sinks/Seq/SeqCompactJsonFormatter.cs b/src/Serilog.Sinks.Seq/Sinks/Seq/SeqCompactJsonFormatter.cs
index 86a8470..22b059f 100644
--- a/src/Serilog.Sinks.Seq/Sinks/Seq/SeqCompactJsonFormatter.cs
+++ b/src/Serilog.Sinks.Seq/Sinks/Seq/SeqCompactJsonFormatter.cs
@@ -22,8 +22,6 @@
using Serilog.Formatting.Json;
using Serilog.Parsing;
using Serilog.Sinks.Seq.Conventions;
-using Serilog.Sinks.Seq.Formatting;
-
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable PossibleMultipleEnumeration
@@ -36,43 +34,47 @@ namespace Serilog.Sinks.Seq;
/// implicit SerilogTracing span support.
public class SeqCompactJsonFormatter: ITextFormatter
{
- static readonly IDottedPropertyNameConvention DottedPropertyNameConvention =
- AppContext.TryGetSwitch("Serilog.Parsing.MessageTemplateParser.AcceptDottedPropertyNames", out var accept) && accept ?
- new UnflattenDottedPropertyNames() :
- new PreserveDottedPropertyNames();
-
- readonly JsonValueFormatter _valueFormatter = new("$type");
+ readonly IDottedPropertyNameConvention _dottedPropertyNameConvention;
+ readonly JsonValueFormatter _valueFormatter;
readonly IFormatProvider _formatProvider;
-
+
+ ///
+ /// Construct a .
+ ///
+ /// A value formatter for s on the event.
/// An that will be used to render log event tokens.
- public SeqCompactJsonFormatter(IFormatProvider? formatProvider = null)
+ public SeqCompactJsonFormatter(IFormatProvider? formatProvider = null, JsonValueFormatter? valueFormatter = null)
{
+ var acceptDottedPropertyNames = AppContext.TryGetSwitch("Serilog.Parsing.MessageTemplateParser.AcceptDottedPropertyNames", out var accept) && accept;
+
+ _dottedPropertyNameConvention = acceptDottedPropertyNames ?
+ new UnflattenDottedPropertyNames() :
+ new PreserveDottedPropertyNames();
+
_formatProvider = formatProvider ?? CultureInfo.InvariantCulture;
+ _valueFormatter = valueFormatter ?? new("$type");
}
///
- /// Format the log event into the output. Subsequent events will be newline-delimited.
+ /// Format the log event into the output. Successive events will be newline-delimited.
///
/// The event to format.
/// The output.
public void Format(LogEvent logEvent, TextWriter output)
{
- FormatEvent(logEvent, output, _valueFormatter, _formatProvider);
+ FormatEvent(logEvent, output);
output.WriteLine();
}
///
- /// Format the log event into the output.
+ /// Format the log event into the output, without newline delimiters.
///
/// The event to format.
/// The output.
- /// A value formatter for s on the event.
- /// An that will be used to render log event tokens.
- public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFormatter valueFormatter, IFormatProvider formatProvider)
+ public void FormatEvent(LogEvent logEvent, TextWriter output)
{
if (logEvent == null) throw new ArgumentNullException(nameof(logEvent));
if (output == null) throw new ArgumentNullException(nameof(output));
- if (valueFormatter == null) throw new ArgumentNullException(nameof(valueFormatter));
output.Write("{\"@t\":\"");
output.Write(logEvent.Timestamp.UtcDateTime.ToString("O"));
@@ -80,16 +82,6 @@ public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFo
output.Write("\",\"@mt\":");
JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output);
- if (!formatProvider.Equals(CultureInfo.InvariantCulture))
- {
- // `@m` is normally created during ingestion, however, it must be sent from the client
- // to honour non-default IFormatProviders
- output.Write(",\"@m\":");
- JsonValueFormatter.WriteQuotedJsonString(
- CleanMessageTemplateFormatter.Format(logEvent.MessageTemplate, logEvent.Properties, formatProvider),
- output);
- }
-
var tokensWithFormat = logEvent.MessageTemplate.Tokens
.OfType()
.Where(pt => pt.Format != null);
@@ -105,9 +97,10 @@ public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFo
output.Write(delim);
delim = ",";
var space = new StringWriter();
- r.Render(logEvent.Properties, space, formatProvider);
+ r.Render(logEvent.Properties, space, _formatProvider);
JsonValueFormatter.WriteQuotedJsonString(space.ToString(), output);
}
+
output.Write(']');
}
@@ -167,7 +160,7 @@ public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFo
}
}
- var properties = DottedPropertyNameConvention.ProcessDottedPropertyNames(logEvent.Properties);
+ var properties = _dottedPropertyNameConvention.ProcessDottedPropertyNames(logEvent.Properties);
foreach (var property in properties)
{
var name = property.Key;
@@ -184,7 +177,7 @@ public static void FormatEvent(LogEvent logEvent, TextWriter output, JsonValueFo
output.Write(',');
JsonValueFormatter.WriteQuotedJsonString(name, output);
output.Write(':');
- valueFormatter.Format(property.Value, output);
+ _valueFormatter.Format(property.Value, output);
}
output.Write('}');
diff --git a/test/Serilog.Sinks.Seq.Tests/SeqCompactJsonFormatterTests.cs b/test/Serilog.Sinks.Seq.Tests/SeqCompactJsonFormatterTests.cs
index 71a5c2c..8ef87e6 100644
--- a/test/Serilog.Sinks.Seq.Tests/SeqCompactJsonFormatterTests.cs
+++ b/test/Serilog.Sinks.Seq.Tests/SeqCompactJsonFormatterTests.cs
@@ -5,25 +5,24 @@
using System.Diagnostics;
using System.Globalization;
using System.IO;
-using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Serilog.Events;
-using Serilog.Parsing;
using Xunit;
// ReSharper disable AccessToDisposedClosure
+// ReSharper disable ParameterOnlyUsedForPreconditionCheck.Local
namespace Serilog.Sinks.Seq.Tests;
public class SeqCompactJsonFormatterTests
{
- JObject AssertValidJson(Action act, IFormatProvider? formatProvider = null, Action? assert = null)
+ static JObject AssertValidJson(Action act, IFormatProvider? formatProvider = null, Action? assert = null)
{
var sw = new StringWriter();
var logger = new LoggerConfiguration()
.Destructure.AsScalar()
.Destructure.AsScalar()
- .WriteTo.TextWriter(new SeqCompactJsonFormatter(formatProvider ?? CultureInfo.InvariantCulture), sw)
+ .WriteTo.TextWriter(new SeqCompactJsonFormatter(formatProvider), sw)
.CreateLogger();
act(logger);
logger.Dispose();
@@ -41,35 +40,27 @@ JObject AssertValidJson(Action act, IFormatProvider? formatProvider = n
}
[Theory]
- [InlineData("fr-FR", "31\u202f415,927 19/07/2024 10:00:59 12\u202f345,67 €", "31\u202f415,927", "12\u202f345,67 €")]
- [InlineData("en-US", "31,415.927 7/19/2024 10:00:59 AM $12,345.67", "31,415.927", "$12,345.67")]
- public void PropertiesFormatCorrectlyForTheFormatProvider(
- string cultureName,
- string expectedMessage,
- string renderedNumber,
- string renderedCurrency)
+ [InlineData("fr-FR")]
+ [InlineData("en-US")]
+ public void PropertiesFormatCorrectlyForTheFormatProvider(string cultureName)
{
- var number = Math.PI * 10000;
- var date = new DateTime(2024, 7, 19, 10, 00, 59);
- var currency = 12345.67M;
+ var cultureInfo = new CultureInfo(cultureName);
- AssertValidJson(log => log.Information("{a:n} {b} {c:C}", number, date, currency), new CultureInfo(cultureName), evt =>
- {
- Assert.Equal(expectedMessage, evt["@m"]!.ToString());
- Assert.Equal(renderedNumber, evt["@r"]![0]!.ToString());
- Assert.Equal(renderedCurrency, evt["@r"]![1]!.ToString());
- });
- }
+ const double number = Math.PI * 10000;
+ var date = new DateTime(2024, 7, 19, 10, 00, 59);
+ const decimal currency = 12345.67M;
- [Fact]
- public void MessageNotRenderedForDefaultFormatProvider()
- {
- AssertValidJson(log => log.Information("{a}", 1.234), null, evt =>
+ // Culture-specific formatting differs by .NET version platform.
+ var expectedNumber = number.ToString("n", cultureInfo);
+ var expectedCurrency = currency.ToString("C", cultureInfo);
+
+ AssertValidJson(log => log.Information("{a:n} {b} {c:C}", number, date, currency), cultureInfo, evt =>
{
- Assert.Null(evt["@m"]);
+ Assert.Equal(expectedNumber, evt["@r"]![0]!.Value());
+ Assert.Equal(expectedCurrency, evt["@r"]![1]!.Value());
});
}
-
+
[Fact]
public void AnEmptyEventIsValidJson()
{
@@ -138,9 +129,7 @@ public void TraceAndSpanIdsGenerateValidJson()
{
var traceId = ActivityTraceId.CreateRandom();
var spanId = ActivitySpanId.CreateRandom();
- var evt = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null,
- new MessageTemplate(Enumerable.Empty()), Enumerable.Empty(),
- traceId, spanId);
+ var evt = new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, MessageTemplate.Empty, [], traceId, spanId);
var json = AssertValidJson(log => log.Write(evt));
Assert.Equal(traceId.ToHexString(), json["@tr"]);
Assert.Equal(spanId.ToHexString(), json["@sp"]);