Skip to content

Commit 50b6501

Browse files
committed
Special-case strings to make them unquoted and add tests for custom formatter
1 parent b063527 commit 50b6501

File tree

3 files changed

+114
-9
lines changed

3 files changed

+114
-9
lines changed

src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Formatting/CleanMessageTemplateFormatter.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,9 @@ static void RenderPropertyValueUnaligned(LogEventPropertyValue propertyValue, Te
9393
return;
9494
}
9595

96+
if (scalar.Value is string && format is null or "")
97+
format = "l"; // NOTE: Uses the literal format to yield unquoted strings
98+
9699
scalar.Render(output, format, formatProvider);
97100
}
98101
}

test/Serilog.Sinks.OpenTelemetry.Tests/CleanMessageTemplateFormatterTests.cs

Lines changed: 91 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ public void FormatsEmbeddedStringsWithoutQuoting()
1717
};
1818

1919
var actual = CleanMessageTemplateFormatter.Format(template, properties, null);
20-
20+
2121
// The default formatter would produce "Hello, \"world\"!" here.
2222
Assert.Equal("Hello, world!", actual);
2323
}
@@ -28,17 +28,103 @@ public void FormatsEmbeddedStructuresAsJson()
2828
var template = new MessageTemplateParser().Parse("Received {Payload}");
2929
var properties = new Dictionary<string, LogEventPropertyValue>
3030
{
31-
["Payload"] = new StructureValue(new []
32-
{
31+
["Payload"] = new StructureValue(
32+
[
3333
// Particulars of the JSON structure are unimportant, this is handed of to Serilog's default
3434
// JSON value formatter.
3535
new LogEventProperty("a", new ScalarValue(42))
36-
})
36+
])
3737
};
3838

3939
var actual = CleanMessageTemplateFormatter.Format(template, properties, null);
40-
40+
4141
// The default formatter would produce "Received {a = 42}" here.
4242
Assert.Equal("Received {\"a\":42}", actual);
4343
}
44+
45+
[Fact]
46+
public void CustomFormatterWorksForScalarValues()
47+
{
48+
var template = new MessageTemplateParser().Parse("Event occurred at: {Timestamp}");
49+
var timestamp = new DateTime(2024, 1, 15, 14, 30, 45);
50+
var properties = new Dictionary<string, LogEventPropertyValue>
51+
{
52+
["Timestamp"] = new ScalarValue(timestamp)
53+
};
54+
55+
var formatProvider = new CustomDateTimeFormatProvider();
56+
var actual = CleanMessageTemplateFormatter.Format(template, properties, formatProvider);
57+
58+
// The CustomDateTimeFormatProvider should be invoked to format the DateTime object
59+
Assert.Equal("Event occurred at: CUSTOM(2024-01-15 14:30:45)", actual);
60+
}
61+
62+
[Fact]
63+
public void CustomFormatterWorksWithMultipleScalarValues()
64+
{
65+
var template = new MessageTemplateParser().Parse("Events: {Start} and {End}");
66+
var start = new DateTime(2024, 1, 15, 9, 0, 0);
67+
var end = new DateTime(2024, 1, 15, 17, 0, 0);
68+
var properties = new Dictionary<string, LogEventPropertyValue>
69+
{
70+
["Start"] = new ScalarValue(start),
71+
["End"] = new ScalarValue(end)
72+
};
73+
74+
var formatProvider = new CustomDateTimeFormatProvider();
75+
var actual = CleanMessageTemplateFormatter.Format(template, properties, formatProvider);
76+
77+
Assert.Equal($"Events: CUSTOM(2024-01-15 09:00:00) and CUSTOM(2024-01-15 17:00:00)", actual);
78+
}
79+
80+
[Fact]
81+
public void CustomFormatterWorksWithMixedTypes()
82+
{
83+
var template = new MessageTemplateParser().Parse("Event at {Timestamp} with {Count} items");
84+
var timestamp = new DateTime(2024, 1, 15, 14, 30, 45);
85+
var properties = new Dictionary<string, LogEventPropertyValue>
86+
{
87+
["Timestamp"] = new ScalarValue(timestamp),
88+
["Count"] = new ScalarValue(42)
89+
};
90+
91+
var formatProvider = new CustomDateTimeFormatProvider();
92+
var actual = CleanMessageTemplateFormatter.Format(template, properties, formatProvider);
93+
94+
// NOTE: DateTime should be custom formatted, Count should use default formatting
95+
Assert.Equal("Event at CUSTOM(2024-01-15 14:30:45) with 42 items", actual);
96+
}
97+
98+
[Fact]
99+
public void CustomFormatterWorksWithAlignment()
100+
{
101+
var template = new MessageTemplateParser().Parse("Event: {Timestamp,30}");
102+
var timestamp = new DateTime(2024, 1, 15, 14, 30, 45);
103+
var properties = new Dictionary<string, LogEventPropertyValue>
104+
{
105+
["Timestamp"] = new ScalarValue(timestamp)
106+
};
107+
108+
var formatProvider = new CustomDateTimeFormatProvider();
109+
var actual = CleanMessageTemplateFormatter.Format(template, properties, formatProvider);
110+
111+
var expected = $"Event: CUSTOM(2024-01-15 14:30:45)";
112+
Assert.Equal(expected, actual);
113+
}
114+
}
115+
116+
class CustomDateTimeFormatProvider : IFormatProvider, ICustomFormatter
117+
{
118+
public const string DateTimeFormatting = "CUSTOM-DATE-TIME-FORMATTING";
119+
public object? GetFormat(Type? formatType) =>
120+
formatType == typeof(ICustomFormatter)
121+
? this
122+
: null;
123+
124+
public string Format(string? format, object? arg, IFormatProvider? formatProvider) => arg switch
125+
{
126+
DateTime dateTime => $"CUSTOM({dateTime:yyyy-MM-dd HH:mm:ss})",
127+
IFormattable formattable => formattable.ToString(format, formatProvider),
128+
_ => arg?.ToString() ?? ""
129+
};
44130
}

