Console log formatting

In .NET 5, support for custom formatting was added to console logs in the Microsoft.Extensions.Logging.Console namespace. There are three predefined formatting options available: Simple, Systemd, and Json.

Important

Previously, the ConsoleLoggerFormat enum allowed for selecting the desired log format, either human readable which was the Default, or single line which is also known as Systemd. However, these were not customizable, and are now deprecated.

In this article, you will learn about console log formatters. The sample source code demonstrates how to:

  • Register a new formatter
  • Select a registered formatter to use
  • Implement a custom formatter

Register formatter

The Console logging provider has several predefined formatters, and exposes the ability to author your own custom formatter. To register any of the available formatters, use the corresponding Add{Type}Console extension method:

Available types Method to register type
ConsoleFormatterNames.Json ConsoleLoggerExtensions.AddJsonConsole
ConsoleFormatterNames.Simple ConsoleLoggerExtensions.AddSimpleConsole
ConsoleFormatterNames.Systemd ConsoleLoggerExtensions.AddSystemdConsole

Simple

To use the Simple console formatter, register it with AddSimpleConsole:

using Microsoft.Extensions.Logging;

namespace Console.ExampleFormatters.Simple
{
    class Program
    {
        static void Main()
        {
            using ILoggerFactory loggerFactory =
                LoggerFactory.Create(builder =>
                    builder.AddSimpleConsole(options =>
                    {
                        options.IncludeScopes = true;
                        options.SingleLine = true;
                        options.TimestampFormat = "hh:mm:ss ";
                    }));

            ILogger<Program> logger = loggerFactory.CreateLogger<Program>();
            using (logger.BeginScope("[scope is enabled]"))
            {
                logger.LogInformation("Hello World!");
                logger.LogInformation("Logs contain timestamp and log level.");
                logger.LogInformation("Each log message is fit in a single line.");
            }
        }
    }
}

In the preceding sample source code, the ConsoleFormatterNames.Simple formatter was registered. It provides logs with the ability to not only wrap information such as time and log level in each log message, but also allows for ANSI color embedding and indentation of messages.

Systemd

The ConsoleFormatterNames.Systemd console logger:

  • Uses the "Syslog" log level format and severities
  • Does not format messages with colors
  • Always logs messages in a single line

This is commonly useful for containers, which often make use of Systemd console logging. With .NET 5, the Simple console logger also enables a compact version that logs in a single line, and also allows for disabling colors as shown in an earlier sample.

using Microsoft.Extensions.Logging;

namespace Console.ExampleFormatters.Systemd
{
    class Program
    {
        static void Main()
        {
            using ILoggerFactory loggerFactory =
                LoggerFactory.Create(builder =>
                    builder.AddSystemdConsole(options =>
                    {
                        options.IncludeScopes = true;
                        options.TimestampFormat = "hh:mm:ss ";
                    }));

            ILogger<Program> logger = loggerFactory.CreateLogger<Program>();
            using (logger.BeginScope("[scope is enabled]"))
            {
                logger.LogInformation("Hello World!");
                logger.LogInformation("Logs contain timestamp and log level.");
                logger.LogInformation("Systemd console logs never provide color options.");
                logger.LogInformation("Systemd console logs always appear in a single line.");
            }
        }
    }
}

Json

To write logs in a JSON format, the Json console formatter is used. The sample source code shows how an ASP.NET Core app might register it. Using the webapp template, create a new ASP.NET Core app with the dotnet new command:

dotnet new webapp -o Console.ExampleFormatters.Json

When running the app, using the template code, you get the default log format below:

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001

By default, the Simple console log formatter is selected with default configuration. You change this by calling AddJsonConsole in the Program.cs:

using System.Text.Json;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Console.ExampleFormatters.Json
{
    class Program
    {
        static Task Main(string[] args) =>
            CreateHostBuilder(args).Build().RunAsync();

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureWebHostDefaults(builder => builder.UseStartup<Startup>())
                .ConfigureLogging(builder =>
                    builder.AddJsonConsole(options =>
                    {
                        options.IncludeScopes = false;
                        options.TimestampFormat = "hh:mm:ss ";
                        options.JsonWriterOptions = new JsonWriterOptions
                        {
                            Indented = true
                        };
                    }));
    }
}

Run the app again, with the above change, the log message is now formatted as JSON:

{
    "Timestamp": "09:08:33 ",
    "EventId": 0,
    "LogLevel": "Information",
    "Category": "Microsoft.Hosting.Lifetime",
    "Message": "Now listening on: https://localhost:5001",
    "State": {
        "Message": "Now listening on: https://localhost:5001",
        "address": "https://localhost:5001",
        "{OriginalFormat}": "Now listening on: {address}"
    }
}

