Search code examples
c#.netexceptionerror-messagingstructured-logging

Two message templates required when structured logging exception message text


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.


Solution

  • 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);
    }