Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,11 @@ public OutputTemplateRenderer(ConsoleTheme theme, string outputTemplate, IFormat
}
else if (pt.PropertyName == OutputProperties.TimestampPropertyName)
{
renderers.Add(new TimestampTokenRenderer(theme, pt, formatProvider));
renderers.Add(new TimestampTokenRenderer(theme, pt, formatProvider, convertToUtc: false));
}
else if (pt.PropertyName == OutputProperties.UtcTimestampPropertyName)
{
renderers.Add(new TimestampTokenRenderer(theme, pt, formatProvider, convertToUtc: true));
}
else if (pt.PropertyName == OutputProperties.PropertiesPropertyName)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,63 +26,85 @@ class TimestampTokenRenderer : OutputTemplateTokenRenderer
{
readonly ConsoleTheme _theme;
readonly PropertyToken _token;
readonly string? _format;
readonly IFormatProvider? _formatProvider;
readonly bool _convertToUtc;

public TimestampTokenRenderer(ConsoleTheme theme, PropertyToken token, IFormatProvider? formatProvider)
public TimestampTokenRenderer(ConsoleTheme theme, PropertyToken token, IFormatProvider? formatProvider, bool convertToUtc)
{
_theme = theme;
_token = token;
_formatProvider = formatProvider;
}
_theme = theme;
_token = token;
_format = token.Format;
_formatProvider = formatProvider;
_convertToUtc = convertToUtc;
}

public override void Render(LogEvent logEvent, TextWriter output)
{
var sv = new DateTimeOffsetValue(logEvent.Timestamp);

var _ = 0;
using (_theme.Apply(output, ConsoleThemeStyle.SecondaryText, ref _))
var _ = 0;
using (_theme.Apply(output, ConsoleThemeStyle.SecondaryText, ref _))
{
if (_token.Alignment is null)
{
if (_token.Alignment is null)
{
sv.Render(output, _token.Format, _formatProvider);
}
else
{
var buffer = new StringWriter();
sv.Render(buffer, _token.Format, _formatProvider);
var str = buffer.ToString();
Padding.Apply(output, str, _token.Alignment);
}
Render(output, logEvent.Timestamp);
}
else
{
var buffer = new StringWriter();
Render(buffer, logEvent.Timestamp);
var str = buffer.ToString();
Padding.Apply(output, str, _token.Alignment);
}
}
}

readonly struct DateTimeOffsetValue
private void Render(TextWriter output, DateTimeOffset timestamp)
{
public DateTimeOffsetValue(DateTimeOffset value)
{
Value = value;
}
// When a DateTimeOffset is converted to a string, the default format automatically adds the "+00:00" explicit offset to the output string.
// As the TimestampTokenRenderer is also used for rendering the UtcTimestamp which is always in UTC by definition, the +00:00 suffix should be avoided.
// 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,
// the DateTimeOffset is converted to a DateTime which then renders as expected.

public DateTimeOffset Value { get; }
var custom = (ICustomFormatter?)_formatProvider?.GetFormat(typeof(ICustomFormatter));
if (custom != null)
{
output.Write(custom.Format(_format, _convertToUtc ? timestamp.UtcDateTime : timestamp, _formatProvider));
return;
}

public void Render(TextWriter output, string? format = null, IFormatProvider? formatProvider = null)
if (_convertToUtc)
{
var custom = (ICustomFormatter?)formatProvider?.GetFormat(typeof(ICustomFormatter));
if (custom != null)
{
output.Write(custom.Format(format, Value, formatProvider));
return;
}
RenderDateTime(output, timestamp.UtcDateTime);
}
else
{
RenderDateTimeOffset(output, timestamp);
}
}

private void RenderDateTimeOffset(TextWriter output, DateTimeOffset timestamp)
{
#if FEATURE_SPAN
Span<char> buffer = stackalloc char[32];
if (Value.TryFormat(buffer, out int written, format, formatProvider ?? CultureInfo.InvariantCulture))
output.Write(buffer.Slice(0, written));
else
output.Write(Value.ToString(format, formatProvider ?? CultureInfo.InvariantCulture));
Span<char> buffer = stackalloc char[32];
if (timestamp.TryFormat(buffer, out int written, _format, _formatProvider ?? CultureInfo.InvariantCulture))
output.Write(buffer.Slice(0, written));
else
output.Write(timestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture));
#else
output.Write(Value.ToString(format, formatProvider ?? CultureInfo.InvariantCulture));
output.Write(timestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture));
#endif
}

private void RenderDateTime(TextWriter output, DateTime utcTimestamp)
{
#if FEATURE_SPAN
Span<char> buffer = stackalloc char[32];
if (utcTimestamp.TryFormat(buffer, out int written, _format, _formatProvider ?? CultureInfo.InvariantCulture))
output.Write(buffer.Slice(0, written));
else
output.Write(utcTimestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture));
#else
output.Write(utcTimestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture));
#endif
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -400,4 +400,36 @@ public void TraceAndSpanAreIncludedWhenPresent()
formatter.Format(evt, sw);
Assert.Equal($"{traceId}/{spanId}", sw.ToString());
}

[Theory]
[InlineData("{Timestamp}", "09/03/2024 14:15:16 +02:00")] // Default Format
[InlineData("{Timestamp:o}", "2024-09-03T14:15:16.0790000+02:00")] // Round-trip Standard Format String
[InlineData("{Timestamp:yyyy-MM-dd HH:mm:ss}", "2024-09-03 14:15:16")] // Custom Format String
public void TimestampTokenRendersLocalTime(string actualToken, string expectedOutput)
{
var logTimestampWithTimeZoneOffset = DateTimeOffset.Parse("2024-09-03T14:15:16.079+02:00", CultureInfo.InvariantCulture);
var formatter = new OutputTemplateRenderer(ConsoleTheme.None, actualToken, CultureInfo.InvariantCulture);
var evt = new LogEvent(logTimestampWithTimeZoneOffset, LogEventLevel.Debug, null,
new MessageTemplate(Enumerable.Empty<MessageTemplateToken>()), Enumerable.Empty<LogEventProperty>());
var sw = new StringWriter();
formatter.Format(evt, sw);
// expect time in local time, unchanged from the input, the +02:00 offset should not affect the output
Assert.Equal(expectedOutput, sw.ToString());
}

[Theory]
[InlineData("{UtcTimestamp}", "09/03/2024 12:15:16")] // Default Format
[InlineData("{UtcTimestamp:o}", "2024-09-03T12:15:16.0790000Z")] // Round-trip Standard Format String
[InlineData("{UtcTimestamp:yyyy-MM-dd HH:mm:ss}", "2024-09-03 12:15:16")] // Custom Format String
public void UtcTimestampTokenRendersUtcTime(string actualToken, string expectedOutput)
{
var logTimestampWithTimeZoneOffset = DateTimeOffset.Parse("2024-09-03T14:15:16.079+02:00", CultureInfo.InvariantCulture);
var formatter = new OutputTemplateRenderer(ConsoleTheme.None, actualToken, CultureInfo.InvariantCulture);
var evt = new LogEvent(logTimestampWithTimeZoneOffset, LogEventLevel.Debug, null,
new MessageTemplate(Enumerable.Empty<MessageTemplateToken>()), Enumerable.Empty<LogEventProperty>());
var sw = new StringWriter();
formatter.Format(evt, sw);
// expect time in UTC, the +02:00 offset must be applied to adjust the hour
Assert.Equal(expectedOutput, sw.ToString());
}
}