1

I am writing a logging behavior like in this blog by Pieter de Rycke, but for NLog. I came up with this code:

public class NLogLogger : IParameterInspector
{
    private void Log(Type instanceType, string operationName, string msg)
    {
        NLog.Logger logger = LogManager.GetLogger(
            instanceType.FullName, instanceType);
        logger.Info(msg, instanceType);
    }

    public object BeforeCall(string operationName, object[] inputs)
    {
        // Retrieve the service instance type for the logger then log the call.
        OperationContext operationContext = OperationContext.Current;
        Type instanceType = operationContext.InstanceContext
            .GetServiceInstance().GetType();
        Log(instanceType, operationName, "BeforeCall");

        return instanceType;
    }

    public void AfterCall(
        string operationName, object[] outputs,
        object returnValue, object correlationState
    )
    {
        if (correlationState is Type)
            Log(correlationState as Type, operationName, "AfterCall");
    }
}

The logging behavior works fine. I injected it into the service Example.MyService using an attribute as described by Pieter. I have this layout in an NLog target:

${longdate} ${callsite} ${level:uppercase=true} ${message}

However the callsite for the operation GetContacts is wrong:

2013-07-11 13:32:53.1379 Common.NLogLogger.Log INFO BeforeCall
2013-07-11 13:32:53.7121 Common.NLogLogger.Log INFO AfterCall

Correct would be this:

2013-07-11 13:32:53.1379 Example.MyService.GetContacts INFO BeforeCall
2013-07-11 13:32:53.7121 Example.MyService.GetContacts INFO AfterCall

What have I tried?

NLog offers a special handling of callsite for logging wrappers or facades, as described in this StackOverflow answer: Pass the class of the callsite to the logging methods.

In fact I did this with logger.Info(msg, instanceType) above in the Log() method. However this does not work because the callsite is not yet in the stack trace when the behavior's BeforeCall() method is running. WCF has not yet even started to run the operation. NLog does not find the callsite in the stack trace and is not capable to unwrap the stack trace.

How can I fake a callsite? Or how can I display the "right" callsite for the logging behavior?

Community
  • 1
  • 1
nalply
  • 26,770
  • 15
  • 78
  • 101
  • 1
    You need to use the Log method. You cannot write a wrapper and simply delegate to NLog's Info/Debug/Trace/etc methods. If you look again at the link you posted (to an answer that I posted some time back), you will see that I showed how to write a wrapper (that preserves call site info) using the Log method. – wageoghe Jul 11 '13 at 13:54
  • I tried out different things, among them also the `Log` method and the `LogEventInfo` class. The problem is that `NLogLogger` is not really a wrapper. Please re-read the thing about the stack trace. – nalply Jul 11 '13 at 17:48
  • 1
    You're right, I misread what you are trying to do. I will add one more suggestion. I don't claim that it is a good one, but it might help. – wageoghe Jul 12 '13 at 13:12

1 Answers1

0

UPDATE:

Thanks to your clarification, I better understand what you are trying to do. You would like the messages logged from the IParameterInspector implementation to reflect a call site of "Example.MyService.GetContacts" where Example.MyService is your service (as indicated by the instanceType parameter) and "GetContacts" is the operation. You could synthesize the call site information manually. You would still use NLog's Logger.Log method and you would still create a LogEventInfo object. Additionally, you can store the "class" and "method" in the LogEventInfo.Properties object. Rather than retrieving a logger (from LogManager) based on the instanceType (i.e. the service), retrieve the logger based on the type of the parameter inspector (NLogLogger in your case). Finally, you can add an additional rule to your NLog.config (and apply it to the NLogLogger type) so that rule has a different logging format. You will manually add a field to the logging format that contains the call site information (that you stored in the LogEventInfo.Properties collection) in the same position as the "real" callsite LayoutRenderer in your other logging rule configurations.

Next I will post a new version of your NLogLogger implementation that does what I described above.

public class NLogLogger : IParameterInspector
{
    private static readonly NLog.Logger logger = LogManager.GetCurrentClassLogger();

