Benefits of structured logging

January 12th 2024 Logging .NET Azure

After you create a new .NET 8 project, you'll soon notice many new diagnostics being enabled by default. If you're using string interpolation in your logging calls like this:

_logger.LogInformation(
    $"Date: {forecast[0].Date}, Temperature: {forecast[0].TemperatureC}"
);

It will now result in the following suggestion in your error list:

CA2254: The logging message template should not vary between calls to LoggerExtensions.LogInformation(ILogger, string?, params object?[])

The linked help page instructs you to replace the interpolated string with a fixed string as the first method argument and add the previously interpolated values as additional arguments:

_logger.LogInformation(
    "Date: {Date}, Temperature: {Temperature}",
    forecast[0].Date,
    forecast[0].TemperatureC
);

From the C# standpoint, this approach is inferior, as it's much less obvious how the final message is constructed. However, it allows the logging providers to implement structured logging.

When using the default console logger, you won't notice any difference between the two calls. Both will be logged the same:

Information: Date: 12/29/2023, Temperature: 29
Information: Date: 12/29/2023, Temperature: 29

If you switch to Application Insights logger, for example, there is a significant difference. The interpolated message will still be the same for both:

Interpolated message in Application Insights

If you want to find all occurrences of this log message based on this interpolated message, your only option is to do a string search:

traces
| where message startswith "Date:"

However, the entry from the second logger call will contain all three method arguments as additional custom dimensions:

  • OriginalFormat: Date: {Date}, Temperature: {Temperature}
  • Date: 12/29/2023
  • Temperature: 29

You can use any of them as search criteria, and you can also include them as separate columns in the search results:

traces
| where customDimensions.OriginalFormat == "Date: {Date}, Temperature: {Temperature}"
| where customDimensions.Temperature > 0
| extend Temperature = customDimensions.Temperature

Not only is this a much more reliable way to find all matching log entries, but since the arguments preserve their data type, you can even do range searches on their values. Additional columns also make it easier to inspect the search results:

Structured logging in Applcation Insights

You can find a sample project with full source code in my GitHub repository. To try it out yourself, you'll have to create your own Application Insights resource in Azure and add its connection string to the appsettings.json file:

{
  "ApplicationInsights": {
    "ConnectionString": "<YourConnectionString>"
  }
}

It's a good practice to use structured logging for all your logger calls, even if you're currently not using a logging provider that supports structured logging. It doesn't make much difference when originally writing the code, but it can save a lot of effort if you ever change your logging provider in the future. I was really glad to see code suggestions for doing this configured by default for new .NET 8 projects.

Get notified when a new blog post is published (usually every Friday):

If you're looking for online one-on-one mentorship on a related topic, you can find me on Codementor.
If you need a team of experienced software engineers to help you with a project, contact us at Razum.
Copyright
Creative Commons License