Adds Console Log Formatting APIs (#38616)
authorMaryam Ariyan <maryam.ariyan@microsoft.com>
Wed, 15 Jul 2020 17:05:09 +0000 (10:05 -0700)
committerGitHub <noreply@github.com>
Wed, 15 Jul 2020 17:05:09 +0000 (10:05 -0700)
47 files changed:
src/libraries/Common/src/Interop/Windows/Kernel32/Interop.GetConsoleMode.cs
src/libraries/Common/src/System/Text/Json/PooledByteBufferWriter.cs [moved from src/libraries/System.Text.Json/src/System/Text/Json/Serialization/PooledByteBufferWriter.cs with 91% similarity]
src/libraries/Microsoft.Extensions.Logging.Abstractions/ref/Microsoft.Extensions.Logging.Abstractions.cs
src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntry.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/ref/Microsoft.Extensions.Logging.Console.cs
src/libraries/Microsoft.Extensions.Logging.Console/ref/Microsoft.Extensions.Logging.Console.csproj
src/libraries/Microsoft.Extensions.Logging.Console/src/AnsiLogConsole.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/AnsiParser.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/AnsiParsingLogConsole.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/AnsiSystemConsole.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatter.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterNames.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterOptions.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLogger.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerFactoryExtensions.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerFormat.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerOptions.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProcessor.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleLoggerProvider.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/FormatterOptionsMonitor.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/IConsole.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatter.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatterOptions.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/LogMessageEntry.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/Microsoft.Extensions.Logging.Console.csproj
src/libraries/Microsoft.Extensions.Logging.Console/src/Properties/InternalsVisibleTo.cs
src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatterOptions.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/TextWriterExtensions.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/src/WindowsLogConsole.cs [deleted file]
src/libraries/Microsoft.Extensions.Logging.Console/tests/AnsiParserTests.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/tests/Console/ConsoleContext.cs [moved from src/libraries/Microsoft.Extensions.Logging/tests/Common/Console/ConsoleContext.cs with 100% similarity]
src/libraries/Microsoft.Extensions.Logging.Console/tests/Console/ConsoleSink.cs [moved from src/libraries/Microsoft.Extensions.Logging/tests/Common/Console/ConsoleSink.cs with 100% similarity]
src/libraries/Microsoft.Extensions.Logging.Console/tests/Console/TestConsole.cs [moved from src/libraries/Microsoft.Extensions.Logging/tests/Common/Console/TestConsole.cs with 78% similarity]
src/libraries/Microsoft.Extensions.Logging.Console/tests/ConsoleFormatterTests.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/tests/ConsoleLoggerExtensionsTests.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/tests/ConsoleLoggerTest.cs [moved from src/libraries/Microsoft.Extensions.Logging/tests/Common/ConsoleLoggerTest.cs with 83% similarity]
src/libraries/Microsoft.Extensions.Logging.Console/tests/JsonConsoleFormatterTests.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests.csproj [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/tests/SimpleConsoleFormatterTests.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/tests/TestFormatterOptionsMonitor.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging.Console/tests/TextWriterExtensionsTests.cs [new file with mode: 0644]
src/libraries/Microsoft.Extensions.Logging/tests/Common/AnsiLogConsoleTest.cs [deleted file]
src/libraries/System.Text.Json/src/System.Text.Json.csproj
src/libraries/System.Text.Json/src/System/Text/Json/ThrowHelper.Serialization.cs

index f081fec..f71ac5b 100644 (file)
@@ -21,5 +21,7 @@ internal partial class Interop
         internal static extern bool SetConsoleMode(IntPtr handle, int mode);
 
         internal const int ENABLE_PROCESSED_INPUT = 0x0001;
+        internal const uint ENABLE_VIRTUAL_TERMINAL_PROCESSING = 0x0004;
+        internal const int STD_OUTPUT_HANDLE = -11;
     }
 }
@@ -3,7 +3,9 @@
 
 using System.Buffers;
 using System.Diagnostics;
+using System.Diagnostics.CodeAnalysis;
 using System.IO;
+using System.Runtime.CompilerServices;
 using System.Threading;
 using System.Threading.Tasks;
 
@@ -166,4 +168,14 @@ namespace System.Text.Json
             Debug.Assert(_rentedBuffer.Length - _index >= sizeHint);
         }
     }
+
+    internal static partial class ThrowHelper
+    {
+        [DoesNotReturn]
+        [MethodImpl(MethodImplOptions.NoInlining)]
+        public static void ThrowOutOfMemoryException_BufferMaximumSizeExceeded(uint capacity)
+        {
+            throw new OutOfMemoryException(SR.Format(SR.BufferMaximumSizeExceeded, capacity));
+        }
+    }
 }
