C# Serilog: how to log with String interpolation and keep argument names in message templates?

8.3k Views Asked by At

How to replace this code:

string name = "John";
logger.Information("length of name '{name}' is {nameLength}", name, name.Length);

with C# String interpolation like this or similar

string name = "John";
// :-( lost benefit of structured logging: property names not passed to logger
logger.Information($"length of name '{name}' is {name.Length}");

but keep the property names for structured logging to work?

Benefits would be:

  1. Increased readability
  2. You'll never forget an argument in arguments list or a property name in message template, especially when you make changes to your logging code
  3. You always know what this property name will print to your log
2

There are 2 best solutions below

8
On BEST ANSWER

Add this file to your project. It has ILogger extension methods VerboseInterpolated(), DebugInterpolated() and so on. There are also unit tests here.

Usage with format string

string name = "John";
// add 'Interpolated' to method name: InformationInterpolated() instead of Information()
// add name of the property after the expression. Name is passed to the logger
logger.InformationInterpolated($"length of name '{name:name}' is {name.Length:Length}");

But be careful: it's all too easy to use the wrong method. If you accidentally use the Serilog's method, for example logger.Debug($"length = {length:propertyNameForLogger}"), it will log length = propertyNameForLogger, so no argument value will be logged. This is due to propertyNameForLogger is format for your value.

Usage with anonymous types

string name = "John";
// add 'Interpolated' to method name: InformationInterpolated() instead of Information()
// create an anonymous object with 'new { propertyName }'. 
// It's much slower because of using Reflection, but allows to write the variable name only once. 
logger.InformationInterpolated($"length of name '{new { name }}' is {new { name.Length }}");
// you can also specify other property names
logger.InformationInterpolated($"length of name '{new { userName = name }}' is {new { lengthOfName = name.Length }}");
0
On

Thanks for a great idea. I've seen one drawback and that is no support for the generic logger when used with DI. I've extended the @Artemious solution. Please let me know if this is not the correct way to do it.

public static void LogInformationInterpolated<T>(this ILogger<T> logger, FormattableString? message) =>
            WriteInterpolated<T>(logger, null, message, Information);

public static void LogInformationInterpolated<T>(this ILogger<T> logger, Exception? ex, FormattableString? message) =>
            WriteInterpolated<T>(logger, ex, message, Information);

public static void WriteInterpolated<T>(this ILogger<T> logger, Exception? ex, FormattableString? message, Serilog.Events.LogEventLevel logEventLevel)
{
    var contextedLogger = Log.ForContext<T>();
    WriteInterpolated(contextedLogger, ex, message, logEventLevel);
}

The ILogger comes from Microsoft.Extensions.Logging namespace, Serilog doesn't seem to have one. But I'm not an expert in this and thus open to suggestions.