Geeks With Blogs
James Timperley

I recently switch from Microsoft's Enterprise Library Logging block to NLog.  In my opinion, NLog offers a simpler and much cleaner configuration section with better use of placeholders, complemented by custom variables. Despite this, I found one deficiency in my migration; I had lost the ability to simply render all details of an exception into our logs and notification emails.

This is easily remedied by implementing a custom layout renderer. Start by extending 'NLog.LayoutRenderers.LayoutRenderer' and overriding the 'Append' method.

using System.Text;
using NLog;
using NLog.Config;
using NLog.LayoutRenderers;
 
[ThreadAgnostic]
[LayoutRenderer(Name)]
public class ExceptionDetailsRenderer : LayoutRenderer
{
    public const string Name = "exceptiondetails";
 
    protected override void Append(StringBuilder builder, LogEventInfo logEvent)
    {
        // Todo: Append details to StringBuilder
    }
}

 

Now that we have a base layout renderer, we simply need to add the formatting logic to add exception details as well as inner exception details. This is done using reflection with some simple filtering for the properties that are already being rendered.

I have added an additional 'Register' method, allowing the definition to be registered in code, rather than in configuration files. This complements by 'LogWrapper' class which standardizes writing log entries throughout my applications.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using NLog;
using NLog.Config;
using NLog.LayoutRenderers;
 
[ThreadAgnostic]
[LayoutRenderer(Name)]
public sealed class ExceptionDetailsRenderer : LayoutRenderer
{
    public const string Name = "exceptiondetails";
    private const string _Spacer = "======================================";
    private List<string> _FilteredProperties; 
 
    private List<string> FilteredProperties
    {
        get
        {
            if (_FilteredProperties == null)
            {
                _FilteredProperties = new List<string>
                    {
                        "StackTrace",
                        "HResult",
                        "InnerException",
                        "Data"
                    };
            }
 
            return _FilteredProperties;
        }
    }
 
    public bool LogNulls { get; set; }
 
    protected override void Append(StringBuilder builder, LogEventInfo logEvent)
    {
        Append(builder, logEvent.Exception, false);
    }
 
    private void Append(StringBuilder builder, Exception exception, bool isInnerException)
    {
        if (exception == null)
        {
            return;
        }
 
        builder.AppendLine();
 
        var type = exception.GetType();
        if (isInnerException)
        {
            builder.Append("Inner ");
        }
 
        builder.AppendLine("Exception Details:")
            .AppendLine(_Spacer)
            .Append("Exception Type: ")
            .AppendLine(type.ToString());
 
        var bindingFlags = BindingFlags.Instance
            | BindingFlags.Public;
        var properties = type.GetProperties(bindingFlags);
        foreach (var property in properties)
        {
            var propertyName = property.Name;
            var isFiltered = FilteredProperties.Any(filter => String.Equals(propertyName, filter, StringComparison.InvariantCultureIgnoreCase));
            if (isFiltered)
            {
                continue;
            }
 
            var propertyValue = property.GetValue(exception, bindingFlags, null, null, null);
            if (propertyValue == null && !LogNulls)
            {
                continue;
            }
 
            var valueText = propertyValue != null ? propertyValue.ToString() : "NULL";
            builder.Append(propertyName)
                .Append(": ")
                .AppendLine(valueText);
        }
 
        AppendStackTrace(builder, exception.StackTrace, isInnerException);
        Append(builder, exception.InnerException, true);
    }
 
    private void AppendStackTrace(StringBuilder builder, string stackTrace, bool isInnerException)
    {
        if (String.IsNullOrEmpty(stackTrace))
        {
            return;
        }
 
        builder.AppendLine();
 
        if (isInnerException)
        {
            builder.Append("Inner ");
        }
 
        builder.AppendLine("Exception StackTrace:")
            .AppendLine(_Spacer)
            .AppendLine(stackTrace);
    }
 
    public static void Register()
    {
        Type definitionType;
        var layoutRenderers = ConfigurationItemFactory.Default.LayoutRenderers;
        if (layoutRenderers.TryGetDefinition(Name, out definitionType))
        {
            return;
        }
 
        layoutRenderers.RegisterDefinition(Name, typeof(ExceptionDetailsRenderer));
        LogManager.ReconfigExistingLoggers();
    }
}

For brevity I have removed the Trace, Debug, Warn, and Fatal methods. They are modelled after the Info methods. As mentioned above, note how the log wrapper automatically registers our custom layout renderer reducing the amount of application configuration required.

using System;
using NLog;
 
public static class LogWrapper
{
    static LogWrapper()
    {
        ExceptionDetailsRenderer.Register();
    }
 
    #region Log Methods
 
    public static void Info(object toLog)
    {
        Log(toLog, LogLevel.Info);
    }
 
