Skip to content

Commit dae7bd4

Browse files
authored
Merge pull request #165 from ManuelRin/support-UtcTimestamp-token
Support UtcTimestamp token in output template (new since Serilog 4.0)
2 parents 828e6d5 + 8b58251 commit dae7bd4

File tree

3 files changed

+99
-41
lines changed

3 files changed

+99
-41
lines changed

src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/OutputTemplateRenderer.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,11 @@ public OutputTemplateRenderer(ConsoleTheme theme, string outputTemplate, IFormat
6868
}
6969
else if (pt.PropertyName == OutputProperties.TimestampPropertyName)
7070
{
71-
renderers.Add(new TimestampTokenRenderer(theme, pt, formatProvider));
71+
renderers.Add(new TimestampTokenRenderer(theme, pt, formatProvider, convertToUtc: false));
72+
}
73+
else if (pt.PropertyName == OutputProperties.UtcTimestampPropertyName)
74+
{
75+
renderers.Add(new TimestampTokenRenderer(theme, pt, formatProvider, convertToUtc: true));
7276
}
7377
else if (pt.PropertyName == OutputProperties.PropertiesPropertyName)
7478
{

src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TimestampTokenRenderer.cs

Lines changed: 62 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -26,63 +26,85 @@ class TimestampTokenRenderer : OutputTemplateTokenRenderer
2626
{
2727
readonly ConsoleTheme _theme;
2828
readonly PropertyToken _token;
29+
readonly string? _format;
2930
readonly IFormatProvider? _formatProvider;
31+
readonly bool _convertToUtc;
3032

31-
public TimestampTokenRenderer(ConsoleTheme theme, PropertyToken token, IFormatProvider? formatProvider)
33+
public TimestampTokenRenderer(ConsoleTheme theme, PropertyToken token, IFormatProvider? formatProvider, bool convertToUtc)
3234
{
33-
_theme = theme;
34-
_token = token;
35-
_formatProvider = formatProvider;
36-
}
35+
_theme = theme;
36+
_token = token;
37+
_format = token.Format;
38+
_formatProvider = formatProvider;
39+
_convertToUtc = convertToUtc;
40+
}
3741

3842
public override void Render(LogEvent logEvent, TextWriter output)
3943
{
40-
var sv = new DateTimeOffsetValue(logEvent.Timestamp);
41-
42-
var _ = 0;
43-
using (_theme.Apply(output, ConsoleThemeStyle.SecondaryText, ref _))
44+
var _ = 0;
45+
using (_theme.Apply(output, ConsoleThemeStyle.SecondaryText, ref _))
46+
{
47+
if (_token.Alignment is null)
4448
{
45-
if (_token.Alignment is null)
46-
{
47-
sv.Render(output, _token.Format, _formatProvider);
48-
}
49-
else
50-
{
51-
var buffer = new StringWriter();
52-
sv.Render(buffer, _token.Format, _formatProvider);
53-
var str = buffer.ToString();
54-
Padding.Apply(output, str, _token.Alignment);
55-
}
49+
Render(output, logEvent.Timestamp);
50+
}
51+
else
52+
{
53+
var buffer = new StringWriter();
54+
Render(buffer, logEvent.Timestamp);
55+
var str = buffer.ToString();
56+
Padding.Apply(output, str, _token.Alignment);
5657
}
5758
}
59+
}
5860

59-
readonly struct DateTimeOffsetValue
61+
private void Render(TextWriter output, DateTimeOffset timestamp)
6062
{
61-
public DateTimeOffsetValue(DateTimeOffset value)
62-
{
63-
Value = value;
64-
}
63+
// When a DateTimeOffset is converted to a string, the default format automatically adds the "+00:00" explicit offset to the output string.
64+
// As the TimestampTokenRenderer is also used for rendering the UtcTimestamp which is always in UTC by definition, the +00:00 suffix should be avoided.
65+
// This is done using the same approach as Serilog's MessageTemplateTextFormatter. In case output should be converted to UTC, in order to avoid a zone specifier,
66+
// the DateTimeOffset is converted to a DateTime which then renders as expected.
6567

66-
public DateTimeOffset Value { get; }
68+
var custom = (ICustomFormatter?)_formatProvider?.GetFormat(typeof(ICustomFormatter));
69+
if (custom != null)
70+
{
71+
output.Write(custom.Format(_format, _convertToUtc ? timestamp.UtcDateTime : timestamp, _formatProvider));
72+
return;
73+
}
6774

68-
public void Render(TextWriter output, string? format = null, IFormatProvider? formatProvider = null)
75+
if (_convertToUtc)
6976
{
70-
var custom = (ICustomFormatter?)formatProvider?.GetFormat(typeof(ICustomFormatter));
71-
if (custom != null)
72-
{
73-
output.Write(custom.Format(format, Value, formatProvider));
74-
return;
75-
}
77+
RenderDateTime(output, timestamp.UtcDateTime);
78+
}
79+
else
80+
{
81+
RenderDateTimeOffset(output, timestamp);
82+
}
83+
}
7684

85+
private void RenderDateTimeOffset(TextWriter output, DateTimeOffset timestamp)
86+
{
7787
#if FEATURE_SPAN
78-
Span<char> buffer = stackalloc char[32];
79-
if (Value.TryFormat(buffer, out int written, format, formatProvider ?? CultureInfo.InvariantCulture))
80-
output.Write(buffer.Slice(0, written));
81-
else
82-
output.Write(Value.ToString(format, formatProvider ?? CultureInfo.InvariantCulture));
88+
Span<char> buffer = stackalloc char[32];
89+
if (timestamp.TryFormat(buffer, out int written, _format, _formatProvider ?? CultureInfo.InvariantCulture))
90+
output.Write(buffer.Slice(0, written));
91+
else
92+
output.Write(timestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture));
8393
#else
84-
output.Write(Value.ToString(format, formatProvider ?? CultureInfo.InvariantCulture));
94+
output.Write(timestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture));
95+
#endif
96+
}
97+
98+
private void RenderDateTime(TextWriter output, DateTime utcTimestamp)
99+
{
100+
#if FEATURE_SPAN
101+
Span<char> buffer = stackalloc char[32];
102+
if (utcTimestamp.TryFormat(buffer, out int written, _format, _formatProvider ?? CultureInfo.InvariantCulture))
103+
output.Write(buffer.Slice(0, written));
104+
else
105+
output.Write(utcTimestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture));
106+
#else
107+
output.Write(utcTimestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture));
85108
#endif
86-
}
87109
}
88110
}