index aa32585..ce0feb9 100644 (file)
@@ -125,6 +125,19 @@ namespace Microsoft.Extensions.Logging
 }
 namespace Microsoft.Extensions.Logging.Abstractions
 {
+    public readonly partial struct LogEntry<TState>
+    {
+        private readonly TState _State_k__BackingField;
+        private readonly object _dummy;
+        private readonly int _dummyPrimitive;
+        public LogEntry(Microsoft.Extensions.Logging.LogLevel logLevel, string category, Microsoft.Extensions.Logging.EventId eventId, TState state, System.Exception exception, System.Func<TState, System.Exception, string> formatter) { throw null; }
+        public string Category { get { throw null; } }
+        public Microsoft.Extensions.Logging.EventId EventId { get { throw null; } }
+        public System.Exception Exception { get { throw null; } }
+        public System.Func<TState, System.Exception, string> Formatter { get { throw null; } }
+        public Microsoft.Extensions.Logging.LogLevel LogLevel { get { throw null; } }
+        public TState State { get { throw null; } }
+    }
     public partial class NullLogger : Microsoft.Extensions.Logging.ILogger
     {
         internal NullLogger() { }
diff --git a/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntry.cs b/src/libraries/Microsoft.Extensions.Logging.Abstractions/src/LogEntry.cs
new file mode 100644 (file)
index 0000000..e0cab84
--- /dev/null
@@ -0,0 +1,62 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System;
+
+namespace Microsoft.Extensions.Logging.Abstractions
+{
+    /// <summary>
+    /// Holds the information for a single log entry.
+    /// </summary>
+    public readonly struct LogEntry<TState>
+    {
+        /// <summary>
+        /// Initializes an instance of the LogEntry struct.
+        /// </summary>
+        /// <param name="logLevel">The log level.</param>
+        /// <param name="category">The category name for the log.</param>
+        /// <param name="eventId">The log event Id.</param>
+        /// <param name="state">The state for which log is being written.</param>
+        /// <param name="exception">The log exception.</param>
+        /// <param name="formatter">The formatter.</param>
+        public LogEntry(LogLevel logLevel, string category, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
+        {
+            LogLevel = logLevel;
+            Category = category;
+            EventId = eventId;
+            State = state;
+            Exception = exception;
+            Formatter = formatter;
+        }
+
+        /// <summary>
+        /// Gets the LogLevel
+        /// </summary>
+        public LogLevel LogLevel { get; }
+
+        /// <summary>
+        /// Gets the log category
+        /// </summary>
+        public string Category { get; }
+
+        /// <summary>
+        /// Gets the log EventId
+        /// </summary>
+        public EventId EventId { get; }
+
+        /// <summary>
+        /// Gets the TState
+        /// </summary>
+        public TState State { get; }
+
+        /// <summary>
+        /// Gets the log exception
+        /// </summary>
+        public Exception Exception { get; }
+
+        /// <summary>
+        /// Gets the formatter
+        /// </summary>
+        public Func<TState, Exception, string> Formatter { get; }
+    }
+}
index 105b55c..13f7dfa 100644 (file)
@@ -10,10 +10,35 @@ namespace Microsoft.Extensions.Logging
     {
         public static Microsoft.Extensions.Logging.ILoggingBuilder AddConsole(this Microsoft.Extensions.Logging.ILoggingBuilder builder) { throw null; }
         public static Microsoft.Extensions.Logging.ILoggingBuilder AddConsole(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action<Microsoft.Extensions.Logging.Console.ConsoleLoggerOptions> configure) { throw null; }
+        public static Microsoft.Extensions.Logging.ILoggingBuilder AddConsoleFormatter<TFormatter, TOptions>(this Microsoft.Extensions.Logging.ILoggingBuilder builder) where TFormatter : Microsoft.Extensions.Logging.Console.ConsoleFormatter where TOptions : Microsoft.Extensions.Logging.Console.ConsoleFormatterOptions { throw null; }
+        public static Microsoft.Extensions.Logging.ILoggingBuilder AddConsoleFormatter<TFormatter, TOptions>(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action<TOptions> configure) where TFormatter : Microsoft.Extensions.Logging.Console.ConsoleFormatter where TOptions : Microsoft.Extensions.Logging.Console.ConsoleFormatterOptions { throw null; }
+        public static Microsoft.Extensions.Logging.ILoggingBuilder AddJsonConsole(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action<Microsoft.Extensions.Logging.Console.JsonConsoleFormatterOptions> configure) { throw null; }
+        public static Microsoft.Extensions.Logging.ILoggingBuilder AddSimpleConsole(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action<Microsoft.Extensions.Logging.Console.SimpleConsoleFormatterOptions> configure) { throw null; }
+        public static Microsoft.Extensions.Logging.ILoggingBuilder AddSystemdConsole(this Microsoft.Extensions.Logging.ILoggingBuilder builder, System.Action<Microsoft.Extensions.Logging.Console.ConsoleFormatterOptions> configure) { throw null; }
     }
 }
 namespace Microsoft.Extensions.Logging.Console
 {
+    public abstract partial class ConsoleFormatter
+    {
+        protected ConsoleFormatter(string name) { }
+        public string Name { get { throw null; } }
+        public abstract void Write<TState>(in Microsoft.Extensions.Logging.Abstractions.LogEntry<TState> logEntry, Microsoft.Extensions.Logging.IExternalScopeProvider scopeProvider, System.IO.TextWriter textWriter);
+    }
+    public static partial class ConsoleFormatterNames
+    {
+        public const string Json = "json";
+        public const string Simple = "simple";
+        public const string Systemd = "systemd";
+    }
+    public partial class ConsoleFormatterOptions
+    {
+        public ConsoleFormatterOptions() { }
+        public bool IncludeScopes { get { throw null; } set { } }
+        public string TimestampFormat { get { throw null; } set { } }
+        public bool UseUtcTimestamp { get { throw null; } set { } }
+    }
+    [System.ObsoleteAttribute("ConsoleLoggerFormat has been deprecated.", false)]
     public enum ConsoleLoggerFormat
     {
         Default = 0,
@@ -22,19 +47,37 @@ namespace Microsoft.Extensions.Logging.Console
     public partial class ConsoleLoggerOptions
     {
         public ConsoleLoggerOptions() { }
+        [System.ObsoleteAttribute("ConsoleLoggerOptions.DisableColors has been deprecated. Please use SimpleConsoleFormatterOptions.DisableColors instead.", false)]
         public bool DisableColors { get { throw null; } set { } }
+        [System.ObsoleteAttribute("ConsoleLoggerOptions.Format has been deprecated. Please use ConsoleLoggerOptions.FormatterName instead.", false)]
         public Microsoft.Extensions.Logging.Console.ConsoleLoggerFormat Format { get { throw null; } set { } }
+        public string FormatterName { get { throw null; } set { } }
+        [System.ObsoleteAttribute("ConsoleLoggerOptions.IncludeScopes has been deprecated. Please use ConsoleFormatterOptions.IncludeScopes instead.", false)]
         public bool IncludeScopes { get { throw null; } set { } }
         public Microsoft.Extensions.Logging.LogLevel LogToStandardErrorThreshold { get { throw null; } set { } }
+        [System.ObsoleteAttribute("ConsoleLoggerOptions.TimestampFormat has been deprecated. Please use ConsoleFormatterOptions.TimestampFormat instead.", false)]
         public string TimestampFormat { get { throw null; } set { } }
+        [System.ObsoleteAttribute("ConsoleLoggerOptions.UseUtcTimestamp has been deprecated. Please use ConsoleFormatterOptions.UseUtcTimestamp instead.", false)]
         public bool UseUtcTimestamp { get { throw null; } set { } }
     }
     [Microsoft.Extensions.Logging.ProviderAliasAttribute("Console")]
     public partial class ConsoleLoggerProvider : Microsoft.Extensions.Logging.ILoggerProvider, Microsoft.Extensions.Logging.ISupportExternalScope, System.IDisposable
     {
         public ConsoleLoggerProvider(Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.Extensions.Logging.Console.ConsoleLoggerOptions> options) { }
+        public ConsoleLoggerProvider(Microsoft.Extensions.Options.IOptionsMonitor<Microsoft.Extensions.Logging.Console.ConsoleLoggerOptions> options, System.Collections.Generic.IEnumerable<Microsoft.Extensions.Logging.Console.ConsoleFormatter> formatters) { }
         public Microsoft.Extensions.Logging.ILogger CreateLogger(string name) { throw null; }
         public void Dispose() { }
         public void SetScopeProvider(Microsoft.Extensions.Logging.IExternalScopeProvider scopeProvider) { }
     }
+    public partial class JsonConsoleFormatterOptions : Microsoft.Extensions.Logging.Console.ConsoleFormatterOptions
+    {
+        public JsonConsoleFormatterOptions() { }
+        public System.Text.Json.JsonWriterOptions JsonWriterOptions { get { throw null; } set { } }
+    }
+    public partial class SimpleConsoleFormatterOptions : Microsoft.Extensions.Logging.Console.ConsoleFormatterOptions
+    {
+        public SimpleConsoleFormatterOptions() { }
+        public bool DisableColors { get { throw null; } set { } }
+        public bool SingleLine { get { throw null; } set { } }
+    }
 }
index 17f4ef6..0756d56 100644 (file)
@@ -5,6 +5,7 @@
 
   <ItemGroup>
     <Compile Include="Microsoft.Extensions.Logging.Console.cs" />
+    <ProjectReference Include="..\..\System.Text.Json\ref\System.Text.Json.csproj" />
     <ProjectReference Include="..\..\Microsoft.Extensions.Logging.Abstractions\ref\Microsoft.Extensions.Logging.Abstractions.csproj" />
     <ProjectReference Include="..\..\Microsoft.Extensions.Logging\ref\Microsoft.Extensions.Logging.csproj" />
     <ProjectReference Include="..\..\Microsoft.Extensions.Options\ref\Microsoft.Extensions.Options.csproj" />
index a241361..da53c00 100644 (file)
@@ -7,119 +7,20 @@ using System.Text;
 namespace Microsoft.Extensions.Logging.Console
 {
     /// <summary>
-    /// For non-Windows platform consoles which understand the ANSI escape code sequences to represent color
+    /// For consoles which understand the ANSI escape code sequences to represent color
     /// </summary>
     internal class AnsiLogConsole : IConsole
     {
-        private readonly StringBuilder _outputBuilder;
         private readonly IAnsiSystemConsole _systemConsole;
 
-        public AnsiLogConsole(IAnsiSystemConsole systemConsole)
+        public AnsiLogConsole(bool stdErr = false)
         {
-            _outputBuilder = new StringBuilder();
-            _systemConsole = systemConsole;
+            _systemConsole = new AnsiSystemConsole(stdErr);
         }
 
-        public void Write(string message, ConsoleColor? background, ConsoleColor? foreground)
+        public void Write(string message)
         {
-            // Order: backgroundcolor, foregroundcolor, Message, reset foregroundcolor, reset backgroundcolor
-            if (background.HasValue)
-            {
-                _outputBuilder.Append(GetBackgroundColorEscapeCode(background.Value));
-            }
-
-            if (foreground.HasValue)
-            {
-                _outputBuilder.Append(GetForegroundColorEscapeCode(foreground.Value));
-            }
-
-            _outputBuilder.Append(message);
-
-            if (foreground.HasValue)
-            {
-                _outputBuilder.Append("\x1B[39m\x1B[22m"); // reset to default foreground color
-            }
-
-            if (background.HasValue)
-            {
-                _outputBuilder.Append("\x1B[49m"); // reset to the background color
-            }
-        }
-
-        public void WriteLine(string message, ConsoleColor? background, ConsoleColor? foreground)
-        {
-            Write(message, background, foreground);
-            _outputBuilder.AppendLine();
-        }
-
-        public void Flush()
-        {
-            _systemConsole.Write(_outputBuilder.ToString());
-            _outputBuilder.Clear();
-        }
-
-        private static string GetForegroundColorEscapeCode(ConsoleColor color)
-        {
-            switch (color)
-            {
-                case ConsoleColor.Black:
-                    return "\x1B[30m";
-                case ConsoleColor.DarkRed:
-                    return "\x1B[31m";
-                case ConsoleColor.DarkGreen:
-                    return "\x1B[32m";
-                case ConsoleColor.DarkYellow:
-                    return "\x1B[33m";
-                case ConsoleColor.DarkBlue:
-                    return "\x1B[34m";
-                case ConsoleColor.DarkMagenta:
-                    return "\x1B[35m";
-                case ConsoleColor.DarkCyan:
-                    return "\x1B[36m";
-                case ConsoleColor.Gray:
-                    return "\x1B[37m";
-                case ConsoleColor.Red:
-                    return "\x1B[1m\x1B[31m";
-                case ConsoleColor.Green:
-                    return "\x1B[1m\x1B[32m";
-                case ConsoleColor.Yellow:
-                    return "\x1B[1m\x1B[33m";
-                case ConsoleColor.Blue:
-                    return "\x1B[1m\x1B[34m";
-                case ConsoleColor.Magenta:
-                    return "\x1B[1m\x1B[35m";
-                case ConsoleColor.Cyan:
-                    return "\x1B[1m\x1B[36m";
-                case ConsoleColor.White:
-                    return "\x1B[1m\x1B[37m";
-                default:
-                    return "\x1B[39m\x1B[22m"; // default foreground color
-            }
-        }
-
-        private static string GetBackgroundColorEscapeCode(ConsoleColor color)
-        {
-            switch (color)
-            {
-                case ConsoleColor.Black:
-                    return "\x1B[40m";
-                case ConsoleColor.Red:
-                    return "\x1B[41m";
-                case ConsoleColor.Green:
-                    return "\x1B[42m";
-                case ConsoleColor.Yellow:
-                    return "\x1B[43m";
-                case ConsoleColor.Blue:
-                    return "\x1B[44m";
-                case ConsoleColor.Magenta:
-                    return "\x1B[45m";
-                case ConsoleColor.Cyan:
-                    return "\x1B[46m";
-                case ConsoleColor.White:
-                    return "\x1B[47m";
-                default:
-                    return "\x1B[49m"; // Use default background color
-            }
+            _systemConsole.Write(message);
         }
     }
 }
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/AnsiParser.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/AnsiParser.cs
new file mode 100644 (file)
index 0000000..4dc6aa9
--- /dev/null
@@ -0,0 +1,207 @@
+// 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.IO;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    internal class AnsiParser
+    {
+        private readonly Action<string, int, int, ConsoleColor?, ConsoleColor?> _onParseWrite;
+        public AnsiParser(Action<string, int, int, ConsoleColor?, ConsoleColor?> onParseWrite)
+        {
+            if (onParseWrite == null)
+            {
+                throw new ArgumentNullException(nameof(onParseWrite));
+            }
+            _onParseWrite = onParseWrite;
+        }
+
+        /// <summary>
+        /// Parses a subset of display attributes
+        /// Set Display Attributes
+        /// Set Attribute Mode [{attr1};...;{attrn}m
+        /// Sets multiple display attribute settings. The following lists standard attributes that are getting parsed:
+        /// 1 Bright
+        /// Foreground Colours
+        /// 30 Black
+        /// 31 Red
+        /// 32 Green
+        /// 33 Yellow
+        /// 34 Blue
+        /// 35 Magenta
+        /// 36 Cyan
+        /// 37 White
+        /// Background Colours
+        /// 40 Black
+        /// 41 Red
+        /// 42 Green
+        /// 43 Yellow
+        /// 44 Blue
+        /// 45 Magenta
+        /// 46 Cyan
+        /// 47 White
+        /// </summary>
+        public void Parse(string message)
+        {
+            int startIndex = -1;
+            int length = 0;
+            ConsoleColor? foreground = null;
+            ConsoleColor? background = null;
+            var span = message.AsSpan();
+            const char EscapeChar = '\x1B';
+            ConsoleColor? color = null;
+            bool isBright = false;
+            for (int i = 0; i < span.Length; i++)
+            {
+                if (span[i] == EscapeChar && span.Length >= i + 4 && span[i + 1] == '[')
+                {
+                    if (span[i + 3] == 'm')
+                    {
+    #if (NETSTANDARD2_0 || NETFRAMEWORK)
+                        if (ushort.TryParse(span.Slice(i + 2, length: 1).ToString(), out ushort escapeCode))
+    #else
+                        if (ushort.TryParse(span.Slice(i + 2, length: 1), out ushort escapeCode))
+    #endif
+                        {
+                            if (startIndex != -1)
+                            {
+                                _onParseWrite(message, startIndex, length, background, foreground);
+                                startIndex = -1;
+                                length = 0;
+                            }
+                            if (escapeCode == 1)
+                                isBright = true;
+                            i += 3;
+                            continue;
+                        }
+                    }
+                    else if (span.Length >= i + 5 && span[i + 4] == 'm')
+                    {
+    #if (NETSTANDARD2_0 || NETFRAMEWORK)
+                        if (ushort.TryParse(span.Slice(i + 2, length: 2).ToString(), out ushort escapeCode))
+    #else
+                        if (ushort.TryParse(span.Slice(i + 2, length: 2), out ushort escapeCode))
+    #endif
+                        {
+                            if (startIndex != -1)
+                            {
+                                _onParseWrite(message, startIndex, length, background, foreground);
+                                startIndex = -1;
+                                length = 0;
+                            }
+                            if (TryGetForegroundColor(escapeCode, isBright, out color))
+                            {
+                                foreground = color;
+                                isBright = false;
+                            }
+                            else if (TryGetBackgroundColor(escapeCode, out color))
+                            {
+                                background = color;
+                            }
+                            i += 4;
+                            continue;
+                        }
+                    }
+                }
+                if (startIndex == -1)
+                {
+                    startIndex = i;
+                }
+                int nextEscapeIndex = -1;
+                if (i < message.Length - 1)
+                {
+                    nextEscapeIndex = message.IndexOf(EscapeChar, i + 1);
+                }
+                if (nextEscapeIndex < 0)
+                {
+                    length = message.Length - startIndex;
+                    break;
+                }
+                length = nextEscapeIndex - startIndex;
+                i = nextEscapeIndex - 1;
+            }
+            if (startIndex != -1)
+            {
+                _onParseWrite(message, startIndex, length, background, foreground);
+            }
+        }
+
+        internal const string DefaultForegroundColor = "\x1B[39m\x1B[22m"; // reset to default foreground color
+        internal const string DefaultBackgroundColor = "\x1B[49m"; // reset to the background color
+
+        internal static string GetForegroundColorEscapeCode(ConsoleColor color)
+        {
+            return color switch
+            {
+                ConsoleColor.Black => "\x1B[30m",
+                ConsoleColor.DarkRed => "\x1B[31m",
+                ConsoleColor.DarkGreen => "\x1B[32m",
+                ConsoleColor.DarkYellow => "\x1B[33m",
+                ConsoleColor.DarkBlue => "\x1B[34m",
+                ConsoleColor.DarkMagenta => "\x1B[35m",
+                ConsoleColor.DarkCyan => "\x1B[36m",
+                ConsoleColor.Gray => "\x1B[37m",
+                ConsoleColor.Red => "\x1B[1m\x1B[31m",
+                ConsoleColor.Green => "\x1B[1m\x1B[32m",
+                ConsoleColor.Yellow => "\x1B[1m\x1B[33m",
+                ConsoleColor.Blue => "\x1B[1m\x1B[34m",
+                ConsoleColor.Magenta => "\x1B[1m\x1B[35m",
+                ConsoleColor.Cyan => "\x1B[1m\x1B[36m",
+                ConsoleColor.White => "\x1B[1m\x1B[37m",
+                _ => DefaultForegroundColor // default foreground color
+            };
+        }
+
+        internal static string GetBackgroundColorEscapeCode(ConsoleColor color)
+        {
+            return color switch
+            {
+                ConsoleColor.Black => "\x1B[40m",
+                ConsoleColor.DarkRed => "\x1B[41m",
+                ConsoleColor.DarkGreen => "\x1B[42m",
+                ConsoleColor.DarkYellow => "\x1B[43m",
+                ConsoleColor.DarkBlue => "\x1B[44m",
+                ConsoleColor.DarkMagenta => "\x1B[45m",
+                ConsoleColor.DarkCyan => "\x1B[46m",
+                ConsoleColor.Gray => "\x1B[47m",
+                _ => DefaultBackgroundColor // Use default background color
+            };
+        }
+
+        private static bool TryGetForegroundColor(int number, bool isBright, out ConsoleColor? color)
+        {
+            color = number switch
+            {
+                30 => ConsoleColor.Black,
+                31 => isBright ? ConsoleColor.Red: ConsoleColor.DarkRed,
+                32 => isBright ? ConsoleColor.Green: ConsoleColor.DarkGreen,
+                33 => isBright ? ConsoleColor.Yellow: ConsoleColor.DarkYellow,
+                34 => isBright ? ConsoleColor.Blue: ConsoleColor.DarkBlue,
+                35 => isBright ? ConsoleColor.Magenta: ConsoleColor.DarkMagenta,
+                36 => isBright ? ConsoleColor.Cyan: ConsoleColor.DarkCyan,
+                37 => isBright ? ConsoleColor.White: ConsoleColor.Gray,
+                _ => null
+            };
+            return color != null || number == 39;
+        }
+
+        private static bool TryGetBackgroundColor(int number, out ConsoleColor? color)
+        {
+            color = number switch
+            {
+                40 => ConsoleColor.Black,
+                41 => ConsoleColor.DarkRed,
+                42 => ConsoleColor.DarkGreen,
+                43 => ConsoleColor.DarkYellow,
+                44 => ConsoleColor.DarkBlue,
+                45 => ConsoleColor.DarkMagenta,
+                46 => ConsoleColor.DarkCyan,
+                47 => ConsoleColor.Gray,
+                _ => null
+            };
+            return color != null || number == 49;
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/AnsiParsingLogConsole.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/AnsiParsingLogConsole.cs
new file mode 100644 (file)
index 0000000..a71570a
--- /dev/null
@@ -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.IO;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    internal class AnsiParsingLogConsole : IConsole
+    {
+        private readonly TextWriter _textWriter;
+        private readonly AnsiParser _parser;
+
+        public AnsiParsingLogConsole(bool stdErr = false)
+        {
+            _textWriter = stdErr ? System.Console.Error : System.Console.Out;
+            _parser = new AnsiParser(WriteToConsole);
+        }
+
+        public void Write(string message)
+        {
+            _parser.Parse(message);
+        }
+
+        private bool SetColor(ConsoleColor? background, ConsoleColor? foreground)
+        {
+            var backgroundChanged = SetBackgroundColor(background);
+            return SetForegroundColor(foreground) || backgroundChanged;
+        }
+
+        private bool SetBackgroundColor(ConsoleColor? background)
+        {
+            if (background.HasValue)
+            {
+                System.Console.BackgroundColor = background.Value;
+                return true;
+            }
+            return false;
+        }
+
+        private bool SetForegroundColor(ConsoleColor? foreground)
+        {
+            if (foreground.HasValue)
+            {
+                System.Console.ForegroundColor = foreground.Value;
+                return true;
+            }
+            return false;
+        }
+
+        private void ResetColor()
+        {
+            System.Console.ResetColor();
+        }
+
+        private void WriteToConsole(string message, int startIndex, int length, ConsoleColor? background, ConsoleColor? foreground)
+        {
+            ReadOnlySpan<char> span = message.AsSpan().Slice(startIndex, length);
+            var colorChanged = SetColor(background, foreground);
+#if (NETSTANDARD2_0 || NETFRAMEWORK)
+            _textWriter.Write(span.ToString());
+#else
+            _textWriter.Write(span);
+#endif
+            if (colorChanged)
+            {
+                ResetColor();
+            }
+        }
+    }
+}
index a44af3e..adfb28f 100644 (file)
@@ -9,7 +9,7 @@ namespace Microsoft.Extensions.Logging.Console
     {
         private readonly TextWriter _textWriter;
 
-        public AnsiSystemConsole(bool stdErr = false)
+        public AnsiSystemConsole(bool stdErr)
         {
             _textWriter = stdErr ? System.Console.Error : System.Console.Out;
         }
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatter.cs
new file mode 100644 (file)
index 0000000..3d19e4b
--- /dev/null
@@ -0,0 +1,38 @@
+// 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.IO;
+using System.Text;
+using Microsoft.Extensions.Logging.Abstractions;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    /// <summary>
+    /// Allows custom log messages formatting
+    /// </summary>
+    public abstract class ConsoleFormatter
+    {
+        protected ConsoleFormatter(string name)
+        {
+            Name = name ?? throw new ArgumentNullException(nameof(name));
+        }
+
+        /// <summary>
+        /// Gets the name associated with the console log formatter.
+        /// </summary>
+        public string Name { get; }
+
+        /// <summary>
+        /// Writes the log message to the specified TextWriter.
+        /// </summary>
+        /// <remarks>
+        /// if the formatter wants to write colors to the console, it can do so by embedding ANSI color codes into the string
+        /// </remarks>
+        /// <param name="logEntry">The log entry.</param>
+        /// <param name="scopeProvider">The provider of scope data.</param>
+        /// <param name="textWriter">The string writer embedding ansi code for colors.</param>
+        /// <typeparam name="TState">The type of the object to be written.</typeparam>
+        public abstract void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter);
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterNames.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterNames.cs
new file mode 100644 (file)
index 0000000..63c46f3
--- /dev/null
@@ -0,0 +1,12 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    public static class ConsoleFormatterNames
+    {
+        public const string Simple = "simple";
+        public const string Json = "json";
+        public const string Systemd = "systemd";
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/ConsoleFormatterOptions.cs
new file mode 100644 (file)
index 0000000..7513ba8
--- /dev/null
@@ -0,0 +1,31 @@
+// 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.Text;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    /// <summary>
+    /// Options for the built-in console log formatter.
+    /// </summary>
+    public class ConsoleFormatterOptions
+    {
+        public ConsoleFormatterOptions() { }
+
+        /// <summary>
+        /// Includes scopes when <see langword="true" />.
+        /// </summary>
+        public bool IncludeScopes { get; set; }
+
+        /// <summary>
+        /// Gets or sets format string used to format timestamp in logging messages. Defaults to <c>null</c>.
+        /// </summary>
+        public string TimestampFormat { get; set; }
+
+        /// <summary>
+        /// Gets or sets indication whether or not UTC timezone should be used to for timestamps in logging messages. Defaults to <c>false</c>.
+        /// </summary>
+        public bool UseUtcTimestamp { get; set; }
+    }
+}
index e790eeb..5e47345 100644 (file)
@@ -3,22 +3,17 @@
 
 using System;
 using System.Diagnostics;
+using System.IO;
 using System.Text;
+using Microsoft.Extensions.Logging.Abstractions;
 
 namespace Microsoft.Extensions.Logging.Console
 {
     internal class ConsoleLogger : ILogger
     {
-        private const string LoglevelPadding = ": ";
-        private static readonly string _messagePadding = new string(' ', GetLogLevelString(LogLevel.Information).Length + LoglevelPadding.Length);
-        private static readonly string _newLineWithMessagePadding = Environment.NewLine + _messagePadding;
-
         private readonly string _name;
         private readonly ConsoleLoggerProcessor _queueProcessor;
 
-        [ThreadStatic]
-        private static StringBuilder _logBuilder;
-
         internal ConsoleLogger(string name, ConsoleLoggerProcessor loggerProcessor)
         {
             if (name == null)
@@ -30,186 +25,40 @@ namespace Microsoft.Extensions.Logging.Console
             _queueProcessor = loggerProcessor;
         }
 
+        internal ConsoleFormatter Formatter { get; set; }
         internal IExternalScopeProvider ScopeProvider { get; set; }
 
         internal ConsoleLoggerOptions Options { get; set; }
 
+        [ThreadStatic]
+        private static StringWriter t_stringWriter;
+
         public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func<TState, Exception, string> formatter)
         {
             if (!IsEnabled(logLevel))
             {
                 return;
             }
-
             if (formatter == null)
             {
                 throw new ArgumentNullException(nameof(formatter));
             }
+            t_stringWriter ??= new StringWriter();
+            LogEntry<TState> logEntry = new LogEntry<TState>(logLevel, _name, eventId, state, exception, formatter);
+            Formatter.Write(in logEntry, ScopeProvider, t_stringWriter);
 
-            string message = formatter(state, exception);
-
-            if (!string.IsNullOrEmpty(message) || exception != null)
-            {
-                WriteMessage(logLevel, _name, eventId.Id, message, exception);
-            }
-        }
-
-        public virtual void WriteMessage(LogLevel logLevel, string logName, int eventId, string message, Exception exception)
-        {
-            ConsoleLoggerFormat format = Options.Format;
-            Debug.Assert(format >= ConsoleLoggerFormat.Default && format <= ConsoleLoggerFormat.Systemd);
-
-            StringBuilder logBuilder = _logBuilder;
-            _logBuilder = null;
-
-            if (logBuilder == null)
-            {
-                logBuilder = new StringBuilder();
-            }
-
-            LogMessageEntry entry;
-            if (format == ConsoleLoggerFormat.Default)
-            {
-                entry = CreateDefaultLogMessage(logBuilder, logLevel, logName, eventId, message, exception);
-            }
-            else if (format == ConsoleLoggerFormat.Systemd)
-            {
-                entry = CreateSystemdLogMessage(logBuilder, logLevel, logName, eventId, message, exception);
-            }
-            else
-            {
-                entry = default;
-            }
-            _queueProcessor.EnqueueMessage(entry);
-
-            logBuilder.Clear();
-            if (logBuilder.Capacity > 1024)
-            {
-                logBuilder.Capacity = 1024;
-            }
-            _logBuilder = logBuilder;
-        }
-
-        private LogMessageEntry CreateDefaultLogMessage(StringBuilder logBuilder, LogLevel logLevel, string logName, int eventId, string message, Exception exception)
-        {
-            // Example:
-            // info: ConsoleApp.Program[10]
-            //       Request received
-
-            ConsoleColors logLevelColors = GetLogLevelConsoleColors(logLevel);
-            string logLevelString = GetLogLevelString(logLevel);
-            // category and event id
-            logBuilder.Append(LoglevelPadding);
-            logBuilder.Append(logName);
-            logBuilder.Append('[');
-            logBuilder.Append(eventId);
-            logBuilder.AppendLine("]");
-
-            // scope information
-            GetScopeInformation(logBuilder, multiLine: true);
-
-            if (!string.IsNullOrEmpty(message))
-            {
-                // message
-                logBuilder.Append(_messagePadding);
-
-                int len = logBuilder.Length;
-                logBuilder.AppendLine(message);
-                logBuilder.Replace(Environment.NewLine, _newLineWithMessagePadding, len, message.Length);
-            }
-
-            // Example:
-            // System.InvalidOperationException
-            //    at Namespace.Class.Function() in File:line X
-            if (exception != null)
-            {
-                // exception message
-                logBuilder.AppendLine(exception.ToString());
-            }
-
-            string timestamp = null;
-            string timestampFormat = Options.TimestampFormat;
-            if (timestampFormat != null)
-            {
-                DateTime dateTime = GetCurrentDateTime();
-                timestamp = dateTime.ToString(timestampFormat);
-            }
-
-            return new LogMessageEntry(
-                message: logBuilder.ToString(),
-                timeStamp: timestamp,
-                levelString: logLevelString,
-                levelBackground: logLevelColors.Background,
-                levelForeground: logLevelColors.Foreground,
-                messageColor: null,
-                logAsError: logLevel >= Options.LogToStandardErrorThreshold
-            );
-        }
-
-        private LogMessageEntry CreateSystemdLogMessage(StringBuilder logBuilder, LogLevel logLevel, string logName, int eventId, string message, Exception exception)
-        {
-            // systemd reads messages from standard out line-by-line in a '<pri>message' format.
-            // newline characters are treated as message delimiters, so we must replace them.
-            // Messages longer than the journal LineMax setting (default: 48KB) are cropped.
-            // Example:
-            // <6>ConsoleApp.Program[10] Request received
-
-            // loglevel
-            string logLevelString = GetSyslogSeverityString(logLevel);
-            logBuilder.Append(logLevelString);
-
-            // timestamp
-            string timestampFormat = Options.TimestampFormat;
-            if (timestampFormat != null)
-            {
-                DateTime dateTime = GetCurrentDateTime();
-                logBuilder.Append(dateTime.ToString(timestampFormat));
-            }
-
-            // category and event id
-            logBuilder.Append(logName);
-            logBuilder.Append('[');
-            logBuilder.Append(eventId);
-            logBuilder.Append(']');
-
-            // scope information
-            GetScopeInformation(logBuilder, multiLine: false);
-
-            // message
-            if (!string.IsNullOrEmpty(message))
+            var sb = t_stringWriter.GetStringBuilder();
+            if (sb.Length == 0)
             {
-                logBuilder.Append(' ');
-                // message
-                AppendAndReplaceNewLine(logBuilder, message);
+                return;
             }
-
-            // exception
-            // System.InvalidOperationException at Namespace.Class.Function() in File:line X
-            if (exception != null)
+            string computedAnsiString = sb.ToString();
+            sb.Clear();
+            if (sb.Capacity > 1024)
             {
-                logBuilder.Append(' ');
-                AppendAndReplaceNewLine(logBuilder, exception.ToString());
+                sb.Capacity = 1024;
             }
-
-            // newline delimiter
-            logBuilder.Append(Environment.NewLine);
-
-            return new LogMessageEntry(
-                message: logBuilder.ToString(),
-                logAsError: logLevel >= Options.LogToStandardErrorThreshold
-            );
-
-            static void AppendAndReplaceNewLine(StringBuilder sb, string message)
-            {
-                int len = sb.Length;
-                sb.Append(message);
-                sb.Replace(Environment.NewLine, " ", len, message.Length);
-            }
-        }
-
-        private DateTime GetCurrentDateTime()
-        {
-            return Options.UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now;
+            _queueProcessor.EnqueueMessage(new LogMessageEntry(computedAnsiString, logAsError: logLevel >= Options.LogToStandardErrorThreshold));
         }
 
         public bool IsEnabled(LogLevel logLevel)
@@ -218,116 +67,5 @@ namespace Microsoft.Extensions.Logging.Console
         }
 
         public IDisposable BeginScope<TState>(TState state) => ScopeProvider?.Push(state) ?? NullScope.Instance;
-
-        private static string GetLogLevelString(LogLevel logLevel)
-        {
-            switch (logLevel)
-            {
-                case LogLevel.Trace:
-                    return "trce";
-                case LogLevel.Debug:
-                    return "dbug";
-                case LogLevel.Information:
-                    return "info";
-                case LogLevel.Warning:
-                    return "warn";
-                case LogLevel.Error:
-                    return "fail";
-                case LogLevel.Critical:
-                    return "crit";
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(logLevel));
-            }
-        }
-
-        private static string GetSyslogSeverityString(LogLevel logLevel)
-        {
-            // 'Syslog Message Severities' from https://tools.ietf.org/html/rfc5424.
-            switch (logLevel)
-            {
-                case LogLevel.Trace:
-                case LogLevel.Debug:
-                    return "<7>"; // debug-level messages
-                case LogLevel.Information:
-                    return "<6>"; // informational messages
-                case LogLevel.Warning:
-                    return "<4>"; // warning conditions
-                case LogLevel.Error:
-                    return "<3>"; // error conditions
-                case LogLevel.Critical:
-                    return "<2>"; // critical conditions
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(logLevel));
-            }
-        }
-
-        private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
-        {
-            if (!Options.DisableColors)
-            {
-                // We must explicitly set the background color if we are setting the foreground color,
-                // since just setting one can look bad on the users console.
-                switch (logLevel)
-                {
-                    case LogLevel.Critical:
-                        return new ConsoleColors(ConsoleColor.White, ConsoleColor.Red);
-                    case LogLevel.Error:
-                        return new ConsoleColors(ConsoleColor.Black, ConsoleColor.Red);
-                    case LogLevel.Warning:
-                        return new ConsoleColors(ConsoleColor.Yellow, ConsoleColor.Black);
-                    case LogLevel.Information:
-                        return new ConsoleColors(ConsoleColor.DarkGreen, ConsoleColor.Black);
-                    case LogLevel.Debug:
-                        return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black);
-                    case LogLevel.Trace:
-                        return new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black);
-                }
-            }
-
-            return new ConsoleColors(null, null);
-        }
-
-        private void GetScopeInformation(StringBuilder stringBuilder, bool multiLine)
-        {
-            IExternalScopeProvider scopeProvider = ScopeProvider;
-            if (Options.IncludeScopes && scopeProvider != null)
-            {
-                int initialLength = stringBuilder.Length;
-
-                scopeProvider.ForEachScope((scope, state) =>
-                {
-                    (StringBuilder builder, int paddAt) = state;
-                    bool padd = paddAt == builder.Length;
-                    if (padd)
-                    {
-                        builder.Append(_messagePadding);
-                        builder.Append("=> ");
-                    }
-                    else
-                    {
-                        builder.Append(" => ");
-                    }
-                    builder.Append(scope);
-                }, (stringBuilder, multiLine ? initialLength : -1));
-
-                if (stringBuilder.Length > initialLength && multiLine)
-                {
-                    stringBuilder.AppendLine();
-                }
-            }
-        }
-
-        private readonly struct ConsoleColors
-        {
-            public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background)
-            {
-                Foreground = foreground;
-                Background = background;
-            }
-
-            public ConsoleColor? Foreground { get; }
-
-            public ConsoleColor? Background { get; }
-        }
     }
 }
