确保错误仅记录一次






2.33/5 (5投票s)
通过利用.NET的一些相当普通的特性,我们可以在异常发生的地方记录错误;保留重要的调试信息,同时避免重复的错误日志记录。
引言
这是一个普通的try
/catch
块,你刚刚捕获了一个错误。你应该记录它吗?它已经被记录了吗?你肯定不想多次记录它,但是你怎么知道呢?
根据MSDN,这是一个真实的问题
避免在调用堆栈较低的位置进行异常报告或日志记录。
...
此类catch块不应在调用堆栈深处记录异常或将其报告给用户。如果异常被记录并重新抛出,则调用堆栈中较高的调用者可能会执行相同的操作,从而导致异常的重复日志条目……
这带来一个问题,因为最好在错误发生点附近记录错误,因为这样你就可以访问方法参数,你也可以记录这些参数来帮助你调试异常。但是根据上面的MSDN,这会导致重复的日志条目。
让我们看一些代码来说明这个问题
public void CreateBookings(string[] people)
{
try
{
foreach (string person in people)
CreateBookingForPerson(person);
}
catch (Exception e)
{
_logger.LogError(e);
}
}
public void CreateBookingForPerson(string person)
{
_bookingRepo.CreateBooking(person);
}
请注意,我们只在第一个方法中捕获和记录错误:在CreateBookingForPerson
中发生的错误将冒泡并记录在CreateBookings
方法中的catch
块中。
现在假设我们要处理一个包含10个人的数组;在第四个项目中,在以下行发生错误
_bookingRepo.CreateBooking(person);
错误将冒泡直到被CreateBookings
中的catch
块捕获。问题在于:此时,我们不再访问导致错误的person
项目:我们可以记录发生了错误,但我们丢失了原因(详细信息)。如果异常是在CreateBookingForPerson
内部捕获的,我们仍然可以访问导致错误的哪个项目,这是调试问题的重要信息。
然而,当然,如果我们确实决定向CreateBookingsForPerson
添加一个日志记录try
/catch
,错误最终会被记录两次:这是我们试图避免的。
解决方案
那么解决方案是什么呢?核心概念是:当捕获异常时
- 记录异常并将其包装在一个自定义异常类型
LoggedException
中。 - 抛出这个新创建的
LoggedException
。 - 忽略/不记录类型为
LoggedException
的异常。
让我们首先看看LoggedException
类的代码。
public class LoggedException : Exception
{
public LoggedException(string message)
: base(message)
{
LogException(message, null);
}
public LoggedException(string message, Exception innerException)
: base(message, innerException)
{
LogException(message, innerException);
}
private void LogException(string message, Exception innerException)
{
//hook up logging framework of choice
}
}
这个类相当简单;它只是围绕内置Exception
类的包装器。错误详细信息的记录在实例化期间通过调用LogException
发生。LogException
是您将连接您选择的日志记录框架的地方。作为一个额外的好处,我们现在已经集中了我们的日志记录功能。如果我们想更换日志记录框架,唯一需要更改的代码就在LogException
内部。
实际上,就是这样。然而,真正的魔力发生在LoggedException
类的实现过程中。
实现
让我们再看看引言中的代码,但这次使用LoggedException
。
public void CreateBookings(string[] people)
{
try
{
foreach (string person in people)
CreateBookingForPerson(person);
}
catch (Exception e) when (!(e is LoggedException))
{
string errorMsg = "An error occurred while creating bookings.";
throw new LoggedException(errorMsg, e);
}
}
public void CreateBookingForPerson(string person)
{
try
{
_bookingRepo.CreateBooking(person);
}
catch (Exception e) when (!(e is LoggedException))
{
string errorMsg = $"An error occurred while creating a booking for person {person}.";
throw new LoggedException(errorMsg, e);
}
}
一旦错误被记录,两个catch
块都会抛出一个LoggedException
。LoggedException
告诉堆栈中较高的catch
块,异常已经被记录。有了这个机制,我们的错误日志可以包含快速查明问题所需的额外细节。
你注意到每个catch
块上的when
关键字了吗?
catch(Exception e) <code>when</code> (!(e is LoggedException))
这些是异常筛选器,这是C# 6的主要新特性之一。它们允许你指定进入catch
块的条件。乍一看,你可能会认为这只是一些额外的语法糖,但它远不止于此。这里的大问题是异常筛选器不会展开堆栈。这太重要了!细节超出了本文的范围,但是对于更深入的讨论,这篇文章做得很好。值得注意的是,实现异常筛选器不是必需的。
结论
通过利用.NET的一些相当普通的特性,我们可以完全避免重复记录错误这一相当麻烦的问题。这种方法能够访问异常发生时的局部变量,提供额外的细节来快速查明问题。
避免在调用堆栈较低的位置进行异常报告或日志记录。
...
此类catch块不应在调用堆栈深处记录异常或将其报告给用户。如果异常被记录并重新抛出,则调用堆栈中较高的调用者可能会执行相同的操作,从而导致异常的重复日志条目……
问题解决。
历史
- 2018-11-28:初始版本