Loupe Agent for ASP.NET Core

Following on from our earlier post about the Loupe Agent for .NET Core, in this post we’ll look at the new Loupe.Agent.AspNetCore package, which provides the same kind of functionality for ASP.NET Core as the Loupe packages for ASP.NET MVC and WebAPI offer, including out-of-the-box logging and metrics.

Configuration

The configuration information is the same as before, so make sure to read that section of the previous post. But let’s also look at another option for storing sensitive details, like your Loupe Repository name, that you don’t want checked into source control (yes, even if you do have a private repository): User Secrets.

User Secrets

User Secrets are stored in your HOME directory on your development workstation, and secrets for different projects are encrypted with different keys. The User Secret configuration provider is automatically included when your app is running in the Development environment; that is, when ASPNETCORE_ENVIRONMENT is set to Development. You can manage User Secrets using either the dotnet CLI, or Visual Studio 2017.

If you are using the .NET Core SDK 2.1.300 or later, the user-secrets command is included by default. If you are using an earlier version of the SDK, you should really upgrade now: 2.1 is a LTS release which will be supported for 3 years, so it’s a good release to get onto.

To set a user secret from the command line, first make sure your current working directory contains the csproj file for your web project. Then run:

dotnet user-secrets set "Loupe:Server:CustomerName" "your-customer-name"
dotnet user-secrets set "Loupe:Server:Repository" "your-repository-name"

To manage user secrets from Visual Studio 2017, you can just right-click on the project in Solution Explorer and select Manage User Secrets, which lets you edit the settings in JSON format just like appsettings.json.

LogLevels

You can specify which “levels” of message get sent to Loupe within the standard Logging section of your configuration. To do so, add a new property called Loupe, and override the default levels, like this:

{
  "Logging": {
    "LogLevel": {
      "Default": "Debug",
      "System": "Information",
      "Microsoft": "Information"
    },
    "Loupe": {
      "LogLevel": {
        "Default": "Information",
        "System": "Warning",
        "Microsoft": "Warning"
      }
    }
  }
}

With these settings, log entries from types in namespaces starting with System. or Microsoft. will be filtered to only include Warning or higher; all other entries will include Information or higher.

Activation

This section assumes you are using a standard ASP.NET Core web application created from the MVC template.

There are two places you need to add a bit of code to get the Loupe Agent up and running in an ASP.NET Core application; one for logging, and one for diagnostics (metrics).

Logging

We configure logging in the CreateWebHostBuilder method of the Program class. By setting up logging early, we can ensure that any messages or exceptions generated while the application is starting up get sent to Loupe.

We use the ConfigureLogging method of IWebHostBuilder to add Loupe logging, like this:

    public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        WebHost.CreateDefaultBuilder(args)
            .ConfigureLogging(builder =>
            {
                builder.AddLoupe();
            })
            .UseStartup<Startup>();

Diagnostics

The Loupe Agent for ASP.NET Core supports the new DiagnosticSource system, described here. The ASP.NET Core MVC framework produces diagnostic entries using this system, and the Loupe Agent uses them to generate the metrics you are used to seeing for ASP.NET MVC and Web API applications.

To activate this feature, in the ConfigureServices method of your application’s Startup class, just add:

    services.AddLoupe()
        .AddAspNetCoreDiagnostics();

Usage

Logging

ASP.NET Core provides a very good logging library out-of-the-box; you can find the full source for it at github.com/aspnet/Logging. Unlike third-party logging systems, including Loupe’s own Log methods, the Microsoft.Extensions.Logging library is used extensively throughout the whole of ASP.NET Core and Entity Framework Core, as well as a lot of NuGet packages and other libraries, so you can get log messages from everywhere without needing to worry about Trace sources and listeners and things like that.

To write your own messages to the logs, you need an instance of ILogger or ILogger<T>. Usually you can get this via dependency injection (which is also supported out-of-the-box now). These interfaces provide familiar methods for logging at different levels, from Trace for the most verbose of logs, to Critical for only the most important messages.

As an example, let’s add logging to an ASP.NET Core 2.1 API controller to record a warning message when a Customer is not found.