index ea76497..f701d0d 100644 (file)
@@ -4,8 +4,11 @@
 using System;
 using Microsoft.Extensions.DependencyInjection;
 using Microsoft.Extensions.DependencyInjection.Extensions;
+using Microsoft.Extensions.Logging.Abstractions;
 using Microsoft.Extensions.Logging.Configuration;
 using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
 
 namespace Microsoft.Extensions.Logging
 {
@@ -19,8 +22,13 @@ namespace Microsoft.Extensions.Logging
         {
             builder.AddConfiguration();
 
+            builder.AddConsoleFormatter<JsonConsoleFormatter, JsonConsoleFormatterOptions>();
+            builder.AddConsoleFormatter<SystemdConsoleFormatter, ConsoleFormatterOptions>();
+            builder.AddConsoleFormatter<SimpleConsoleFormatter, SimpleConsoleFormatterOptions>();
+
             builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, ConsoleLoggerProvider>());
             LoggerProviderOptions.RegisterProviderOptions<ConsoleLoggerOptions, ConsoleLoggerProvider>(builder.Services);
+
             return builder;
         }
 
@@ -41,5 +49,104 @@ namespace Microsoft.Extensions.Logging
 
             return builder;
         }
+
+        /// <summary>
+        /// Add and configure a console log formatter named 'json' to the factory.
+        /// </summary>
+        /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
+        /// <param name="configure">A delegate to configure the <see cref="ConsoleLogger"/> options for the built-in default log formatter.</param>
+        public static ILoggingBuilder AddSimpleConsole(this ILoggingBuilder builder, Action<SimpleConsoleFormatterOptions> configure)
+        {
+            return builder.AddConsoleWithFormatter<SimpleConsoleFormatterOptions>(ConsoleFormatterNames.Simple, configure);
+        }
+
+        /// <summary>
+        /// Add and configure a console log formatter named 'json' to the factory.
+        /// </summary>
+        /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
+        /// <param name="configure">A delegate to configure the <see cref="ConsoleLogger"/> options for the built-in json log formatter.</param>
+        public static ILoggingBuilder AddJsonConsole(this ILoggingBuilder builder, Action<JsonConsoleFormatterOptions> configure)
+        {
+            return builder.AddConsoleWithFormatter<JsonConsoleFormatterOptions>(ConsoleFormatterNames.Json, configure);
+        }
+
+        /// <summary>
+        /// Add and configure a console log formatter named 'systemd' to the factory.
+        /// </summary>
+        /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
+        /// <param name="configure">A delegate to configure the <see cref="ConsoleLogger"/> options for the built-in systemd log formatter.</param>
+        public static ILoggingBuilder AddSystemdConsole(this ILoggingBuilder builder, Action<ConsoleFormatterOptions> configure)
+        {
+            return builder.AddConsoleWithFormatter<ConsoleFormatterOptions>(ConsoleFormatterNames.Systemd, configure);
+        }
+
+        internal static ILoggingBuilder AddConsoleWithFormatter<TOptions>(this ILoggingBuilder builder, string name, Action<TOptions> configure)
+            where TOptions : ConsoleFormatterOptions
+        {
+            if (configure == null)
+            {
+                throw new ArgumentNullException(nameof(configure));
+            }
+            builder.AddConsole((ConsoleLoggerOptions options) => options.FormatterName = name);
+            builder.Services.Configure(configure);
+
+            return builder;
+        }
+
+        /// <summary>
+        /// Adds a custom console logger formatter 'TFormatter' to be configured with options 'TOptions'.
+        /// </summary>
+        /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
+        public static ILoggingBuilder AddConsoleFormatter<TFormatter, TOptions>(this ILoggingBuilder builder)
+            where TOptions : ConsoleFormatterOptions
+            where TFormatter : ConsoleFormatter
+        {
+            builder.AddConfiguration();
+
+            builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ConsoleFormatter, TFormatter>());
+            builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<TOptions>, ConsoleLoggerFormatterConfigureOptions<TFormatter, TOptions>>());
+            builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IOptionsChangeTokenSource<TOptions>, ConsoleLoggerFormatterOptionsChangeTokenSource<TFormatter, TOptions>>());
+
+            return builder;
+        }
+
+        /// <summary>
+        /// Adds a custom console logger formatter 'TFormatter' to be configured with options 'TOptions'.
+        /// </summary>
+        /// <param name="builder">The <see cref="ILoggingBuilder"/> to use.</param>
+        /// <param name="configure">A delegate to configure options 'TOptions' for custom formatter 'TFormatter'.</param>
+        public static ILoggingBuilder AddConsoleFormatter<TFormatter, TOptions>(this ILoggingBuilder builder, Action<TOptions> configure)
+            where TOptions : ConsoleFormatterOptions
+            where TFormatter : ConsoleFormatter
+        {
+            if (configure == null)
+            {
+                throw new ArgumentNullException(nameof(configure));
+            }
+
+            builder.AddConsoleFormatter<TFormatter, TOptions>();
+            builder.Services.Configure(configure);
+            return builder;
+        }
+    }
+
+    internal class ConsoleLoggerFormatterConfigureOptions<TFormatter, TOptions> : ConfigureFromConfigurationOptions<TOptions>
+        where TOptions : ConsoleFormatterOptions
+        where TFormatter : ConsoleFormatter
+    {
+        public ConsoleLoggerFormatterConfigureOptions(ILoggerProviderConfiguration<ConsoleLoggerProvider> providerConfiguration) :
+            base(providerConfiguration.Configuration.GetSection("FormatterOptions"))
+        {
+        }
+    }
+
+    internal class ConsoleLoggerFormatterOptionsChangeTokenSource<TFormatter, TOptions> : ConfigurationChangeTokenSource<TOptions>
+        where TOptions : ConsoleFormatterOptions
+        where TFormatter : ConsoleFormatter
+    {
+        public ConsoleLoggerFormatterOptionsChangeTokenSource(ILoggerProviderConfiguration<ConsoleLoggerProvider> providerConfiguration)
+            : base(providerConfiguration.Configuration.GetSection("FormatterOptions"))
+        {
+        }
     }
 }
index 818b56c..d5baee2 100644 (file)
@@ -6,6 +6,7 @@ namespace Microsoft.Extensions.Logging.Console
     /// <summary>
     /// Format of <see cref="ConsoleLogger" /> messages.
     /// </summary>
+    [System.ObsoleteAttribute("ConsoleLoggerFormat has been deprecated.", false)]
     public enum ConsoleLoggerFormat
     {
         /// <summary>
index 6b67539..cda6f89 100644 (file)
@@ -10,21 +10,18 @@ namespace Microsoft.Extensions.Logging.Console
     /// </summary>
     public class ConsoleLoggerOptions
     {
-        private ConsoleLoggerFormat _format = ConsoleLoggerFormat.Default;
-
-        /// <summary>
-        /// Includes scopes when <see langword="true" />.
-        /// </summary>
-        public bool IncludeScopes { get; set; }
-
         /// <summary>
         /// Disables colors when <see langword="true" />.
         /// </summary>
+        [System.ObsoleteAttribute("ConsoleLoggerOptions.DisableColors has been deprecated. Please use SimpleConsoleFormatterOptions.DisableColors instead.", false)]
         public bool DisableColors { get; set; }
 
+#pragma warning disable CS0618
+        private ConsoleLoggerFormat _format = ConsoleLoggerFormat.Default;
         /// <summary>
         /// Gets or sets log message format. Defaults to <see cref="ConsoleLoggerFormat.Default" />.
         /// </summary>
+        [System.ObsoleteAttribute("ConsoleLoggerOptions.Format has been deprecated. Please use ConsoleLoggerOptions.FormatterName instead.", false)]
         public ConsoleLoggerFormat Format
         {
             get => _format;
@@ -36,9 +33,21 @@ namespace Microsoft.Extensions.Logging.Console
                 }
                 _format = value;
             }
+#pragma warning restore CS0618
         }
 
         /// <summary>
+        /// Name of the log message formatter to use. Defaults to "simple" />.
+        /// </summary>
+        public string FormatterName { get; set; }
+
+        /// <summary>
+        /// Includes scopes when <see langword="true" />.
+        /// </summary>
+        [System.ObsoleteAttribute("ConsoleLoggerOptions.IncludeScopes has been deprecated. Please use ConsoleFormatterOptions.IncludeScopes instead.", false)]
+        public bool IncludeScopes { get; set; }
+
+        /// <summary>
         /// Gets or sets value indicating the minimum level of messages that would get written to <c>Console.Error</c>.
         /// </summary>
         public LogLevel LogToStandardErrorThreshold { get; set; } = LogLevel.None;
@@ -46,11 +55,13 @@ namespace Microsoft.Extensions.Logging.Console
         /// <summary>
         /// Gets or sets format string used to format timestamp in logging messages. Defaults to <c>null</c>.
         /// </summary>
+        [System.ObsoleteAttribute("ConsoleLoggerOptions.TimestampFormat has been deprecated. Please use ConsoleFormatterOptions.TimestampFormat instead.", false)]
         public string TimestampFormat { get; set; }
 
         /// <summary>
         /// Gets or sets indication whether or not UTC timezone should be used to for timestamps in logging messages. Defaults to <c>false</c>.
         /// </summary>
+        [System.ObsoleteAttribute("ConsoleLoggerOptions.UseUtcTimestamp has been deprecated. Please use ConsoleFormatterOptions.UseUtcTimestamp instead.", false)]
         public bool UseUtcTimestamp { get; set; }
     }
-}
+}
\ No newline at end of file
index 4c5109a..83a49c2 100644 (file)
@@ -49,22 +49,10 @@ namespace Microsoft.Extensions.Logging.Console
         }
 
         // for testing
-        internal virtual void WriteMessage(LogMessageEntry message)
+        internal virtual void WriteMessage(LogMessageEntry entry)
         {
-            IConsole console = message.LogAsError ? ErrorConsole : Console;
-
-            if (message.TimeStamp != null)
-            {
-                console.Write(message.TimeStamp, message.MessageColor, message.MessageColor);
-            }
-
-            if (message.LevelString != null)
-            {
-                console.Write(message.LevelString, message.LevelBackground, message.LevelForeground);
-            }
-
-            console.Write(message.Message, message.MessageColor, message.MessageColor);
-            console.Flush();
+            IConsole console = entry.LogAsError ? ErrorConsole : Console;
+            console.Write(entry.Message);
         }
 
         private void ProcessLogQueue()
index 2a9d02b..c988043 100644 (file)
@@ -3,7 +3,10 @@
 
 using System;
 using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
 using System.Runtime.InteropServices;
+using Microsoft.Extensions.Logging.Configuration;
 using Microsoft.Extensions.Options;
 
 namespace Microsoft.Extensions.Logging.Console
