Back to all posts

ASP.NET Core Logging - Making Logging Usable with elmah.io

Posted on Dec 14, 2019

Posted in category:
Development
ASP.NET

A little more than two years ago, I wrote a blog post Real-World ASP.NET Core Logging Configuration which covered how to get ASP.NET Core to generate logs that you can use. In the time since creating that posting, I have been continually evolving the manner that I work with logging and have recently implemented a new strategy that provides a more holistic solution building upon the prior sample. I would strongly suggest reading the prior post, as although I'll be including the full code samples within this post; however, I do not provide as much insight as to WHY the code is the way that it is.

Goals

Expanding upon the examples from before where we created an effective logging process to local files, we have more lofty goals with this posting.

  • All log messages, Information or higher, shall be logged to a local file with 30 days of history, one file per day.
  • All log messages, Warning or higher, shall also be logged to an external service that provides notifications for immediate action
  • All log messages shared to the service as mentioned above should contain as much ASP.NET Context as possible
  • Logging should have as minimal of an impact to the performance of the application as possible
  • Logging configuration should still pull from appsettings.json allowing configuration changes during deployment etc

As you can tell here, I'm trying to solve the age-old issue of having all kinds of logging but never actually doing anything with it. When was the last time you looked at your logs? I'm willing to guess it was shortly after someone mentioned that they encountered an error, with this new process we can be proactive and manage errors.

Introducing elmah.io

I've spent the past two years trying to investigate all of the various tools and applications for storing logging and finally landed upon elmah.io as the true contender. They describe their service best: "elmah.io is the easy error logging and uptime monitoring service for .NET. Take back control of your errors with support for all .NET web and logging frameworks."

The best part, their documentation is amazing, and their support team has put up with all of my annoying feature requests, questions, and oddities without skipping a beat! Yes, there is a cost, but it is minimal for the gains that you get

Solution Implementation

Enough backstory, let's get to implementing! I will walk through the entire solution, end-to-end, including the introduction of Serilog and other items. This is current, and tested, with .NET Core 3.1 and will work on earlier versions as well.

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, File-based logging, Environment Information, usage of a background thread for logging, logging to elmah.io, enriching elmah.io entries with context, 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.File
Install-Package Serilog.Sinks.Async
Install-Package Serilog.Sinks.ElmahIo
Install-Package Elmah.Io.AspNetCore.Serilog

You might also elect to install SeriLog.Sinks.Console for development purposes if desired. At this point, our project is ready to be configured. For those that read/implemented the solution from the prior blog post be sure to note the switch to File from RollingFile due to Serilog platform changes.

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.

JSON Additions
"Serilog": {
  "Using": [
    "Serilog.Sinks.File",
    "Serilog.Sinks.Async",
    "Serilog.Sinks.ElmahIo"
  ],
  "MinimumLevel": {
    "Default": "Information",
    "Override": {
      "Microsoft": "Warning"
    }
  },
  "WriteTo": [
    {
      "Name": "ElmahIo",
      "Args": {
        "apiKey": "YOURAPIKEYHERE",
        "logId": "YOURLOGIDHERE",
        "restrictedToMinimumLevel": "Warning"
      }
    },
    {
      "Name": "Async",
      "Args": {
        "configure": [
          {
            "Name": "File",
            "Args": {
              "path": "Logs/log.log",
              "rollingInterval": "Day"
            }
          }
        ]
      }
    }
  ],
  "Enrich": [
    "FromLogContext",
    "WithMachineName",
    "WithThreadId"
  ],
  "Properties": {
    "Application": "ICGWebsite"
  }
}

This information is fairly easy to understand; however, it is worth noting a few key items. The "Using" simply declares the various sinks that you are requesting for use; if one is missing in the project, your application will fail to start. 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. Also note that the minimum level set at this portion of the configuration applies to ALL logging targets, an important factor for the below.

After this, we configure our "WriteTo" section, or more specifically where the log messages go. In our case, we have defined two targets. The first target is elmah.io. We set our configuration values, which are available via elmah.io website, and we specify a different minimum level for this logging, restricting it to Warning or higher. Our second destination is then a file-based solution, using an async background thread due to the higher number of anticipated logging events. This is very similar to the process from the prior blog post.

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.

Updating Program.cs

With the proper NuGet packages and configuration in our project, we can now update our program.cs file to start logging as soon as our application starts. I explain this process in detail in the prior blog posting. The code below is my current program.cs for .NET Core 3.0.

Updated Program.cs W/ Logging
public class Program
{
    public static int Main(string[] args)
    {
        var configuration = new ConfigurationBuilder()
            .AddJsonFile("appsettings.json")
            .AddEnvironmentVariables()
            .Build();

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

        try
        {
            Log.Information("Starting web host");

            CreateHostBuilder(args).Build().Run();

            return 0;
        }
        catch (Exception ex)
        {
            Log.Fatal(ex, "Host terminated unexpectedly");
            return 1;
        }
        finally
        {
            Log.CloseAndFlush();
        }
    }
    
    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder
                    .UseSerilog()
                    .UseStartup();
            });
}

Enable elmah.io Enrichment

The final step in the process is to add inside of your Startup.cs Configure method the following 1 line of code to enable elmah.io enrichment.

elmah.io Enrichment
app.UseElmahIoSerilog()

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. When things happen that are a higher severity, we can see details in elmah similar to the below.

Sample elmah.io dashboard

Conclusion

Logging is only valuable if you can properly react to your logging, this solution greatly improves the ability to respond/react to issues. I hope this was helpful.