[Route("customers")]
public class CustomerController : Controller
{
    private readonly CustomerContext _context;
    private readonly ILogger<CustomerController> _logger;
    
    public CustomerController(CustomerContext context, ILogger<CustomerController> logger)
    {
        _context = context;
        _logger = logger;
    }
    
    [HttpGet("{id}")]
    public async Task<ActionResult<Customer>> Get(int id)
    {
        var customer = await _context.Customers.FindAsync(id);
        if (customer != null)
        {
            return customer;
        }
        _logger.LogWarning("Customer not found: {Id}", id);
        return NotFound();
    }
}

You might notice that the LogWarning call looks a bit like a string-interpolation construct, even though it’s not. That’s because the library supports “structured logging”; it is possible to extract hierarchal data from the log entry for persistence - For example to route into the Details field of Loupe log messages.

Bonus Logging Tip

Microsoft’s logging library provides a neat extension to reduce the cost of logging in high-performance scenarios: the LoggerMessage class. This provides some static methods that let you pre-compile and cache delegates to write specific log messages.

Look at the example above. Every time that call to LogWarning is made, two things have to happen that have a cost. First, the int id parameter has to be boxed and an array has to be allocated because the method takes a params object[] as its second argument. Second, the message string has to be parsed for interpolation. If your server is running at high load, or if you have logging on a “hot path” (such as in a loop), that’s a lot of extra work to be done.

With LoggerMessage, you create an Action delegate that pre-parses the message string, and is generically-typed to avoid boxing values. In practice, it’s a good idea to wrap these in a static class with extension methods on the ILogger type, and use those for logging; this also helps to keep your messages consistent if you’re writing the same log from multiple places. Let’s refactor our Customer not found log into an extension method using LoggerMessage.

internal static class CommentLoggerExtensions
{
    private static readonly Action<ILogger, int, Exception> LogCustomerNotFound =
        LoggerMessage.Define<int>(LogLevel.Warning, 
            new EventId(4041, nameof(CustomerNotFound)),
            "Customer not found: {Id}");
        
    public static void CustomerNotFound(this ILogger logger, int id) =>
        LogCustomerNotFound(logger, id, hashid, null);
}

Now our Get action can use this extension method:

[HttpGet("{id}")]
public async Task<ActionResult<Customer>> Get(int id)
{
    var customer = await _context.Customers.FindAsync(id);
    if (customer != null)
    {
        return customer;
    }
    _logger.CustomerNotFound(id);
    return NotFound();
}

Note that the delegates created by LoggerMessage.Define will always include an Exception parameter at the end

Metrics

Although Loupe Agent for ASP.NET Core works with DiagnosticSource for the in-framework diagnostics, you should continue to use Loupe metrics as before for your own measurements. Loupe has a built-in capability of tracking nearly any kind of telemetry you might need for your application. We’ve built in a useful set of request tracking in the Loupe Agent for ASP.NET but you can also do your own custom telemetry. For more information see our guide on Loupe Metrics.

Sample application

To see all of this in use, you can check out this demo application created by RendleLabs. It’s a lightweight link-sharing and discussion application called, for some reason, Flbbl.

Here are some screenshots taken from the desktop Loupe client showing the log messages and default ASP.NET Core metrics gathered from an instance of Flbbl in Development mode.

As the application runs you can see what gets recorded in the detail log, viewable using [Loupe Desktop]((/local-logging/free-net-log-viewer) or the Loupe Web UI.

ASP.NET Core Log

Each controller hit also records performance and analytics data so you can perform a range of analysis - from simple checks on average response time over time to drilling into exactly what activities are driving that response time.

ASP.NET Core Metrics Analysis

What can Loupe Do For You?

You can take advantage of all of the .NET Core features with Loupe absolutely free - The various agents are published to NuGet and the source code is available on Github. Gibraltar Software provides full commercial support for the agents as well as the free Loupe Desktop, so you’re covered. If you’re looking to centralize all of your log data and get more out of it for your team then try out Loupe Cloud-Hosted.

Finally, we don’t just say that we’re open to pull requests - if we can make the agents better for you just log an issue on GitHub or contact our support team and we’ll make it happen!

Rock solid centralized logging

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