April 11, 2024

Using Caller Context with Microsoft Logging

Here are the CallerContext and ILoggerExtension class. They allow the compiler special Caller member attributes to be logged during logging. I believe this to be of enormous benefit when trying to fix bugs in the field.

using System.Runtime.CompilerServices;

/// <summary>
/// A class to record the context of a method call.
/// Records the member name, file and line number of where the method call was made.
/// </summary>
public record CallerContext
{
    public CallerContext(
        [CallerMemberName] string memberName = "",
        [CallerFilePath] string sourceFilePath = "",
        [CallerLineNumber] int sourceLineNumber = 0)
    {
        MemberName = memberName;
        FilePath = sourceFilePath;
        LineNumber = sourceLineNumber;
    }
    
    public static CallerContext Create(
        [CallerMemberName] string memberName = "",
        [CallerFilePath] string sourceFilePath = "",
        [CallerLineNumber] int sourceLineNumber = 0)
    {
        return new CallerContext(memberName, sourceFilePath, sourceLineNumber);
    }

    /// <summary>
    /// Member where the call was made
    /// </summary>
    public string MemberName { get; init; }
    /// <summary>
    /// File where the call was made
    /// </summary>
    public string FilePath { get; init; }
    /// <summary>
    /// Line number in the file where the call was made
    /// </summary>
    public int LineNumber { get; init; }
}

/// <summary>
/// ILogger extensions
/// </summary>
public static class ILoggerExt
{
    /// <summary>
    /// Log something but with extra Caller context information <seealso cref="CallerContext"/>
    /// </summary>
    /// <param name="logger">ILogger being invoked</param>
    /// <param name="logLevel">Level of the logging</param>
    /// <param name="context">The caller context <see cref="CallerContext"/></param>
    /// <param name="message">The message to log</param>
    /// <param name="args">The message parameters to log.</param>
    public static void Log(this ILogger logger,
        LogLevel logLevel,
        CallerContext context,
        string message,
        params object[] args)
    {
        Debug.Assert(logger != null, "trying to use a null logger to log something");
        var enhancedMessage = "{@CallerContext} "  + (message ?? "");

        Debug.Assert(args != null, "args cannot be null");

        object[] newArgs = new object[args.Length + 1];
        newArgs[0] = context; // Prepend the context argument
        // Drawback: Could this cause a slowdown in logging over a long period ?
        Array.Copy(args, sourceIndex: 0, newArgs, destinationIndex:1, args.Length); // add the other arguments

        logger.Log(logLevel, enhancedMessage, newArgs.ToArray());
    }
}

Example Usage

ILogger logger = ...
logger.Log(new CallerContext(), LogLevel.Info, "Message with {@parameters} goes here", parameters);
// OR
logger.Log(CallerContext.Create(), LogLevel.Info, "Message with {@parameters} goes here", parameters); // I prefer this one

No comments: