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.