From ca3dbc5035ec97680cc6b3f2122887a558fde866 Mon Sep 17 00:00:00 2001 From: Martin Taillefer Date: Thu, 6 Jun 2024 12:20:07 -0700 Subject: [PATCH] Introduce IBufferedLogger --- ...crosoft.Extensions.Logging.Abstractions.cs | 19 +++++ .../src/BufferedLogRecord.cs | 71 +++++++++++++++++++ .../src/IBufferedLogger.cs | 37 ++++++++++ ...oft.Extensions.Logging.Abstractions.csproj | 3 +- .../src/src.sln | 25 +++++++ .../src/ConsoleFormatter.cs | 2 +- .../src/ConsoleLogger.cs | 30 +++++++- .../src/JsonConsoleFormatter.cs | 48 ++++++++----- .../src/SimpleConsoleFormatter.cs | 32 ++++++--- .../src/SystemdConsoleFormatter.cs | 32 ++++++--- .../ConsoleLoggerTest.cs | 62 ++++++++++++++++ ...ft.Extensions.Logging.Console.Tests.csproj | 2 +- .../tests/tests.sln | 25 +++++++ 13 files changed, 348 insertions(+), 40 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferedLogRecord.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/IBufferedLogger.cs create mode 100644 src/libraries/Microsoft.Extensions.Logging.Abstractions/src/src.sln create mode 100644 src/libraries/Microsoft.Extensions.Logging.Console/tests/tests.sln 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 eac0fb38df2c4d..336afa75629840 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,23 @@ 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 EventId EventId { get; } + public abstract string? Exception { get; } +#if NET8_0_OR_GREATER + public abstract System.Diagnostics.ActivitySpanId ActivitySpanId { get; } + public abstract System.Diagnostics.ActivityTraceId ActivityTraceId { get; } +#endif + public abstract int? ManagedThreadId { get; } + public abstract string? FormattedMessage { get; } + public abstract string? MessageTemplate { get; } + public abstract System.Collections.Generic.IReadOnlyList> Attributes { get; } + } + public interface IBufferedLogger + { + void Log(System.Collections.Generic.IReadOnlyList records); + } } 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 00000000000000..a94f369d6a65c8 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/BufferedLogRecord.cs @@ -0,0 +1,71 @@ +// 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 +{ + /// + /// State representing a buffered log record. + /// + /// + /// Objects of this type are reused over time to reduce + /// allocations. + /// + public abstract class BufferedLogRecord + { + /// + /// Gets the time when the log record was first created. + /// + public abstract DateTimeOffset Timestamp { get; } + + /// + /// Gets the record's log level, indicating it rough importance + /// + public abstract LogLevel LogLevel { get; } + + /// + /// Gets the records event id. + /// + public abstract EventId EventId { get; } + + /// + /// Gets an optional exception string for this record. + /// + public abstract string? Exception { get; } + +#if NET8_0_OR_GREATER + /// + /// Gets an activity span id for this record, representing the state of the thread that created the record. + /// + public abstract ActivitySpanId ActivitySpanId { get; } + + /// + /// Gets an activity trace id for this record, representing the state of the thread that created the record. + /// + public abstract ActivityTraceId ActivityTraceId { get; } +#endif + + /// + /// Gets the ID of the thread that created the log record. + /// + public abstract int? ManagedThreadId { get; } + + /// + /// Gets the formatted log message. + /// + public abstract string? FormattedMessage { get; } + + /// + /// Gets the original log message template. + /// + public abstract string? MessageTemplate { get; } + + /// + /// Gets the variable set of name/value pairs associated with the record. + /// + public abstract IReadOnlyList> Attributes { get; } + } +} 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 00000000000000..8b7fb906a37a21 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/IBufferedLogger.cs @@ -0,0 +1,37 @@ +// 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 +{ + /// + /// Logging providers can implement this interface to indicate they support buffered logging. + /// + /// + /// A logging provider normally exposes an interface that gets invoked by the + /// logging infrastructure whenever it’s time to log a piece of state. + /// + /// The logging infrastructure will type-test the ILogger object to determine if + /// it supports the IBufferedLogger interface also. If it does, that tells the + /// logging infrastructure that the logging provider supports buffering. Whenever log + /// buffering is enabled, buffered log records will be delivered to the logging provider + /// via the IBufferedLogger interface. + /// + /// If a logging provider does not support log buffering, then it will always be given + /// unbuffered log records. In other words, whether or not buffering is requested by + /// the user, it will not happen for those log providers. + /// + public interface IBufferedLogger + { + /// + /// Delivers a batch of buffered log records to a logging provider. + /// + /// The buffered log records to log. + /// + /// Once this function returns, it should no longer access the records + /// or state referenced by these records. + /// + void Log(IReadOnlyList 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 28d8bcddd18515..b93658be96e243 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 @@ -1,4 +1,4 @@ - + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) @@ -51,6 +51,7 @@ Microsoft.Extensions.Logging.Abstractions.NullLogger + diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/src.sln b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/src.sln new file mode 100644 index 00000000000000..aae0905c624430 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/src.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Logging.Abstractions", "Microsoft.Extensions.Logging.Abstractions.csproj", "{2C37E902-3EB9-4E48-82DB-9D4C4BB6AAD0}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {2C37E902-3EB9-4E48-82DB-9D4C4BB6AAD0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2C37E902-3EB9-4E48-82DB-9D4C4BB6AAD0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2C37E902-3EB9-4E48-82DB-9D4C4BB6AAD0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2C37E902-3EB9-4E48-82DB-9D4C4BB6AAD0}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {69124FB1-1525-4BBA-AB0C-0E6753566E44} + EndGlobalSection +EndGlobal diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatter.cs index d963e6af7f1123..262a976d9e9524 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 3a542e6063635b..3ea71619f6f349 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,33 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except _queueProcessor.EnqueueMessage(new LogMessageEntry(computedAnsiString, logAsError: logLevel >= Options.LogToStandardErrorThreshold)); } + /// + public void Log(IReadOnlyList records) + { + ThrowHelper.ThrowIfNull(records); + + t_stringWriter ??= new StringWriter(); + + foreach (var rec in records) + { + var logEntry = new LogEntry(rec.LogLevel, _name, rec.EventId, rec, null, (s, e) => s.FormattedMessage ?? string.Empty); + Formatter.Write(in logEntry, null, t_stringWriter); + + var sb = t_stringWriter.GetStringBuilder(); + if (sb.Length == 0) + { + continue; + } + string computedAnsiString = sb.ToString(); + sb.Clear(); + if (sb.Capacity > 1024) + { + sb.Capacity = 1024; + } + _queueProcessor.EnqueueMessage(new LogMessageEntry(computedAnsiString, logAsError: rec.LogLevel >= Options.LogToStandardErrorThreshold)); + } + } + /// 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 945e6ebb23584e..3300fe4700c645 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatter.cs @@ -28,20 +28,37 @@ 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; + if (bufferedRecord.Exception == null && message == null) + { + return; + } + + WriteInternal(scopeProvider, textWriter, message, bufferedRecord.LogLevel, logEntry.Category, bufferedRecord.EventId.Id, bufferedRecord.Exception, + bufferedRecord.Attributes.Count > 0, null, bufferedRecord.Attributes as IReadOnlyList>, 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.UseUtcTimestamp ? DateTimeOffset.UtcNow : DateTimeOffset.Now; + + // 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 +69,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 +78,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 +87,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 +147,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 +166,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 79cacb9b3baafa..62743dcda071bc 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs @@ -46,19 +46,32 @@ 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; + if (bufferedRecord.Exception == null && message == null) + { + return; + } + + WriteInternal(scopeProvider, 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 +80,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 +126,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex if (exception != null) { // exception message - WriteMessage(textWriter, exception.ToString(), singleLine); + WriteMessage(textWriter, exception, singleLine); } if (singleLine) { diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs index 2d306fee1d0a4b..d2a110c856e9a4 100644 --- a/src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs +++ b/src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs @@ -36,19 +36,32 @@ 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; + if (bufferedRecord.Exception == null && message == null) + { + return; + } + + WriteInternal(scopeProvider, 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 +77,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 +102,7 @@ private void WriteInternal(IExternalScopeProvider? scopeProvider, TextWriter tex if (exception != null) { textWriter.Write(' '); - WriteReplacingNewLine(textWriter, exception.ToString()); + WriteReplacingNewLine(textWriter, exception); } // newline delimiter 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 5a71dad69bbdbd..05754e804eec9a 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 @@ -551,6 +551,68 @@ public void Log_LogsCorrectTimestamp(ConsoleLoggerFormat format, LogLevel level) } } + class BufferedLogRecord : IBufferedLogRecord + { + private readonly string _state; + private readonly DateTimeOffset _creationTime;; + + public BufferedLogRecord(string state, DateTimeOffset creationTime) + { + _state = state; + _creationTime = creationTime; + } + + public override string ToString() => _state; + + public void ExtractMetadata(out LogRecordMetadata metadata) + { + metadata = new LogRecordMetadata(); + metadata.CreationTime = _creationTime; + } + } + + [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(TimeSpan.FromTicks(12345)); + + // Act + logger.Log(level, 0, new BufferedLogRecord(_state, now), ex, _defaultFormatter); + + // 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(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(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 31c7dd27484029..2beeab918e6969 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 @@ + - diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/tests.sln b/src/libraries/Microsoft.Extensions.Logging.Console/tests/tests.sln new file mode 100644 index 00000000000000..d8adc3ea29275d --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Logging.Console/tests/tests.sln @@ -0,0 +1,25 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.5.002.0 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Logging.Console.Tests", "Microsoft.Extensions.Logging.Console.Tests\Microsoft.Extensions.Logging.Console.Tests.csproj", "{102F40FA-B0D2-454C-8961-246C8526E1DB}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {102F40FA-B0D2-454C-8961-246C8526E1DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {102F40FA-B0D2-454C-8961-246C8526E1DB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {102F40FA-B0D2-454C-8961-246C8526E1DB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {102F40FA-B0D2-454C-8961-246C8526E1DB}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {0FC155C1-F2A6-4E5E-A4BA-EDAE40CCBCD5} + EndGlobalSection +EndGlobal