    public static void Info(string messageFormat, params object[] parameters)
    {
        Log(messageFormat, parameters, LogLevel.Info);
    }
 
    public static void Error(object toLog)
    {
        Log(toLog, LogLevel.Error);
    }
 
    public static void Error(string message, Exception exception)
    {
        Log(message, exception, LogLevel.Error);
    }
 
    private static void Log(string messageFormat, object[] parameters, LogLevel logLevel)
    {
        string message = parameters.Length == 0 ? messageFormat
            : string.Format(messageFormat, parameters);
        Log(message, (Exception)null, logLevel);
    }
 
    private static void Log(object toLog, LogLevel logLevel, LogType logType = LogType.General)
    {
        if (toLog == null)
        {
            throw new ArgumentNullException("toLog");
        }
 
        if (toLog is Exception)
        {
            var exception = toLog as Exception;
            Log(exception.Message, exception, logLevel, logType);
        }
        else
        {
            var message = toLog.ToString();
            Log(message, null, logLevel, logType);
        }
    }
 
    private static void Log(string message, Exception exception, LogLevel logLevel, LogType logType = LogType.General)
    {
        if (exception == null && String.IsNullOrEmpty(message))
        {
            return;
        }
 
        var logger = GetLogger(logType);
        // Note: Using the default constructor doesn't set the current date/time
        var logInfo = new LogEventInfo(logLevel, logger.Name, message);
        logInfo.Exception = exception;
        logger.Log(logInfo);
    }
 
    private static Logger GetLogger(LogType logType)
    {
        var loggerName = logType.ToString();
        return LogManager.GetLogger(loggerName);
    }
 
    #endregion
 
    #region LogType
    private enum LogType
    {
        General
    }
    #endregion
}

The following configuration is similar to what is provided for each of my applications. The 'application' variable is all that differentiates the various applications in all of my environments, the rest has been standardized. Depending on your needs to tweak this configuration while developing and debugging, this section could easily be pushed back into code similar to the registering of our custom layout renderer.

 

<?xml version="1.0"?>
 
<configuration>
    <configSections>
        <section name="nlog" type="NLog.Config.ConfigSectionHandler, NLog"/>
    </configSections>
    <nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
        <variable name="application" value="Example"/>
        <targets>
            <target type="EventLog"
                name="EventLog"
                source="${application}"
                log="${application}"
                layout="${message}${onexception: ${newline}${exceptiondetails}}"/>
            <target type="Mail"
                name="Email"
                smtpServer="smtp.example.local"
                from="errors@example.com"
                to="everyone@example.com"
                subject="(${machinename}) ${application}: ${level}"
                body="Machine: ${machinename}${newline}Timestamp: ${longdate}${newline}Level: ${level}${newline}Message: ${message}${onexception: ${newline}${exceptiondetails}}"/>
        </targets>
        <rules>
            <logger name="*" minlevel="Debug" writeTo="EventLog" />
            <logger name="*" minlevel="Error" writeTo="Email" />
        </rules>
    </nlog>
</configuration>

 

Now go forward, create your custom exceptions without concern for including their custom properties in your exception logs and notifications.

Posted on Sunday, July 28, 2013 9:59 PM NLog , Enterprise Library , Exceptions , .Net , Logging , LayoutRenderer | Back to top


Comments on this post: NLog Exception Details Renderer

# re: NLog Exception Details Renderer
Requesting Gravatar...
Why do you suppress the Data property of the Exception? - I've found this dictionary to be a fertile source of additional information about failures, especially in ADO.NET exceptions.
Left by Bevan on Jul 28, 2013 7:42 PM

# re: NLog Exception Details Renderer
Requesting Gravatar...
The Data property is currently being suppressed because the implementation doesn't render the contents of the Dictionary. It would be simply display its type name 'System.Collections.IDictionary'.

Personally, I have never stuffed anything into the Data property of my exceptions so I have never found it useful. I create custom exceptions with properties to represent the data explicitly.
Left by James Timperley on Jul 28, 2013 7:48 PM

# re: NLog Exception Details Renderer
Requesting Gravatar...
One of the good things about the NLog is the easiness of creating extensions like this without really bothering thinking whether it is worth it. However everyone will see it fit is the best for me.
But I would like to point you a detail that you are mising in the code.

Layout Renderers should never hard code AppendLine, because you don't know the storing mechanism and layout. Think about how NLog is structured by keeping separated targets and layouts. You can even re-use layouts or combinations by creating variables.

Try avoiding these kind of coding assumptions, unless you are the only one who is ever going to configure the nlog.
Left by Alex on Jul 29, 2013 3:38 AM

Your comment:
 (will show your gravatar)


Copyright © jtimperley | Powered by: GeeksWithBlogs.net