From f5aa6e6e626f9bc0db89cef315b4d6a1a0016c7a Mon Sep 17 00:00:00 2001 From: Arad Alvand Date: Mon, 18 Aug 2025 12:25:38 -0400 Subject: [PATCH 1/5] Add support for `ICustomFormatter` --- .../Formatting/CleanMessageTemplateFormatter.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs index 2efba91..006e7ae 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs @@ -95,6 +95,12 @@ static void RenderPropertyValueUnaligned(LogEventPropertyValue propertyValue, Te var value = scalar.Value; + if (formatProvider?.GetFormat(typeof(ICustomFormatter)) is ICustomFormatter customFormatter) + { + output.Write(customFormatter.Format(format, value, formatProvider)); + return; + } + if (value == null) { output.Write("null"); From 70a67deb6d0f043ba876babe3f09986d6861a218 Mon Sep 17 00:00:00 2001 From: Arad Alvand Date: Mon, 18 Aug 2025 12:33:12 -0400 Subject: [PATCH 2/5] Simplify `RenderPropertyValueUnaligned` by delegating to `string.Format` --- .../CleanMessageTemplateFormatter.cs | 47 +------------------ 1 file changed, 1 insertion(+), 46 deletions(-) diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs index 006e7ae..ac8f3f8 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs @@ -95,57 +95,12 @@ static void RenderPropertyValueUnaligned(LogEventPropertyValue propertyValue, Te var value = scalar.Value; - if (formatProvider?.GetFormat(typeof(ICustomFormatter)) is ICustomFormatter customFormatter) - { - output.Write(customFormatter.Format(format, value, formatProvider)); - return; - } - 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); + output.Write(string.Format(formatProvider, "{0}", value)); } } From b06352701e25b833727ca6604cf2ca09ab94a256 Mon Sep 17 00:00:00 2001 From: Arad Alvand Date: Mon, 18 Aug 2025 12:50:02 -0400 Subject: [PATCH 3/5] Further simplify `RenderPropertyValueUnaligned` by delegating to `ScalarValue.Render` --- .../Formatting/CleanMessageTemplateFormatter.cs | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs index ac8f3f8..efda2ff 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs @@ -93,14 +93,6 @@ static void RenderPropertyValueUnaligned(LogEventPropertyValue propertyValue, Te return; } - var value = scalar.Value; - - if (value == null) - { - output.Write("null"); - return; - } - - output.Write(string.Format(formatProvider, "{0}", value)); + scalar.Render(output, format, formatProvider); } } From 50b6501581dde6a5701dc0e80e88cc0a8bbd5a1d Mon Sep 17 00:00:00 2001 From: Arad Alvand Date: Tue, 19 Aug 2025 15:01:40 -0400 Subject: [PATCH 4/5] Special-case strings to make them unquoted and add tests for custom formatter --- .../CleanMessageTemplateFormatter.cs | 3 + .../CleanMessageTemplateFormatterTests.cs | 96 ++++++++++++++++++- .../OpenTelemetryLogsSinkTests.cs | 24 ++++- 3 files changed, 114 insertions(+), 9 deletions(-) diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs index efda2ff..020f91a 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs @@ -93,6 +93,9 @@ static void RenderPropertyValueUnaligned(LogEventPropertyValue propertyValue, Te return; } + if (scalar.Value is string && format is null or "") + format = "l"; // NOTE: Uses the literal format to yield unquoted strings + scalar.Render(output, format, formatProvider); } } diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/CleanMessageTemplateFormatterTests.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/CleanMessageTemplateFormatterTests.cs index 8c5840f..0f41ffd 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/CleanMessageTemplateFormatterTests.cs +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/CleanMessageTemplateFormatterTests.cs @@ -17,7 +17,7 @@ public void FormatsEmbeddedStringsWithoutQuoting() }; var actual = CleanMessageTemplateFormatter.Format(template, properties, null); - + // The default formatter would produce "Hello, \"world\"!" here. Assert.Equal("Hello, world!", actual); } @@ -28,17 +28,103 @@ public void FormatsEmbeddedStructuresAsJson() var template = new MessageTemplateParser().Parse("Received {Payload}"); var properties = new Dictionary { - ["Payload"] = new StructureValue(new [] - { + ["Payload"] = new StructureValue( + [ // Particulars of the JSON structure are unimportant, this is handed of to Serilog's default // JSON value formatter. new LogEventProperty("a", new ScalarValue(42)) - }) + ]) }; var actual = CleanMessageTemplateFormatter.Format(template, properties, null); - + // The default formatter would produce "Received {a = 42}" here. Assert.Equal("Received {\"a\":42}", actual); } + + [Fact] + public void CustomFormatterWorksForScalarValues() + { + var template = new MessageTemplateParser().Parse("Event occurred at: {Timestamp}"); + var timestamp = new DateTime(2024, 1, 15, 14, 30, 45); + var properties = new Dictionary + { + ["Timestamp"] = new ScalarValue(timestamp) + }; + + var formatProvider = new CustomDateTimeFormatProvider(); + var actual = CleanMessageTemplateFormatter.Format(template, properties, formatProvider); + + // The CustomDateTimeFormatProvider should be invoked to format the DateTime object + Assert.Equal("Event occurred at: CUSTOM(2024-01-15 14:30:45)", actual); + } + + [Fact] + public void CustomFormatterWorksWithMultipleScalarValues() + { + var template = new MessageTemplateParser().Parse("Events: {Start} and {End}"); + var start = new DateTime(2024, 1, 15, 9, 0, 0); + var end = new DateTime(2024, 1, 15, 17, 0, 0); + var properties = new Dictionary + { + ["Start"] = new ScalarValue(start), + ["End"] = new ScalarValue(end) + }; + + var formatProvider = new CustomDateTimeFormatProvider(); + var actual = CleanMessageTemplateFormatter.Format(template, properties, formatProvider); + + Assert.Equal($"Events: CUSTOM(2024-01-15 09:00:00) and CUSTOM(2024-01-15 17:00:00)", actual); + } + + [Fact] + public void CustomFormatterWorksWithMixedTypes() + { + var template = new MessageTemplateParser().Parse("Event at {Timestamp} with {Count} items"); + var timestamp = new DateTime(2024, 1, 15, 14, 30, 45); + var properties = new Dictionary + { + ["Timestamp"] = new ScalarValue(timestamp), + ["Count"] = new ScalarValue(42) + }; + + var formatProvider = new CustomDateTimeFormatProvider(); + var actual = CleanMessageTemplateFormatter.Format(template, properties, formatProvider); + + // NOTE: DateTime should be custom formatted, Count should use default formatting + Assert.Equal("Event at CUSTOM(2024-01-15 14:30:45) with 42 items", actual); + } + + [Fact] + public void CustomFormatterWorksWithAlignment() + { + var template = new MessageTemplateParser().Parse("Event: {Timestamp,30}"); + var timestamp = new DateTime(2024, 1, 15, 14, 30, 45); + var properties = new Dictionary + { + ["Timestamp"] = new ScalarValue(timestamp) + }; + + var formatProvider = new CustomDateTimeFormatProvider(); + var actual = CleanMessageTemplateFormatter.Format(template, properties, formatProvider); + + var expected = $"Event: CUSTOM(2024-01-15 14:30:45)"; + Assert.Equal(expected, actual); + } +} + +class CustomDateTimeFormatProvider : IFormatProvider, ICustomFormatter +{ + public const string DateTimeFormatting = "CUSTOM-DATE-TIME-FORMATTING"; + public object? GetFormat(Type? formatType) => + formatType == typeof(ICustomFormatter) + ? this + : null; + + public string Format(string? format, object? arg, IFormatProvider? formatProvider) => arg switch + { + DateTime dateTime => $"CUSTOM({dateTime:yyyy-MM-dd HH:mm:ss})", + IFormattable formattable => formattable.ToString(format, formatProvider), + _ => arg?.ToString() ?? "" + }; } diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryLogsSinkTests.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryLogsSinkTests.cs index ed772ef..5c8fad3 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryLogsSinkTests.cs +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryLogsSinkTests.cs @@ -16,7 +16,7 @@ public async Task DefaultScopeIsNull() var scopeLogs = Assert.Single(resourceLogs.ScopeLogs); Assert.Null(scopeLogs.Scope); } - + [Fact] public async Task SourceContextNameIsInstrumentationScope() { @@ -27,7 +27,7 @@ public async Task SourceContextNameIsInstrumentationScope() var scopeLogs = Assert.Single(resourceLogs.ScopeLogs); Assert.Equal(contextType.FullName, scopeLogs.Scope.Name); } - + [Fact] public async Task ScopeLogsAreGrouped() { @@ -47,10 +47,26 @@ public async Task ScopeLogsAreGrouped() Assert.Single(resourceLogs.ScopeLogs.Single(r => r.Scope == null).LogRecords); } - static async Task ExportAsync(IReadOnlyCollection events) + [Fact] + public async Task CustomFormatterSupportedEndToEnd() + { + var timestamp = new DateTime(2024, 6, 15, 10, 30, 0); + + var events = CollectingSink.Collect(log => + log.Information("Event occurred at: {Timestamp}", timestamp)); + + var request = await ExportAsync(events, new CustomDateTimeFormatProvider()); + var resourceLogs = Assert.Single(request.ResourceLogs); + var scopeLogs = Assert.Single(resourceLogs.ScopeLogs); + var logRecord = Assert.Single(scopeLogs.LogRecords); + + Assert.Equal($"Event occurred at: CUSTOM(2024-06-15 10:30:00)", logRecord.Body.StringValue); + } + + static async Task ExportAsync(IReadOnlyCollection events, IFormatProvider? formatProvider = null) { var exporter = new CollectingExporter(); - var sink = new OpenTelemetryLogsSink(exporter, null, new Dictionary(), OpenTelemetrySinkOptions.DefaultIncludedData); + var sink = new OpenTelemetryLogsSink(exporter, formatProvider, new Dictionary(), OpenTelemetrySinkOptions.DefaultIncludedData); await sink.EmitBatchAsync(events); return Assert.Single(exporter.ExportLogsServiceRequests); } From fcd988eae458f7cb7dca68ba5acb612e385e97f5 Mon Sep 17 00:00:00 2001 From: Arad Alvand Date: Wed, 27 Aug 2025 19:16:12 -0400 Subject: [PATCH 5/5] Don't delegate to `ScalarValue.Render` and embed `ICustomFormatter` support instead --- .../CleanMessageTemplateFormatter.cs | 23 ++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs index 020f91a..1e9df4c 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs @@ -93,9 +93,26 @@ static void RenderPropertyValueUnaligned(LogEventPropertyValue propertyValue, Te return; } - if (scalar.Value is string && format is null or "") - format = "l"; // NOTE: Uses the literal format to yield unquoted strings + var value = scalar.Value; - scalar.Render(output, format, formatProvider); + if (value == null) + { + output.Write("null"); + return; + } + + if (value is string str) + { + output.Write(str); + return; + } + + if (formatProvider?.GetFormat(typeof(ICustomFormatter)) is ICustomFormatter customFormatter) + { + output.Write(customFormatter.Format(format, value, formatProvider)); + return; + } + + output.Write(value); } }