What Do I Need to Add Logging for .NET Core?

We have all been there before: You’re starting a new project, things were going smoothly, but then something doesn’t work the way you thought it should.
After trying to find a solution without a clue, you decide to “add logging” to the application. Sounds very simple on the surface. But in practice, it requires a few more decisions than you may expect.

When you “add logging” to an application in .NET Core or .NET 5, you need to add three items:

diagram showing the relationsip between the logging api, the logging framework, and the log sink. Each item flows to the next.

  • Logging API: This is the part that you write your code to. When you want to create a log or when an exception happens, the API transfers that information from your application to the logging framework.

  • Logging Framework: It interprets the data provided by the API and constructs the exact log message to be recorded - merging data from the context the log message was written in, applying layouts, and other formatting.

  • Log Sink: This collects, stores, and presents your logs. This may also be referred to as the provider in some documentation (i.e., Microsoft’s documentation).

All together, these items take the information and exceptions from your application and create readable logs. You have likely set each part up before, but did you consider how each portion contributes to the whole?

How helpful your logging is depends on the data passed through, how easy the logs are to access, search, read, and much more. In this article, we will discuss each portion independently and how they impact logging .NET applications. By the end, hopefully, you know what distinguishes the API from the framework, the framework from the sink, and know how they rely on each other.

What is a Logging API?

The Logging API is the interface between your application code and the logging framework. The API you choose will alter the syntax for calling the framework, and the frameworks you can use.

For .NET Core and .NET 5, the most common logging API is Microsoft Extensions Logging. It’s considered the default at this point, and we like it because it’s compatible with pretty much any framework you would want to use.

Before Microsoft Extensions Logging was a thing, we didn’t always discuss the distinction between the logging API and the logging framework. If you wanted to use Serilog, you would use the Serilog framework with the Serilog API. Same for our own Loupe framework. It was largely unnecessary to differentiate the API and framework in most cases. But the Microsoft Extensions Logging API can interface with itself, Serilog, NLog, Log4Net, Loupe, and many others.

What it allows you to do is:

  1. Reference full-fat frameworks like Serilog, Nlog, and Loupe in places it makes sense, like in your main process. Then libraries can just reference the API, avoiding the added dependencies and weight of the logging framework.
  2. Switch frameworks with minimal impact on your application. You will only need to change the code for the parts of the application that bypass the API and go directly to the logging framework.

These are two significant advantages, but not critical for an application. You can still use the Serilog, NLog, Loupe, or other API if you want to. There is no emergency reason to chance to the Microsoft Extensions Logging API if you currently use something else. But if you’re starting a new project or replacing your logging framework, consider using Microsoft Extensions Logging at the API level. It will provide the most flexibility for new .NET Core and .NET 5 projects, which is why we have ensured Loupe works well with MEL.

What is a Logging Framework?

The logging framework is what does the heavy lifting. It takes the information passed to the API by your application, formats and extends it, then passes it to one or more Log Sinks to store or display.
Your selection of a Logging Framework depends mainly on what features you value at this level - for example, message templates or structured logging.

These are some common logging frameworks used by .NET developers:

  • Microsoft Extensions Logging: You do not need to overcomplicate it if your application doesn’t need anything special. This is the default logging framework for .NET as well.
  • Serilog: Instead of rendering messages as text, data structures can be recorded with a template message to be rendered and evaluated in a log viewer (i.e., structured logging). A well supported and powerful choice.
  • Nlog: Designed from the ground up for .NET, NLog has the flexibility of Log4Net with more capabilities and better performance. Still well supported by the Nlog team and works with the most recent versions of .NET.

We have a Loupe logging framework as well, with the goal being to combine logging and metrics into a single framework.

The framework is responsible for formatting your log data. Generally, the goal is to take the raw error text, make it easier to read, and provide additional context that may be helpful, like the user that ran into the error and what version of the application they were using. You may also want to suppress some logs and only send logs with a certain severity to the sink.

Modifying these settings requires some form of configuration. Many are configured in the appsettings.json for .NET 5 and .NET Core. We’ve moved beyond XML! Let’s look at an edited example from the Serilog documentation on GitHub:

