Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Serilog.Sinks.Console/Serilog.Sinks.Console.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<Description>A Serilog sink that writes log events to the console/terminal.</Description>
<VersionPrefix>6.0.0</VersionPrefix>
<VersionPrefix>6.1.0</VersionPrefix>
<Authors>Serilog Contributors</Authors>
<TargetFrameworks Condition=" '$(OS)' == 'Windows_NT'">net462;net471</TargetFrameworks>
<TargetFrameworks>$(TargetFrameworks);netstandard2.0;net6.0;net8.0</TargetFrameworks>
Expand Down
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());
}
}