Adding custom data to an exception for logger

Looking at an error log, it is not obvious how to reproduce an exception just by inspecting stack trace or error message. Of course, if you are super smart or lucky you can guess or try to catch the bug, however, it is not always possible. Usually, I need more data related to the exception to take into consideration a state of a system. Context, please!

Problem

It would be nice to see data connected to the moment when the exception occurred, e.g. UserId, PageUrl, RefererUrl, Browser, SuperDuperSpecificEntityId. How can it be implemented without changing existing exception types? Moreover, not all exception types are under my control.

Solution

It turns out that the .NET framework has a built-in feature for that from the very beginning. It is Exception.Data as IDictionary.

We can write related data to Exception.Data dictionary then read it before logging. The obtained data may be logged along with the exception.

Let’s imagine there is super reliable and stable function :)

void StableTask(string stableTaskId, string semistableTask)
{
    try
    {
        SemistableTask(semistableTask);
    }
    catch (Exception ex)
    {
        ex.Data["stableTaskId"] = stableTaskId;
        throw;
    }
}

void SemistableTask(string semistableTaskId)
{
    try
    {
        UnstableTask();
    }
    catch (Exception ex)
    {
        ex.Data["semistableTaskId"] = semistableTaskId;
        throw new Exception("SemistableTask", ex);
    }
}

void UnstableTask()
{
    throw new Exception("UnstableTask");
}

Here is demo how it logs the exception via Serilog.

[Test]
public void Serilog_should_log_exception_data()
{
    // Given
    var logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
    var entityId = "AAA";
    var correlationId = "BBB";

    // When
    try
    {
        StableTask(entityId, correlationId);
    }
    catch (Exception ex)
    {
        logger.Error(ex, "My test fails {Data}", GetData(ex));
    }

    // Then
    Assert.That(ConsoleOutput, Does.Contain(entityId));
    Assert.That(ConsoleOutput, Does.Contain(correlationId));
}

Where [GetData](https://github.com/gaevoy/Gaev.Blog.Examples/blob/1.2.0/Gaev.Blog.Examples.ExceptionCustomData/ErrorLoggingTests.cs#L66-L77{:target=”_blank”} is boilerplate code to convert IDictionary to typed dictionary and it combines InnerException.Data. ConsoleOutput is console output which I captured in here.

The same demo for NLog.

[Test]
public void NLog_should_log_exception_data()
{
    // Given
    var logger = new LogFactory(WriteToConsoleConfig()).GetCurrentClassLogger();
    var entityId = "AAA";
    var correlationId = "BBB";

    // When
    try
    {
        StableTask(entityId, correlationId);
    }
    catch (Exception ex)
    {
        logger.Error(ex, "My test fails {Data}", GetData(ex));
    }

    // Then
    Assert.That(ConsoleOutput, Does.Contain(entityId));
    Assert.That(ConsoleOutput, Does.Contain(correlationId));
}

It works like a charm!

alt text

The complete example is in Gaev.Blog.Examples.ExceptionCustomData.

Caveat

Be careful what you are putting to Exception.Data. It needs to be serializable.

Chat with blog beta
  • Assistant: Hello, I'm a blog assistant powered by GPT-3.5 Turbo. Ask me about the article.