Tip

The Json console formatter, by default, logs each message in a single line. In order to make it more readable while configuring the formatter, set JsonWriterOptions.Indented to true.

Set formatter with configuration

The previous samples have shown how to register a formatter programmatically. Alternatively, this can be done with configuration. Consider the previous web application sample source code, if you update the appsettings.json file rather than calling ConfigureLogging in the Program.cs file, you could get the same outcome. The updated appsettings.json file would configure the formatter as follows:

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        },
        "Console": {
            "LogLevel": {
                "Default": "Information",
                "Microsoft": "Warning",
                "Microsoft.Hosting.Lifetime": "Information"
            },
            "FormatterName": "json",
            "FormatterOptions": {
                "SingleLine": true,
                "IncludeScopes": true,
                "TimestampFormat": "HH:mm:ss ",
                "UseUtcTimestamp": true,
                "JsonWriterOptions": {
                    "Indented": true
                }
            }
        }
    },
    "AllowedHosts": "*"
}

The two key values that need to be set are "FormatterName" and "FormatterOptions". If a formatter with the value set for "FormatterName" is already registered, that formatter is selected, and its properties can be configured as long as they are provided as a key inside the "FormatterOptions" node. The predefined formatter names are reserved under ConsoleFormatterNames:

Implement a custom formatter

To implement a custom formatter, you need to:

Create an extension method to handle this for you:

using Microsoft.Extensions.Logging;
using System;

namespace Console.ExampleFormatters.Custom
{
    public static class ConsoleLoggerExtensions
    {
        public static ILoggingBuilder AddCustomFormatter(
            this ILoggingBuilder builder,
            Action<CustomOptions> configure) =>
            builder.AddConsole(options => options.FormatterName = "customName")
                .AddConsoleFormatter<CustomFormatter, CustomOptions>(configure);
    }
}

The CustomOptions are defined as follows:

using Microsoft.Extensions.Logging.Console;

namespace Console.ExampleFormatters.Custom
{
    public class CustomOptions : ConsoleFormatterOptions
    {
        public string CustomPrefix { get; set; }
    }
}

In the preceding code, the options are a subclass of ConsoleFormatterOptions.

The AddConsoleFormatter API:

  • Registers a subclass of ConsoleFormatter
  • Handles configuration:
using Microsoft.Extensions.Logging;

namespace Console.ExampleFormatters.Custom
{
    class Program
    {
        static void Main()
        {
            using ILoggerFactory loggerFactory =
                LoggerFactory.Create(builder =>
                    builder.AddCustomFormatter(options =>
                        options.CustomPrefix = " ~~~~~ "));

            ILogger<Program> logger = loggerFactory.CreateLogger<Program>();
            using (logger.BeginScope("TODO: Add logic to enable scopes"))
            {
                logger.LogInformation("Hello World!");
                logger.LogInformation("TODO: Add logic to enable timestamp and log level info.");
            }
        }
    }
}

Define a CustomerFormatter subclass of ConsoleFormatter:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Options;
using System;
using System.IO;

namespace Console.ExampleFormatters.Custom
{
    public sealed class CustomFormatter : ConsoleFormatter, IDisposable
    {
        private readonly IDisposable _optionsReloadToken;
        private CustomOptions _formatterOptions;

        public CustomFormatter(IOptionsMonitor<CustomOptions> options)
            // Case insensitive
            : base("customName") =>
            (_optionsReloadToken, _formatterOptions) =
                (options.OnChange(ReloadLoggerOptions), options.CurrentValue);

        private void ReloadLoggerOptions(CustomOptions options) =>
            _formatterOptions = options;

        public override void Write<TState>(
            in LogEntry<TState> logEntry,
            IExternalScopeProvider scopeProvider,
            TextWriter textWriter)
        {
            if (logEntry.Exception is null)
            {
                return;
            }

            string message =
                logEntry.Formatter(
                    logEntry.State, logEntry.Exception);
            
            if (message == null)
            {
                return;
            }
            
            CustomLogicGoesHere(textWriter);
            textWriter.WriteLine(message);
        }

        private void CustomLogicGoesHere(TextWriter textWriter)
        {
            textWriter.Write(_formatterOptions.CustomPrefix);
        }

        public void Dispose() => _optionsReloadToken?.Dispose();
    }
}

The preceding CustomFormatter.Write<TState> API dictates what text gets wrapped around each log message. A standard ConsoleFormatter should be able to wrap around scopes, time stamps, and severity level of logs at a minimum. Additionally, you can encode ANSI colors in the log messages, and provide desired indentations as well. The implementation of the CustomFormatter.Write<TState> lacks these capabilities.