test/Serilog.Sinks.Console.Tests/Output/OutputTemplateRendererTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,4 +400,36 @@ public void TraceAndSpanAreIncludedWhenPresent()
400400
formatter.Format(evt, sw);
401401
Assert.Equal($"{traceId}/{spanId}", sw.ToString());
402402
}
403+
404+
[Theory]
405+
[InlineData("{Timestamp}", "09/03/2024 14:15:16 +02:00")] // Default Format
406+
[InlineData("{Timestamp:o}", "2024-09-03T14:15:16.0790000+02:00")] // Round-trip Standard Format String
407+
[InlineData("{Timestamp:yyyy-MM-dd HH:mm:ss}", "2024-09-03 14:15:16")] // Custom Format String
408+
public void TimestampTokenRendersLocalTime(string actualToken, string expectedOutput)
409+
{
410+
var logTimestampWithTimeZoneOffset = DateTimeOffset.Parse("2024-09-03T14:15:16.079+02:00", CultureInfo.InvariantCulture);
411+
var formatter = new OutputTemplateRenderer(ConsoleTheme.None, actualToken, CultureInfo.InvariantCulture);
412+
var evt = new LogEvent(logTimestampWithTimeZoneOffset, LogEventLevel.Debug, null,
413+
new MessageTemplate(Enumerable.Empty<MessageTemplateToken>()), Enumerable.Empty<LogEventProperty>());
414+
var sw = new StringWriter();
415+
formatter.Format(evt, sw);
416+
// expect time in local time, unchanged from the input, the +02:00 offset should not affect the output
417+
Assert.Equal(expectedOutput, sw.ToString());
418+
}
419+
420+
[Theory]
421+
[InlineData("{UtcTimestamp}", "09/03/2024 12:15:16")] // Default Format
422+
[InlineData("{UtcTimestamp:o}", "2024-09-03T12:15:16.0790000Z")] // Round-trip Standard Format String
423+
[InlineData("{UtcTimestamp:yyyy-MM-dd HH:mm:ss}", "2024-09-03 12:15:16")] // Custom Format String
424+
public void UtcTimestampTokenRendersUtcTime(string actualToken, string expectedOutput)
425+
{
426+
var logTimestampWithTimeZoneOffset = DateTimeOffset.Parse("2024-09-03T14:15:16.079+02:00", CultureInfo.InvariantCulture);
427+
var formatter = new OutputTemplateRenderer(ConsoleTheme.None, actualToken, CultureInfo.InvariantCulture);
428+
var evt = new LogEvent(logTimestampWithTimeZoneOffset, LogEventLevel.Debug, null,
429+
new MessageTemplate(Enumerable.Empty<MessageTemplateToken>()), Enumerable.Empty<LogEventProperty>());
430+
var sw = new StringWriter();
431+
formatter.Format(evt, sw);
432+
// expect time in UTC, the +02:00 offset must be applied to adjust the hour
433+
Assert.Equal(expectedOutput, sw.ToString());
434+
}
403435
}

0 commit comments

Comments
 (0)