    private void Log(Type instanceType, string operationName, string msg)
    {
        NLog.Logger serviceLogger = LogManager.GetLogger(
            instanceType.FullName, instanceType);

        //Create LogEventInfo with the Logger.Name from the logger associated with the service
        LogEventInfo le = new LogEventInfo(LogLevel.Info, serviceLogger.Name, msg);
        le.Properties.Add("fakecallsite", string.Format("{0}.{1}",instanceType.ToString(),operationName);

        //Log the message using the parameter inspector's logger.
        logger.Log(typeof(NLogLogger), le);
    }

    public object BeforeCall(string operationName, object[] inputs)
    {
        // Retrieve the service instance type for the logger then log the call.
        OperationContext operationContext = OperationContext.Current;
        Type instanceType = operationContext.InstanceContext
            .GetServiceInstance().GetType();
        Log(instanceType, operationName, "BeforeCall");

        return instanceType;
    }

    public void AfterCall(
        string operationName, object[] outputs,
        object returnValue, object correlationState
    )
    {
        if (correlationState is Type)
            Log(correlationState, operationName, "AfterCall");
    }
}

Your NLog.config will have rules something like this. One rule is specifically for your NLogLogger Parameter Inspector. It logs to "f1" and is a "final" rule, meaning that logging messages from the parameter inspector won't be logged by any other rules. The other rule is for all other loggers. Each logs to a different file target, but both file targets write to the same file (which works, I think). The key is that each file has its own layout.

<logger name="Your.Full.NameSpace.NLogLogger" minlevel="*" writeTo="f1" final="true" /> 
<logger name="*" minlevel="*" writeTo="f2" /> 

Your targets and layouts would look something like this. We are defining a variable whose value is the value of the EventPropertiesLayoutRenderer, which is the fake call site that we stored in LogEventInfo.Properties["fakecallsite"].

  <variable name="fakecallsite" value="${event-properties:fakecallsite}"/>
  <variable name="f1layout" value="${longdate} | ${level} | ${logger} | ${fakecallsite} | ${message}"/>
  <variable name="f2layout" value="${longdate} | ${level} | ${logger} | ${callsite} | ${message}"/>
  <targets>
    <target name="f1" xsi:type="File" layout="${f1layout}" fileName="${basedir}/${shortdate}.log" />
    <target name="f2" xsi:type="File" layout="${f2layout}" fileName="${basedir}/${shortdate}.log"        />
  </targets>

Note that I have not tried this, but I think it should work (or should be close enough that you can get it working). One limitation is that, since we are calculating the fake call site, we cannot use the real callsite LayoutRenderer to manipulate the contents of the fakecallsite field in the output. If this important, it can probably be simulated by storing the class and method separately (in LogEventInfo.Properties) and then setting the "fakecallsite" variable in the NLog.config to contain the class, the method, or both.

END UPDATE

Your wrapper should use the Log method. Also, the type you pass to the NLog Logger.Log method should be the type of your NLog Logger wrapper, not the type of the type of the service instance. You can still use the type of your service instance to retrieve the right Logger instance. It should look something like this:

public class NLogLogger : IParameterInspector
{
    private void Log(Type instanceType, string operationName, string msg)
    {
        NLog.Logger logger = LogManager.GetLogger(
            instanceType.FullName, instanceType);

        //This is the key to preserving the call site in a wrapper.  Create a LogEventInfo
        //then use NLog's Logger.Log method to log the message, passing the type of your 
        //wrapper as the first argument.

        LogEventInfo le = new LogEventInfo(LogLevel.Info, logger.Name, msg);
        logger.Log(typeof(NLogLogger), le);
    }

    public object BeforeCall(string operationName, object[] inputs)
    {
        // Retrieve the service instance type for the logger then log the call.
        OperationContext operationContext = OperationContext.Current;
        Type instanceType = operationContext.InstanceContext
            .GetServiceInstance().GetType();
        Log(instanceType, operationName, "BeforeCall");

        return instanceType;
    }

    public void AfterCall(
        string operationName, object[] outputs,
        object returnValue, object correlationState
    )
    {
        if (correlationState is Type)
            Log(correlationState, operationName, "AfterCall");
    }
}
wageoghe
  • 27,390
  • 13
  • 88
  • 116