test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryLogsSinkTests.cs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ public async Task DefaultScopeIsNull()
1616
var scopeLogs = Assert.Single(resourceLogs.ScopeLogs);
1717
Assert.Null(scopeLogs.Scope);
1818
}
19-
19+
2020
[Fact]
2121
public async Task SourceContextNameIsInstrumentationScope()
2222
{
@@ -27,7 +27,7 @@ public async Task SourceContextNameIsInstrumentationScope()
2727
var scopeLogs = Assert.Single(resourceLogs.ScopeLogs);
2828
Assert.Equal(contextType.FullName, scopeLogs.Scope.Name);
2929
}
30-
30+
3131
[Fact]
3232
public async Task ScopeLogsAreGrouped()
3333
{
@@ -47,10 +47,26 @@ public async Task ScopeLogsAreGrouped()
4747
Assert.Single(resourceLogs.ScopeLogs.Single(r => r.Scope == null).LogRecords);
4848
}
4949

50-
static async Task<ExportLogsServiceRequest> ExportAsync(IReadOnlyCollection<LogEvent> events)
50+
[Fact]
51+
public async Task CustomFormatterSupportedEndToEnd()
52+
{
53+
var timestamp = new DateTime(2024, 6, 15, 10, 30, 0);
54+
55+
var events = CollectingSink.Collect(log =>
56+
log.Information("Event occurred at: {Timestamp}", timestamp));
57+
58+
var request = await ExportAsync(events, new CustomDateTimeFormatProvider());
59+
var resourceLogs = Assert.Single(request.ResourceLogs);
60+
var scopeLogs = Assert.Single(resourceLogs.ScopeLogs);
61+
var logRecord = Assert.Single(scopeLogs.LogRecords);
62+
63+
Assert.Equal($"Event occurred at: CUSTOM(2024-06-15 10:30:00)", logRecord.Body.StringValue);
64+
}
65+
66+
static async Task<ExportLogsServiceRequest> ExportAsync(IReadOnlyCollection<LogEvent> events, IFormatProvider? formatProvider = null)
5167
{
5268
var exporter = new CollectingExporter();
53-
var sink = new OpenTelemetryLogsSink(exporter, null, new Dictionary<string, object>(), OpenTelemetrySinkOptions.DefaultIncludedData);
69+
var sink = new OpenTelemetryLogsSink(exporter, formatProvider, new Dictionary<string, object>(), OpenTelemetrySinkOptions.DefaultIncludedData);
5470
await sink.EmitBatchAsync(events);
5571
return Assert.Single(exporter.ExportLogsServiceRequests);
5672
}

0 commit comments

Comments
 (0)