1

EDIT: While similar, this is not the same as the questions about using an NLog wrapper. Extension methods add another level of indirection which makes even a proper wrapper report the wrong callsite

I currently use a logging wrapper around NLog, and I use the trick they show in the example in their source for getting accurate callsite info. For a new project I started, I wanted to create a simpler class, so I just implemented something like the following interface:

public interface ILogger
{
    void Log( LogEntry entry );
}

And then I created an extension method class like:

public static class LoggerExtensions
{
    public static void Debug( this ILogger logger, string format, params object[] args)
    {
        logger.Log( new LogEntry( LogLevel.Debug, format, args ) );
    }

    ...
}

The problem is that NLog then shows the callsite as the extension method instead of the caller of the extension method. I did a little searching around but couldn't find anything about NLog and extension methods.

Is it possible to fix the callsite information in this case? Or is the only way to include the Debug,Info, etc functions in the interface itself?

bj0
  • 7,893
  • 5
  • 38
  • 49
  • possible duplicate of [How to retain callsite information when wrapping NLog](http://stackoverflow.com/questions/7412156/how-to-retain-callsite-information-when-wrapping-nlog) –  May 03 '13 at 20:04
  • as i mentioned in the question, it's different than using a wrapper, which i was already doing – bj0 May 03 '13 at 20:15

3 Answers3

8

Late to the party but I had issue with this solution given that I use extension methods for my wrapper. By passing the assembly to LogManager I was able to get NLog to ignore my extension class.

    /// <summary>
    /// Bootstrap the wrapper class
    /// </summary>
    static Logger()
    {
        LogManager.AddHiddenAssembly(typeof(LoggingExtensions).Assembly);
    }

There isn't much detail from the docs other than

Adds the given assembly which will be skipped when NLog is trying to find the calling method on stack trace.

from NLog Documentation

With this setup, I've even managed to get extension methods + DI working with SimpleInjector.

To show you can still have a callsite within the same assembly using this method

My Logger() lives in a Utilities project along with a SettingsHelper() I setup a test with output from SettingsHelper:

2015-08-18 20:44:07.5352 | vstest.executionengine.x86 | Debug | Utilities.Settings.SettingsHelper | A test from the same assembly as Logger() | ActionTests.LoggingTest+LogTest.RunLogTest

The bold bit is the ${callsite}

My SettingsHelper() test:

ILogger logger = new Logger(typeof(SettingsHelper));
logger.Debug("A test from the same assembly as Logger()");

Don't forget also to use the overload that takes LogEventInfo()

_logger.Log(typeof(Logger), logEvent);
diZzyCoDeR
  • 89
  • 1
  • 4
  • @bj0 My answer was deleted b/c I posted it to other related questions. But here it is again for you; it works for me so it should suit you. I even tested calling Logger() from the same assembly as LoggerExtensions (my **AddHiddenAssembly**) – diZzyCoDeR Aug 19 '15 at 12:21
  • It looks like your callsite information is from ```ActionTests.LoggingTest+LogTest.RunLogTest```, not from ```SettingsHelper```. My experience is the same, it skips past the callsite if it is inside the Assembly and shows the first caller oustide of the assembly. – bj0 Aug 19 '15 at 17:39
  • Derp. i missed that. you are right! maybe we need to crack open the source and make a pull req to fix this.. or, separate your Logger() in a diff assembly altogether =/ – diZzyCoDeR Aug 19 '15 at 17:42
  • Even better, I'm thinking of turning the loggerType into a params and then I can pass `.Log(logEvent, typeof(Logger), typeof(LoggingExtensions))` in the NLog method `private static bool IsNonUserStackFrame([NotNull] MethodBase method, [NotNull] Type loggerType)` – diZzyCoDeR Aug 19 '15 at 20:09
  • That might be cleaner than a global setting. It's more explicit and gives you more control over when to ignore classes. Not sure how it would impact performance though – bj0 Aug 19 '15 at 20:57
  • Alright, I did it. The code was not tricky, it was compiling the binary and replacing the nuget package w/ my new custom DLL. Works a treat. Modify IsNonUserStackFrame to take params & do a check on .Any() `return declaringType != null && (loggerType.Any(logType => logType.IsAssignableFrom(declaringType)));` then track backwards adding params Type[] to all the loggerType parameters. You have to change the signature of public void Log(wrapperType, logEventInfo) – diZzyCoDeR Aug 21 '15 at 02:18
1

EDIT: Unfortunately, this answer no longer works. NLog broke this functionality in 3.2.0 and it looks like they don't plan to fix it: https://github.com/NLog/NLog/issues/696.

I found a workaround. While not identical to the solution for just an NLog wrapper, it turns out to be similar.

Instead of having the ILogger implementer (the NLog wrapper) simply pass it's own type to NLog, I created an overload that allowed passing it a type from the caller:

public void Log( LogEntry entry )
{
    this.Log( this.GetType(), entry );
}

public void Log( Type type, LogEntry entry)
{
    NLogLogger.Log( type, new NLog.LogEventInfo( ... ) );
}

This requires adding the overload to interface, which makes it sort of ugly (and NLog specific):

public interface ILogger
{
    void Log( LogEntry entry );
    void Log( Type type, LogEntry entry );
}

And then the extension methods can be modified:

public static class LoggerExtensions
{
    public static void Debug( this ILogger logger, string format params object[] args )
    {
        logger.Log( typeof(LoggerExtensions), LogLevel.Debug, format, args ) );
    }

    ...
}

While not as clean as simply using a wrapper, this does allow the use of extension methods while retaining callsite information. If anyone has a cleaner way I'd like to see it.

bj0
  • 7,893
  • 5
  • 38
  • 49
0

Is LogEntry your own class/struct? Maybe you could add a field/property to it to hold the logger type. In your extension method, when you create a LogEntry to send to your ILogger, populate the LogEntry.LoggerType with tyepof(LoggerExtensions).

So, your LoggerExtensions class might look something like this (uncompiled and untested):

public static class LoggerExtensions
{
    public static void Debug( this ILogger logger, string format, params object[] args)
    {
        logger.Log( new LogEntry( typeof(LoggerExtensions), LogLevel.Debug, format, args ) );
    }

    ...
}

Since log4net uses a similar scheme (of looking at the logger type to tell which stack frame corresponds to the actual call site), you could write a log4net wrapper and corresponding log4net wrapper extension methods, if you were so inclined.

wageoghe
  • 27,390
  • 13
  • 88
  • 116
  • That's not a bad idea, it would eliminate the overload in the interface while essentially providing the same solution. I guess the downside would be that you are putting information in LogEntry that's not strictly part of the log entry, thus angering the OO gods. – bj0 May 07 '13 at 17:38