Nikerboker
Nikerboker

Reputation: 843

How to log exceptions with structured arguments in .NET Core

I can't find a consistent way to log exceptions in my .NET Core microservice. Informational messages logging guidelines are simple (Microsoft.Extension.Logging is used):

_logger.LogInformation($"Reading file {path}..."); // bad
_logger.LogInformation("Reading file {Path}...", path); // good

The benefit of the second variant is structured information: by using a clever log event router (like Serilog with RenderedCompactJsonFormatter) the path is written as separate property of the log event.

Things are going worse with errors logging. The requirements are obvious:

  1. Errors are implemented as exceptions.
  2. An error is logged in the catch block where it is handled.
  3. Each error is logged in a structured form.

So, I'd expect the error reporting to look like

throw new MyException("Failed to read file {Path}", path);

and error logging - like

catch(MyException e)
{
  _logger.LogError(e, "Processing error");
}

LogError method here logs the complete error description, but it is not structured: the path is not added as a property. I tried to make MyException hold the message template and the arguments, but there are 2 problems with this approach:

  1. How to render the exception message based on a template with named arguments?
  2. An argument may be disposed when an exception is processed in a catch block.

Please tell me how you deal with this.

Upvotes: 7

Views: 5441

Answers (2)

Tomas Kubes
Tomas Kubes

Reputation: 25158

I would suggest to create FormattableException and use FormattableString inside as StructuredMessage property. For backward compatibility save formatted message to Message property

Upvotes: 0

Vivendi
Vivendi

Reputation: 21057

Exceptions in .NET don't support structured parameters like that. Since you are using custom exceptions, you could add this functionality to your own exception class.

For example:

public class MyException : Exception
{
    public object[] Props { get; }

    public MyException()
    {
    }

    public MyException(string message)
        : base(message)
    {
    }

    // Add custom "props" parameter to your exception class
    public MyException(string message, params object[] props)
        : base(message)
    {
        Props = props;
    }

    public MyException(string message, Exception inner)
        : base(message, inner)
    {
    }
}

Now in your code you could do something like this:

try
{
    var file = "image.png";
    var path = "./my/path/";
    throw new MyException("Failed to read file '{file}' in path '{path}'", file, path);
}
catch (MyException e)
{
    _logger.LogError(e, e.Message, e.Props);
}

If you look at your logs (I'm using InvisionLog here), then you should see that it is structured.

I think this is the only way to catch your exceptions and log them in a structured manner.

enter image description here

Upvotes: 2

Related Questions