diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs index eac0fb38df2c4..c924efd9d2e02 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs @@ -202,4 +202,21 @@ public NullLogger() { } public bool IsEnabled(Microsoft.Extensions.Logging.LogLevel logLevel) { throw null; } public void Log(Microsoft.Extensions.Logging.LogLevel logLevel, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception? exception, System.Func formatter) { } } + public abstract class BufferedLogRecord + { + public abstract System.DateTimeOffset Timestamp { get; } + public abstract Microsoft.Extensions.Logging.LogLevel LogLevel { get; } + public abstract Microsoft.Extensions.Logging.EventId EventId { get; } + public virtual string? Exception { get; } + public virtual System.Diagnostics.ActivitySpanId? ActivitySpanId { get; } + public virtual System.Diagnostics.ActivityTraceId? ActivityTraceId { get; } + public virtual int? ManagedThreadId { get; } + public virtual string? FormattedMessage { get; } + public virtual string? MessageTemplate { get; } + public virtual System.Collections.Generic.IReadOnlyList> Attributes { get; } + } + public interface IBufferedLogger + { + void LogRecords(System.Collections.Generic.IEnumerable records); + } } diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.csproj index 90242dbf8b96f..ff6d90a86e99b 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.csproj +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.csproj @@ -9,5 +9,6 @@ + diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferedLogRecord.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferedLogRecord.cs new file mode 100644 index 0000000000000..8f35ff97b35a6 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferedLogRecord.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace Microsoft.Extensions.Logging.Abstractions +{ + /// + /// Represents a buffered log record to be written in batch to an . + /// + /// + /// Instances of this type may be pooled and reused. Implementations of must + /// not hold onto instance of passed to its method + /// beyond the invocation of that method. + /// + public abstract class BufferedLogRecord + { + /// + /// Gets the time when the log record was first created. + /// + public abstract DateTimeOffset Timestamp { get; } + + /// + /// Gets the record's logging severity level. + /// + public abstract LogLevel LogLevel { get; } + + /// + /// Gets the record's event id. + /// + public abstract EventId EventId { get; } + + /// + /// Gets an exception string for this record. + /// + public virtual string? Exception => null; + + /// + /// Gets an activity span ID for this record, representing the state of the thread that created the record. + /// + public virtual ActivitySpanId? ActivitySpanId => null; + + /// + /// Gets an activity trace ID for this record, representing the state of the thread that created the record. + /// + public virtual ActivityTraceId? ActivityTraceId => null; + + /// + /// Gets the ID of the thread that created the log record. + /// + public virtual int? ManagedThreadId => null; + + /// + /// Gets the formatted log message. + /// + public virtual string? FormattedMessage => null; + + /// + /// Gets the original log message template. + /// + public virtual string? MessageTemplate => null; + + /// + /// Gets the variable set of name/value pairs associated with the record. + /// + public virtual IReadOnlyList> Attributes => []; + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/IBufferedLogger.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/IBufferedLogger.cs new file mode 100644 index 0000000000000..769a0e6b6bf40 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/IBufferedLogger.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Collections.Generic; + +namespace Microsoft.Extensions.Logging.Abstractions +{ + /// + /// Represents the ability of a logging provider to support buffered logging. + /// + /// + /// A logging provider implements the interface that gets invoked by the + /// logging infrastructure whenever it’s time to log a piece of state. + /// + /// A logging provider may also optionally implement the interface. + /// The logging infrastructure may type-test the object to determine if + /// it supports the interface. If it does, that indicates to the + /// logging infrastructure that the logging provider supports buffering. Whenever log + /// buffering is enabled, buffered log records may be delivered to the logging provider + /// in a batch via . + /// + /// If a logging provider does not support log buffering, then it will always be given + /// unbuffered log records. If a logging provider does support log buffering, whether its + /// or implementation is used is + /// determined by the log producer. + /// + public interface IBufferedLogger + { + /// + /// Delivers a batch of buffered log records to a logging provider. + /// + /// The buffered log records to log. + /// + /// Once this function returns, the implementation should no longer access the records + /// or state referenced by these records since the instances may be reused to represent other logs. + /// + void LogRecords(IEnumerable records); + } +} diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/Microsoft.Extensions.Logging.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/Microsoft.Extensions.Logging.Abstractions.csproj index a14ac672bb75b..00d83a691461b 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/Microsoft.Extensions.Logging.Abstractions.csproj +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/Microsoft.Extensions.Logging.Abstractions.csproj @@ -53,6 +53,7 @@ Microsoft.Extensions.Logging.Abstractions.NullLogger + diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatter.cs index d963e6af7f112..c44aead487ab3 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatter.cs @@ -32,7 +32,7 @@ protected ConsoleFormatter(string name) /// Writes the log message to the specified TextWriter. /// /// - /// if the formatter wants to write colors to the console, it can do so by embedding ANSI color codes into the string + /// If the formatter wants to write colors to the console, it can do so by embedding ANSI color codes into the string. /// /// The log entry. /// The provider of scope data. diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs index 3a542e6063635..c030eb059e372 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System; +using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.IO; using System.Runtime.Versioning; @@ -13,7 +14,7 @@ namespace Microsoft.Extensions.Logging.Console /// A logger that writes messages in the console. /// [UnsupportedOSPlatform("browser")] - internal sealed class ConsoleLogger : ILogger + internal sealed class ConsoleLogger : ILogger, IBufferedLogger { private readonly string _name; private readonly ConsoleLoggerProcessor _queueProcessor; @@ -69,6 +70,35 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except _queueProcessor.EnqueueMessage(new LogMessageEntry(computedAnsiString, logAsError: logLevel >= Options.LogToStandardErrorThreshold)); } + /// + public void LogRecords(IEnumerable records) + { + ThrowHelper.ThrowIfNull(records); + + StringWriter writer = t_stringWriter ??= new StringWriter(); + + var sb = writer.GetStringBuilder(); + foreach (var rec in records) + { + var logEntry = new LogEntry(rec.LogLevel, _name, rec.EventId, rec, null, static (s, _) => s.FormattedMessage ?? string.Empty); + Formatter.Write(in logEntry, null, writer); + + if (sb.Length == 0) + { + continue; + } + + string computedAnsiString = sb.ToString(); + sb.Clear(); + _queueProcessor.EnqueueMessage(new LogMessageEntry(computedAnsiString, logAsError: rec.LogLevel >= Options.LogToStandardErrorThreshold)); + } + + if (sb.Capacity > 1024) + { + sb.Capacity = 1024; + } + } + /// public bool IsEnabled(LogLevel logLevel) { diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatter.cs index 945e6ebb23584..1be9425aa310b 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatter.cs @@ -28,20 +28,34 @@ public JsonConsoleFormatter(IOptionsMonitor options public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter) { - string message = logEntry.Formatter(logEntry.State, logEntry.Exception); - if (logEntry.Exception == null && message == null) + if (logEntry.State is BufferedLogRecord bufferedRecord) { - return; + string message = bufferedRecord.FormattedMessage ?? string.Empty; + WriteInternal(null, textWriter, message, bufferedRecord.LogLevel, logEntry.Category, bufferedRecord.EventId.Id, bufferedRecord.Exception, + bufferedRecord.Attributes.Count > 0, null, bufferedRecord.Attributes, bufferedRecord.Timestamp); } + else + { + string message = logEntry.Formatter(logEntry.State, logEntry.Exception); + if (logEntry.Exception == null && message == null) + { + return; + } - // We extract most of the work into a non-generic method to save code size. If this was left in the generic - // method, we'd get generic specialization for all TState parameters, but that's unnecessary. - WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.Category, logEntry.EventId.Id, logEntry.Exception, - logEntry.State != null, logEntry.State?.ToString(), logEntry.State as IReadOnlyCollection>); + DateTimeOffset stamp = FormatterOptions.TimestampFormat != null + ? (FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now) + : DateTimeOffset.MinValue; + + // We extract most of the work into a non-generic method to save code size. If this was left in the generic + // method, we'd get generic specialization for all TState parameters, but that's unnecessary. + WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.Category, logEntry.EventId.Id, logEntry.Exception?.ToString(), + logEntry.State != null, logEntry.State?.ToString(), logEntry.State as IReadOnlyList>, stamp); + } } - private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter textWriter, string message, LogLevel logLevel, - string category, int eventId, Exception? exception, bool hasState, string? stateMessage, IReadOnlyCollection>? stateProperties) + private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter textWriter, string? message, LogLevel logLevel, + string category, int eventId, string? exception, bool hasState, string? stateMessage, IReadOnlyList>? stateProperties, + DateTimeOffset stamp) { const int DefaultBufferSize = 1024; using (var output = new PooledByteBufferWriter(DefaultBufferSize)) @@ -52,8 +66,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex var timestampFormat = FormatterOptions.TimestampFormat; if (timestampFormat != null) { - DateTimeOffset dateTimeOffset = FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now; - writer.WriteString("Timestamp", dateTimeOffset.ToString(timestampFormat)); + writer.WriteString("Timestamp", stamp.ToString(timestampFormat)); } writer.WriteNumber(nameof(LogEntry.EventId), eventId); writer.WriteString(nameof(LogEntry.LogLevel), GetLogLevelString(logLevel)); @@ -62,7 +75,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex if (exception != null) { - writer.WriteString(nameof(Exception), exception.ToString()); + writer.WriteString(nameof(Exception), exception); } if (hasState) @@ -71,7 +84,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex writer.WriteString("Message", stateMessage); if (stateProperties != null) { - foreach (KeyValuePair item in stateProperties) + foreach (KeyValuePair item in stateProperties) { WriteItem(writer, item); } @@ -131,11 +144,11 @@ private void WriteScopeInformation(Utf8JsonWriter writer, IExternalScopeProvider writer.WriteStartArray("Scopes"); scopeProvider.ForEachScope((scope, state) => { - if (scope is IEnumerable> scopeItems) + if (scope is IEnumerable> scopeItems) { state.WriteStartObject(); state.WriteString("Message", scope.ToString()); - foreach (KeyValuePair item in scopeItems) + foreach (KeyValuePair item in scopeItems) { WriteItem(state, item); } @@ -150,7 +163,7 @@ private void WriteScopeInformation(Utf8JsonWriter writer, IExternalScopeProvider } } - private static void WriteItem(Utf8JsonWriter writer, KeyValuePair item) + private static void WriteItem(Utf8JsonWriter writer, KeyValuePair item) { var key = item.Key; switch (item.Value) diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs index 79cacb9b3baaf..9d99836c45b13 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs @@ -46,19 +46,27 @@ public void Dispose() public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter) { - string message = logEntry.Formatter(logEntry.State, logEntry.Exception); - if (logEntry.Exception == null && message == null) + if (logEntry.State is BufferedLogRecord bufferedRecord) { - return; + string message = bufferedRecord.FormattedMessage ?? string.Empty; + WriteInternal(null, textWriter, message, bufferedRecord.LogLevel, bufferedRecord.EventId.Id, bufferedRecord.Exception, logEntry.Category, bufferedRecord.Timestamp); } + else + { + string message = logEntry.Formatter(logEntry.State, logEntry.Exception); + if (logEntry.Exception == null && message == null) + { + return; + } - // We extract most of the work into a non-generic method to save code size. If this was left in the generic - // method, we'd get generic specialization for all TState parameters, but that's unnecessary. - WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.EventId.Id, logEntry.Exception, logEntry.Category); + // We extract most of the work into a non-generic method to save code size. If this was left in the generic + // method, we'd get generic specialization for all TState parameters, but that's unnecessary. + WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.EventId.Id, logEntry.Exception?.ToString(), logEntry.Category, GetCurrentDateTime()); + } } private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter textWriter, string message, LogLevel logLevel, - int eventId, Exception? exception, string category) + int eventId, string? exception, string category, DateTimeOffset stamp) { ConsoleColors logLevelColors = GetLogLevelConsoleColors(logLevel); string logLevelString = GetLogLevelString(logLevel); @@ -67,8 +75,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex string? timestampFormat = FormatterOptions.TimestampFormat; if (timestampFormat != null) { - DateTimeOffset dateTimeOffset = GetCurrentDateTime(); - timestamp = dateTimeOffset.ToString(timestampFormat); + timestamp = stamp.ToString(timestampFormat); } if (timestamp != null) { @@ -114,7 +121,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex if (exception != null) { // exception message - WriteMessage(textWriter, exception.ToString(), singleLine); + WriteMessage(textWriter, exception, singleLine); } if (singleLine) { @@ -148,7 +155,9 @@ static void WriteReplacing(TextWriter writer, string oldValue, string newValue, private DateTimeOffset GetCurrentDateTime() { - return FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now; + return FormatterOptions.TimestampFormat != null + ? (FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now) + : DateTimeOffset.MinValue; } private static string GetLogLevelString(LogLevel logLevel) diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs index 2d306fee1d0a4..0df001a0267c7 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs @@ -36,19 +36,27 @@ public void Dispose() public override void Write(in LogEntry logEntry, IExternalScopeProvider? scopeProvider, TextWriter textWriter) { - string message = logEntry.Formatter(logEntry.State, logEntry.Exception); - if (logEntry.Exception == null && message == null) + if (logEntry.State is BufferedLogRecord bufferedRecord) { - return; + string message = bufferedRecord.FormattedMessage ?? string.Empty; + WriteInternal(null, textWriter, message, bufferedRecord.LogLevel, logEntry.Category, bufferedRecord.EventId.Id, bufferedRecord.Exception, bufferedRecord.Timestamp); } + else + { + string message = logEntry.Formatter(logEntry.State, logEntry.Exception); + if (logEntry.Exception == null && message == null) + { + return; + } - // We extract most of the work into a non-generic method to save code size. If this was left in the generic - // method, we'd get generic specialization for all TState parameters, but that's unnecessary. - WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.Category, logEntry.EventId.Id, logEntry.Exception); + // We extract most of the work into a non-generic method to save code size. If this was left in the generic + // method, we'd get generic specialization for all TState parameters, but that's unnecessary. + WriteInternal(scopeProvider, textWriter, message, logEntry.LogLevel, logEntry.Category, logEntry.EventId.Id, logEntry.Exception?.ToString(), GetCurrentDateTime()); + } } private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter textWriter, string message, LogLevel logLevel, string category, - int eventId, Exception? exception) + int eventId, string? exception, DateTimeOffset stamp) { // systemd reads messages from standard out line-by-line in a 'message' format. // newline characters are treated as message delimiters, so we must replace them. @@ -64,8 +72,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex string? timestampFormat = FormatterOptions.TimestampFormat; if (timestampFormat != null) { - DateTimeOffset dateTimeOffset = GetCurrentDateTime(); - textWriter.Write(dateTimeOffset.ToString(timestampFormat)); + textWriter.Write(stamp.ToString(timestampFormat)); } // category and event id @@ -90,7 +97,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex if (exception != null) { textWriter.Write(' '); - WriteReplacingNewLine(textWriter, exception.ToString()); + WriteReplacingNewLine(textWriter, exception); } // newline delimiter @@ -105,7 +112,9 @@ static void WriteReplacingNewLine(TextWriter writer, string message) private DateTimeOffset GetCurrentDateTime() { - return FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now; + return FormatterOptions.TimestampFormat != null + ? (FormatterOptions.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now) + : DateTimeOffset.MinValue; } private static string GetSyslogSeverityString(LogLevel logLevel) diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs index 5a71dad69bbdb..900d667f8e198 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/ConsoleLoggerTest.cs @@ -10,6 +10,7 @@ using Microsoft.DotNet.RemoteExecutor; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Logging.Test.Console; using Microsoft.Extensions.Options; using Xunit; @@ -551,6 +552,69 @@ public void Log_LogsCorrectTimestamp(ConsoleLoggerFormat format, LogLevel level) } } + private sealed class BufferedLogRecordImpl : BufferedLogRecord + { + private readonly DateTimeOffset _timestamp; + private readonly LogLevel _level; + private readonly string _exception; + + public BufferedLogRecordImpl(DateTimeOffset timestamp, LogLevel level, string exception) + { + _timestamp = timestamp; + _level = level; + _exception = exception; + } + + public override DateTimeOffset Timestamp => _timestamp; + public override LogLevel LogLevel => _level; + public override EventId EventId => new EventId(0); + public override string? Exception => _exception; + } + + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] + [MemberData(nameof(FormatsAndLevels))] + public void Log_LogsCorrectOverrideTimestamp(ConsoleLoggerFormat format, LogLevel level) + { + // Arrange + using var t = SetUp(new ConsoleLoggerOptions { TimestampFormat = "yyyy-MM-ddTHH:mm:sszz ", Format = format, UseUtcTimestamp = false }); + var levelPrefix = t.GetLevelPrefix(level); + var logger = t.Logger; + var sink = t.Sink; + var ex = new Exception("Exception message" + Environment.NewLine + "with a second line"); + var now = new DateTimeOffset(DateTime.Now); + var round_now = new DateTimeOffset(now.Year, now.Month, now.Day, now.Hour, now.Minute, now.Second, now.Offset); + var bufferedRecord = new BufferedLogRecordImpl(now, level, ex.ToString()); + + // Act + logger.LogRecords(new [] { bufferedRecord }); + + // Assert + switch (format) + { + case ConsoleLoggerFormat.Default: + { + Assert.Equal(3, sink.Writes.Count); + Assert.StartsWith(levelPrefix, sink.Writes[1].Message); + Assert.Matches(@"^\d{4}\D\d{2}\D\d{2}\D\d{2}\D\d{2}\D\d{2}\D\d{2}\s$", sink.Writes[0].Message); + var parsedDateTime = DateTimeOffset.Parse(sink.Writes[0].Message.Trim()); + Assert.Equal(round_now, parsedDateTime); + } + break; + case ConsoleLoggerFormat.Systemd: + { + Assert.Single(sink.Writes); + Assert.StartsWith(levelPrefix, sink.Writes[0].Message); + var regexMatch = Regex.Match(sink.Writes[0].Message, @"^<\d>(\d{4}\D\d{2}\D\d{2}\D\d{2}\D\d{2}\D\d{2}\D\d{2})\s[^\s]"); + Assert.True(regexMatch.Success); + var parsedDateTime = DateTimeOffset.Parse(regexMatch.Groups[1].Value); + Assert.Equal(round_now, parsedDateTime); + } + break; + default: + throw new ArgumentOutOfRangeException(nameof(format)); + } + } + [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))] [MemberData(nameof(FormatsAndLevels))] public void WriteCore_LogsCorrectTimestampInUtc(ConsoleLoggerFormat format, LogLevel level) diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/Microsoft.Extensions.Logging.Console.Tests.csproj b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/Microsoft.Extensions.Logging.Console.Tests.csproj index 31c7dd2748402..2beeab918e696 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/Microsoft.Extensions.Logging.Console.Tests.csproj +++ b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests/Microsoft.Extensions.Logging.Console.Tests.csproj @@ -10,6 +10,6 @@ + -