Loupe Agent for .NET Core

A while ago we released the first version of the (open-source) Loupe Agent package for .NET Core: Loupe.Agent.Core. This package lets you write log messages from your .NET Core applications to your Loupe server, whether in Loupe Cloud or on-premises. We’re hard at work creating higher-level packages for ASP.NET Core and EF Core that will offer the same kind of automatic, detailed logging you expect from our ASP.NET and ASP.NET MVC/WebAPI packages, but for ASP.NET Core 2.0 and up.

This post will outline how to use the new Loupe Agent for .NET Core in a basic application.

A brief note on terms used: writing about .NET vs .NET Core can be confusing. In this post, I’ll use “.NET Framework” to refer to the Windows-only .NET that includes ASP.NET WebForms, MVC 4.x, WPF, Windows Forms and all that; I’ll use “.NET Core” to refer to the new, cross-platform .NET implementation.

Configuration

If you’re using Loupe in a .NET Framework application, chances are you’re configuring it in a web.config or app.config file, where it has its own special section (with an XSD schema and everything). .NET Core doesn’t have these .config files, so we needed a new way to configure the Loupe Agent.

The ASP.NET Core team came up with a new, more flexible way of handling configuration and options, allowing you to load settings from JSON, XML or even INI files, as well as environment variables and command line arguments. The Microsoft.Extensions.Configuration system is fully extensible as well, so you can grab other configuration packages from NuGet to support sources like Azure Key Vault or Kubernetes Secrets.

We fully support this new configuration system, so let’s look at how it works for Loupe. We’ll use JSON as the main example, because it’s the most readable format.

For this to work in your project, you’ll need references to the various Microsoft.Extensions.Configuration packages. If it’s an ASP.NET Core application you’ll already have them, but for a regular console application or service you’ll need to add package references for Microsoft.Extensions.Configuration.Json and Microsoft.Extensions.Configuration.Binder.

The .NET Core AgentConfiguration type exposes six strongly-typed properties for different configuration areas, each with multiple sub-properties for the actual settings:

  • Listener for settings related to the Trace listener;
  • SessionFile for settings related to the local session file;
  • Packager for settings related to the formatting of log entries;
  • Publisher for settings related to the publishing application;
  • Server for Server settings (address and so on);
  • NetworkViewer for settings related to the network messenger.

We map all these properties to configuration sections and properties with the same names, so you just need a top-level section in your JSON for "Loupe", and then sub-sections for each of these properties, like this:

appsettings.json

{
  "Loupe": {
    "SessionFile": {
      "Enabled": true
    },
    "Publisher": {
      "ProductName": "Spoonmaster 9000",
      "ApplicationName": "Recognition AI",
      "EnvironmentName": "Development"
    },
    "Server": {
      "UseGibraltarService": true,
      "CustomerName": "Your_Loupe_Service_Name_Here"
    }
  }
}

Mapping all these settings to the AgentConfiguration type is easy, thanks to the Binder package. Here’s all the code required to load that JSON file and bind it to our config:

private AgentConfiguration LoadConfig()
{
  var config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .Build();

  var agentConfig = new AgentConfiguration();
  config.GetSection("Loupe").Bind(agentConfig);
  return agentConfig;
}

Job done! Of course, there are settings that we don’t want to put in our appsettings.json file, like our Loupe server details, environment type (Dev/QA/Prod) or whatever. Those, we want to get from other sources, like environment variables or command line arguments. So let’s add those packages to our project: Microsoft.Extensions.Configuration.EnvironmentVariables and Microsoft.Extensions.Configuration.CommandLine.

Then change the LoadConfig method to add the new sources:

private AgentConfiguration LoadConfig(string[] args)
{
  var config = new ConfigurationBuilder()
    .AddJsonFile("appsettings.json")
    .AddEnvironmentVariables()
    .AddCommandLine(args)
    .Build();

  var agentConfig = new AgentConfiguration();
  config.GetSection("Loupe").Bind(agentConfig);
  return agentConfig;
}

Note that we added the args parameter, for which we just supply the args from our Program.Main method.