For inspiration on further customizing formatting, see the existing implementations in the Microsoft.Extensions.Logging.Console namespace:

Implement custom color formatting

In order to properly enable color capabilities in your custom logging formatter, you can extend the SimpleConsoleFormatterOptions as it has a SimpleConsoleFormatterOptions.ColorBehavior property that can be useful for enabling colors in logs.

Create a CustomColorOptions that derives from SimpleConsoleFormatterOptions:

using Microsoft.Extensions.Logging.Console;

namespace Console.ExampleFormatters.Custom
{
    public class CustomColorOptions : SimpleConsoleFormatterOptions
    {
        public string CustomPrefix { get; set; }
    }
}

Next, write some extension methods in a TextWriterExtensions class that allow for conveniently embedding ANSI coded colors within formatted log messages:

using System;
using System.IO;

namespace Console.ExampleFormatters.Custom
{
    public static class TextWriterExtensions
    {
        const string DefaultForegroundColor = "\x1B[39m\x1B[22m";
        const string DefaultBackgroundColor = "\x1B[49m";

        public static void WriteWithColor(
            this TextWriter textWriter,
            string message,
            ConsoleColor? background,
            ConsoleColor? foreground)
        {
            // Order:
            //   1. background color
            //   2. foreground color
            //   3. message
            //   4. reset foreground color
            //   5. reset background color

            var backgroundColor = background.HasValue ? GetBackgroundColorEscapeCode(background.Value) : null;
            var foregroundColor = foreground.HasValue ? GetForegroundColorEscapeCode(foreground.Value) : null;

            if (backgroundColor != null)
            {
                textWriter.Write(backgroundColor);
            }
            if (foregroundColor != null)
            {
                textWriter.Write(foregroundColor);
            }

            textWriter.WriteLine(message);

            if (foregroundColor != null)
            {
                textWriter.Write(DefaultForegroundColor);
            }
            if (backgroundColor != null)
            {
                textWriter.Write(DefaultBackgroundColor);
            }
        }

        static string GetForegroundColorEscapeCode(ConsoleColor color) =>
            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
            };

        static string GetBackgroundColorEscapeCode(ConsoleColor color) =>
            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
            };
    }
}

A custom color formatter that handles applying custom colors could be defined as follows:

using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Logging.Console;
using Microsoft.Extensions.Options;
using System;
using System.IO;

namespace Console.ExampleFormatters.Custom
{
    public sealed class CustomColorFormatter : ConsoleFormatter, IDisposable
    {
        private readonly IDisposable _optionsReloadToken;
        private CustomColorOptions _formatterOptions;

        private bool ConsoleColorFormattingEnabled =>
            _formatterOptions.ColorBehavior == LoggerColorBehavior.Enabled ||
            _formatterOptions.ColorBehavior == LoggerColorBehavior.Default &&
            System.Console.IsOutputRedirected == false;

        public CustomColorFormatter(IOptionsMonitor<CustomColorOptions> options)
            // Case insensitive
            : base("customName") =>
            (_optionsReloadToken, _formatterOptions) =
                (options.OnChange(ReloadLoggerOptions), options.CurrentValue);

        private void ReloadLoggerOptions(CustomColorOptions options) =>
            _formatterOptions = options;

        public override void Write<TState>(
            in LogEntry<TState> logEntry,
            IExternalScopeProvider scopeProvider,
            TextWriter textWriter)
        {
            if (logEntry.Exception is null)
            {
                return;
            }

            string message =
                logEntry.Formatter(
                    logEntry.State, logEntry.Exception);

            if (message == null)
            {
                return;
            }

            CustomLogicGoesHere(textWriter);
            textWriter.WriteLine(message);
        }

        private void CustomLogicGoesHere(TextWriter textWriter)
        {
            if (ConsoleColorFormattingEnabled)
            {
                textWriter.WriteWithColor(
                    _formatterOptions.CustomPrefix,
                    ConsoleColor.Black,
                    ConsoleColor.Green);
            }
            else
            {
                textWriter.Write(_formatterOptions.CustomPrefix);
            }
        }

        public void Dispose() => _optionsReloadToken?.Dispose();
    }
}

When you run the application, the logs will show the CustomPrefix message in the color green when FormatterOptions.ColorBehavior is Enabled.

Note

When LoggerColorBehavior is Disabled, log messages do not interpret embedded ANSI color codes within log messages. Instead, they output the raw message. For example, consider the following:

logger.LogInformation("Random log \x1B[42mwith green background\x1B[49m message");

This would output the verbatim string, and it is not colorized.

Random log \x1B[42mwith green background\x1B[49m message

See also