18

We're just getting started with Log4Net (and wishing we'd done it earlier). Whilst we can see inner exceptions, etc. the one thing that seems to be missing from the output when logging an exception is any key/value information held inside the "Exception.Data". Is there anyway we can do this "out of the box"? If not, as we really are only just starting out where should be looking to find a way to implement this functionality?

As an example please see the very basic pseudo code below. We don't want to pollute the exception message with context information just what the problem was (We'd probably have lost more information in the data which would help in investigating the actual problem). But right now all we see in our logs is the type of exception, the message, any stack trace - but no exception "data". This means in our logs we lose the customer id, etc. How can we easily get this information into our logs (without having to code it by hand in each exception catch).

try
{
   var ex = new ApplicationException("Unable to update customer");
   ex.Data.Add("id", customer.Id);
   throw ex;
}
catch(ApplicationException ex)
{
   logger.Error("An error occurred whilst doing something", ex);
   throw;
}
Paul Hadfield
  • 6,088
  • 2
  • 35
  • 56

5 Answers5

20

Following Stefan's lead:

namespace YourNamespace {
    public sealed class ExceptionDataPatternConverter : PatternLayoutConverter {

        protected override void Convert(TextWriter writer, LoggingEvent loggingEvent) {
            var data = loggingEvent.ExceptionObject.Data;
            if (data != null) {
                foreach(var key in data.Keys) {
                    writer.Write("Data[{0}]={1}" + Environment.NewLine, key, data[key]);
                }
            }

        }
    }   
}

And in your configuration add %ex_data and the converter:

<appender ...>
  ...
  <layout type="log4net.Layout.PatternLayout,log4net">
    <conversionPattern value="%date %d{HH:mm:ss.fff} [%t] %-5p %c %l - %m%n %ex_data"/>
    <converter>
      <name value="ex_data" />
      <type value="YourNamespace.ExceptionDataPatternConverter" />
    </converter>
  </layout>

Jeroen K
  • 10,258
  • 5
  • 41
  • 40
  • PatternLayout also by default assumes no exception is rendered so don't forget to tell that you are going to render the exception by: in the Layout section, otherwise your appender might do it for you the way it wants – Denis Mar 14 '14 at 16:25
7

If you have multiple appenders defined you can use a custom renderer rather than defining the converter for every layout.

web/app.config

<log4net>
    ...
    <renderer renderingClass="YourNamespace.ExceptionObjectLogger, YourAssembly" renderedClass="System.Exception" />
    ...
</log4net>

ExceptionObjectLogger

public class ExceptionObjectLogger : IObjectRenderer
{
    public void RenderObject(RendererMap rendererMap, object obj, TextWriter writer)
    {
        var ex = obj as Exception;

        if (ex == null)
        {
            // Shouldn't happen if only configured for the System.Exception type.
            rendererMap.DefaultRenderer.RenderObject(rendererMap, obj, writer);
        }
        else
        {
            rendererMap.DefaultRenderer.RenderObject(rendererMap, obj, writer);

            const int MAX_DEPTH = 10;
            int currentDepth = 0;

            while (ex != null && currentDepth <= MAX_DEPTH)
            {
                this.RenderExceptionData(rendererMap, ex, writer, currentDepth);
                ex = ex.InnerException;

                currentDepth++;
            }
        }
    }

    private void RenderExceptionData(RendererMap rendererMap, Exception ex, TextWriter writer, int depthLevel)
    {
        var dataCount = ex.Data.Count;
        if (dataCount == 0)
        {
            return;
        }

        writer.WriteLine();

        writer.WriteLine($"Exception data on level {depthLevel} ({dataCount} items):");

        var currentElement = 0;
        foreach (DictionaryEntry entry in ex.Data)
        {
            currentElement++;

            writer.Write("[");
            ExceptionObjectLogger.RenderValue(rendererMap, writer, entry.Key);
            writer.Write("]: ");

            ExceptionObjectLogger.RenderValue(rendererMap, writer, entry.Value);

            if (currentElement < dataCount)
            {
                writer.WriteLine();
            }
        }
    }

    private static void RenderValue(RendererMap rendererMap, TextWriter writer, object value)
    {
        if (value is string)
        {
            writer.Write(value);
        }
        else
        {
            IObjectRenderer keyRenderer = rendererMap.Get(value.GetType());
            keyRenderer.RenderObject(rendererMap, value, writer);
        }
    }
}
SeriousM
  • 3,374
  • 28
  • 33
Daniel Ballinger
  • 13,187
  • 11
  • 69
  • 96
  • 3
    I really like this idea, but your solution has a problems rendering inner exceptions. Since you're using `rendererMap.DefaultRenderer.RenderObject(rendererMap, obj, writer);` in the loop it will render the exception-stack times the numer of stacked exceptions. I will propose a change of your implementation so that it works as it should. – SeriousM Oct 27 '15 at 14:58
6

I think a more log4net way of approaching this problem would be to write a PatternLayoutConverter. An example can be found here.

In the convert method you can access your data like this (and write it the way you like):

override protected void Convert(TextWriter writer, LoggingEvent loggingEvent)
{
   var data = loggingEvent.ExceptionObject.Data;
}
Community
  • 1
  • 1
Stefan Egli
  • 17,398
  • 3
  • 54
  • 75
0

you could create an Extension method for your logger to log the customer Id : you should not add important information to the exception

You can abstract the concept of "Additional Information to log" and create an interface with a method that return the additional information you want to log

 public interface IDataLogger
    {
        string GetAdditionalInfo();
    }

    public class UserDataLogger: IDataLogger
    {
        public string GetAdditionalInfo()
        {
            return "UserName";
        }
    }

    public class MoreDataLogger : IDataLogger
    {
        public string GetAdditionalInfo()
        {
            return "something";
        }
    }

you can create different "data Logger" and maybe combine them together

then you could create an generic extension method that get the type of the logger

public static class ExLog4Net
    {
        public static void Error<T>(this ILog log, Exception ex) where T:IDataLogger,new()
        {
            var dataLogger=new T();
            log.Error(ex.ToString() + " " + dataLogger.GetAdditionalInfo());
        }

    }

you will be able to do the below:

      try
        {

        }
        catch (Exception ex)
        {
            logger.Error<UserDataLogger>(ex);
            logger.Error<MoreDataLogger>(ex);
            throw;
        }
Massimiliano Peluso
  • 26,379
  • 6
  • 61
  • 70
  • Thanks, I'm afraid that would only move the manual work into an extension method whilst losing a lot of the benefit of passing the exception into the log4net method. Everytime we had some data we wanted we'd have to write a modified extension method, etc. – Paul Hadfield Aug 04 '11 at 17:53
  • this is just a "draft"...I'll post a better solution but the idea is separate the info logging from the exception logging – Massimiliano Peluso Aug 04 '11 at 18:49
0

I think Massimiliano has the right idea but I would modify his solution slightly.

If you plan on sticking all of of your additional data in the dictionary Data within an exception I would change his extension method to the following:

public static class ExLog4Net
{
    public static void Error(this ILog log, Exception ex)
    {
        StringBuilder formattedError = new StringBuilder();
        formattedError.AppendFormat("Exception: {0}\r\n", ex.ToString());

        foreach (DictionaryEntry de in ex.Data)
            formattedError.AppendFormat("{0}: {1}\r\n", de.Key, de.Value);

        log.Error(formattedError.ToString());
    }
 }

You would then stick this method extension in a library you would use in all of your applications. If you don't have one you would have to add this to every project.

Cole W
  • 15,123
  • 6
  • 51
  • 85