@@ -16,6 +19,7 @@ namespace Microsoft.Extensions.Logging.Console
     {
         private readonly IOptionsMonitor<ConsoleLoggerOptions> _options;
         private readonly ConcurrentDictionary<string, ConsoleLogger> _loggers;
+        private ConcurrentDictionary<string, ConsoleFormatter> _formatters;
         private readonly ConsoleLoggerProcessor _messageQueue;
 
         private IDisposable _optionsReloadToken;
@@ -26,43 +30,148 @@ namespace Microsoft.Extensions.Logging.Console
         /// </summary>
         /// <param name="options">The options to create <see cref="ConsoleLogger"/> instances with.</param>
         public ConsoleLoggerProvider(IOptionsMonitor<ConsoleLoggerOptions> options)
+            : this(options, Enumerable.Empty<ConsoleFormatter>()) { }
+
+        /// <summary>
+        /// Creates an instance of <see cref="ConsoleLoggerProvider"/>.
+        /// </summary>
+        /// <param name="options">The options to create <see cref="ConsoleLogger"/> instances with.</param>
+        /// <param name="formatters">Log formatters added for <see cref="ConsoleLogger"/> insteaces.</param>
+        public ConsoleLoggerProvider(IOptionsMonitor<ConsoleLoggerOptions> options, IEnumerable<ConsoleFormatter> formatters)
         {
             _options = options;
             _loggers = new ConcurrentDictionary<string, ConsoleLogger>();
+            SetFormatters(formatters);
 
             ReloadLoggerOptions(options.CurrentValue);
             _optionsReloadToken = _options.OnChange(ReloadLoggerOptions);
 
             _messageQueue = new ConsoleLoggerProcessor();
-            if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            if (DoesConsoleSupportAnsi())
+            {
+                _messageQueue.Console = new AnsiLogConsole();
+                _messageQueue.ErrorConsole = new AnsiLogConsole(stdErr: true);
+            }
+            else
+            {
+                _messageQueue.Console = new AnsiParsingLogConsole();
+                _messageQueue.ErrorConsole = new AnsiParsingLogConsole(stdErr: true);
+            }
+        }
+
+        private static bool DoesConsoleSupportAnsi()
+        {
+            if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
+            {
+                return true;
+            }
+            // for Windows, check the console mode
+            var stdOutHandle = Interop.Kernel32.GetStdHandle(Interop.Kernel32.STD_OUTPUT_HANDLE);
+            if (!Interop.Kernel32.GetConsoleMode(stdOutHandle, out int consoleMode))
+            {
+                return false;
+            }
+
+            return (consoleMode & Interop.Kernel32.ENABLE_VIRTUAL_TERMINAL_PROCESSING) == Interop.Kernel32.ENABLE_VIRTUAL_TERMINAL_PROCESSING;
+        }
+
+        private void SetFormatters(IEnumerable<ConsoleFormatter> formatters = null)
+        {
+            _formatters = new ConcurrentDictionary<string, ConsoleFormatter>(StringComparer.OrdinalIgnoreCase);
+            if (formatters == null || !formatters.Any())
             {
-                _messageQueue.Console = new WindowsLogConsole();
-                _messageQueue.ErrorConsole = new WindowsLogConsole(stdErr: true);
+                var defaultMonitor = new FormatterOptionsMonitor<SimpleConsoleFormatterOptions>(new SimpleConsoleFormatterOptions());
+                var systemdMonitor = new FormatterOptionsMonitor<ConsoleFormatterOptions>(new ConsoleFormatterOptions());
+                var jsonMonitor = new FormatterOptionsMonitor<JsonConsoleFormatterOptions>(new JsonConsoleFormatterOptions());
+                _formatters.GetOrAdd(ConsoleFormatterNames.Simple, formatterName => new SimpleConsoleFormatter(defaultMonitor));
+                _formatters.GetOrAdd(ConsoleFormatterNames.Systemd, formatterName => new SystemdConsoleFormatter(systemdMonitor));
+                _formatters.GetOrAdd(ConsoleFormatterNames.Json, formatterName => new JsonConsoleFormatter(jsonMonitor));
             }
             else
             {
-                _messageQueue.Console = new AnsiLogConsole(new AnsiSystemConsole());
-                _messageQueue.ErrorConsole = new AnsiLogConsole(new AnsiSystemConsole(stdErr: true));
+                foreach (ConsoleFormatter formatter in formatters)
+                {
+                    _formatters.GetOrAdd(formatter.Name, formatterName => formatter);
+                }
             }
         }
 
+        // warning:  ReloadLoggerOptions can be called before the ctor completed,... before registering all of the state used in this method need to be initialized
         private void ReloadLoggerOptions(ConsoleLoggerOptions options)
         {
-            foreach (System.Collections.Generic.KeyValuePair<string, ConsoleLogger> logger in _loggers)
+            if (options.FormatterName == null || !_formatters.TryGetValue(options.FormatterName, out ConsoleFormatter logFormatter))
+            {
+#pragma warning disable CS0618
+                logFormatter = options.Format switch
+                {
+                    ConsoleLoggerFormat.Systemd => _formatters[ConsoleFormatterNames.Systemd],
+                    _ => _formatters[ConsoleFormatterNames.Simple],
+                };
+                if (options.FormatterName == null)
+                {
+                    UpdateFormatterOptions(logFormatter, options);
+                }
+#pragma warning restore CS0618
+            }
+
+            foreach (KeyValuePair<string, ConsoleLogger> logger in _loggers)
             {
                 logger.Value.Options = options;
+                logger.Value.Formatter = logFormatter;
             }
         }
 
         /// <inheritdoc />
         public ILogger CreateLogger(string name)
         {
+            if (_options.CurrentValue.FormatterName == null || !_formatters.TryGetValue(_options.CurrentValue.FormatterName, out ConsoleFormatter logFormatter))
+            {
+#pragma warning disable CS0618
+                logFormatter = _options.CurrentValue.Format switch
+                {
+                    ConsoleLoggerFormat.Systemd => _formatters[ConsoleFormatterNames.Systemd],
+                    _ => _formatters[ConsoleFormatterNames.Simple],
+                };
+                if (_options.CurrentValue.FormatterName == null)
+                {
+                    UpdateFormatterOptions(logFormatter, _options.CurrentValue);
+                }
+#pragma warning disable CS0618
+            }
+
             return _loggers.GetOrAdd(name, loggerName => new ConsoleLogger(name, _messageQueue)
             {
                 Options = _options.CurrentValue,
-                ScopeProvider = _scopeProvider
+                ScopeProvider = _scopeProvider,
+                Formatter = logFormatter,
             });
         }
+#pragma warning disable CS0618
+        private void UpdateFormatterOptions(ConsoleFormatter formatter, ConsoleLoggerOptions deprecatedFromOptions)
+        {
+            // kept for deprecated apis:
+            if (formatter is SimpleConsoleFormatter defaultFormatter)
+            {
+                defaultFormatter.FormatterOptions = new SimpleConsoleFormatterOptions()
+                {
+                    DisableColors = deprecatedFromOptions.DisableColors,
+                    IncludeScopes = deprecatedFromOptions.IncludeScopes,
+                    TimestampFormat = deprecatedFromOptions.TimestampFormat,
+                    UseUtcTimestamp = deprecatedFromOptions.UseUtcTimestamp,
+                };
+            }
+            else
+            if (formatter is SystemdConsoleFormatter systemdFormatter)
+            {
+                systemdFormatter.FormatterOptions = new ConsoleFormatterOptions()
+                {
+                    IncludeScopes = deprecatedFromOptions.IncludeScopes,
+                    TimestampFormat = deprecatedFromOptions.TimestampFormat,
+                    UseUtcTimestamp = deprecatedFromOptions.UseUtcTimestamp,
+                };
+            }
+        }
+#pragma warning restore CS0618
 
         /// <inheritdoc />
         public void Dispose()
@@ -80,7 +189,6 @@ namespace Microsoft.Extensions.Logging.Console
             {
                 logger.Value.ScopeProvider = _scopeProvider;
             }
-
         }
     }
 }
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/FormatterOptionsMonitor.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/FormatterOptionsMonitor.cs
new file mode 100644 (file)
index 0000000..6a36b69
--- /dev/null
@@ -0,0 +1,28 @@
+// 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.IO;
+using System.Text;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    internal class FormatterOptionsMonitor<TOptions> : IOptionsMonitor<TOptions> where TOptions : ConsoleFormatterOptions
+    {
+        private TOptions _options;
+        public FormatterOptionsMonitor(TOptions options)
+        {
+            _options = options;
+        }
+
+        public TOptions Get(string name) => _options;
+
+        public IDisposable OnChange(Action<TOptions, string> listener)
+        {
+            return null;
+        }
+
+        public TOptions CurrentValue => _options;
+    }
+}
\ No newline at end of file
index b1a4ae5..5fb5642 100644 (file)
@@ -7,8 +7,6 @@ namespace Microsoft.Extensions.Logging.Console
 {
     internal interface IConsole
     {
-        void Write(string message, ConsoleColor? background, ConsoleColor? foreground);
-        void WriteLine(string message, ConsoleColor? background, ConsoleColor? foreground);
-        void Flush();
+        void Write(string message);
     }
 }
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatter.cs
new file mode 100644 (file)
index 0000000..fb4cce0
--- /dev/null
@@ -0,0 +1,159 @@
+// 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.Buffers;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Globalization;
+using System.IO;
+using System.Linq;
+using System.Linq.Expressions;
+using System.Reflection;
+using System.Text;
+using System.Text.Json;
+using System.Text.Json.Serialization;
+using System.Text.RegularExpressions;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    internal class JsonConsoleFormatter : ConsoleFormatter, IDisposable
+    {
+        private IDisposable _optionsReloadToken;
+
+        public JsonConsoleFormatter(IOptionsMonitor<JsonConsoleFormatterOptions> options)
+            : base (ConsoleFormatterNames.Json)
+        {
+            ReloadLoggerOptions(options.CurrentValue);
+            _optionsReloadToken = options.OnChange(ReloadLoggerOptions);
+        }
+
+        public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter)
+        {
+            string message = logEntry.Formatter(logEntry.State, logEntry.Exception);
+            if (logEntry.Exception == null && message == null)
+            {
+                return;
+            }
+            LogLevel logLevel = logEntry.LogLevel;
+            string category = logEntry.Category;
+            int eventId = logEntry.EventId.Id;
+            Exception exception = logEntry.Exception;
+            const int DefaultBufferSize = 1024;
+            using (var output = new PooledByteBufferWriter(DefaultBufferSize))
+            {
+                using (var writer = new Utf8JsonWriter(output, FormatterOptions.JsonWriterOptions))
+                {
+                    writer.WriteStartObject();
+                    string timestamp = null;
+                    var timestampFormat = FormatterOptions.TimestampFormat;
+                    if (timestampFormat != null)
+                    {
+                        var dateTime = FormatterOptions.UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now;
+                        timestamp = dateTime.ToString(timestampFormat);
+                    }
+                    writer.WriteString("Timestamp", timestamp);
+                    writer.WriteNumber(nameof(logEntry.EventId), eventId);
+                    writer.WriteString(nameof(logEntry.LogLevel), GetLogLevelString(logLevel));
+                    writer.WriteString(nameof(logEntry.Category), category);
+                    writer.WriteString("Message", message);
+
+                    if (exception != null)
+                    {
+                        writer.WriteStartObject(nameof(Exception));
+                        writer.WriteString(nameof(exception.Message), exception.Message.ToString());
+                        writer.WriteString("Type", exception.GetType().ToString());
+                        writer.WriteStartArray(nameof(exception.StackTrace));
+                        string stackTrace = exception?.StackTrace;
+                        if (stackTrace != null)
+                        {
+#if (NETSTANDARD2_0 || NETFRAMEWORK)
+                            foreach (var stackTraceLines in stackTrace?.Split(new string[] { Environment.NewLine }, StringSplitOptions.None))
+#else
+                            foreach (var stackTraceLines in stackTrace?.Split(Environment.NewLine))
+#endif
+                            {
+                                writer.WriteStringValue(stackTraceLines);
+                            }
+                        }
+                        writer.WriteEndArray();
+                        writer.WriteNumber(nameof(exception.HResult), exception.HResult);
+                        writer.WriteEndObject();
+                    }
+
+                    if (logEntry.State is IReadOnlyCollection<KeyValuePair<string, object>> stateDictionary)
+                    {
+                        foreach (KeyValuePair<string, object> item in stateDictionary)
+                        {
+                            writer.WriteString(item.Key, Convert.ToString(item.Value, CultureInfo.InvariantCulture));
+                        }
+                    }
+                    WriteScopeInformation(writer, scopeProvider);
+                    writer.WriteEndObject();
+                    writer.Flush();
+                }
+#if (NETSTANDARD2_0 || NETFRAMEWORK)
+                textWriter.Write(Encoding.UTF8.GetString(output.WrittenMemory.Span.ToArray()));
+#else
+                textWriter.Write(Encoding.UTF8.GetString(output.WrittenMemory.Span));
+#endif
+            }
+            textWriter.Write(Environment.NewLine);
+        }
+
+        private static string GetLogLevelString(LogLevel logLevel)
+        {
+            return logLevel switch
+            {
+                LogLevel.Trace => "Trace",
+                LogLevel.Debug => "Debug",
+                LogLevel.Information => "Information",
+                LogLevel.Warning => "Warning",
+                LogLevel.Error => "Error",
+                LogLevel.Critical => "Critical",
+                _ => throw new ArgumentOutOfRangeException(nameof(logLevel))
+            };
+        }
+
+        private void WriteScopeInformation(Utf8JsonWriter writer, IExternalScopeProvider scopeProvider)
+        {
+            if (FormatterOptions.IncludeScopes && scopeProvider != null)
+            {
+                int numScopes = 0;
+                writer.WriteStartObject("Scopes");
+                scopeProvider.ForEachScope((scope, state) =>
+                {
+                    if (scope is IReadOnlyCollection<KeyValuePair<string, object>> scopeDictionary)
+                    {
+                        foreach (KeyValuePair<string, object> item in scopeDictionary)
+                        {
+                            state.WriteString(item.Key, Convert.ToString(item.Value, CultureInfo.InvariantCulture));
+                        }
+                    }
+                    else
+                    {
+                        state.WriteString(numScopes++.ToString(), scope.ToString());
+                    }
+                }, writer);
+                writer.WriteEndObject();
+            }
+        }
+
+        internal JsonConsoleFormatterOptions FormatterOptions { get; set; }
+
+        private void ReloadLoggerOptions(JsonConsoleFormatterOptions options)
+        {
+            FormatterOptions = options;
+        }
+
+        public void Dispose()
+        {
+            _optionsReloadToken?.Dispose();
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatterOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/JsonConsoleFormatterOptions.cs
new file mode 100644 (file)
index 0000000..fd4df18
--- /dev/null
@@ -0,0 +1,22 @@
+// 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.Text;
+using System.Text.Json;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    /// <summary>
+    /// Options for the built-in json console log formatter.
+    /// </summary>
+    public class JsonConsoleFormatterOptions : ConsoleFormatterOptions
+    {
+        public JsonConsoleFormatterOptions() { }
+
+        /// <summary>
+        /// Gets or sets JsonWriterOptions.
+        /// </summary>
+        public JsonWriterOptions JsonWriterOptions { get; set; }
+    }
+}
index 6297b01..ead0a75 100644 (file)
@@ -2,27 +2,18 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Collections.Concurrent;
 
 namespace Microsoft.Extensions.Logging.Console
 {
     internal readonly struct LogMessageEntry
     {
-        public LogMessageEntry(string message, string timeStamp = null, string levelString = null, ConsoleColor? levelBackground = null, ConsoleColor? levelForeground = null, ConsoleColor? messageColor = null, bool logAsError = false)
+        public LogMessageEntry(string message, bool logAsError = false)
         {
-            TimeStamp = timeStamp;
-            LevelString = levelString;
-            LevelBackground = levelBackground;
-            LevelForeground = levelForeground;
-            MessageColor = messageColor;
             Message = message;
             LogAsError = logAsError;
         }
 
-        public readonly string TimeStamp;
-        public readonly string LevelString;
-        public readonly ConsoleColor? LevelBackground;
-        public readonly ConsoleColor? LevelForeground;
-        public readonly ConsoleColor? MessageColor;
         public readonly string Message;
         public readonly bool LogAsError;
     }
index 3940959..9e07f91 100644 (file)
@@ -4,6 +4,7 @@
     <TargetFrameworks>$(NetCoreAppCurrent);netstandard2.0;net461;$(NetFrameworkCurrent)</TargetFrameworks>
     <ExcludeCurrentFullFrameworkFromPackage>true</ExcludeCurrentFullFrameworkFromPackage>
     <EnableDefaultItems>true</EnableDefaultItems>
+    <Nullable>annotations</Nullable>
   </PropertyGroup>
   <PropertyGroup>
     <!-- Ensure Assemblies are first resolved via targeting pack when targeting net461 -->
              Link="Common\src\Extensions\Logging\NullExternalScopeProvider.cs" />
     <Compile Include="$(CommonPath)Extensions\Logging\NullScope.cs"
              Link="Common\src\Extensions\Logging\NullScope.cs" />
+    <Compile Include="$(CommonPath)System\Text\Json\PooledByteBufferWriter.cs"
+             Link="Common\System\Text\Json\PooledByteBufferWriter.cs" />
   </ItemGroup>
 
   <ItemGroup>
+    <Compile Include="$(CommonPath)Interop\Windows\Interop.Libraries.cs"
+             Link="Common\Interop\Windows\Interop.Libraries.cs" />
+    <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetConsoleMode.cs"
+             Link="Common\Interop\Windows\Interop.GetConsoleMode.cs" />
+    <Compile Include="$(CommonPath)Interop\Windows\Kernel32\Interop.GetStdHandle.cs"
+             Link="Common\Interop\Windows\Interop.GetStdHandle.cs" />
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(TargetFramework)' != '$(NetCoreAppCurrent)'">
+    <Compile Include="$(CommonPath)System\Runtime\InteropServices\SuppressGCTransitionAttribute.internal.cs"
+             Link="Common\System\Runtime\InteropServices\SuppressGCTransitionAttribute.internal.cs" />
+  </ItemGroup>
+
+  <ItemGroup Condition="'$(TargetFramework)' != '$(NetCoreAppCurrent)'">
+    <Reference Include="System.Buffers" />
+    <Reference Include="Microsoft.Bcl.AsyncInterfaces" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <Reference Include="System.Collections" />
+    <Reference Include="System.Linq" />
+    <Reference Include="System.Memory" />
+    <Reference Include="System.Text.Json" />
+    <Reference Include="System.Runtime.InteropServices" />
     <Reference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
     <Reference Include="Microsoft.Extensions.Logging" />
     <Reference Include="Microsoft.Extensions.Logging.Abstractions" />
     <Reference Include="Microsoft.Extensions.Logging.Configuration" />
     <Reference Include="Microsoft.Extensions.Options" />
+    <Reference Include="Microsoft.Extensions.Options.ConfigurationExtensions" />
+    <Reference Include="Microsoft.Extensions.Configuration.Abstractions" />
+    <Reference Include="Microsoft.Extensions.Configuration" />
   </ItemGroup>
 
   <ItemGroup Condition="$(TargetFramework.StartsWith('net4'))">
     <Reference Include="mscorlib" />
     <Reference Include="System" />
+    <Reference Include="System.Core" />
     <Reference Include="System.ValueTuple" />
     <Reference Include="System.Runtime" />
     <PackageReference Include="System.Runtime.InteropServices.RuntimeInformation" Version="4.3.0" Condition="'$(TargetFramework)' == 'net461'" />
index c9d3b51..401c4ac 100644 (file)
@@ -3,4 +3,4 @@
 
 using System.Runtime.CompilerServices;
 
-[assembly: InternalsVisibleTo("Microsoft.Extensions.Logging.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
+[assembly: InternalsVisibleTo("Microsoft.Extensions.Logging.Console.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx b/src/libraries/Microsoft.Extensions.Logging.Console/src/Resources/Strings.resx
new file mode 100644 (file)
index 0000000..5ef4b88
--- /dev/null
@@ -0,0 +1,123 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="BufferMaximumSizeExceeded" xml:space="preserve">
+    <value>Cannot allocate a buffer of size {0}.</value>
+  </data>
+</root>
\ No newline at end of file
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatter.cs
new file mode 100644 (file)
index 0000000..e92aa0c
--- /dev/null
@@ -0,0 +1,210 @@
+// 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.IO;
+using System.Collections.Generic;
+using System.Linq;
+using System.Reflection;
+using System.Text;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    internal class SimpleConsoleFormatter : ConsoleFormatter, IDisposable
+    {
+        private const string LoglevelPadding = ": ";
+        private static readonly string _messagePadding = new string(' ', GetLogLevelString(LogLevel.Information).Length + LoglevelPadding.Length);
+        private static readonly string _newLineWithMessagePadding = Environment.NewLine + _messagePadding;
+        private IDisposable _optionsReloadToken;
+
+        public SimpleConsoleFormatter(IOptionsMonitor<SimpleConsoleFormatterOptions> options)
+            : base (ConsoleFormatterNames.Simple)
+        {
+            ReloadLoggerOptions(options.CurrentValue);
+            _optionsReloadToken = options.OnChange(ReloadLoggerOptions);
+        }
+
+        private void ReloadLoggerOptions(SimpleConsoleFormatterOptions options)
+        {
+            FormatterOptions = options;
+        }
+
+        public void Dispose()
+        {
+            _optionsReloadToken?.Dispose();
+        }
+
+        internal SimpleConsoleFormatterOptions FormatterOptions { get; set; }
+
+        public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter)
+        {
+            string message = logEntry.Formatter(logEntry.State, logEntry.Exception);
+            if (logEntry.Exception == null && message == null)
+            {
+                return;
+            }
+            LogLevel logLevel = logEntry.LogLevel;
+            ConsoleColors logLevelColors = GetLogLevelConsoleColors(logLevel);
+            string logLevelString = GetLogLevelString(logLevel);
+
+            string timestamp = null;
+            string timestampFormat = FormatterOptions.TimestampFormat;
+            if (timestampFormat != null)
+            {
+                DateTime dateTime = GetCurrentDateTime();
+                timestamp = dateTime.ToString(timestampFormat);
+            }
+            if (timestamp != null)
+            {
+                textWriter.Write(timestamp);
+            }
+            if (logLevelString != null)
+            {
+                textWriter.WriteColoredMessage(logLevelString, logLevelColors.Background, logLevelColors.Foreground);
+            }
+            CreateDefaultLogMessage(textWriter, logEntry, message, scopeProvider);
+        }
+
+        private void CreateDefaultLogMessage<TState>(TextWriter textWriter, in LogEntry<TState> logEntry, string message, IExternalScopeProvider scopeProvider)
+        {
+            bool singleLine = FormatterOptions.SingleLine;
+            int eventId = logEntry.EventId.Id;
+            Exception exception = logEntry.Exception;
+
+            // Example:
+            // info: ConsoleApp.Program[10]
+            //       Request received
+
+            // category and event id
+            textWriter.Write(LoglevelPadding + logEntry.Category + '[' + eventId + "]");
+            if (!singleLine)
+            {
+                textWriter.Write(Environment.NewLine);
+            }
+
+            // scope information
+            WriteScopeInformation(textWriter, scopeProvider, singleLine);
+            if (singleLine)
+            {
+                textWriter.Write(' ');
+            }
+            WriteMessage(textWriter, message, singleLine);
+
+            // Example:
+            // System.InvalidOperationException
+            //    at Namespace.Class.Function() in File:line X
+            if (exception != null)
+            {
+                // exception message
+                WriteMessage(textWriter, exception.ToString(), singleLine);
+            }
+            if (singleLine)
+            {
+                textWriter.Write(Environment.NewLine);
+            }
+        }
+
+        private void WriteMessage(TextWriter textWriter, string message, bool singleLine)
+        {
+            if (!string.IsNullOrEmpty(message))
+            {
+                if (singleLine)
+                {
+                    WriteReplacing(textWriter, Environment.NewLine, " ", message);
+                }
+                else
+                {
+                    textWriter.Write(_messagePadding);
+                    WriteReplacing(textWriter, Environment.NewLine, _newLineWithMessagePadding, message);
+                    textWriter.Write(Environment.NewLine);
+                }
+            }
+
+            static void WriteReplacing(TextWriter writer, string oldValue, string newValue, string message)
+            {
+                string newMessage = message.Replace(oldValue, newValue);
+                writer.Write(newMessage);
+            }
+        }
+
+        private DateTime GetCurrentDateTime()
+        {
+            return FormatterOptions.UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now;
+        }
+
+        private static string GetLogLevelString(LogLevel logLevel)
+        {
+            return logLevel switch
+            {
+                LogLevel.Trace => "trce",
+                LogLevel.Debug => "dbug",
+                LogLevel.Information => "info",
+                LogLevel.Warning => "warn",
+                LogLevel.Error => "fail",
+                LogLevel.Critical => "crit",
+                _ => throw new ArgumentOutOfRangeException(nameof(logLevel))
+            };
+        }
+
+        private ConsoleColors GetLogLevelConsoleColors(LogLevel logLevel)
+        {
+            if (FormatterOptions.DisableColors)
+            {
+                return new ConsoleColors(null, null);
+            }
+            // We must explicitly set the background color if we are setting the foreground color,
+            // since just setting one can look bad on the users console.
+            return logLevel switch
+            {
+                LogLevel.Trace => new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black),
+                LogLevel.Debug => new ConsoleColors(ConsoleColor.Gray, ConsoleColor.Black),
+                LogLevel.Information => new ConsoleColors(ConsoleColor.DarkGreen, ConsoleColor.Black),
+                LogLevel.Warning => new ConsoleColors(ConsoleColor.Yellow, ConsoleColor.Black),
+                LogLevel.Error => new ConsoleColors(ConsoleColor.Black, ConsoleColor.DarkRed),
+                LogLevel.Critical => new ConsoleColors(ConsoleColor.White, ConsoleColor.DarkRed),
+                _ => new ConsoleColors(null, null)
+            };
+        }
+
+        private void WriteScopeInformation(TextWriter textWriter, IExternalScopeProvider scopeProvider, bool singleLine)
+        {
+            if (FormatterOptions.IncludeScopes && scopeProvider != null)
+            {
+                bool paddingNeeded = !singleLine;
+                scopeProvider.ForEachScope((scope, state) =>
+                {
+                    if (paddingNeeded)
+                    {
+                        paddingNeeded = false;
+                        state.Write(_messagePadding + "=> ");
+                    }
+                    else
+                    {
+                        state.Write(" => ");
+                    }
+                    state.Write(scope);
+                }, textWriter);
+
+                if (!paddingNeeded && !singleLine)
+                {
+                    textWriter.Write(Environment.NewLine);
+                }
+            }
+        }
+
+        private readonly struct ConsoleColors
+        {
+            public ConsoleColors(ConsoleColor? foreground, ConsoleColor? background)
+            {
+                Foreground = foreground;
+                Background = background;
+            }
+
+            public ConsoleColor? Foreground { get; }
+
+            public ConsoleColor? Background { get; }
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatterOptions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/SimpleConsoleFormatterOptions.cs
new file mode 100644 (file)
index 0000000..18abca5
--- /dev/null
@@ -0,0 +1,27 @@
+// 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.Text;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    /// <summary>
+    /// Options for the built-in default console log formatter.
+    /// </summary>
+    public class SimpleConsoleFormatterOptions : ConsoleFormatterOptions
+    {
+        public SimpleConsoleFormatterOptions() { }
+
+        /// <summary>
+        /// Disables colors when <see langword="true" />.
+        /// </summary>
+        public bool DisableColors { get; set; }
+
+        /// <summary>
+        /// When <see langword="false" />, the entire message gets logged in a single line.
+        /// </summary>
+        public bool SingleLine { get; set; }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/SystemdConsoleFormatter.cs
new file mode 100644 (file)
index 0000000..0140adf
--- /dev/null
@@ -0,0 +1,131 @@
+// 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.IO;
+using System.Text;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Options;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    internal class SystemdConsoleFormatter : ConsoleFormatter, IDisposable
+    {
+        private IDisposable _optionsReloadToken;
+
+        public SystemdConsoleFormatter(IOptionsMonitor<ConsoleFormatterOptions> options)
+            : base(ConsoleFormatterNames.Systemd)
+        {
+            ReloadLoggerOptions(options.CurrentValue);
+            _optionsReloadToken = options.OnChange(ReloadLoggerOptions);
+        }
+
+        private void ReloadLoggerOptions(ConsoleFormatterOptions options)
+        {
+            FormatterOptions = options;
+        }
+
+        public void Dispose()
+        {
+            _optionsReloadToken?.Dispose();
+        }
+
+        internal ConsoleFormatterOptions FormatterOptions { get; set; }
+
+        public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter)
+        {
+            string message = logEntry.Formatter(logEntry.State, logEntry.Exception);
+            if (logEntry.Exception == null && message == null)
+            {
+                return;
+            }
+            LogLevel logLevel = logEntry.LogLevel;
+            string category = logEntry.Category;
+            int eventId = logEntry.EventId.Id;
+            Exception exception = logEntry.Exception;
+            // systemd reads messages from standard out line-by-line in a '<pri>message' format.
+            // newline characters are treated as message delimiters, so we must replace them.
+            // Messages longer than the journal LineMax setting (default: 48KB) are cropped.
+            // Example:
+            // <6>ConsoleApp.Program[10] Request received
+
+            // loglevel
+            string logLevelString = GetSyslogSeverityString(logLevel);
+            textWriter.Write(logLevelString);
+
+            // timestamp
+            string timestampFormat = FormatterOptions.TimestampFormat;
+            if (timestampFormat != null)
+            {
+                DateTime dateTime = GetCurrentDateTime();
+                textWriter.Write(dateTime.ToString(timestampFormat));
+            }
+
+            // category and event id
+            textWriter.Write(category);
+            textWriter.Write('[');
+            textWriter.Write(eventId);
+            textWriter.Write(']');
+
+            // scope information
+            WriteScopeInformation(textWriter, scopeProvider);
+
+            // message
+            if (!string.IsNullOrEmpty(message))
+            {
+                textWriter.Write(' ');
+                // message
+                WriteReplacingNewLine(textWriter, message);
+            }
+
+            // exception
+            // System.InvalidOperationException at Namespace.Class.Function() in File:line X
+            if (exception != null)
+            {
+                textWriter.Write(' ');
+                WriteReplacingNewLine(textWriter, exception.ToString());
+            }
+
+            // newline delimiter
+            textWriter.Write(Environment.NewLine);
+
+            static void WriteReplacingNewLine(TextWriter writer, string message)
+            {
+                string newMessage = message.Replace(Environment.NewLine, " ");
+                writer.Write(newMessage);
+            }
+        }
+
+        private DateTime GetCurrentDateTime()
+        {
+            return FormatterOptions.UseUtcTimestamp ? DateTime.UtcNow : DateTime.Now;
+        }
+
+        private static string GetSyslogSeverityString(LogLevel logLevel)
+        {
+            // 'Syslog Message Severities' from https://tools.ietf.org/html/rfc5424.
+            return logLevel switch
+            {
+                LogLevel.Trace => "<7>",
+                LogLevel.Debug => "<7>",        // debug-level messages
+                LogLevel.Information => "<6>",  // informational messages
+                LogLevel.Warning => "<4>",     // warning conditions
+                LogLevel.Error => "<3>",       // error conditions
+                LogLevel.Critical => "<2>",    // critical conditions
+                _ => throw new ArgumentOutOfRangeException(nameof(logLevel))
+            };
+        }
+
+        private void WriteScopeInformation(TextWriter textWriter, IExternalScopeProvider scopeProvider)
+        {
+            if (FormatterOptions.IncludeScopes && scopeProvider != null)
+            {
+                scopeProvider.ForEachScope((scope, state) =>
+                {
+                    state.Write(" => ");
+                    state.Write(scope);
+                }, textWriter);
+            }
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/TextWriterExtensions.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/TextWriterExtensions.cs
new file mode 100644 (file)
index 0000000..aeea1d6
--- /dev/null
@@ -0,0 +1,35 @@
+// 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.IO;
+using System.Text;
+using System.Collections.Concurrent;
+
+namespace Microsoft.Extensions.Logging.Console
+{
+    internal static class TextWriterExtensions
+    {
+        public static void WriteColoredMessage(this TextWriter textWriter, string message, ConsoleColor? background, ConsoleColor? foreground)
+        {
+            // Order: backgroundcolor, foregroundcolor, Message, reset foregroundcolor, reset backgroundcolor
+            if (background.HasValue)
+            {
+                textWriter.Write(AnsiParser.GetBackgroundColorEscapeCode(background.Value));
+            }
+            if (foreground.HasValue)
+            {
+                textWriter.Write(AnsiParser.GetForegroundColorEscapeCode(foreground.Value));
+            }
+            textWriter.Write(message);
+            if (foreground.HasValue)
+            {
+                textWriter.Write(AnsiParser.DefaultForegroundColor); // reset to default foreground color
+            }
+            if (background.HasValue)
+            {
+                textWriter.Write(AnsiParser.DefaultBackgroundColor); // reset to the background color
+            }
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/src/WindowsLogConsole.cs b/src/libraries/Microsoft.Extensions.Logging.Console/src/WindowsLogConsole.cs
deleted file mode 100644 (file)
index 9bbc8f9..0000000
+++ /dev/null
@@ -1,64 +0,0 @@
-// 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.IO;
-
-namespace Microsoft.Extensions.Logging.Console
-{
-    internal class WindowsLogConsole : IConsole
-    {
-        private readonly TextWriter _textWriter;
-
-        public WindowsLogConsole(bool stdErr = false)
-        {
-            _textWriter = stdErr ? System.Console.Error : System.Console.Out;
-        }
-
-        private bool SetColor(ConsoleColor? background, ConsoleColor? foreground)
-        {
-            if (background.HasValue)
-            {
-                System.Console.BackgroundColor = background.Value;
-            }
-
-            if (foreground.HasValue)
-            {
-                System.Console.ForegroundColor = foreground.Value;
-            }
-
-            return background.HasValue || foreground.HasValue;
-        }
-
-        private void ResetColor()
-        {
-            System.Console.ResetColor();
-        }
-
-        public void Write(string message, ConsoleColor? background, ConsoleColor? foreground)
-        {
-            bool colorChanged = SetColor(background, foreground);
-            _textWriter.Write(message);
-            if (colorChanged)
-            {
-                ResetColor();
-            }
-        }
-
-        public void WriteLine(string message, ConsoleColor? background, ConsoleColor? foreground)
-        {
-            bool colorChanged = SetColor(background, foreground);
-            _textWriter.WriteLine(message);
-            if (colorChanged)
-            {
-                ResetColor();
-            }
-        }
-
-        public void Flush()
-        {
-            // No action required as for every write, data is sent directly to the console
-            // output stream
-        }
-    }
-}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/AnsiParserTests.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/AnsiParserTests.cs
new file mode 100644 (file)
index 0000000..67b2e21
--- /dev/null
@@ -0,0 +1,273 @@
+// 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.Linq;
+using System.Collections.Generic;
+using Microsoft.Extensions.Logging.Test.Console;
+using Microsoft.Extensions.Logging.Console;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Console.Test
+{
+    public class AnsiParserTests
+    {
+        private const char EscapeChar = '\x1B';
+
+        [Theory]
+        [InlineData(1, "No Color", "No Color")]
+        [InlineData(2, "\x1B[41mColored\x1B[49mNo Color", "No Color")]
+        [InlineData(2, "\x1B[41m\x1B[1m\x1B[31mmColored\x1B[39m\x1B[49mNo Color", "No Color")]
+        public void Parse_CheckTimesWrittenToConsole(int numSegments, string message, string lastSegment)
+        {
+            // Arrange
+            var segments = new List<ConsoleContext>();
+            Action<string, int, int, ConsoleColor?, ConsoleColor?> onParseWrite = (message, startIndex, length, bg, fg) => {
+                segments.Add(new ConsoleContext() {
+                    BackgroundColor = bg,
+                    ForegroundColor = fg,
+                    Message = message.AsSpan().Slice(startIndex, length).ToString()
+                });
+            };
+            var parser = new AnsiParser(onParseWrite);
+
+            // Act
+            parser.Parse(message);
+
+            // Assert
+            Assert.Equal(numSegments, segments.Count);
+            Assert.Equal(lastSegment, segments.Last().Message);
+        }
+
+        [Theory]
+        [MemberData(nameof(Colors))]
+        public void Parse_SetBackgroundForegroundAndMessageThenReset_Success(ConsoleColor background, ConsoleColor foreground)
+        {
+            // Arrange
+            var message = AnsiParser.GetBackgroundColorEscapeCode(background)
+                + AnsiParser.GetForegroundColorEscapeCode(foreground)
+                + "Request received"
+                + AnsiParser.DefaultForegroundColor //resets foreground color
+                + AnsiParser.DefaultBackgroundColor; //resets background color
+            var segments = new List<ConsoleContext>();
+            Action<string, int, int, ConsoleColor?, ConsoleColor?> onParseWrite = (message, startIndex, length, bg, fg) => {
+                segments.Add(new ConsoleContext() {
+                    BackgroundColor = bg,
+                    ForegroundColor = fg,
+                    Message = message.AsSpan().Slice(startIndex, length).ToString()
+                });
+            };
+            var parser = new AnsiParser(onParseWrite);
+
+            // Act
+            parser.Parse(message);
+
+            // Assert
+            Assert.Equal(1, segments.Count);
+            Assert.Equal("Request received", segments[0].Message);
+            VerifyForeground(foreground, segments[0]);
+            VerifyBackground(background, segments[0]);
+        }
+
+        [Fact]
+        public void Parse_MessageWithMultipleColors_ParsedIntoMultipleSegments()
+        {
+            // Arrange
+            var message = AnsiParser.GetBackgroundColorEscapeCode(ConsoleColor.DarkRed)
+                + AnsiParser.GetForegroundColorEscapeCode(ConsoleColor.Gray)
+                + "Message1"
+                + AnsiParser.DefaultForegroundColor
+                + AnsiParser.DefaultBackgroundColor
+                + "NoColor"
+                + AnsiParser.GetBackgroundColorEscapeCode(ConsoleColor.DarkGreen)
+                + AnsiParser.GetForegroundColorEscapeCode(ConsoleColor.Cyan)
+                + "Message2"
+                + AnsiParser.GetBackgroundColorEscapeCode(ConsoleColor.DarkBlue)
+                + AnsiParser.GetForegroundColorEscapeCode(ConsoleColor.Yellow)
+                + "Message3"
+                + AnsiParser.DefaultForegroundColor
+                + AnsiParser.DefaultBackgroundColor;
+            var segments = new List<ConsoleContext>();
+            Action<string, int, int, ConsoleColor?, ConsoleColor?> onParseWrite = (message, startIndex, length, bg, fg) => {
+                segments.Add(new ConsoleContext() {
+                    BackgroundColor = bg,
+                    ForegroundColor = fg,
+                    Message = message.AsSpan().Slice(startIndex, length).ToString()
+                });
+            };
+            var parser = new AnsiParser(onParseWrite);
+
+            // Act
+            parser.Parse(message);
+
+            // Assert
+            Assert.Equal(4, segments.Count);
+            Assert.Equal("NoColor", segments[1].Message);
+            Assert.Null(segments[1].ForegroundColor);
+            Assert.Null(segments[1].BackgroundColor);
+
+            Assert.Equal("Message1", segments[0].Message);
+            Assert.Equal("Message2", segments[2].Message);
+            Assert.Equal("Message3", segments[3].Message);
+            VerifyBackground(ConsoleColor.DarkRed, segments[0]);
+            VerifyBackground(ConsoleColor.DarkGreen, segments[2]);
+            VerifyBackground(ConsoleColor.DarkBlue, segments[3]);
+            VerifyForeground(ConsoleColor.Gray, segments[0]);
+            VerifyForeground(ConsoleColor.Cyan, segments[2]);
+            VerifyForeground(ConsoleColor.Yellow, segments[3]);
+        }
+
+        [Fact]
+        public void Parse_RepeatedColorChange_PicksLastSet()
+        {
+            // Arrange
+            var message = AnsiParser.GetBackgroundColorEscapeCode(ConsoleColor.DarkRed)
+                + AnsiParser.GetBackgroundColorEscapeCode(ConsoleColor.DarkGreen)
+                + AnsiParser.GetBackgroundColorEscapeCode(ConsoleColor.DarkBlue)
+                + AnsiParser.GetForegroundColorEscapeCode(ConsoleColor.Gray)
+                + AnsiParser.GetForegroundColorEscapeCode(ConsoleColor.Cyan)
+                + AnsiParser.GetForegroundColorEscapeCode(ConsoleColor.Yellow)
+                + "Request received"
+                + AnsiParser.DefaultForegroundColor //resets foreground color
+                + AnsiParser.DefaultBackgroundColor; //resets background color
+            var segments = new List<ConsoleContext>();
+            Action<string, int, int, ConsoleColor?, ConsoleColor?> onParseWrite = (message, startIndex, length, bg, fg) => {
+                segments.Add(new ConsoleContext() {
+                    BackgroundColor = bg,
+                    ForegroundColor = fg,
+                    Message = message.AsSpan().Slice(startIndex, length).ToString()
+                });
+            };
+            var parser = new AnsiParser(onParseWrite);
+
+            // Act
+            parser.Parse(message);
+
+            // Assert
+            Assert.Equal(1, segments.Count);
+            Assert.Equal("Request received", segments[0].Message);
+            VerifyBackground(ConsoleColor.DarkBlue, segments[0]);
+            VerifyForeground(ConsoleColor.Yellow, segments[0]);
+        }
+
+        [Theory]
+        // supported
+        [InlineData("\x1B[77mInfo", "Info")]
+        [InlineData("\x1B[77m\x1B[1m\x1B[2m\x1B[0mInfo\x1B[1m", "Info")]
+        [InlineData("\x1B[7mInfo", "Info")]
+        [InlineData("\x1B[40m\x1B[1m\x1B[33mwarn\x1B[39m\x1B[22m\x1B[49m:", "warn", ":")]
+        // unsupported: skips
+        [InlineData("Info\x1B[77m:", "Info", ":")]
+        [InlineData("Info\x1B[7m:", "Info", ":")]
+        // treats as content
+        [InlineData("\x1B", "\x1B")]
+        [InlineData("\x1B ", "\x1B ")]
+        [InlineData("\x1Bm", "\x1Bm")]
+        [InlineData("\x1B m", "\x1B m")]
+        [InlineData("\x1Bxym", "\x1Bxym")]
+        [InlineData("\x1B[", "\x1B[")]
+        [InlineData("\x1B[m", "\x1B[m")]
+        [InlineData("\x1B[ ", "\x1B[ ")]
+        [InlineData("\x1B[ m", "\x1B[ m")]
+        [InlineData("\x1B[xym", "\x1B[xym")]
+        [InlineData("\x1B[7777m", "\x1B[7777m")]
+        [InlineData("\x1B\x1B\x1B", "\x1B\x1B\x1B")]
+        [InlineData("Message\x1B\x1B\x1B", "Message\x1B\x1B\x1B")]
+        [InlineData("\x1B\x1BMessage\x1B", "\x1B\x1BMessage\x1B")]
+        [InlineData("\x1B\x1B\x1BMessage", "\x1B\x1B\x1BMessage")]
+        [InlineData("Message\x1B ", "Message\x1B ")]
+        [InlineData("\x1BmMessage", "\x1BmMessage")]
+        [InlineData("\x1B[77m\x1B m\x1B[40m", "\x1B m")]
+        [InlineData("\x1B mMessage\x1Bxym", "\x1B mMessage\x1Bxym")]
+        public void Parse_ValidSupportedOrUnsupportedCodesInMessage_MessageParsedSuccessfully(string messageWithUnsupportedCode, params string[] output)
+        {
+            // Arrange
+            var message = messageWithUnsupportedCode;
+            var segments = new List<ConsoleContext>();
+            Action<string, int, int, ConsoleColor?, ConsoleColor?> onParseWrite = (message, startIndex, length, bg, fg) => {
+                segments.Add(new ConsoleContext() {
+                    BackgroundColor = bg,
+                    ForegroundColor = fg,
+                    Message = message.AsSpan().Slice(startIndex, length).ToString()
+                });
+            };
+            var parser = new AnsiParser(onParseWrite);
+
+            // Act
+            parser.Parse(messageWithUnsupportedCode);
+
+            // Assert
+            Assert.Equal(output.Length, segments.Count);
+            for (int i = 0; i < output.Length; i++)
+                Assert.Equal(output[i], segments[i].Message);
+        }
+
+        [Fact]
+        public void NullDelegate_Throws()
+        {
+            Assert.Throws<ArgumentNullException>(() => new AnsiParser(null));
+        }
+
+        public static TheoryData<ConsoleColor, ConsoleColor> Colors
+        {
+            get
+            {
+                var data = new TheoryData<ConsoleColor, ConsoleColor>();
+                foreach (ConsoleColor background in Enum.GetValues(typeof(ConsoleColor)))
+                {
+                    foreach (ConsoleColor foreground in Enum.GetValues(typeof(ConsoleColor)))
+                    {
+                        data.Add(background, foreground);
+                    }
+                }
+                return data;
+            }
+        }
+
+        private void VerifyBackground(ConsoleColor background, ConsoleContext segment)
+        {
+            if (IsBackgroundColorNotSupported(background))
+            {
+                Assert.Null(segment.BackgroundColor);
+            }
+            else
+            {
+                Assert.Equal(background, segment.BackgroundColor);
+            }
+        }
+
+        private void VerifyForeground(ConsoleColor foreground, ConsoleContext segment)
+        {
+            if (IsForegroundColorNotSupported(foreground))
+            {
+                Assert.Null(segment.ForegroundColor);
+            }
+            else
+            {
+                Assert.Equal(foreground, segment.ForegroundColor);
+            }
+        }
+
+        private class TestAnsiSystemConsole : IAnsiSystemConsole
+        {
+            public string Message { get; private set; }
+
+            public void Write(string message)
+            {
+                Message = message;
+            }
+        }
+
+        private static bool IsBackgroundColorNotSupported(ConsoleColor color)
+        {
+            return AnsiParser.GetBackgroundColorEscapeCode(color).Equals(
+                AnsiParser.DefaultBackgroundColor, StringComparison.OrdinalIgnoreCase);
+        }
+
+        private static bool IsForegroundColorNotSupported(ConsoleColor color)
+        {
+            return AnsiParser.GetForegroundColorEscapeCode(color).Equals(
+                AnsiParser.DefaultForegroundColor, StringComparison.OrdinalIgnoreCase);
+        }
+    }
+}
\ No newline at end of file
@@ -12,10 +12,12 @@ namespace Microsoft.Extensions.Logging.Test.Console
         public static readonly ConsoleColor? DefaultForegroundColor;
 
         private ConsoleSink _sink;
+        private AnsiParser _parser;
 
         public TestConsole(ConsoleSink sink)
         {
             _sink = sink;
+            _parser = new AnsiParser(OnParseWrite);
             BackgroundColor = DefaultBackgroundColor;
             ForegroundColor = DefaultForegroundColor;
         }
@@ -24,10 +26,15 @@ namespace Microsoft.Extensions.Logging.Test.Console
 
         public ConsoleColor? ForegroundColor { get; private set; }
 
-        public void Write(string message, ConsoleColor? background, ConsoleColor? foreground)
+        public void Write(string message)
+        {
+            _parser.Parse(message);
+        }
+
+        public void OnParseWrite(string message, int startIndex, int length, ConsoleColor? background, ConsoleColor? foreground)
         {
             var consoleContext = new ConsoleContext();
-            consoleContext.Message = message;
+            consoleContext.Message = message.AsSpan().Slice(startIndex, length).ToString();
 
             if (background.HasValue)
             {
@@ -44,15 +51,6 @@ namespace Microsoft.Extensions.Logging.Test.Console
             ResetColor();
         }
 
-        public void WriteLine(string message, ConsoleColor? background, ConsoleColor? foreground)
-        {
-            Write(message + Environment.NewLine, background, foreground);
-        }
-
-        public void Flush()
-        {
-        }
-
         private void ResetColor()
         {
             BackgroundColor = DefaultBackgroundColor;
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/ConsoleFormatterTests.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/ConsoleFormatterTests.cs
new file mode 100644 (file)
index 0000000..d4d8d08
--- /dev/null
@@ -0,0 +1,275 @@
+// 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.Concurrent;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Encodings.Web;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Logging.Test.Console;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Console.Test
+{
+    public class ConsoleFormatterTests
+    {
+        protected const string _loggerName = "test";
+        protected const string _state = "This is a test, and {curly braces} are just fine!";
+        protected readonly Func<object, Exception, string> _defaultFormatter = (state, exception) => state.ToString();
+
+        protected string GetMessage(List<ConsoleContext> contexts)
+        {
+            return string.Join("", contexts.Select(c => c.Message));
+        }
+
+        internal static (ConsoleLogger Logger, ConsoleSink Sink, ConsoleSink ErrorSink, Func<LogLevel, string> GetLevelPrefix, int WritesPerMsg) SetUp(
+            ConsoleLoggerOptions options = null,
+            SimpleConsoleFormatterOptions simpleOptions = null,
+            ConsoleFormatterOptions systemdOptions = null,
+            JsonConsoleFormatterOptions jsonOptions = null)
+        {
+            // Arrange
+            var sink = new ConsoleSink();
+            var errorSink = new ConsoleSink();
+            var console = new TestConsole(sink);
+            var errorConsole = new TestConsole(errorSink);
+            var consoleLoggerProcessor = new TestLoggerProcessor();
+            consoleLoggerProcessor.Console = console;
+            consoleLoggerProcessor.ErrorConsole = errorConsole;
+
+            var logger = new ConsoleLogger(_loggerName, consoleLoggerProcessor);
+            logger.ScopeProvider = new LoggerExternalScopeProvider();
+            logger.Options = options ?? new ConsoleLoggerOptions();
+            var formatters = new ConcurrentDictionary<string, ConsoleFormatter>(ConsoleLoggerTest.GetFormatters(simpleOptions, systemdOptions, jsonOptions).ToDictionary(f => f.Name));
+
+            Func<LogLevel, string> levelAsString;
+            int writesPerMsg;
+            switch (logger.Options.FormatterName)
+            {
+                case ConsoleFormatterNames.Simple:
+                    levelAsString = ConsoleLoggerTest.LogLevelAsStringDefault;
+                    writesPerMsg = 2;
+                    logger.Formatter = formatters[ConsoleFormatterNames.Simple];
+                    break;
+                case ConsoleFormatterNames.Systemd:
+                    levelAsString = ConsoleLoggerTest.GetSyslogSeverityString;
+                    writesPerMsg = 1;
+                    logger.Formatter = formatters[ConsoleFormatterNames.Systemd];
+                    break;
+                case ConsoleFormatterNames.Json:
+                    levelAsString = ConsoleLoggerTest.GetJsonLogLevelString;
+                    writesPerMsg = 1;
+                    logger.Formatter = formatters[ConsoleFormatterNames.Json];
+                    break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(logger.Options.FormatterName));
+            }
+
+            return (logger, sink, errorSink, levelAsString, writesPerMsg);
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void ConsoleLoggerOptions_TimeStampFormat_IsReloaded()
+        {
+            // Arrange
+            var monitor = new TestOptionsMonitor(new ConsoleLoggerOptions() { FormatterName = "NonExistentFormatter" });
+            var loggerProvider = new ConsoleLoggerProvider(monitor, ConsoleLoggerTest.GetFormatters());
+            var logger = (ConsoleLogger)loggerProvider.CreateLogger("Name");
+
+            // Act & Assert
+            Assert.Equal("NonExistentFormatter", logger.Options.FormatterName);
+            Assert.Equal(ConsoleFormatterNames.Simple, logger.Formatter.Name);
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [MemberData(nameof(FormatterNames))]
+        public void InvalidLogLevel_Throws(string formatterName)
+        {
+            // Arrange
+            var t = SetUp(
+                new ConsoleLoggerOptions { FormatterName = formatterName }
+            );
+            var logger = (ILogger)t.Logger;
+
+            // Act/Assert
+            Assert.Throws<ArgumentOutOfRangeException>(() => logger.Log((LogLevel)8, 0, _state, null, _defaultFormatter));
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [MemberData(nameof(FormatterNamesAndLevels))]
+        public void NoMessageOrException_Noop(string formatterName, LogLevel level)
+        {
+            // Arrange
+            var t = SetUp(new ConsoleLoggerOptions { FormatterName = formatterName });
+            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");
+
+            // Act
+            Func<object, Exception, string> formatter = (state, exception) => null;
+            logger.Log(level, 0, _state, null, formatter);
+
+            // Assert
+            Assert.Equal(0, sink.Writes.Count);
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [MemberData(nameof(FormatterNamesAndLevels))]
+        public void Log_LogsCorrectTimestamp(string formatterName, LogLevel level)
+        {
+            // Arrange
+            var t = SetUp(
+                new ConsoleLoggerOptions { FormatterName = formatterName },
+                new SimpleConsoleFormatterOptions { TimestampFormat = "yyyy-MM-ddTHH:mm:sszz ", UseUtcTimestamp = false },
+                new ConsoleFormatterOptions { TimestampFormat = "yyyy-MM-ddTHH:mm:sszz ", UseUtcTimestamp = false },
+                new JsonConsoleFormatterOptions {
+                    TimestampFormat = "yyyy-MM-ddTHH:mm:sszz ",
+                    UseUtcTimestamp = false,
+                    JsonWriterOptions = new JsonWriterOptions()
+                    {
+                        // otherwise escapes for timezone formatting from + to \u002b
+                        Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
+                        Indented = true
+                    }
+                });
+            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");
+
+            // Act
+            logger.Log(level, 0, _state, ex, _defaultFormatter);
+
+            // Assert
+            switch (formatterName)
+            {
+                case ConsoleFormatterNames.Simple:
+                {
+                    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(DateTimeOffset.Now.Offset, parsedDateTime.Offset);
+                }
+                break;
+                case ConsoleFormatterNames.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(DateTimeOffset.Now.Offset, parsedDateTime.Offset);
+                }
+                break;
+                case ConsoleFormatterNames.Json:
+                {
+                    Assert.Single(sink.Writes);
+                    var regexMatch = Regex.Match(sink.Writes[0].Message, @"(\d{4}\D\d{2}\D\d{2}\D\d{2}\D\d{2}\D\d{2}\D\d{2})");
+                    Assert.True(regexMatch.Success, sink.Writes[0].Message);
+                    var parsedDateTime = DateTimeOffset.Parse(regexMatch.Groups[1].Value);
+                    Assert.Equal(DateTimeOffset.Now.Offset, parsedDateTime.Offset);
+                }
+                break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(formatterName));
+            }
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void NullFormatterName_Throws()
+        {
+            // Arrange
+            Assert.Throws<ArgumentNullException>(() => new NullNameConsoleFormatter());
+        }
+
+        private class NullNameConsoleFormatter : ConsoleFormatter
+        {
+            public NullNameConsoleFormatter() : base(null) { }
+            public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter) { }
+        }
+
+        public static TheoryData<string, LogLevel> FormatterNamesAndLevels
+        {
+            get
+            {
+                var data = new TheoryData<string, LogLevel>();
+                foreach (LogLevel level in Enum.GetValues(typeof(LogLevel)))
+                {
+                    if (level == LogLevel.None)
+                    {
+                        continue;
+                    }
+                    data.Add(ConsoleFormatterNames.Simple, level);
+                    data.Add(ConsoleFormatterNames.Systemd, level);
+                    data.Add(ConsoleFormatterNames.Json, level);
+                }
+                return data;
+            }
+        }
+
+        public static TheoryData<string> FormatterNames
+        {
+            get
+            {
+                var data = new TheoryData<string>();
+                data.Add(ConsoleFormatterNames.Simple);
+                data.Add(ConsoleFormatterNames.Systemd);
+                data.Add(ConsoleFormatterNames.Json);
+                return data;
+            }
+        }
+
+        public static TheoryData<LogLevel> Levels
+        {
+            get
+            {
+                var data = new TheoryData<LogLevel>();
+                foreach (LogLevel value in Enum.GetValues(typeof(LogLevel)))
+                {
+                    data.Add(value);
+                }
+                return data;
+            }
+        }
+
+    }
+
+    public class TestFormatter : ConsoleFormatter, IDisposable
+    {
+        private IDisposable _optionsReloadToken;
+
+        public TestFormatter(IOptionsMonitor<SimpleConsoleFormatterOptions> options)
+            : base ("TestFormatter")
+        {
+            FormatterOptions = options.CurrentValue;
+            ReloadLoggerOptions(options.CurrentValue);
+            _optionsReloadToken = options.OnChange(ReloadLoggerOptions);
+        }
+
+        private void ReloadLoggerOptions(SimpleConsoleFormatterOptions options)
+        {
+            FormatterOptions = options;
+        }
+
+        public void Dispose()
+        {
+            _optionsReloadToken?.Dispose();
+        }
+
+        internal SimpleConsoleFormatterOptions FormatterOptions { get; set; }
+        public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter)
+        {
+            ;
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/ConsoleLoggerExtensionsTests.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/ConsoleLoggerExtensionsTests.cs
new file mode 100644 (file)
index 0000000..90cccc8
--- /dev/null
@@ -0,0 +1,476 @@
+
+// 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.Text.Json;
+using System.Text.Encodings.Web;
+using System.IO;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Test
+{
+    public class ConsoleLoggerExtensionsTests
+    {
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void AddConsole_NullConfigure_Throws()
+        {
+            Assert.Throws<ArgumentNullException>(() => 
+                new ServiceCollection()
+                    .AddLogging(builder => 
+                    {
+                        builder.AddConsole(null);
+                    }));
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void AddSimpleConsole_NullConfigure_Throws()
+        {
+            Assert.Throws<ArgumentNullException>(() => 
+                new ServiceCollection()
+                    .AddLogging(builder => 
+                    {
+                        builder.AddSimpleConsole(null);
+                    }));
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void AddSystemdConsole_NullConfigure_Throws()
+        {
+            Assert.Throws<ArgumentNullException>(() => 
+                new ServiceCollection()
+                    .AddLogging(builder => 
+                    {
+                        builder.AddSystemdConsole(null);
+                    }));
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void AddJsonConsole_NullConfigure_Throws()
+        {
+            Assert.Throws<ArgumentNullException>(() => 
+                new ServiceCollection()
+                    .AddLogging(builder => 
+                    {
+                        builder.AddJsonConsole(null);
+                    }));
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void AddConsoleFormatter_NullConfigure_Throws()
+        {
+            Assert.Throws<ArgumentNullException>(() => 
+                new ServiceCollection()
+                    .AddLogging(builder => 
+                    {
+                        builder.AddConsoleFormatter<CustomFormatter, CustomOptions>(null);
+                    }));
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [MemberData(nameof(FormatterNames))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddConsole_ConsoleLoggerOptionsFromConfigFile_IsReadFromLoggingConfiguration(string formatterName)
+        {
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] {
+                new KeyValuePair<string, string>("Console:FormatterName", formatterName)
+            }).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddConsole())
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Equal(formatterName, logger.Options.FormatterName);
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddConsoleFormatter_CustomFormatter_IsReadFromLoggingConfiguration()
+        {
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] {
+                new KeyValuePair<string, string>("Console:FormatterName", "custom"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:CustomLabel", "random"),
+            }).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddConsoleFormatter<CustomFormatter, CustomOptions>(fOptions => { fOptions.CustomLabel = "random"; })
+                    .AddConsole(o => { o.FormatterName = "custom"; })
+                )
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Equal("custom", logger.Options.FormatterName);
+            var formatter = Assert.IsType<CustomFormatter>(logger.Formatter);
+            Assert.Equal("random", formatter.FormatterOptions.CustomLabel);
+        }
+
+        private class CustomFormatter : ConsoleFormatter, IDisposable
+        {
+            private IDisposable _optionsReloadToken;
+
+            public CustomFormatter(IOptionsMonitor<CustomOptions> options)
+                : base("custom")
+            {
+                ReloadLoggerOptions(options.CurrentValue);
+                _optionsReloadToken = options.OnChange(ReloadLoggerOptions);
+            }
+
+            private void ReloadLoggerOptions(CustomOptions options)
+            {
+                FormatterOptions = options;
+            }
+
+            public CustomOptions FormatterOptions { get; set; }
+            public string CustomLog { get; set; }
+
+            public override void Write<TState>(in LogEntry<TState> logEntry, IExternalScopeProvider scopeProvider, TextWriter textWriter)
+            {
+                CustomLog = logEntry.Formatter(logEntry.State, logEntry.Exception);
+            }
+
+            public void Dispose()
+            {
+                _optionsReloadToken?.Dispose();
+            }
+        }
+
+        private class CustomOptions : ConsoleFormatterOptions
+        {
+            public string CustomLabel { get; set; }
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddSimpleConsole_ChangeProperties_IsReadFromLoggingConfiguration()
+        {
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] {
+                new KeyValuePair<string, string>("Console:FormatterOptions:DisableColors", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:SingleLine", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:TimestampFormat", "HH:mm "),
+                new KeyValuePair<string, string>("Console:FormatterOptions:UseUtcTimestamp", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:IncludeScopes", "true"),
+            }).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddSimpleConsole(o => {})
+                )
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Equal(ConsoleFormatterNames.Simple, logger.Options.FormatterName);
+            var formatter = Assert.IsType<SimpleConsoleFormatter>(logger.Formatter);
+            Assert.True(formatter.FormatterOptions.DisableColors);
+            Assert.True(formatter.FormatterOptions.SingleLine);
+            Assert.Equal("HH:mm ", formatter.FormatterOptions.TimestampFormat);
+            Assert.True(formatter.FormatterOptions.UseUtcTimestamp);
+            Assert.True(formatter.FormatterOptions.IncludeScopes);
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddSystemdConsole_ChangeProperties_IsReadFromLoggingConfiguration()
+        {
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] {
+                new KeyValuePair<string, string>("Console:FormatterOptions:TimestampFormat", "HH:mm "),
+                new KeyValuePair<string, string>("Console:FormatterOptions:UseUtcTimestamp", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:IncludeScopes", "true"),
+            }).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddSystemdConsole(o => {})
+                )
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Equal(ConsoleFormatterNames.Systemd, logger.Options.FormatterName);
+            var formatter = Assert.IsType<SystemdConsoleFormatter>(logger.Formatter);
+            Assert.Equal("HH:mm ", formatter.FormatterOptions.TimestampFormat);
+            Assert.True(formatter.FormatterOptions.UseUtcTimestamp);
+            Assert.True(formatter.FormatterOptions.IncludeScopes);
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddJsonConsole_ChangeProperties_IsReadFromLoggingConfiguration()
+        {
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] {
+                new KeyValuePair<string, string>("Console:FormatterOptions:TimestampFormat", "HH:mm "),
+                new KeyValuePair<string, string>("Console:FormatterOptions:UseUtcTimestamp", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:IncludeScopes", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:JsonWriterOptions:Indented", "true"),
+            }).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddJsonConsole(o => {})
+                )
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Equal(ConsoleFormatterNames.Json, logger.Options.FormatterName);
+            var formatter = Assert.IsType<JsonConsoleFormatter>(logger.Formatter);
+            Assert.Equal("HH:mm ", formatter.FormatterOptions.TimestampFormat);
+            Assert.True(formatter.FormatterOptions.UseUtcTimestamp);
+            Assert.True(formatter.FormatterOptions.IncludeScopes);
+            Assert.True(formatter.FormatterOptions.JsonWriterOptions.Indented);
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddJsonConsole_OutsideConfig_TakesProperty()
+        {
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] {
+                new KeyValuePair<string, string>("Console:FormatterOptions:TimestampFormat", "HH:mm "),
+                new KeyValuePair<string, string>("Console:FormatterOptions:UseUtcTimestamp", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:IncludeScopes", "true"),
+            }).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddJsonConsole(o => {
+                        o.JsonWriterOptions = new JsonWriterOptions()
+                        {
+                            Indented = false,
+                            Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping
+                        };
+                    })
+                )
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Equal(ConsoleFormatterNames.Json, logger.Options.FormatterName);
+            var formatter = Assert.IsType<JsonConsoleFormatter>(logger.Formatter);
+            Assert.Equal("HH:mm ", formatter.FormatterOptions.TimestampFormat);
+            Assert.True(formatter.FormatterOptions.UseUtcTimestamp);
+            Assert.True(formatter.FormatterOptions.IncludeScopes);
+            Assert.False(formatter.FormatterOptions.JsonWriterOptions.Indented);
+            Assert.Equal(JavaScriptEncoder.UnsafeRelaxedJsonEscaping, formatter.FormatterOptions.JsonWriterOptions.Encoder);
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddConsole_NullFormatterNameUsingSystemdFormat_AnyDeprecatedPropertiesOverwriteFormatterOptions()
+        {
+            var configs = new[] {
+                new KeyValuePair<string, string>("Console:Format", "Systemd"),
+                new KeyValuePair<string, string>("Console:TimestampFormat", "HH:mm:ss "),
+                new KeyValuePair<string, string>("Console:FormatterOptions:TimestampFormat", "HH:mm "),
+            };
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(configs).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddConsole(o => { })
+                )
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Null(logger.Options.FormatterName);
+            var formatter = Assert.IsType<SystemdConsoleFormatter>(logger.Formatter);
+            Assert.Equal("HH:mm:ss ", formatter.FormatterOptions.TimestampFormat);
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddConsole_NullFormatterName_UsingSystemdFormat_IgnoreFormatterOptionsAndUseDeprecatedInstead()
+        {
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] {
+                new KeyValuePair<string, string>("Console:Format", "Systemd"),
+                new KeyValuePair<string, string>("Console:IncludeScopes", "true"),
+                new KeyValuePair<string, string>("Console:TimestampFormat", "HH:mm:ss "),
+                new KeyValuePair<string, string>("Console:FormatterOptions:TimestampFormat", "HH:mm "),
+            }).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddConsole(o => { 
+#pragma warning disable CS0618
+                        o.IncludeScopes = false;
+#pragma warning restore CS0618
+                    })
+                )
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Null(logger.Options.FormatterName);
+            var formatter = Assert.IsType<SystemdConsoleFormatter>(logger.Formatter);
+            
+            Assert.Equal("HH:mm:ss ", formatter.FormatterOptions.TimestampFormat);  // ignore FormatterOptions, using deprecated one
+            Assert.False(formatter.FormatterOptions.UseUtcTimestamp);               // not set anywhere, defaulted to false
+            Assert.False(formatter.FormatterOptions.IncludeScopes);                 // setup using lambda wins over config
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddConsole_NullFormatterName_UsingDefaultFormat_IgnoreFormatterOptionsAndUseDeprecatedInstead()
+        {
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] {
+                new KeyValuePair<string, string>("Console:Format", "Default"),
+                new KeyValuePair<string, string>("Console:IncludeScopes", "true"),
+                new KeyValuePair<string, string>("Console:TimestampFormat", "HH:mm:ss "),
+                new KeyValuePair<string, string>("Console:FormatterOptions:SingleLine", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:TimestampFormat", "HH:mm "),
+            }).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddConsole(o => { 
+#pragma warning disable CS0618
+                        o.DisableColors = true;
+                        o.IncludeScopes = false;
+#pragma warning restore CS0618
+                    })
+                )
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Null(logger.Options.FormatterName);
+#pragma warning disable CS0618
+            Assert.True(logger.Options.DisableColors);
+#pragma warning restore CS0618
+            var formatter = Assert.IsType<SimpleConsoleFormatter>(logger.Formatter);
+            
+            Assert.False(formatter.FormatterOptions.SingleLine);                    // ignored
+            Assert.Equal("HH:mm:ss ", formatter.FormatterOptions.TimestampFormat);  // ignore FormatterOptions, using deprecated one
+            Assert.False(formatter.FormatterOptions.UseUtcTimestamp);               // not set anywhere, defaulted to false
+            Assert.False(formatter.FormatterOptions.IncludeScopes);                 // setup using lambda wins over config
+            Assert.True(formatter.FormatterOptions.DisableColors);                  // setup using lambda
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [InlineData("missingFormatter")]
+        [InlineData("simple")]
+        [InlineData("Simple")]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddConsole_FormatterNameIsSet_UsingDefaultFormat_IgnoreDeprecatedAndUseFormatterOptionsInstead(string formatterName)
+        {
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] {
+                new KeyValuePair<string, string>("Console:Format", "Default"),
+                new KeyValuePair<string, string>("Console:FormatterName", formatterName),
+                new KeyValuePair<string, string>("Console:TimestampFormat", "HH:mm:ss "),
+                new KeyValuePair<string, string>("Console:FormatterOptions:IncludeScopes", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:SingleLine", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:TimestampFormat", "HH:mm "),
+            }).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddConsole(o => { 
+#pragma warning disable CS0618
+                        o.DisableColors = true;
+                        o.IncludeScopes = false;
+#pragma warning restore CS0618
+                    })
+                )
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Equal(formatterName, logger.Options.FormatterName);
+#pragma warning disable CS0618
+            Assert.True(logger.Options.DisableColors);
+#pragma warning restore CS0618
+            var formatter = Assert.IsType<SimpleConsoleFormatter>(logger.Formatter);
+
+            Assert.True(formatter.FormatterOptions.SingleLine);                    // picked from FormatterOptions
+            Assert.Equal("HH:mm ", formatter.FormatterOptions.TimestampFormat);     // ignore deprecated, using FormatterOptions instead
+            Assert.False(formatter.FormatterOptions.UseUtcTimestamp);               // not set anywhere, defaulted to false
+            Assert.True(formatter.FormatterOptions.IncludeScopes);                  // ignore deprecated set in lambda use FormatterOptions instead
+            Assert.False(formatter.FormatterOptions.DisableColors);                 // ignore deprecated set in lambda, defaulted to false
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [InlineData("missingFormatter")]
+        [InlineData("systemd")]
+        [InlineData("Systemd")]
+        [ActiveIssue("https://github.com/dotnet/runtime/issues/38337", TestPlatforms.Browser)]
+        public void AddConsole_FormatterNameIsSet_UsingSystemdFormat_IgnoreDeprecatedAndUseFormatterOptionsInstead(string formatterName)
+        {
+            var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] {
+                new KeyValuePair<string, string>("Console:Format", "Systemd"),
+                new KeyValuePair<string, string>("Console:FormatterName", formatterName),
+                new KeyValuePair<string, string>("Console:TimestampFormat", "HH:mm:ss "),
+                new KeyValuePair<string, string>("Console:FormatterOptions:IncludeScopes", "true"),
+                new KeyValuePair<string, string>("Console:FormatterOptions:TimestampFormat", "HH:mm "),
+            }).Build();
+
+            var loggerProvider = new ServiceCollection()
+                .AddLogging(builder => builder
+                    .AddConfiguration(configuration)
+                    .AddConsole(o => { 
+#pragma warning disable CS0618
+                        o.UseUtcTimestamp = true;
+                        o.IncludeScopes = false;
+#pragma warning restore CS0618
+                    })
+                )
+                .BuildServiceProvider()
+                .GetRequiredService<ILoggerProvider>();
+
+            var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
+            var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
+            Assert.Equal(formatterName, logger.Options.FormatterName);
+#pragma warning disable CS0618
+            Assert.True(logger.Options.UseUtcTimestamp);
+#pragma warning restore CS0618
+            var formatter = Assert.IsType<SystemdConsoleFormatter>(logger.Formatter);
+
+            Assert.Equal("HH:mm ", formatter.FormatterOptions.TimestampFormat);     // ignore deprecated, using FormatterOptions instead
+            Assert.True(formatter.FormatterOptions.IncludeScopes);                  // ignore deprecated set in lambda use FormatterOptions instead
+            Assert.False(formatter.FormatterOptions.UseUtcTimestamp);               // ignore deprecated set in lambda, defaulted to false
+        }
+
+        public static TheoryData<string> FormatterNames
+        {
+            get
+            {
+                var data = new TheoryData<string>();
+                data.Add(ConsoleFormatterNames.Simple);
+                data.Add(ConsoleFormatterNames.Systemd);
+                data.Add(ConsoleFormatterNames.Json);
+                return data;
+            }
+        }
+    }
+}
\ No newline at end of file
@@ -2,17 +2,20 @@
 // The .NET Foundation licenses this file to you under the MIT license.
 
 using System;
+using System.Collections.Concurrent;
 using System.Collections.Generic;
 using System.Linq;
 using System.Text.RegularExpressions;
 using Microsoft.Extensions.Configuration;
 using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Logging.Configuration;
 using Microsoft.Extensions.Logging.Console;
 using Microsoft.Extensions.Logging.Test.Console;
 using Microsoft.Extensions.Options;
 using Xunit;
+#pragma warning disable CS0618
 
-namespace Microsoft.Extensions.Logging.Test
+namespace Microsoft.Extensions.Logging.Console.Test
 {
     public class ConsoleLoggerTest
     {
@@ -21,6 +24,22 @@ namespace Microsoft.Extensions.Logging.Test
         private const string _state = "This is a test, and {curly braces} are just fine!";
         private readonly Func<object, Exception, string> _defaultFormatter = (state, exception) => state.ToString();
 
+        internal static IEnumerable<ConsoleFormatter> GetFormatters(
+            SimpleConsoleFormatterOptions simpleOptions = null,
+            ConsoleFormatterOptions systemdOptions = null,
+            JsonConsoleFormatterOptions jsonOptions = null)
+        {
+            var defaultMonitor = new TestFormatterOptionsMonitor<SimpleConsoleFormatterOptions>(simpleOptions ?? new SimpleConsoleFormatterOptions());
+            var systemdMonitor = new TestFormatterOptionsMonitor<ConsoleFormatterOptions>(systemdOptions ?? new ConsoleFormatterOptions());
+            var jsonMonitor = new TestFormatterOptionsMonitor<JsonConsoleFormatterOptions>(jsonOptions ?? new JsonConsoleFormatterOptions());
+            var formatters = new List<ConsoleFormatter>() { 
+                new SimpleConsoleFormatter(defaultMonitor),
+                new SystemdConsoleFormatter(systemdMonitor),
+                new JsonConsoleFormatter(jsonMonitor)
+            };
+            return formatters;
+        }
+
         private static (ConsoleLogger Logger, ConsoleSink Sink, ConsoleSink ErrorSink, Func<LogLevel, string> GetLevelPrefix, int WritesPerMsg) SetUp(ConsoleLoggerOptions options = null)
         {
             // Arrange
@@ -35,6 +54,8 @@ namespace Microsoft.Extensions.Logging.Test
             var logger = new ConsoleLogger(_loggerName, consoleLoggerProcessor);
             logger.ScopeProvider = new LoggerExternalScopeProvider();
             logger.Options = options ?? new ConsoleLoggerOptions();
+            var formatters = new ConcurrentDictionary<string, ConsoleFormatter>(GetFormatters().ToDictionary(f => f.Name));
+
             Func<LogLevel, string> levelAsString;
             int writesPerMsg;
             switch (logger.Options.Format)
@@ -42,18 +63,62 @@ namespace Microsoft.Extensions.Logging.Test
                 case ConsoleLoggerFormat.Default:
                     levelAsString = LogLevelAsStringDefault;
                     writesPerMsg = 2;
+                    logger.Formatter = formatters[ConsoleFormatterNames.Simple];
                     break;
                 case ConsoleLoggerFormat.Systemd:
                     levelAsString = GetSyslogSeverityString;
                     writesPerMsg = 1;
+                    logger.Formatter = formatters[ConsoleFormatterNames.Systemd];
                     break;
                 default:
                     throw new ArgumentOutOfRangeException(nameof(logger.Options.Format));
             }
+
+            UpdateFormatterOptions(logger.Formatter, logger.Options);
+            VerifyDeprecatedPropertiesUsedOnNullFormatterName(logger);
             return (logger, sink, errorSink, levelAsString, writesPerMsg);
         }
 
-        private static string LogLevelAsStringDefault(LogLevel level)
+        private static void VerifyDeprecatedPropertiesUsedOnNullFormatterName(ConsoleLogger logger)
+        {
+            Assert.Null(logger.Options.FormatterName);
+            if (ConsoleLoggerFormat.Default == logger.Options.Format)
+            {
+                var formatter = Assert.IsType<SimpleConsoleFormatter>(logger.Formatter);
+                Assert.Equal(formatter.FormatterOptions.IncludeScopes, logger.Options.IncludeScopes);
+                Assert.Equal(formatter.FormatterOptions.UseUtcTimestamp, logger.Options.UseUtcTimestamp);
+                Assert.Equal(formatter.FormatterOptions.TimestampFormat, logger.Options.TimestampFormat);
+                Assert.Equal(formatter.FormatterOptions.DisableColors, logger.Options.DisableColors);   
+            }
+            else
+            {
+                var formatter = Assert.IsType<SystemdConsoleFormatter>(logger.Formatter);
+                Assert.Equal(formatter.FormatterOptions.IncludeScopes, logger.Options.IncludeScopes);   
+                Assert.Equal(formatter.FormatterOptions.UseUtcTimestamp, logger.Options.UseUtcTimestamp);
+                Assert.Equal(formatter.FormatterOptions.TimestampFormat, logger.Options.TimestampFormat);
+            }
+        }
+
+        private static void UpdateFormatterOptions(ConsoleFormatter formatter, ConsoleLoggerOptions deprecatedFromOptions)
+        {
+            // kept for deprecated apis:
+            if (formatter is SimpleConsoleFormatter defaultFormatter)
+            {
+                defaultFormatter.FormatterOptions.DisableColors = deprecatedFromOptions.DisableColors;
+                defaultFormatter.FormatterOptions.IncludeScopes = deprecatedFromOptions.IncludeScopes;
+                defaultFormatter.FormatterOptions.TimestampFormat = deprecatedFromOptions.TimestampFormat;
+                defaultFormatter.FormatterOptions.UseUtcTimestamp = deprecatedFromOptions.UseUtcTimestamp;
+            }
+            else
+            if (formatter is SystemdConsoleFormatter systemdFormatter)
+            {
+                systemdFormatter.FormatterOptions.IncludeScopes = deprecatedFromOptions.IncludeScopes;
+                systemdFormatter.FormatterOptions.TimestampFormat = deprecatedFromOptions.TimestampFormat;
+                systemdFormatter.FormatterOptions.UseUtcTimestamp = deprecatedFromOptions.UseUtcTimestamp;
+            }
+        }
+
+        internal static string LogLevelAsStringDefault(LogLevel level)
         {
             switch (level)
             {
@@ -74,7 +139,7 @@ namespace Microsoft.Extensions.Logging.Test
             }
         }
 
-        private static string GetSyslogSeverityString(LogLevel level)
+        internal static string GetSyslogSeverityString(LogLevel level)
         {
             switch (level)
             {
@@ -94,6 +159,20 @@ namespace Microsoft.Extensions.Logging.Test
             }
         }
 
+        internal static string GetJsonLogLevelString(LogLevel logLevel)
+        {
+            return logLevel switch
+            {
+                LogLevel.Trace => "Trace",
+                LogLevel.Debug => "Debug",
+                LogLevel.Information => "Information",
+                LogLevel.Warning => "Warning",
+                LogLevel.Error => "Error",
+                LogLevel.Critical => "Critical",
+                _ => throw new ArgumentOutOfRangeException(nameof(logLevel))
+            };
+        }
+
         [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
         public void LogsWhenMessageIsNotProvided()
         {
@@ -112,17 +191,17 @@ namespace Microsoft.Extensions.Logging.Test
             Assert.Equal(6, sink.Writes.Count);
             Assert.Equal(
                 "crit: test[0]" + Environment.NewLine +
-                "      [null]" + Environment.NewLine,
+                _paddingString + "[null]" + Environment.NewLine,
                 GetMessage(sink.Writes.GetRange(0 * t.WritesPerMsg, t.WritesPerMsg)));
             Assert.Equal(
                 "crit: test[0]" + Environment.NewLine +
-                "      [null]" + Environment.NewLine,
+                _paddingString + "[null]" + Environment.NewLine,
                 GetMessage(sink.Writes.GetRange(1 * t.WritesPerMsg, t.WritesPerMsg)));
 
             Assert.Equal(
                 "crit: test[0]" + Environment.NewLine +
-                "      [null]" + Environment.NewLine +
-                "System.InvalidOperationException: Invalid value" + Environment.NewLine,
+                _paddingString + "[null]" + Environment.NewLine +
+                _paddingString + "System.InvalidOperationException: Invalid value" + Environment.NewLine,
                 GetMessage(sink.Writes.GetRange(2 * t.WritesPerMsg, t.WritesPerMsg)));
         }
 
@@ -133,12 +212,12 @@ namespace Microsoft.Extensions.Logging.Test
             var t = SetUp();
             var logger = (ILogger)t.Logger;
             var sink = t.Sink;
-            var logMessage = "Route with name 'Default' was not found.";
+            var logMessage = "Route with name 'Simple' was not found.";
             var expected1 = @"crit: test[0]" + Environment.NewLine +
-                            "      Route with name 'Default' was not found." + Environment.NewLine;
+                            _paddingString + "Route with name 'Simple' was not found." + Environment.NewLine;
 
             var expected2 = @"crit: test[10]" + Environment.NewLine +
-                            "      Route with name 'Default' was not found." + Environment.NewLine;
+                            _paddingString + "Route with name 'Simple' was not found." + Environment.NewLine;
 
             // Act
             logger.LogCritical(logMessage);
@@ -155,7 +234,49 @@ namespace Microsoft.Extensions.Logging.Test
         }
 
         [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
-        [InlineData("Route with name 'Default' was not found.")]
+        [InlineData(null, 0)]
+        [InlineData(null, 1)]
+        [InlineData("missingFormatter", 0)]
+        [InlineData("missingFormatter", 1)]
+        public void Options_FormatterNameNull_UsesDeprecatedProperties(string formatterName, int formatNumber)
+        {
+            // Arrange
+            ConsoleLoggerFormat format = (ConsoleLoggerFormat)formatNumber;
+            var options = new ConsoleLoggerOptions() { FormatterName = formatterName };
+            var monitor = new TestOptionsMonitor(options);
+            var loggerProvider = new ConsoleLoggerProvider(monitor, GetFormatters());
+            var logger = (ConsoleLogger)loggerProvider.CreateLogger("Name");
+
+            // Act
+            monitor.Set(new ConsoleLoggerOptions() { FormatterName = null, Format = format, TimestampFormat = "HH:mm:ss " });
+
+            // Assert
+            switch (format)
+            {
+                case ConsoleLoggerFormat.Default:
+                {
+                    Assert.Null(logger.Options.FormatterName);
+                    Assert.Equal(ConsoleFormatterNames.Simple, logger.Formatter.Name);
+                    var formatter = Assert.IsType<SimpleConsoleFormatter>(logger.Formatter);
+                    Assert.Equal("HH:mm:ss ", formatter.FormatterOptions.TimestampFormat);
+                }
+                break;
+                case ConsoleLoggerFormat.Systemd:
+                {
+                    Assert.Null(logger.Options.FormatterName);
+                    Assert.Equal(ConsoleFormatterNames.Systemd, logger.Formatter.Name);
+                    var formatter = Assert.IsType<SystemdConsoleFormatter>(logger.Formatter);
+                    Assert.Equal("HH:mm:ss ", formatter.FormatterOptions.TimestampFormat);
+                }
+                break;
+                default:
+                    throw new ArgumentOutOfRangeException(nameof(format));
+            }
+            loggerProvider?.Dispose();
+        }
+
+        [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        [InlineData("Route with name 'Simple' was not found.")]
         public void Writes_NewLine_WhenExceptionIsProvided(string message)
         {
             // Arrange
@@ -168,7 +289,7 @@ namespace Microsoft.Extensions.Logging.Test
             var expectedMessage =
                 _paddingString + message + Environment.NewLine;
             var expectedExceptionMessage =
-                exception.ToString() + Environment.NewLine;
+                _paddingString + exception.ToString() + Environment.NewLine;
 
             // Act
             logger.LogCritical(eventId, exception, message);
@@ -224,7 +345,7 @@ namespace Microsoft.Extensions.Logging.Test
             // Assert
             Assert.Equal(2, sink.Writes.Count);
             var write = sink.Writes[0];
-            Assert.Equal(ConsoleColor.Red, write.BackgroundColor);
+            Assert.Equal(ConsoleColor.DarkRed, write.BackgroundColor);
             Assert.Equal(ConsoleColor.White, write.ForegroundColor);
             write = sink.Writes[1];
             Assert.Equal(TestConsole.DefaultBackgroundColor, write.BackgroundColor);
@@ -245,7 +366,7 @@ namespace Microsoft.Extensions.Logging.Test
             // Assert
             Assert.Equal(2, sink.Writes.Count);
             var write = sink.Writes[0];
-            Assert.Equal(ConsoleColor.Red, write.BackgroundColor);
+            Assert.Equal(ConsoleColor.DarkRed, write.BackgroundColor);
             Assert.Equal(ConsoleColor.Black, write.ForegroundColor);
             write = sink.Writes[1];
             Assert.Equal(TestConsole.DefaultBackgroundColor, write.BackgroundColor);
@@ -352,7 +473,7 @@ namespace Microsoft.Extensions.Logging.Test
             }
 
             // Assert
-            Assert.Equal(2 * levelSequence, sink.Writes.Count);
+            Assert.Equal(levelSequence, sink.Writes.Count);
             foreach (ConsoleContext write in sink.Writes)
             {
                 Assert.Null(write.ForegroundColor);
@@ -362,7 +483,7 @@ namespace Microsoft.Extensions.Logging.Test
 
         [ConditionalTheory(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
         [MemberData(nameof(FormatsAndLevels))]
-        public void WriteCore_LogsCorrectTimestamp(ConsoleLoggerFormat format, LogLevel level)
+        public void Log_LogsCorrectTimestamp(ConsoleLoggerFormat format, LogLevel level)
         {
             // Arrange
             var t = SetUp(new ConsoleLoggerOptions { TimestampFormat = "yyyy-MM-ddTHH:mm:sszz ", Format = format, UseUtcTimestamp = false });
@@ -464,9 +585,9 @@ namespace Microsoft.Extensions.Logging.Test
                     Assert.Equal(2, sink.Writes.Count);
                     Assert.Equal(
                         levelPrefix + ": test[0]" + Environment.NewLine +
-                        "      This is a test, and {curly braces} are just fine!" + Environment.NewLine +
-                        "System.Exception: Exception message" + Environment.NewLine +
-                        "with a second line" + Environment.NewLine,
+                        _paddingString + "This is a test, and {curly braces} are just fine!" + Environment.NewLine +
+                        _paddingString + "System.Exception: Exception message" + Environment.NewLine +
+                        _paddingString + "with a second line" + Environment.NewLine,
                         GetMessage(sink.Writes));
                 }
                 break;
@@ -662,6 +783,8 @@ namespace Microsoft.Extensions.Logging.Test
                 {
                     // Assert
                     Assert.Equal(2, sink.Writes.Count);
+                    var formatter = Assert.IsType<SimpleConsoleFormatter>(logger.Formatter);
+                    Assert.True(formatter.FormatterOptions.IncludeScopes);
                     // scope
                     var write = sink.Writes[1];
                     Assert.Equal(header + Environment.NewLine
@@ -675,6 +798,8 @@ namespace Microsoft.Extensions.Logging.Test
                 {
                     // Assert
                     Assert.Single(sink.Writes);
+                    var formatter = Assert.IsType<SystemdConsoleFormatter>(logger.Formatter);
+                    Assert.True(formatter.FormatterOptions.IncludeScopes);
                     // scope
                     var write = sink.Writes[0];
                     Assert.Equal(levelPrefix + header + " " + scope + " " + message + Environment.NewLine, write.Message);
@@ -811,13 +936,13 @@ namespace Microsoft.Extensions.Logging.Test
                     Assert.Equal(2, sink.Writes.Count);
                     Assert.Equal(
                         "info: test[0]" + Environment.NewLine +
-                        "      Info" + Environment.NewLine,
+                        _paddingString + "Info" + Environment.NewLine,
                         GetMessage(sink.Writes));
 
                     Assert.Equal(2, errorSink.Writes.Count);
                     Assert.Equal(
                         "warn: test[0]" + Environment.NewLine +
-                        "      Warn" + Environment.NewLine,
+                        _paddingString + "Warn" + Environment.NewLine,
                         GetMessage(errorSink.Writes));
                 }
                 break;
@@ -863,8 +988,8 @@ namespace Microsoft.Extensions.Logging.Test
                     Assert.Equal(2, sink.Writes.Count);
                     Assert.Equal(
                         levelPrefix + ": test[0]" + Environment.NewLine +
-                        "System.Exception: Exception message" + Environment.NewLine +
-                        "with a second line" + Environment.NewLine,
+                        _paddingString + "System.Exception: Exception message" + Environment.NewLine +
+                        _paddingString + "with a second line" + Environment.NewLine,
                         GetMessage(sink.Writes));
                 }
                 break;
@@ -906,8 +1031,8 @@ namespace Microsoft.Extensions.Logging.Test
                     Assert.Equal(2, sink.Writes.Count);
                     Assert.Equal(
                         levelPrefix + ": test[0]" + Environment.NewLine +
-                        "System.Exception: Exception message" + Environment.NewLine +
-                        "with a second line" + Environment.NewLine,
+                        _paddingString + "System.Exception: Exception message" + Environment.NewLine +
+                        _paddingString + "with a second line" + Environment.NewLine,
                         GetMessage(sink.Writes));
                 }
                 break;
@@ -948,7 +1073,7 @@ namespace Microsoft.Extensions.Logging.Test
                     Assert.Equal(2, sink.Writes.Count);
                     Assert.Equal(
                         levelPrefix + ": test[0]" + Environment.NewLine +
-                        "      This is a test, and {curly braces} are just fine!" + Environment.NewLine,
+                        _paddingString + "This is a test, and {curly braces} are just fine!" + Environment.NewLine,
                         GetMessage(sink.Writes));
                 }
                 break;
@@ -992,9 +1117,13 @@ namespace Microsoft.Extensions.Logging.Test
             var console = new TestConsole(sink);
             var processor = new ConsoleLoggerProcessor();
             processor.Console = console;
+            var formatters = new ConcurrentDictionary<string, ConsoleFormatter>(GetFormatters().ToDictionary(f => f.Name));
 
             var logger = new ConsoleLogger(_loggerName, loggerProcessor: processor);
             logger.Options = new ConsoleLoggerOptions();
+            Assert.Null(logger.Options.FormatterName);
+            logger.Formatter = formatters[ConsoleFormatterNames.Simple];
+            UpdateFormatterOptions(logger.Formatter, logger.Options);
             // Act
             processor.Dispose();
             logger.LogInformation("Logging after dispose");
@@ -1032,6 +1161,12 @@ namespace Microsoft.Extensions.Logging.Test
         }
 
         [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void ConsoleLoggerOptions_InvalidFormat_Throws()
+        {
+            Assert.Throws<ArgumentOutOfRangeException>(() => new ConsoleLoggerOptions() { Format = (ConsoleLoggerFormat)10 });
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
         public void ConsoleLoggerOptions_DisableColors_IsReadFromLoggingConfiguration()
         {
             var configuration = new ConfigurationBuilder().AddInMemoryCollection(new[] { new KeyValuePair<string, string>("Console:DisableColors", "true") }).Build();
@@ -1167,6 +1302,8 @@ namespace Microsoft.Extensions.Logging.Test
             var consoleLoggerProvider = Assert.IsType<ConsoleLoggerProvider>(loggerProvider);
             var logger = (ConsoleLogger)consoleLoggerProvider.CreateLogger("Category");
             Assert.NotNull(logger.ScopeProvider);
+            var formatter = Assert.IsType<SimpleConsoleFormatter>(logger.Formatter);
+            Assert.True(formatter.FormatterOptions.IncludeScopes);
         }
 
         public static TheoryData<ConsoleLoggerFormat, LogLevel> FormatsAndLevels
@@ -1233,18 +1370,17 @@ namespace Microsoft.Extensions.Logging.Test
                     throw new ArgumentOutOfRangeException(nameof(format));
             }
         }
+    }
 
-
-        private class TestLoggerProcessor : ConsoleLoggerProcessor
+    internal class TestLoggerProcessor : ConsoleLoggerProcessor
+    {
+        public TestLoggerProcessor()
         {
-            public TestLoggerProcessor()
-            {
-            }
+        }
 
-            public override void EnqueueMessage(LogMessageEntry message)
-            {
-                WriteMessage(message);
-            }
+        public override void EnqueueMessage(LogMessageEntry message)
+        {
+            WriteMessage(message);
         }
     }
 
@@ -1275,3 +1411,4 @@ namespace Microsoft.Extensions.Logging.Test
         }
     }
 }
+#pragma warning restore CS0618
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/JsonConsoleFormatterTests.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/JsonConsoleFormatterTests.cs
new file mode 100644 (file)
index 0000000..20e987c
--- /dev/null
@@ -0,0 +1,56 @@
+// 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.Concurrent;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Logging.Test.Console;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Console.Test
+{
+    public class JsonConsoleFormatterTests : ConsoleFormatterTests
+    {
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void NoLogScope_DoesNotWriteAnyScopeContentToOutput_Json()
+        {
+            // Arrange
+            var t = ConsoleFormatterTests.SetUp(
+                new ConsoleLoggerOptions { FormatterName = ConsoleFormatterNames.Json },
+                new SimpleConsoleFormatterOptions { IncludeScopes = true },
+                new ConsoleFormatterOptions { IncludeScopes = true },
+                new JsonConsoleFormatterOptions {
+                    IncludeScopes = true,
+                    JsonWriterOptions = new JsonWriterOptions() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping } 
+                });
+            var logger = t.Logger;
+            var sink = t.Sink;
+
+            // Act
+            using (logger.BeginScope("Scope with named parameter {namedParameter}", 123))
+            using (logger.BeginScope("SimpleScope"))
+                logger.Log(LogLevel.Warning, 0, "Message with {args}", 73, _defaultFormatter);
+
+            // Assert
+            Assert.Equal(1, sink.Writes.Count);
+            var write = sink.Writes[0];
+            Assert.Equal(TestConsole.DefaultBackgroundColor, write.BackgroundColor);
+            Assert.Equal(TestConsole.DefaultForegroundColor, write.ForegroundColor);
+            Assert.Contains("Message with {args}", write.Message);
+            Assert.Contains("73", write.Message);
+            Assert.Contains("{OriginalFormat}", write.Message);
+            Assert.Contains("namedParameter", write.Message);
+            Assert.Contains("123", write.Message);
+            Assert.Contains("SimpleScope", write.Message);
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests.csproj b/src/libraries/Microsoft.Extensions.Logging.Console/tests/Microsoft.Extensions.Logging.Console.Tests.csproj
new file mode 100644 (file)
index 0000000..2524f77
--- /dev/null
@@ -0,0 +1,13 @@
+<Project Sdk="Microsoft.NET.Sdk">
+
+  <PropertyGroup>
+    <TargetFrameworks>$(NetCoreAppCurrent);$(NetFrameworkCurrent)</TargetFrameworks>
+    <TestRuntime>true</TestRuntime>
+    <EnableDefaultItems>true</EnableDefaultItems>
+  </PropertyGroup>
+
+  <ItemGroup>
+    <ReferenceFromRuntime Include="Microsoft.Extensions.Logging.Console" />
+  </ItemGroup>
+
+</Project>
\ No newline at end of file
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/SimpleConsoleFormatterTests.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/SimpleConsoleFormatterTests.cs
new file mode 100644 (file)
index 0000000..f4df50c
--- /dev/null
@@ -0,0 +1,108 @@
+// 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.Concurrent;
+using System.Collections.Generic;
+using System.Text;
+using System.Text.Json;
+using System.Text.RegularExpressions;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Logging.Test.Console;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Console.Test
+{
+    public class SimpleConsoleFormatterTests : ConsoleFormatterTests
+    {
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void Log_WritingScopes_LogsWithCorrectColors()
+        {
+            // Arrange
+            var t = SetUp(
+                new ConsoleLoggerOptions { FormatterName = ConsoleFormatterNames.Simple },
+                new SimpleConsoleFormatterOptions { IncludeScopes = true }
+                );
+            var logger = t.Logger;
+            var sink = t.Sink;
+            var id = Guid.NewGuid();
+            var scopeMessage = "RequestId: {RequestId}";
+
+            // Act
+            using (logger.BeginScope(scopeMessage, id))
+            {
+                logger.Log(LogLevel.Information, 0, _state, null, _defaultFormatter);
+            }
+
+            // Assert
+            Assert.Equal(2, sink.Writes.Count);
+            var write = sink.Writes[0];
+            Assert.Equal(ConsoleColor.Black, write.BackgroundColor);
+            Assert.Equal(ConsoleColor.DarkGreen, write.ForegroundColor);
+            write = sink.Writes[1];
+            Assert.Equal(TestConsole.DefaultBackgroundColor, write.BackgroundColor);
+            Assert.Equal(TestConsole.DefaultForegroundColor, write.ForegroundColor);
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void Log_NoLogScope_DoesNotWriteAnyScopeContentToOutput()
+        {
+            // Arrange
+            var t = SetUp(
+                new ConsoleLoggerOptions { FormatterName = ConsoleFormatterNames.Simple },
+                new SimpleConsoleFormatterOptions { IncludeScopes = true }
+            );
+            var logger = t.Logger;
+            var sink = t.Sink;
+
+            // Act
+            logger.Log(LogLevel.Warning, 0, _state, null, _defaultFormatter);
+
+            // Assert
+            Assert.Equal(2, sink.Writes.Count);
+            var write = sink.Writes[0];
+            Assert.Equal(ConsoleColor.Black, write.BackgroundColor);
+            Assert.Equal(ConsoleColor.Yellow, write.ForegroundColor);
+            write = sink.Writes[1];
+            Assert.Equal(TestConsole.DefaultBackgroundColor, write.BackgroundColor);
+            Assert.Equal(TestConsole.DefaultForegroundColor, write.ForegroundColor);
+        }
+
+        [ConditionalFact(typeof(PlatformDetection), nameof(PlatformDetection.IsThreadingSupported))]
+        public void Log_SingleLine_LogsWhenMessageIsNotProvided()
+        {
+            // Arrange
+            var t = SetUp(
+                new ConsoleLoggerOptions { FormatterName = ConsoleFormatterNames.Simple },
+                new SimpleConsoleFormatterOptions { SingleLine = true }
+            );
+            var logger = (ILogger)t.Logger;
+            var sink = t.Sink;
+            var exception = new InvalidOperationException("Invalid value");
+
+            // Act
+            logger.LogCritical(eventId: 0, exception: null, message: null);
+            logger.LogCritical(eventId: 0, message: null);
+            logger.LogCritical(eventId: 0, message: null, exception: exception);
+
+            // Assert
+            Assert.Equal(6, sink.Writes.Count);
+            Assert.Equal(
+                "crit: test[0]" + " " + "[null]" + Environment.NewLine,
+                GetMessage(sink.Writes.GetRange(0 * t.WritesPerMsg, t.WritesPerMsg)));
+            Assert.Equal(
+                "crit: test[0]" + " " + "[null]" + Environment.NewLine,
+                GetMessage(sink.Writes.GetRange(1 * t.WritesPerMsg, t.WritesPerMsg)));
+
+            Assert.Equal(
+                "crit: test[0]" + " " + "[null]" + "System.InvalidOperationException: Invalid value" + Environment.NewLine,
+                GetMessage(sink.Writes.GetRange(2 * t.WritesPerMsg, t.WritesPerMsg)));
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/TestFormatterOptionsMonitor.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/TestFormatterOptionsMonitor.cs
new file mode 100644 (file)
index 0000000..e0cd308
--- /dev/null
@@ -0,0 +1,35 @@
+// 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 Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Console.Test
+{
+    internal class TestFormatterOptionsMonitor<TOptions> : IOptionsMonitor<TOptions> where TOptions : ConsoleFormatterOptions
+    {
+        private TOptions _options;
+        private event Action<TOptions, string> _onChange;
+        public TestFormatterOptionsMonitor(TOptions options)
+        {
+            _options = options;
+        }
+
+        public TOptions Get(string name) => _options;
+
+        public IDisposable OnChange(Action<TOptions, string> listener)
+        {
+                _onChange += listener;
+                return null;
+        }
+
+        public TOptions CurrentValue => _options;
+
+        public void Set(TOptions options)
+        {
+            _options = options;
+            _onChange?.Invoke(options, "");
+        }
+    }
+}
diff --git a/src/libraries/Microsoft.Extensions.Logging.Console/tests/TextWriterExtensionsTests.cs b/src/libraries/Microsoft.Extensions.Logging.Console/tests/TextWriterExtensionsTests.cs
new file mode 100644 (file)
index 0000000..2d6ecaf
--- /dev/null
@@ -0,0 +1,84 @@
+// 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.Text;
+using System.IO;
+using System.Reflection;
+using Microsoft.Extensions.Logging.Abstractions;
+using Microsoft.Extensions.Logging.Console;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Microsoft.Extensions.Logging.Console.Test
+{
+    public class TextWriterExtensionsTests
+    {
+        [Fact]
+        public void WriteColoredMessage_WithForegroundEscapeCode_AndNoBackgroundColorSpecified()
+        {
+            // Arrange
+            var message = "Request received";
+            var expectedMessage = AnsiParser.GetForegroundColorEscapeCode(ConsoleColor.DarkGreen)
+                + message
+                + "\x1B[39m\x1B[22m"; //resets foreground color
+            var textWriter = new StringWriter();
+
+            // Act
+            textWriter.WriteColoredMessage(message, background: null, foreground: ConsoleColor.DarkGreen);
+
+            // Assert
+            Assert.Equal(expectedMessage, textWriter.ToString());
+        }
+
+        [Fact]
+        public void WriteColoredMessage_WithBackgroundEscapeCode_AndNoForegroundColorSpecified()
+        {
+            // Arrange
+            var message = "Request received";
+            var expectedMessage = AnsiParser.GetBackgroundColorEscapeCode(ConsoleColor.Red)
+                + message
+                + "\x1B[49m"; //resets background color
+            var textWriter = new StringWriter();
+
+            // Act
+            textWriter.WriteColoredMessage(message, background: ConsoleColor.Red, foreground: null);
+
+            // Assert
+            Assert.Equal(expectedMessage, textWriter.ToString());
+        }
+
+        [Fact]
+        public void WriteColoredMessage_InOrder_WhenBothForegroundOrBackgroundColorsSpecified()
+        {
+            // Arrange
+            var message = "Request received";
+            var expectedMessage = AnsiParser.GetBackgroundColorEscapeCode(ConsoleColor.Red)
+                + AnsiParser.GetForegroundColorEscapeCode(ConsoleColor.DarkGreen)
+                + "Request received"
+                + "\x1B[39m\x1B[22m" //resets foreground color
+                + "\x1B[49m"; //resets background color
+            var textWriter = new StringWriter();
+
+            // Act
+            textWriter.WriteColoredMessage(message, ConsoleColor.Red, ConsoleColor.DarkGreen);
+
+            // Assert
+            Assert.Equal(expectedMessage, textWriter.ToString());
+        }
+
+        [Fact]
+        public void WriteColoredMessage_NullColors_NoAnsiEmbedded()
+        {
+            // Arrange
+            var message = "Request received";
+            var textWriter = new StringWriter();
+
+            // Act
+            textWriter.WriteColoredMessage(message, null, null);
+
+            // Assert
+            Assert.Equal(message, textWriter.ToString());
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/libraries/Microsoft.Extensions.Logging/tests/Common/AnsiLogConsoleTest.cs b/src/libraries/Microsoft.Extensions.Logging/tests/Common/AnsiLogConsoleTest.cs
deleted file mode 100644 (file)
index cc84f5d..0000000
+++ /dev/null
@@ -1,179 +0,0 @@
-// 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 Microsoft.Extensions.Logging.Console;
-using Xunit;
-
-namespace Microsoft.Extensions.Logging
-{
-    public class AnsiLogConsoleTest
-    {
-        [Fact]
-        public void DoesNotAddNewLine()
-        {
-            // Arrange
-            var systemConsole = new TestAnsiSystemConsole();
-            var console = new AnsiLogConsole(systemConsole);
-            var message = "Request received";
-            var expectedMessage = message;
-
-            // Act
-            console.Write(message, background: null, foreground: null);
-            console.Flush();
-
-            // Assert
-            Assert.Equal(expectedMessage, systemConsole.Message);
-        }
-
-        [Fact]
-        public void NotCallingFlush_DoesNotWriteData_ToSystemConsole()
-        {
-            // Arrange
-            var systemConsole = new TestAnsiSystemConsole();
-            var console = new AnsiLogConsole(systemConsole);
-            var message = "Request received";
-            var expectedMessage = message + Environment.NewLine;
-
-            // Act
-            console.WriteLine(message, background: null, foreground: null);
-
-            // Assert
-            Assert.Null(systemConsole.Message);
-        }
-
-        [Fact]
-        public void CallingFlush_ClearsData_FromOutputBuilder()
-        {
-            // Arrange
-            var systemConsole = new TestAnsiSystemConsole();
-            var console = new AnsiLogConsole(systemConsole);
-            var message = "Request received";
-            var expectedMessage = message + Environment.NewLine;
-
-            // Act
-            console.WriteLine(message, background: null, foreground: null);
-            console.Flush();
-            console.WriteLine(message, background: null, foreground: null);
-            console.Flush();
-
-            // Assert
-            Assert.Equal(expectedMessage, systemConsole.Message);
-        }
-
-        [Fact]
-        public void WritesMessage_WithoutEscapeCodes_AndNoForegroundOrBackgroundColorsSpecified()
-        {
-            // Arrange
-            var systemConsole = new TestAnsiSystemConsole();
-            var console = new AnsiLogConsole(systemConsole);
-            var message = "Request received";
-            var expectedMessage = message + Environment.NewLine;
-
-            // Act
-            console.WriteLine(message, background: null, foreground: null);
-            console.Flush();
-
-            // Assert
-            Assert.Equal(expectedMessage, systemConsole.Message);
-        }
-
-        [Fact]
-        public void WritesMessage_WithForegroundEscapeCode_AndNoBackgroundColorSpecified()
-        {
-            // Arrange
-            var systemConsole = new TestAnsiSystemConsole();
-            var console = new AnsiLogConsole(systemConsole);
-            var message = "Request received";
-            var expectedMessage = GetForegroundColorEscapeCode(ConsoleColor.DarkGreen)
-                + message
-                + "\x1B[39m\x1B[22m"; //resets foreground color
-
-            // Act
-            console.WriteLine(message, background: null, foreground: ConsoleColor.DarkGreen);
-            console.Flush();
-
-            // Assert
-            Assert.Equal(expectedMessage + Environment.NewLine, systemConsole.Message);
-        }
-
-        [Fact]
-        public void WritesMessage_WithBackgroundEscapeCode_AndNoForegroundColorSpecified()
-        {
-            // Arrange
-            var systemConsole = new TestAnsiSystemConsole();
-            var console = new AnsiLogConsole(systemConsole);
-            var message = "Request received";
-            var expectedMessage = GetBackgroundColorEscapeCode(ConsoleColor.Red)
-                + message
-                + "\x1B[49m"; //resets background color
-
-            // Act
-            console.WriteLine(message, background: ConsoleColor.Red, foreground: null);
-            console.Flush();
-
-            // Assert
-            Assert.Equal(expectedMessage + Environment.NewLine, systemConsole.Message);
-        }
-
-        [Fact]
-        public void WriteMessage_InOrder_WhenBothForegroundOrBackgroundColorsSpecified()
-        {
-            // Arrange
-            var systemConsole = new TestAnsiSystemConsole();
-            var console = new AnsiLogConsole(systemConsole);
-            var message = "Request received";
-            var expectedMessage = GetBackgroundColorEscapeCode(ConsoleColor.Red)
-                + GetForegroundColorEscapeCode(ConsoleColor.DarkGreen)
-                + "Request received"
-                + "\x1B[39m\x1B[22m" //resets foreground color
-                + "\x1B[49m" //resets background color
-                + Environment.NewLine;
-
-            // Act
-            console.WriteLine(message, background: ConsoleColor.Red, foreground: ConsoleColor.DarkGreen);
-            console.Flush();
-
-            // Assert
-            Assert.Equal(expectedMessage, systemConsole.Message);
-        }
-
-        private class TestAnsiSystemConsole : IAnsiSystemConsole
-        {
-            public string Message { get; private set; }
-
-            public void Write(string message)
-            {
-                Message = message;
-            }
-        }
-
-        private static string GetForegroundColorEscapeCode(ConsoleColor color)
-        {
-            switch (color)
-            {
-                case ConsoleColor.Red:
-                    return "\x1B[31m";
-                case ConsoleColor.DarkGreen:
-                    return "\x1B[32m";
-                case ConsoleColor.DarkYellow:
-                    return "\x1B[33m";
-                case ConsoleColor.Gray:
-                    return "\x1B[37m";
-                default:
-                    return "\x1B[39m";
-            }
-        }
-
-        private static string GetBackgroundColorEscapeCode(ConsoleColor color)
-        {
-            switch (color)
-            {
-                case ConsoleColor.Red:
-                    return "\x1B[41m";
-                default:
-                    return "\x1B[49m";
-            }
-        }
-    }
-}
index 2cd2e12..e381a1b 100644 (file)
@@ -19,6 +19,7 @@
   </PropertyGroup>
   <ItemGroup>
     <Compile Include="$(CommonPath)System\HexConverter.cs" Link="Common\System\HexConverter.cs" />
+    <Compile Include="$(CommonPath)System\Text\Json\PooledByteBufferWriter.cs" Link="Common\System\Text\Json\PooledByteBufferWriter.cs" />
     <Compile Include="System\Text\Json\BitStack.cs" />
     <Compile Include="System\Text\Json\Document\JsonDocument.cs" />
     <Compile Include="System\Text\Json\Document\JsonDocument.DbRow.cs" />
     <Compile Include="System\Text\Json\Serialization\MemberAccessor.cs" />
     <Compile Include="System\Text\Json\Serialization\MetadataPropertyName.cs" />
     <Compile Include="System\Text\Json\Serialization\ParameterRef.cs" />
-    <Compile Include="System\Text\Json\Serialization\PooledByteBufferWriter.cs" />
     <Compile Include="System\Text\Json\Serialization\PreserveReferenceHandler.cs" />
     <Compile Include="System\Text\Json\Serialization\PreserveReferenceResolver.cs" />
     <Compile Include="System\Text\Json\Serialization\PropertyRef.cs" />
index a308329..ee48654 100644 (file)
@@ -13,13 +13,6 @@ namespace System.Text.Json
     {
         [DoesNotReturn]
         [MethodImpl(MethodImplOptions.NoInlining)]
-        public static void ThrowOutOfMemoryException_BufferMaximumSizeExceeded(uint capacity)
-        {
-            throw new OutOfMemoryException(SR.Format(SR.BufferMaximumSizeExceeded, capacity));
-        }
-
-        [DoesNotReturn]
-        [MethodImpl(MethodImplOptions.NoInlining)]
         public static void ThrowArgumentException_DeserializeWrongType(Type type, object value)
         {
             throw new ArgumentException(SR.Format(SR.DeserializeWrongType, type, value.GetType()));