Using Serilog Logging for LaunchDarkly

Overview

Background

I was recently trying to get LaunchDarkly to write its logs using our standard logging configuration - which is Serilog on top of Microsoft.Extensions.Logging. According to the LaunchDarkly docs, this was pretty straight-forward but it turned out to be trickier than I thought.

The solution involved getting services from an IServiceCollection before the standard IServiceProvider is built with the builder.Build() call in ASP.NET applications.

tl;dr: Just wanna see the working code? It's here: https://github.com/dahlsailrunner/launch-darkly-logging

The Documentation

The issue was in the code being suggested:

1using LaunchDarkly.Logging;
2using LaunchDarkly.Sdk.Server;
3
4var config = Configuration.Builder("sdk-key-123abc")
5    .Logging(Logs.CoreLogging)
6    .Build();

All of the above looks simple enough - but what is Logs.CoreLogging? There are two options for this argument:

  • ILogAdapter: Defined by LaunchDarkly as a hook into things like Microsoft.Extensions.Logging
  • IComponentConfigurer<LoggingConfiguration>: Also defined by LaunchDarkly for lower-level control

The ILogAdapter was the route to pursue - but how to get one of those?

There's another NuGet package (LaunchDarkly.Logging.Microsoft) which defined a static method that can return an ILogAdapter if you provide an instance of the standard ILoggerFactory interface.

We're getting close now!!

With the config object from the above snippet of code, we need to register a singleton instance for LaunchDarkly into the IServiceCollection as we're starting the application:

1builder.Services.AddSingleton<ILdClient>(_ => new LdClient(ldConfig));

The Problem

During application startup, you have code something like this:

1var builder = WebApplication.CreateBuilder(args);
2// register services with builder.Services.AddXXXXX() calls
3
4builder.Services.AddSingleton<ILdClient>(_ => new LdClient(ldConfig));
5
6var app = builder.Build(); // builds IServiceProvider from IServiceCollection (builder.Services)

We need to be able to use an ILoggerFactory instance to create the ldConfig variable above - but we won't be building the IServiceProvider until later.

The Solution

We can use an intermediate building of an IServiceProvider to do what we need. A simplified version of the code above looks like this:

1var builder = WebApplication.CreateBuilder(args);
2// register services with builder.Services.AddXXXXX() calls
3
4var sp = builder.Services.BuildServiceProvider(); 
5var loggerFactory = sp.GetService<ILoggerFactory>();
6// .. use loggerFactory when building ldConfig variable
7builder.Services.AddSingleton<ILdClient>(_ => new LdClient(ldConfig));
8
9var app = builder.Build(); // builds IServiceProvider from IServiceCollection (builder.Services)
Caution

The above code will work fine -- GREAT! -- but there's also a caution here. Calling BuildServiceProvider will create instances of singletons that you've registered - and then the app = builder.Build() line will do that again. So definitely avoid registering singleton services before that added call.

The Result

With the above code in place, I started getting great logs from the standard logging pipeline I had set up with Serilog.

Here's a sample entry shown in Seq:

ld-log-entry

And the configuration of filtering by level was just as simple as it should be:

 1{
 2  "Serilog": {
 3    "MinimumLevel": {
 4      "Default": "Debug",
 5      "Override": {
 6        "Microsoft.AspNetCore": "Warning",
 7        "System": "Warning",
 8        "LaunchDarkly": "Debug"
 9      }
10    }
11  }
12}

The Github repo listed above has a fully working minimal API that shows all of this in action. It's here: https://github.com/dahlsailrunner/launch-darkly-logging

Happy coding!