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.