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

Tip

All of the logging example source code is available in the Samples Browser for download. For more information, see Browse code samples: Logging in .NET.

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;

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;

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;

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)
    {
        string? message =
            logEntry.Formatter?.Invoke(
                logEntry.State, logEntry.Exception);

        if (message is 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:

Custom configuration options

To further customize the logging extensibility, your derived ConsoleFormatterOptions class can be configured from any configuration provider. For example, you could use the JSON configuration provider to define your custom options. First define your ConsoleFormatterOptions subclass.

using Microsoft.Extensions.Logging.Console;

namespace Console.ExampleFormatters.CustomWithConfig;

public class CustomWrappingConsoleFormatterOptions : ConsoleFormatterOptions
{
    public string? CustomPrefix { get; set; }

    public string? CustomSuffix { get; set; }
}

The preceding console formatter options class defines two custom properties, representing a prefix and suffix. Next, define the appsettings.json file that will configure your console formatter options.

{
    "Logging": {
        "LogLevel": {
            "Default": "Information",
            "Microsoft": "Warning",
            "Microsoft.Hosting.Lifetime": "Information"
        },
        "Console": {
            "LogLevel": {
                "Default": "Information",
                "Microsoft": "Warning",
                "Microsoft.Hosting.Lifetime": "Information"
            },
            "FormatterName": "CustomTimePrefixingFormatter",
            "FormatterOptions": {
                "CustomPrefix": "|-<[",
                "CustomSuffix": "]>-|",
                "SingleLine": true,
                "IncludeScopes": true,
                "TimestampFormat": "HH:mm:ss.ffff ",
                "UseUtcTimestamp": true,
                "JsonWriterOptions": {
                    "Indented": true
                }
            }
        }
    },
    "AllowedHosts": "*"
}

In the preceding JSON config file:

  • The "Logging" node defines a "Console".
  • The "Console" node specifies a "FormatterName" of "CustomTimePrefixingFormatter", which maps to a custom formatter.
  • The "FormatterOptions" node defines a "CustomPrefix", and "CustomSuffix", as well as a few other derived options.

Tip

The $.Logging.Console.FormatterOptions JSON path is reserved, and will map to a custom ConsoleFormatterOptions when added using the AddConsoleFormatter extension method. This provides the ability to define custom properties, in addition to the ones available.

Consider the following CustomDatePrefixingFormatter:

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

namespace Console.ExampleFormatters.CustomWithConfig;

public sealed class CustomTimePrefixingFormatter : ConsoleFormatter, IDisposable
{
    private readonly IDisposable _optionsReloadToken;
    private CustomWrappingConsoleFormatterOptions _formatterOptions;

    public CustomTimePrefixingFormatter(IOptionsMonitor<CustomWrappingConsoleFormatterOptions> options)
        // Case insensitive
        : base(nameof(CustomTimePrefixingFormatter)) =>
        (_optionsReloadToken, _formatterOptions) =
            (options.OnChange(ReloadLoggerOptions), options.CurrentValue);

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

    public override void Write<TState>(
        in LogEntry<TState> logEntry,
        IExternalScopeProvider scopeProvider,
        TextWriter textWriter)
    {
        string message =
            logEntry.Formatter(
                logEntry.State, logEntry.Exception);

        if (message == null)
        {
            return;
        }

        WritePrefix(textWriter);
        textWriter.Write(message);
        WriteSuffix(textWriter);
    }

    private void WritePrefix(TextWriter textWriter)
    {
        DateTime now = _formatterOptions.UseUtcTimestamp
            ? DateTime.UtcNow
            : DateTime.Now;

        textWriter.Write($"{_formatterOptions.CustomPrefix} {now.ToString(_formatterOptions.TimestampFormat)}");
    }

    private void WriteSuffix(TextWriter textWriter) => textWriter.WriteLine($" {_formatterOptions.CustomSuffix}");

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

In the preceding formatter implementation:

To use custom configuration options, with custom formatter implementations, add when calling ConfigureLogging(IHostBuilder, Action<HostBuilderContext,ILoggingBuilder>).

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Console.ExampleFormatters.CustomWithConfig;

class Program
{
    static void Main(string[] args)
    {
        using IHost host = Host.CreateDefaultBuilder(args)
            .ConfigureLogging(builder =>
            {
                builder.AddConsole()
                    .AddConsoleFormatter
                        <CustomTimePrefixingFormatter, CustomWrappingConsoleFormatterOptions>();
            })
            .Build();

        ILoggerFactory loggerFactory = host.Services.GetRequiredService<ILoggerFactory>();
        ILogger<Program> logger = loggerFactory.CreateLogger<Program>();

        using (logger.BeginScope("Logging scope"))
        {
            logger.LogInformation("Hello World!");
            logger.LogInformation("The .NET developer community happily welcomes you.");
        }
    }
}

The following console output is similar to what you might expect to see from using this CustomTimePrefixingFormatter.

|-<[ 15:03:15.6179 Hello World! ]>-|
|-<[ 15:03:15.6347 The .NET developer community happily welcomes you. ]>-|

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:

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;

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?.Invoke(
                logEntry.State, logEntry.Exception);

        if (message is null)
        {
            return;
        }

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

    private void CustomLogicGoesHere(TextWriter textWriter)
    {
        if (ConsoleColorFormattingEnabled)
        {
            textWriter.WriteWithColor(
                _formatterOptions.CustomPrefix ?? string.Empty,
                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