Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support UtcTimestamp token in output template (new since Serilog 4.0) #165

Open
wants to merge 6 commits into
base: dev
Choose a base branch
from
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,34 @@ public void TraceAndSpanAreIncludedWhenPresent()
formatter.Format(evt, sw);
Assert.Equal($"{traceId}/{spanId}", sw.ToString());
}

[Fact]
public void TimestampTokenRendersLocalTime()
{
var logTimestampWithTimeZoneOffset = DateTimeOffset.Parse("2024-09-03T14:15:16.079+02:00", CultureInfo.InvariantCulture);
var formatter = new OutputTemplateRenderer(ConsoleTheme.None,
"Default Format: {Timestamp} / Custom Format String: {Timestamp:yyyy-MM-dd HH:mm:ss}",
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("Default Format: 09/03/2024 14:15:16 +02:00 / Custom Format String: 2024-09-03 14:15:16", sw.ToString());
}

[Fact]
public void UtcTimestampTokenRendersUtcTime()
{
var logTimestampWithTimeZoneOffset = DateTimeOffset.Parse("2024-09-03T14:15:16.079+02:00", CultureInfo.InvariantCulture);
var formatter = new OutputTemplateRenderer(ConsoleTheme.None,
"Default Format: {UtcTimestamp} / Custom Format String: {UtcTimestamp:yyyy-MM-dd HH:mm:ss}",
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("Default Format: 09/03/2024 12:15:16 / Custom Format String: 2024-09-03 12:15:16", sw.ToString());
}
}