The order in which we add these sources to our ConfigurationBuilder is important, because the sources added later in the list can overwrite settings from sources added earlier. This allows you to provide default settings in your JSON file and override them with command line arguments, for example.

Let’s say we want to override the Loupe:Publisher:EnvironmentName setting (the colon delimiters are how Configuration handles nested sections and values internally). We have two options:

  • Use an environment variable. On non-Windows systems, environment variable names don’t allow the : character, so we use a double-underscore instead:
    • $ export Loupe__Publisher__EnvironmentName=Production
  • Pass a command line argument when starting the application:
    • $ dotnet run myapp.dll Loupe:Publisher:EnvironmentName=Production

You can use similar techniques to provide values from other sources, allowing you to tweak and fine-tune your Loupe Agent configuration in all the different environments where your application runs.

In an ASP.NET Core 2.0 application, the Configuration object is created for you and available in the Startup class, so you can just grab it from there. And when we release the higher-level packages for ASP.NET Core, all of these steps will be handled for you just by enabling the Loupe service.

Find out more about ASP.NET Core configuration in the Microsoft documentation.

Logging

The Loupe Agent for .NET Core provides the same static Log class for writing log messages, errors and so on that the .NET Framework agent supports, so if you want to carry on using that, you can. But like configuration, the ASP.NET Core team have provided a new abstraction for logging, called Microsoft.Extensions.Logging. We support this new system with our own Loupe.Extensions.Logging package.

There are two advantages to using this new abstraction layer. The first applies to your code, and the second applies to Loupe’s ability to process and store log messages.

Logging in Your Code

By using the Logging abstraction layer, you can write your application-level log messages to a single “log” object and have those messages directed to multiple different targets (or “sinks”). Those might include plain old Console or plain-text file output when working in Development mode, and Loupe in QA and Production.

To get started, we need to use the LoggerFactory from the Microsoft.Extensions.Logging package. We create an instance of the factory, then add the Loupe target to it like this:

private ILoggerFactory CreateLoggerFactory()
{
    var factory = new LoggerFactory();
    factory.AddLoupe();
    return factory;
}

That’s it. We can now use this factory to create logger objects throughout our application, and anything we write to them will be written to our Loupe server:

public class MessageService
{
  private readonly ILogger _logger;

  public MessageService(ILoggerFactory loggerFactory)
  {
    _logger = loggerFactory.CreateLogger<MessageService>();
  }

  public void ProcessMessage(Message message)
  {
    _logger.LogInfo($"Processing message {message.Id}");
    try
    {
      // Do something with message
      _logger.LogInfo($"Processed message {message.Id}");
    }
    catch (Exception ex)
    {
      _logger.LogError(0, ex, $"Error processing message {message.Id}");
      throw;
    }
  }
}

The call to LogError is a bit more complicated, because the overload that takes an Exception argument also requires an EventId argument. Event IDs are a useful way of organizing and searching for events, and we’ll be adding proper support for them in a future release.

LogLevel & LogMessageSeverity

In the .NET Core Logging abstraction, messages are assigned a LogLevel such as Error or Information. These map pretty closely to the Loupe LogMessageSeverity enumeration; this table shows the equivalent levels:

LogLevel LogMessageSeverity
None None
Critical Critical
Error Error
Warning Warning
Information Information
Debug Verbose
Trace Verbose

Logging in Loupe

As well as being able to record your application-level messages, using this common abstraction layer means Loupe can also record messages from the ASP.NET Core framework, and any other components you might be using that support Microsoft Logging, such as DI frameworks, database providers, etc. More and more third-party packages are adding support for the new Logging, which is great news for anyone who cares about what’s going on in their running applications.

We have a bit more work to do on this, but the upcoming Loupe Agent for ASP.NET Core 2.0 will let you add the Loupe provider within WebHostBuilder.ConfigureLogging (and this will also work on the new generic HostBuilder in .NET Core 2.1). We’ll also add the ability to filter log messages by level for different components; for example, you might choose to log “Warning” messages from your own application code, but only “Error” messages from Microsoft framework components. This will be configurable from code, or by adding a “Loupe” entry to the standard “Logging” section in your configuration.

Rock solid centralized logging

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