Two message templates required when structured logging exception message text

555 Views Asked by At

How can I avoid this pattern? I wish to capture an illegal state, such as found in the contrived example below. Log a structured message followed by throwing an exception containing the same message.

public async Task<int> DoSomeWork(int numerator, int denominator)
{
  if (denominator == 0)
  {
    Logger.LogError("The division : {Numerator}/{Denominator} is invalid as the denominator is equal to zero", numerator, denominator);

    throw new ApplicationException($"The division : {numerator}/{denominator} is invalid as the denominator is equal to zero.");

  }

  //Yes the solution must work with async methods
  await Task.Delay(TimeSpan.FromSeconds(1));

  //this would have thrown a DivideByZeroException
  return (numerator / denominator);

}

I have the above pattern all over my code and it seems crazy, yet I can't find an alternative.

I want the goodness of structured logging, and I also want my Exception messages to align with the log message. Yet I don't want to have to duplicate my error message template strings as seen above.

1

There are 1 best solutions below

0
On

One approach is to add a custom exception that allows an args collection to be supplied, which can in turn be used with the structured logging. A delegate to the log action can also be added so that whatever handles the exception can call the action supplying an ILogger instance.

public abstract class BaseStructuredLoggingException : Exception
{
    private readonly object[] _args;
        
    protected BaseStructuredLoggingException(string message, params object[] args)
        : base(message)
    {
        _args = args;
    }
        
    public Action<ILogger<T>> LogAction<T>()
    {
        return l => l.LogError(this, Message, _args);
    }
}
    
public sealed class DivideException : BaseStructuredLoggingException
{
    public DivideException(string message, params object[] args) 
        : base(message, args) 
    { }
}

Then in whatever class is handling the exception

private void HandleException(Exception ex)
{
    if (ex is BaseStructuredLoggingException exception)
    {
        var log = exception.LogAction<ErrorHandler>();
        log(_logger);
    }
    else
    {
        _logger.LogError(ex, ex.Message);
    }
}

and finally your application code

public async Task<int> DoSomeWork(int numerator, int denominator)
{
  if (denominator == 0)
  {
    throw new DivideException("The division : {Numerator}/{Denominator} is invalid as the denominator is equal to zero", numerator, denominator);
  }

  //Yes the solution must work with async methods
  await Task.Delay(TimeSpan.FromSeconds(1));

  //this would have thrown a DivideByZeroException
  return (numerator / denominator);
}