Exception Filter Logging
Defining LoggerMessages
Existential provides extension methods for exceptions that allow them to be logged from within exception filters , which were introduced in 2015 as part of C# 6.0. The reason for doing this is to ensure that stack information is evaluated where the exception is thrown, as described in the article A New Pattern for Exception Logging, by Stephen Cleary.
To use these extension methods, first of all you need to define your logging messages using the LoggerMessage pattern for high performance logging.
Use the Define methods on the LoggerMessage class to define the log message, with a log level, event ID and a message template for structured logging. The definition can include zero to six strongly-typed parameters - the example below includes a float to record a temperature value.
public static readonly Action<ILogger, float, Exception> TemperatureReport =
LoggerMessage.Define(
LogLevel.Information,
new EventId(1234, nameof(TemperatureReport)),
"Reported temperature: {temperature}°C.");
Logging in an exception filter
Once logger messages have been defined, they can be used with the Existential extension methods in an exception filter. Existential provides two extension methods for System.Exception and two for Microsoft.Extensions.Logging.ILogger , each overloaded to support all the LoggerMessage definitions . The overloaded methods are:
- Exception.ShouldBeHandledLog(...)
- Exception.ShouldBeUnhandledLog(...)
- ILogger.LogThenHandle(...)
- ILogger.LogThenRethrow(...)
... with the main arguments being the exception and an ILogger (with one of these as a this
parameter,
depending on the syntax chosen) and a message definition. The arguments can also include as many
additional strongly-typed parameters as are required by the message definition. (There is also one
addiitonal optional parameter which will be discussed later.)
The difference between the methods is simply that ShouldBeHandledLog
or LogThenHandle
returns true
, so that the catch block will be entered after logging; while
ShouldBeUnhandledLog
or LogThenRethrow
returns false
, resulting in the exception being
rethrown after logging. The difference between the Exception
and ILogger
methods is purely
a matter of choice of syntax preference - they share implementations.
The example below logs a TemperatureReport message as defined above, then the exception will be handled.
public void LogAndCatchAnException(ILogger inLogger)
{
try
{
float temperature = GetTemperature();
throw new TemperatureException(temperature);
}
catch(TemperatureException inException)
when (inException.ShouldBeHandledLog(
TemperatureReport,
inLogger,
inException.Temperature))
{
// Handle the exception here.
}
}
Although the syntax in the when
clause is different, this example is equivalent to the one above:
public void LogAndCatchAnException(ILogger inLogger)
{
try
{
float temperature = GetTemperature();
throw new TemperatureException(temperature);
}
catch(TemperatureException inException)
when (inLogger.LogThenHandle(
inException,
TemperatureReport,
inException.Temperature))
{
// Handle the exception here.
}
}
Logging the exception without handling it
Logging without handling the exception - so it will be rethrown - looks like the two examples below:
public void LogAndRethrowAnException(ILogger inLogger)
{
try
{
float temperature = GetTemperature();
throw new TemperatureException(temperature);
}
catch(TemperatureException inException)
when (inException.ShouldBeUnhandledLog(
TemperatureReport,
inLogger,
inException.Temperature))
{
// Catch block won't be entered.
}
}
public void LogAndRethrowAnException(ILogger inLogger)
{
try
{
float temperature = GetTemperature();
throw new TemperatureException(temperature);
}
catch(TemperatureException inException)
when (inLogger.LogThenRethrow(
inException,
TemperatureReport,
inException.Temperature))
{
// Catch block won't be entered.
}
}
What happens if the logging fails?
If the logging itself throws an exception, by default a Trace message (at Error level) will be created instead.
However, a final optional parameter allows a TextWriter implentation to be provided that will attempt a customisable fallback notification of the issue. The default argument of this parameter is null, and the Trace message will be created instead. If a different TextWriter implementation is used, tracing will not occur unless also implemented by the custom TextWriter.
If sending the message to multiple output targets is desired, the MultiTextWriter class can be used with appropriate implementations of IWriteStrategy.