Back to all posts

Real-World ASP.NET Core Logging Configuration

Posted on Oct 09, 2017

Posted in category:
Development
ASP.NET

ASP.NET Core provides a much better foundation for the integration of logging into your projects. .NET Core allows you to quickly aggregate your logging with log messages coming from the runtime creating a full logging stream. Although a great platform when working inside of .NET Core for your first few projects trying to figure out exactly what it takes to get logging setup to a level that records it where you want it when you want it, and with the required granularity might having you wanting to pull your hair out! In this post, we will investigate a process to introduce complete logging to an ASP.NET Core project.

Goals

Before diving too far into the process lets set the goals of what I wanted to accomplish as part of my logging setup. In my application, I wanted to accomplish the following tasks.

  • All existing logging should not be modified, and included if configured, in any logging output
  • Logging should be file-based, keeping a history of around one month automatically
  • If needed, optional logging to Email or other escalation should be easy to implement
  • Exceptions on application startup should NOT be omitted from logging
  • Logging configuration should pull from appsettings.json as well as Environment Variables for environment-specific changes

This is a bare minimum set of goals for an application. For other applications, I might have other requirements, but this is the foundation.

Introducing Serilog

When looking for the right solution, I quickly stumbled across Serilog and was quite impressed with its ability. Serilog supports many "sinks" or destinations for logging (File, SQL, AI, Email, etc.), a robust configuration system, as well as a process that will seamlessly capture ALL logging using the out-of-the-box ILog, ILogger, and related implementations.

Documentation for Serilog is quite nice. However, I found it to be a bit fragmented. You have to find the documentation on Rolling File Logging in one place, on appsettings.json configuration in another, and similar through the process. It is understandable, but an undertaking to truly configure. Thus this post!

Implementation in My ASP.NET Core Project

Enough backstory, let's get to implementing! The following sub-sections outline all changes completed to implement logging in my ASP.NET Core 2.0 project, and I have repeated these processes on others in the hope of making this a systematic process.

Add NuGet Packages

The first step is to include all of the needed NuGet packages to our environment. For our example we want to support ASP.NET Core, Rolling File-based logging, Environment Information, usage of a background thread for logging, and loading configuration from appsettings.json, resulting in the following minimum NuGet package inclusions.

Package Installation
Install-Package Serilog.AspNetCore
Install-Package Serilog.Enrichers.Environment
Install-Package Serilog.Settings.Configuration
Install-Package Serilog.Sinks.RollingFile
Install-Package Serilog.Sinks.Async

You might also elect to install SeriLog.Sinks.Console for development purposes if desired. At this point, our project is ready to be configured.

Create JSON Settings

Given my desire to configure logging from appsettings.json, we might as well start there. The following is my configuration for SeriLog. NOTE: There appear to be issues with some users seeing the below JSON.  You can download the JSON snippet here.

JSON Additions
"Serilog": {
  "Using": [ "Serilog.Sinks.RollingFile", "Serilog.Sinks.Async" ],
  "MinimumLevel": {
    "Default": "Information",
    "Override": {
      "Microsoft": "Warning"
    }
  },
  "WriteTo": [
    {
      "Name": "Async",
      "Args": {
        "configure": [
          {
            "Name": "RollingFile",
            "Args": { "pathFormat": "Logs/log-{Date}.log" }
          }
        ]
      }
    }
  ],
      "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ],
  "Properties": {
    "Application": "MyApplicationName"
  }
}

This information is fairly easy to understand, however, it is worth noting a few key items. The "Using" simply declares the various sinks that will be in use. Within "MinimumLevel" we are configuring the logging level desired for the application. The key with this configuration is that we are using a default value of "Information," but for Microsoft items we require it to be a warning or higher severity. The configuration in this manner is critical, as with Microsoft set to a lower level you will see outputs of all Entity Framework Queries and more. This information could be helpful in development but would add overhead and disk usage in production.

After this, we configure our "WriteTo" section, or more specifically where the log messages go. In our case, we add a destination of "Async" allowing for items to be logged using a background thread to improve performance. Within this configuration, we set that the final destination is a RollingFile and each file should be named log-{Date}.log inside of the log folder. With no additional configuration here 31 days of log history is assumed.

Lastly, we configure enrichments including MachineId, ThreadId, and the ability to add Contextual information as desired. For more information on the usage of these items in your logging via the Enrichments section of the Serilog documentation. I'll include more about enhancing your in-code logging in a future blog post.

Wiring Things Together

The final step in the process is to write this all together. We do this inside of the program.cs file within our web project. The below snippet shows the pre-existing Main method for a .NET Core 2.0 project.

Default Main Method
public static void Main(string[] args)
{
    BuildWebHost(args).Run();
}

We will modify this to configure our logging first, then start the application. This way we can log all aspects of the application, including startup failures. The following shows our modified Main method; you will need to add "using Serilog" and "using Microsoft.Extensions.Configuration" statements the top of your code file.

New Main() Method
public static int Main(string[] args)
{
    //Build Config
    var currentEnv = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
    var configuration = new ConfigurationBuilder()
        .AddJsonFile("appsettings.json")
        .AddJsonFile($"appsettings.{currentEnv}.json", optional: true)
        .AddEnvironmentVariables()
        .Build();

    //Configure logger
    Log.Logger = new LoggerConfiguration()
        .ReadFrom.Configuration(configuration)
        .CreateLogger();

    try
    {
        Log.Information("Starting web host");
        BuildWebHost(args).Run();
        return 0;
    }
    catch (Exception ex)
    {
        Log.Fatal(ex, "Web Host terminated unexpectedly");
        return 1;
    }
    finally
    {
        Log.CloseAndFlush();
    }
}

Lastly, you will update the BuildWebHost Method to tell it that you want to use SeriLog for the application by adding the UseSerilog() statement. An example of how this might look is below.

Builder Changes

WebHost.CreateDefaultBuilder(args)
    .UseStartup<startup>()
    .UseSerilog()
    .Build();

With this, now when we run our application we will have a daily log file created. We can adjust configuration using the appsettings.json file, or environment variables.

Next Steps

There is so much more that I could discuss here, but this creates a comprehensive, and workable logging solution for an application. From here you can look at other Sinks available for logging such as Email for fatal errors, etc. Share your comments/feedback below!