Originally posted on: http://geekswithblogs.net/jtimperley/archive/2013/07/28/nlog-exception-details-renderer.aspxI 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="
[email protected]"
to="
[email protected]"
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.