blog post image
Andrew Lock avatar

Andrew Lock

~4 min read

New in ASP.NET Core 3.0: structured logging for startup messages

Exploring ASP.NET Core 3.0 - Part 6

In this post I describe a small change to the way ASP.NET Core logs messages on startup in ASP.NET Core 3.0. Instead of logging messages directly to the console, ASP.NET Core now uses the logging infrastructure properly, producing structured logs.

Annoying unstructured logs in ASP.NET Core 2.x

When you start your application in ASP.NET Core 2.x, by default ASP.NET Core will output some useful information about your application to the console, such as the current environment, the content root path, and the URLs Kestrel is listening on:

Using launch settings from C:\repos\andrewlock\blog-examples\suppress-console-messages\Properties\launchSettings.json...
Hosting environment: Development
Content root path: C:\repos\andrewlock\blog-examples\suppress-console-messages
Now listening on: https://localhost:5001
Now listening on: http://localhost:5000
Application started. Press Ctrl+C to shut down.

This message, written by the WebHostBuilder, gives you a handy overview of your app, but it's written directly to the console, not through the ASP.NET Core Logging infrastructure provided by Microsoft.Extensions.Logging and used by the rest of the application.

This has two main downsides:

  • This useful information is only written to the console, so it won't be written to any of your other logging infrastructure.
  • The messages written to the console are unstructured and they will be in a different format to any other logs written to the console. They don't even have a log level, or a source.

The last point is especially annoying, as it's common when running in Docker to write structured logs to the standard output (Console), and have another process read these logs and send them to a central location, using fluentd for example.

Startup and shutdown messages are unstructured text in otherwise structured output

Luckily, in ASP.NET Core 2.1 there was a way to disable these messages with an environment variable, as I showed in a previous post. The only downside is that the messages are completely disabled, so that handy information isn't logged at all:

The startup messages are suppressed

Luckily, a small change in ASP.NET Core 3.0 gives us the best of both worlds!

Proper logging in ASP.NET Core 3.0

If you start an ASP.NET Core 3.0 application using dotnet run, you'll notice a subtle difference in the log messages written to the console:

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://localhost:5001
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://localhost:5000
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
      Content root path: C:\repos\andrewlock\blog-examples\suppress-console-messages

The startup messages are now written using structured logging! But the change isn't as simple as using Logger instead of Console. In ASP.NET Core 2.x, it was the WebHost that was responsible for logging these messages. In ASP.NET Core 3.0, these messages are logged by the ConsoleLifetime - the default IHostLifetime registered by the generic host.

I described the role of IHostLifetime (and the ConsoleLifetime in particular) in my previous post, but in summary this class is responsible for listening for the Ctrl+C key press in the console, and starting the shutdown procedure.

The ConsoleLifetime also registers callbacks during its WaitForStartAsync() method, that are invoked when the ApplicationLifetime.ApplicationStarted event is triggered, and also when the ApplicationLifetime.ApplicationStopping event is triggered:

public Task WaitForStartAsync(CancellationToken cancellationToken)
{
    if (!Options.SuppressStatusMessages)
    {
        // Register the callbacks for ApplicationStarted
        _applicationStartedRegistration = ApplicationLifetime.ApplicationStarted.Register(state =>
        {
            ((ConsoleLifetime)state).OnApplicationStarted();
        },
        this);

        // Register the callbacks for ApplicationStopping
        _applicationStoppingRegistration = ApplicationLifetime.ApplicationStopping.Register(state =>
        {
            ((ConsoleLifetime)state).OnApplicationStopping();
        },
        this);
    }

    // ...

    return Task.CompletedTask;
}

These callbacks run the OnApplicationStarted() and OnApplicationStopping() methods (shown below) which simply write to the logging infrastructure:

private void OnApplicationStarted()
{
    Logger.LogInformation("Application started. Press Ctrl+C to shut down.");
    Logger.LogInformation("Hosting environment: {envName}", Environment.EnvironmentName);
    Logger.LogInformation("Content root path: {contentRoot}", Environment.ContentRootPath);
}

private void OnApplicationStopping()
{
    Logger.LogInformation("Application is shutting down...");
}

The SystemdLifetime and WindowsServiceLifetime implementations use the same approach to write log files using the standard logging infrastructure, though the exact messages vary slightly.

Suppressing the startup messages with the ConsoleLifetime

One slightly surprising change caused by the startup messages been created by the ConsoleLifetime, is that you can no longer suppress the messages in the ways I described in my previous post. Setting ASPNETCORE_SUPPRESSSTATUSMESSAGES apparently has no effect - the messages will continue to be logged whether the environment variable is set or not!

As I've already pointed out, this isn't really a big issue now, seeing as the messages are logged properly using the Microsoft.Extensions.Logging infrastructure. But if those messages offend you for some reason, and you really want to get rid of them, you can configure the ConsoleLifetimeOptions in Startup.cs:

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        // ... other configuration
        services.Configure<ConsoleLifetimeOptions>(opts => opts.SuppressStatusMessages = true);
    }
}

You could even set the SuppressStatusMessages field based on the presence of the ASPNETCORE_SUPPRESSSTATUSMESSAGES environment variable if you want:

public class Startup
{
    public IConfiguration Configuration { get; }

    public Startup(IConfiguration configuration) => Configuration = configuration;

    public void ConfigureServices(IServiceCollection services)
    {
        // ... other configuration
        services.Configure<ConsoleLifetimeOptions>(opts 
                => opts.SuppressStatusMessages = Configuration["SuppressStatusMessages"] != null);
    }
}

If you do chose to suppress the messages, note that Kestrel will still log the URLs it's listening on; there's no way to suppress those:

info: Microsoft.Hosting.Lifetime[0]
      Now listening on: http://0.0.0.0:5000
info: Microsoft.Hosting.Lifetime[0]
      Now listening on: https://0.0.0.0:5001

Summary

In this post I showed how the annoyingly-unstructured logs that were written on app startup in ASP.NET Core 2.x apps are now written using structured logging in 3.0. This ensures the logs are written to all your configured loggers, as well as having a standard format when written to the console. I described the role of IHostLifetime in the log messages, and showed how you could configure the ConsoleLifetimeOptions to suppress the status messages if you wish.

Andrew Lock | .Net Escapades
Want an email when
there's new posts?