{
  "Serilog": {
    "Using":  [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
    "MinimumLevel": "Warning", //Write logs only for warnings and above, no info, debug
    "WriteTo": [ //This is selecting a sink, here we write to console and a file
      { "Name": "Console" },
      { "Name": "File", "Args": { "path": "Logs/log.txt" } }
    ],
    "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], //Adds additional information to the logs
    //...
  }
}

This JSON example shows us a non-intrusive way to configure the Serilog framework. With these settings, I can control the amount of information accepted from the API with the minimum level, add information/context to each log, modify the syntax for each log, and select the log sink.

What is a Log Sink?

The log sink/provider is the final destination for your logs which could be immediate display (to a console), written to a file, buffered in memory, or even sent across a network to a centralized logging system.

You may be familiar with the following sinks for .NET:

  • Debug: Many IDEs have a debug window that you can log to directly. It is handy while actively developing an application as your log data writes to the same interface you use to build the application. And it works in real time. The downside is that these logs disappear once you close your IDE.
  • Console: You can write logs directly to a console, which may allow for more complicated color coding than debug while also working in real time. But it suffers the same downside. Once you close the console, the logs are gone for good.
  • Text File: Many applications use a text file as a sink and automatically store those files in a specific directory. By storing logs to a file, you can refer to them in the future without worrying about keeping anything open. But it’s not a perfect solution. Storage space can become a concern, and searching through thousands of logs with directory based search tools can be hit or miss. Also, this solution will not work for anyone concerned with real-time log viewing.

You can also opt to use a Dedicated Log Viewer. There are many dedicated log viewers for .NET, including our own free one, Loupe Desktop. Any log viewer you would consider using should basically have all the strengths of the previous options while minimizing the downsides of each option.

Adding the Sink

Of course, no matter which sink you choose, you will actually need to write it in as the destination in your application’s code. How you add a sink depends on the framework used, as you are really adding the sink to your framework. With Serilog, I can call the sink directly from the JSON like the previous code sample. If I am using Microsoft Extensions Logging in .NET 5 or .NET Core, I would generally add the sink in the host builder. If I wanted to add the console as a sink, I could use the code from Microsoft’s documentation:

static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureLogging(logging =>
        {
            logging.AddConsole();
        });

If I wanted to add Loupe as a sink, I can use the code found in the Loupe “Getting Started” Documentation:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .AddLoupeLogging();

To fully set up Loupe and tell Loupe to capture metrics and telemetry directly from common .NET libraries, you’d add a little more code to your initialization:

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .AddLoupe(builder => builder.AddAspNetCoreDiagnostics()
                .AddClientLogging() //The Loupe endpoint for client logging
                .AddEntityFrameworkCoreDiagnostics() //EF Core monitoring
                .AddPerformanceCounters()) //Windows Perf Counter monitoring
            .AddLoupeLogging();

Once you decide the sink you want to use and how you want to write to it, your logging setup is complete. With all three items in place, your application can successfully call the logging framework, collect the appropriate data, and send it to a convenient location.

But What About Selecting the Sink?

Chances are, you have some familiarity with some of the sinks I have mentioned so far. If you are looking to use something more capable than the standard choices though, consider trying Loupe Desktop You don’t need to change your whole logging stack to get its benefits, a few lines in the program and startup files, and you can take advantage of the features of Loupe Desktop:

  • Long-term, optimized log storage: We will save each individual session of an application to ensure you have all the data you need. But it’s optimized well enough that you don’t need to worry about buying a new hard drive.
  • Easy log searches: Loupe Desktop allows you to use full text search through all of your log data, so it’s easy to find logs from a specific application, day, user, and more.
  • Performance capture: Loupe can collect performance metrics from your application while it’s running, so you know how your app performed in any given session.
  • Real-Time Logging: View your logs as they are written, and search through them the same way you would a previous session.

Best of all, it’s completely free. If you are interested in upgrading your logging sink, you can learn more about Loupe Desktop in the link below.

Get Loupe Desktop

Rock solid centralized logging

Unlimited applications, unlimited errors, scalable from solo startup to enterprise.