Extended Logging with Caller Info Attributes

.NET 4.5 caller info attributes may be one of those features that do not get much airtime, but nonetheless are a great addition to the framework.

These attributes will allow you to programmatically access information about the caller of a given method, more specifically, the code file full path, the member name of the caller and the line number at which the method was called.

They are implemented by taking advantage of C# 4.0 optional parameters and are a compile time feature so as an added bonus the returned member name is not affected by obfuscation.

The main usage scenario will be for tracing and debugging routines as will see right now. In this sample code I’ll be using NLog, but the example is also applicable to other logging frameworks like log4net.

First an helper class, without any dependencies and that can be used anywhere to obtain caller information:

using System;
using System.IO;
using System.Runtime.CompilerServices;

public sealed class CallerInfo
{
    private CallerInfo(string filePath, string memberName, int lineNumber)
    {
        this.FilePath = filePath;
        this.MemberName = memberName;
        this.LineNumber = lineNumber;
    }

    public static CallerInfo Create(
        [CallerFilePath] string filePath = "",
        [CallerMemberName] string memberName = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        return new CallerInfo(filePath, memberName, lineNumber);
    }

    public string FilePath { get; private set; }

    public string FileName
    {
        get
        {
            return this.fileName ?? (this.fileName = Path.GetFileName(this.FilePath));
        }
    }

    public string MemberName { get; private set; }

    public int LineNumber { get; private set; }

    public override string ToString()
    {
        return string.Concat(this.FilePath, "|", this.MemberName, "|", this.LineNumber);
    }

    private string fileName;
}

Then an extension class specific for NLog Logger:

using System;
using System.Runtime.CompilerServices;
using NLog;

public static class LoggerExtensions
{
    public static void TraceMemberEntry(
        this Logger logger,
        [CallerFilePath] string filePath = "",
        [CallerMemberName] string memberName = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        LogMemberEntry(logger, LogLevel.Trace, filePath, memberName, lineNumber);
    }

    public static void TraceMemberExit(
        this Logger logger,
        [CallerFilePath] string filePath = "",
        [CallerMemberName] string memberName = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        LogMemberExit(logger, LogLevel.Trace, filePath, memberName, lineNumber);
    }

    public static void DebugMemberEntry(
        this Logger logger,
        [CallerFilePath] string filePath = "",
        [CallerMemberName] string memberName = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        LogMemberEntry(logger, LogLevel.Debug, filePath, memberName, lineNumber);
    }

    public static void DebugMemberExit(
        this Logger logger,
        [CallerFilePath] string filePath = "",
        [CallerMemberName] string memberName = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        LogMemberExit(logger, LogLevel.Debug, filePath, memberName, lineNumber);
    }

    public static void LogMemberEntry(
        this Logger logger,
        LogLevel logLevel,
        [CallerFilePath] string filePath = "",
        [CallerMemberName] string memberName = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        const string MsgFormat = "Entering member {1} at line {2}";

        InternalLog(logger, logLevel, MsgFormat, filePath, memberName, lineNumber);
    }

    public static void LogMemberExit(
        this Logger logger,
        LogLevel logLevel,
        [CallerFilePath] string filePath = "",
        [CallerMemberName] string memberName = "",
        [CallerLineNumber] int lineNumber = 0)
    {
        const string MsgFormat = "Exiting member {1} at line {2}";

        InternalLog(logger, logLevel, MsgFormat, filePath, memberName, lineNumber);
    }

    private static void InternalLog(
        Logger logger,
        LogLevel logLevel,
        string format,
        string filePath,
        string memberName,
        int lineNumber)
    {
        if (logger == null)
            throw new ArgumentNullException("logger");

        if (logLevel == null)
            throw new ArgumentNullException("logLevel");

        logger.Log(logLevel, format, filePath, memberName, lineNumber);
    }
}

Finally an usage example:

using NLog;

internal static class Program
{
    private static readonly Logger Logger = LogManager.GetCurrentClassLogger();

    private static void Main(string[] args)
    {
        Logger.TraceMemberEntry();

        // Compile time feature
        //   Next three lines output the same except for line number
        Logger.Trace(CallerInfo.Create().ToString());
        Logger.Trace(() => CallerInfo.Create().ToString());
        Logger.Trace(delegate() { return CallerInfo.Create().ToString(); });

        Logger.TraceMemberExit();
    }
}

NOTE: Code for helper class and Logger extension also available here.

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s