Where to Initialize a Logger on .NET 5?

Note: The content of this article applies to the standard .NET Core and .NET 5 startup. For .NET 6, there is a new optional startup. If you want to learn more about the minimal hosting model, you can read about it here.

I have been doing some work with the Loupe trial recently (testing, making tutorials, etc.), and one page I visit often is this one:

Screenshot of the first page of the Loupe trial, showing directions for adding Loupe to your application

There is nothing obviously controversial about this page. It’s a set of directions to help users initialize a logger for ASP.NET (Loupe Server) in the startup.ConfigureServices method. But, I then look at our documentation for setting up Loupe for ASP.NET on .NET 5, and I see these directions:

Screenshot of the first page of the getting started documentation for getting started with ASP.NET Core

This page shows you how to initialize Loupe in the host builder. Both sets of directions will get the job done just fine, but I’ll admit that I’m a bit bothered that we have two “recommended” ways to set up Loupe Server. So, I decided to figure out which is generally the best way to initialize Loupe, and I learned a bit about logging the startup process for .NET Core and .NET 5.

Where Can I Initialize Loupe?

There are three ways I can initialize Loupe for ASP.NET Core:

  • Option 1: Program.cs file in the Main method (we generally do not recommend this, but I did end up using this option for my testing).
    public static void Main(string[] args)
          {
              Gibraltar.Agent.Log.StartSession();
              CreateHostBuilder(args).Build().Run();
          }
    
  • Option 2: Program.cs file in the host builder.
          public static IHostBuilder CreateHostBuilder(string[] args) =>
              Host.CreateDefaultBuilder(args)
                  .AddLoupe(builder => builder
                          .AddEntityFrameworkCoreDiagnostics()
                  .ConfigureWebHostDefaults(webBuilder =>
                  {   
                      webBuilder.UseStartup<Startup>();
                  });
    
  • Option 3: Startup.cs file during the startup.ConfigureServices method.
          public void ConfigureServices(IServiceCollection services)
          {   
              services.AddLoupe()
                     .AddEntityFrameworkCoreDiagnostics()
                 services.AddLogging(builder =>
                 {
                     builder.AddLoupe();
                 });
          //Add any other services
          }
    

Our general recommendation is Option 2 (despite the initial tutorial text in our own product). If I initialized Loupe in the Program.cs during the host builder, Loupe is available for more of the startup process than Option 3. So I added a simple log message to every method in the startup to confirm the coverage differences. But when I wrote Microsoft Extensions Logging into the startup.ConfigureServices method, I was greeted by this error:

Screenshot of error notification in program.cs file due to attempting to log in the startup.cs startup.ConfigureServices method

It turns out that Microsoft Extensions Logging is not supported in the places I tested. Which I somehow forgot. For those sections of the application, I need to find a different logging framework.

Wait, Haven’t You Said to Use Microsoft Extensions Logging?

I have written about using Microsoft Extensions Logging with .NET 5 in the past, and I still use it! But, it cannot be used in the startup constructor or the startup.ConfigureServices method. This is a bit of a problem if I need logging for the whole startup process.

Logging the entire startup process is one of the few spots where Microsoft Extensions Logging is not the easy built-in solution for the problem. But it does work along with other loggers as well, and I’m not necessarily required to rip it out the rest of my application. So I decided that I would try to use Serilog and the Loupe API as loggers for this process and see how they work.

Logging the Startup Process

Using Serilog

I chose to try Serilog because Loupe supports it, and we have users interested in using it with .NET 5. Those are two good enough reasons for me. But I have never actually worked with Serilog before, so I first needed to know why people like it. These are the highlights:

  • Easy Structured logging: Want to add extra data to your logs (like the user, the time elapsed, machine name, etc.)? It sounds like you want a form of structured logging, and Serilog makes it easy. I’m a big fan because structured logging makes organizing and searching through logs much more manageable (although loupe users do not need Serilog for structured logging).
  • Well integrated with .NET: Serilog works with .NET Framework, .NET Core, and .NET 5. You can likely utilize any familiarity you have with Serilog on other projects in the .NET ecosystem. It’s a good choice as a framework to get to know better.

The other nice thing about Serilog is that I can initialize it before building the host from the Main method like so:

public static int Main(string[] args)
        {
            /* Gibraltar.Agent.Log.StartSession(); */ //Option 1
            Log.Logger = new LoggerConfiguration()
            .WriteTo.Loupe() //Call Loupe as Serilog Sink
            .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();
            }
        }

This would work for any sink. The only parts specific to Loupe are .WriteTo.Loupe() and the commented out Gibraltar.Agent.Log.StartSession(); (initialization Option 1). But where I initialize Loupe will determine when Serilog can actually start successfully writing to Loupe.

If I initialize Loupe with Option 2 or Option 3, these are the first messages I will see in the recorded session in Loupe:

Loupe desktop showing a "Session Started" log, "Application Started..." log, and "Hosting environment: Development" log

But if I initialize Loupe with Option 1, I can see all the additional information logs I have added to my startup.cs:

Loupe destop showing 4 additional logs that appear before the "Application started..." log

I can see four additional logs when I initialize Loupe before Serilog (logs I added to the Main method, the startup.Configure method, the startup.ConfigureServices method, and a log describing the key repository).

Using the Loupe API

If you do not use Serilog but still want to log your startup process, you can also consider the LoupeAPI.

I can add an information log easily to any process by using Gibraltar.Agent.Log.TraceInformation("Put Message Here");. So, if I’m not using Serilog but am using Loupe, I can still write directly to it during pretty much any process.

So, if my goal is to track as much of the startup process as possible, I found my best option. But it’s not the only thing I want out of my Logger, and there are some real downsides.

The Problems with Initializing in Main

What about Configuration?

The main issue is that this method initializes Loupe before the application configuration is enabled. Generally, the HostBuilder setup (Host.CreateDefaultBuilder) calls for the appsettings.json. If we want to configure Loupe with the appsettings.json file, we should initialize Loupe after it’s available, not before. The earliest point at which I can do that in most cases is with Option 2.

Is My Premise Even Correct?

Another issue here is that I am assuming that I need to log the entire startup process, which in many cases is simply not true. If we think about what we’re missing out if we don’t initialize logging until the HostBuilder setup, they’re “top level” process failures - the kind of things that will cause the application to immediately exit and generally dump an exception message and a stack trace to the console.

In most runtime environments, this works pretty well - immediate feedback to the invoking process (be it the console, Kubernetes, Docker, etc.) that we failed to start and a message & exception to work with. If we failed so early we couldn’t even initialize the HostBuilder it’s likely a catastrophic, and clear, problem.

So, Do I Need to Log the Startup Process?

The answer is I want to log as much as I can reasonably, but not at the expense of unneeded complexity. For many Loupe users and myself, the additional scenarios requiring Option 1 just don’t seem worth the extra effort. The amount of the startup I can log with Microsoft.Extensions.Logging will be OK and I don’t need to obsess over logging outside of that. With Option 2, I get a slight edge over Option 3 by catching the rare exception before/during startup.ConfigureServices method. So for any users setting up Loupe, I recommend following our directions as laid out in the documentation for getting started on .NET 5 and in the video below.


Of course, you can disagree with me, and that’s fine. With Serilog or the Loupe API, you can log the entire startup process using Option 1. We still support it with Loupe.

Interested in trying this for yourself? I tested everything I did in this article using Loupe Desktop, a free log viewer for .NET Framework, .NET Core, .NET 5 and .NET 6. You can learn more and install it below.

Loupe Desktop - A Local .NET and Java Log Viewer

Completely free local log viewer
Works with the technologies you use
Adds additional metrics and supplemental data