动态记录方法入口和出口点






4.25/5 (3投票s)
记录方法入口和出口点。
引言
我正在编写并支持在全球范围内使用的软件。机器通常在断网环境下运行。因此,当出现问题时,分析问题的唯一方法是分析日志文件。我们使用 log4net 库进行日志记录。
问题往往是之前发生的事情的后果。了解执行历史有助于找出问题的根本原因。因此,为了查明哪里出了问题,我们经常记录方法的进入和退出点。我们记录方法名称,如果需要,还会记录参数值和返回值。在 .NET 4.5 之前,记录方法名称是硬编码的(除非你“爬行”调用堆栈)。如果你在日志调用中硬编码方法名称,当方法名称改变时就会出现问题:你必须同时更改日志进入和日志退出调用。自 .NET 4.5 以来,我们可以在不显式检查堆栈的情况下动态记录方法名称。目的不是要达到 面向切面 方法的程度。
在本文中,我们将介绍 log4net 库 ILog
接口的一些扩展方法,以动态方式记录进入和退出点。我们以您可以自定义日志记录方式的方式设计它。其中包含测试,可作为如何使用这个小库的文档。
文章的其余部分将解释接口及其实现。
实现
我们在 log4net.Extension
命名空间中的 (static
) 类 LoggerExtension
中实现了扩展点。由于我们依赖 log4net 的基本服务,因此我们引用了 log4net 程序集。本文中解释的类的源代码已包含在内。该解决方案还包含一个第二个项目,其中实现了 LoggerExtension
的单元测试。我们使用 NUnit 来实现测试。
以下是本文其余部分的结构。我们将从简单的 LogBlock
方法开始,并解释其工作原理。LogBlock
方法在一个调用中记录方法的进入和退出点。LogBlock
方法有几个重载,我们将对此进行讨论。
如果您想以不同方式记录进入和退出点(例如不同的日志级别),可以使用 DebugEntry
和 DebugExit
或 LogEntry
和 LogExit
。DebugEntry
和 DebugExit
方法以 调试 级别记录。这些方法将在 LogBlock
方法之后解释。接下来,我们将解释 LogEntry
和 LogExit
方法,它们允许您以 任何 日志级别记录,如果需要,还可以记录方法参数。最后,我将解释如何自定义库。
LogBlock
在 .NET 4.5 之前,我们以下列方式记录 MyMethod
方法的进入点和退出点(Logger
属性返回一个已初始化的 log4net 记录器)
void MyMethod()
{
Logger.Debug(">MyMethod");
...
Logger.Debug("<MyMethod");
}
方法入口以“>”为前缀,出口以“<”为前缀。更改方法名称意味着更改三行代码。从 .NET 4.5 开始,通过使用我们的扩展方法,我们可以以下列方式记录方法 MyMethod
的入口和出口点
void MyFunction()
{
using(Logger.LogBlock())
{
...
}
}
正如您所看到的,`string` 参数消失了。当方法名称改变时,日志条目将自动改变。以下是 `LogBlock` 扩展方法的定义方式,以实现此目的
static public ExitLogger LogBlock(this ILog logger, [CallerMemberName]string name = null)
该方法被定义为 `static` 方法,其第一个参数为 `this` 参数,因为这些方法是 扩展方法。扩展的类型是 log4net 库的 `ILog` 接口。下一个可选参数是名称,默认值为 `null`。该参数用 CallerMemberName 属性进行注解。由于此注解,如果调用者没有为该参数传递值,则该参数将被赋值为调用者方法的名称。
正如您所看到的,该方法返回一个 `ExitLogger` 类型的对象。当该对象超出作用域时,它会执行退出点的实际日志记录。通过使用 `using` 语句,我们保证即使在执行的代码块中抛出异常,退出点也会被记录。
当您使用参数值调用 `LogBlock` 时,该参数的值将被记录(而不是调用方的方法名称),并带有定义的前缀(请参阅 后面)。当您使用 `null` 作为参数值调用 `LogBlock` 时,只会记录前缀。因此,以下代码
void MyFunction()
{
using(Logger.LogBlock("test"))
{
...
}
}
结果是 `strings` ">test
" 和 "<test
" 以调试级别记录。
上面的定义以 调试 级别记录。在以下带有额外 `Level` 参数的方法重载中,您可以以 任何 级别记录。`Level` 类型在 log4net 库中定义
static public ExitLogger LogBlock(this ILog logger, Level level, [CallerMemberName]string name = null)
如前所述,我们喜欢记录方法参数。这可以通过使用以下 `LogBlock` 的重载来完成
static public ExitLogger LogBlock(this ILog logger, Level level, object[] entryParameters,
object[] exitParameters, [CallerMemberName]string name = null)
static public ExitLogger LogBlock(this ILog logger, Level level, object[] entryParameters,
[CallerMemberName]string name = null)
与上述定义相比,`LogBlock` 方法的这些实现具有额外的参数:`entryParameters` 和 `exitParameters`,两者都是对象数组。对象数组可以保存要记录的额外信息。`entryParameters` 保存要在方法进入时记录的信息(通常是调用参数),而 `exitParameters` 保存要在方法退出时记录的信息(通常是返回对象)。对象数组(默认情况下)被序列化为 `string` 并用空格分隔。此代码在方法进入时以信息级别记录 ">MyFunction test
",在方法退出时记录 "<MyFunction
"
void MyFunction()
{
object[] parameters = { "test" };
using(Logger.LogBlock(Level.Info, parameters))
{
...
}
}
当向 `LogBlock` 调用传递 `null` 值时,`entryParameters` 和 `exitParameters` 参数将被忽略。
如果您希望更严格地控制方法入口或方法出口处记录的内容,这是可能的。这将在本文的其余部分讨论,从 `DebugEntry` 和 `DebugExit` 方法开始。
DebugEntry & DebugExit
使用 `DebugEntry` 可以在方法入口处进行调试日志记录方法名称。使用 `DebugExit` 可以在方法出口处记录方法名称。以下是 `DebugEntry` 和 `DebugExit` 扩展方法的定义方式,以实现此目的
static public void DebugEntry(this ILog logger, [CallerMemberName]string name = null)
static public ExitLogger DebugExit(this ILog logger, [CallerMemberName]string name = null)
这些参数的使用方式与上面讨论的 `LogBlock` 相同。下面的代码与不带参数的 `LogBlock` 调用具有相同的效果
void MyFunction()
{
Logger.DebugEntry();
using(Logger.DebugExit())
{
...
}
}
LogEntry & LogExit
为了在非调试级别的任何其他日志记录级别记录方法入口,并支持方法参数的日志记录,我们定义了 `LogEntry` 扩展方法
static public void LogEntry(this ILog logger,
Level level,
[CallerMemberName]string methodName = null)
static public void LogEntry(this ILog logger,
Level level,
object[] parameters,
[CallerMemberName]string methodName = null)
与 `DebugEntry` 方法相比,`LogEntry` 方法的第一个实现多了一个参数:日志记录级别。`Level` 类型在 `log4net` 库中定义。此代码以信息级别记录“`>MyFunction`”(如果 `EntryPrefix` 属性未更改,请参阅后文)
void MyMethod()
{
Logger.LogEntry(Level.Info);
…
}
与 `DebugEntry` 方法相比,`LogEntry` 方法的第二个实现多出了两个参数:日志级别和对象数组。对象数组可以保存要记录的额外信息,通常是调用参数。对象数组(默认情况下)被序列化为 `string` 并用空格分隔。此代码以信息级别记录“`>MyMethod test`”
void MyMethod ()
{
object[] parameters = { "test"};
Logger.LogEntry(Level.Info, parameters);
...
}
当向 `LogEntry` 调用传递 `null` 值时,`parameters` 参数将被忽略。`methodName` 参数可以像 `DebugEntry` 方法中那样传递,并具有相同的效果(参见上文)。
为了记录方法的退出,我们定义了 `LogExit` 方法,其使用方式与前面解释的 `LogBlock` 方法相同
static public ExitLogger LogExit(this ILog logger,
Level level,
[CallerMemberName]string methodName = null)
static public ExitLogger LogExit (this ILog logger,
Level level,
object[] parameters,
[CallerMemberName]string methodName = null)
LogExit
和 LogEntry
方法的唯一区别在于前缀:对于 LogExit
,前缀是 "<
",对于 LogEntry
,前缀是 ">
",并且 LogExit
返回一个 ExitLogger
,原因如上所述。
定制库
该库有三个定制点
- 可以设置进入和退出函数(分别为 `LogBlock, DebugEntry, LogEntry` 和 `LogBlock, DebugExit, LogExit`)的前缀。
- 参数的序列化方式可以定制。
- 方法名称(包括前缀)的序列化方式可以定制。
在本节中,将逐一解释这三个自定义点。
EntryPrefix & ExitPrefix
如前所述,入口调用(`LogBlock`, `DebugEntry`, `LogEntry`)的默认前缀是 ">",退出调用(`LogBlock`, `DebugExit`, `LogExit`)的默认前缀是 "<
"。你可以通过设置 `LoggerExtension` 类的 `EntryPrefix` 和 `ExitPrefix` 属性,将这些默认值替换为任何其他 `string`。这些属性定义如下
public static string EntryPrefix { get; set; }
public static string ExitPrefix { get; set; }
因此,以下示例将在方法入口处添加日志条目“`in MyFunction`”,在方法出口处添加“`out MyFunction`”(两者均为调试日志级别)
void MyFunction()
{
LoggerExtension.EntryPrefix = "in ";
LoggerExtension.ExitPrefix = "out ";
using(Logger.LogBlock())
{
...
}
}
请注意:这些属性是 `static` 属性。因此,一旦设置,它们对所有对扩展点的调用都适用,直到设置为其他值。这是设计使然:您可能希望以一致的方式指示方法入口/出口。
定制参数日志
参数的日志记录方式可以自定义。参数通过执行在 `LoggerExtension` 类的 `MsgParametersAction` 属性中定义的操作进行序列化。您可以通过设置此类的 `MsgParametersAction` 属性并引用要执行的自己的代码来提供自己的实现。`MsgParametersAction` 属性定义如下
public static Action<StringBuilder, Severity, object[]> MsgParametersAction { get; set; }
`MsgParametersAction` 的默认实现是通过迭代对象数组(第二个参数),然后逐一追加一个空格,接着将对象的 `string` 表示形式追加到传递的 `StringBuilder` 实例(第一个参数)来实现参数的序列化。
您可以通过提供自己的 `Action` 对象来定制此功能。同样,此属性是静态定义的,因此操作替换将对所有对库的调用生效。以下是一个示例,它将始终记录“`test`”而不是作为参数传递的任何值
const string firstParam = "test";
object[] parameters = { "xx" };
LoggerExtension.MsgParametersAction =
delegate(StringBuilder builder, Level level, object[] prms)
{
builder.Append(" ");
builder.Append(firstParam);
};
Logger.LogEntry(Level.Info, parameters);
自定义方法名称日志记录
方法名称(包括前缀)的日志记录方式可以自定义。方法名称通过执行在 `LoggerExtension` 类的 `MsgPrefixAction` 属性中定义的操作进行序列化。您可以通过设置此类的 `MsgPrefixAction` 属性并引用要执行的自己的代码来提供自己的实现。`MsgPrefixAction` 属性定义如下
public static Action<StringBuilder, Severity, string, string> MsgPrefixAction { get; set; }
此 `MsgPrefixAction` 的默认实现是将前缀(第 3 个参数)附加到 `StringBuilder` 实例(第 1 个参数),然后附加方法名称(第 4 个参数)。
您可以通过提供自己的动作来定制此动作。同样,此属性是静态定义的,因此动作替换将对所有对库的调用生效。以下是一个示例,它将记录硬编码的“`test`” `string` 而不是前缀后跟方法名称
LoggerExtension.MsgPrefixAction =
delegate(StringBuilder bldr, Level lvl , string prefix, string name)
{
builder.Append("test");
};
Logger.LogEntry(Level.Info, parameters);
结论
本文讨论了 log4net 扩展点的实现,它使用 .NET 4.5 `CallerMemberName` 属性以动态方式记录方法入口和出口点。文章解释了如何根据自己的需求定制库。项目包含最新发布的 log4net 版本。它包括可用作工作示例的单元测试。您可以定义自己的接口和扩展点,并利用此库中的基本功能。将相同的功能应用于其他日志框架应该很容易。在享受了如此多的开源代码之后,我希望其他人能发现这个小库很有用。
Using the Code
此代码仅适用于 .NET 4.5(或更高版本)。可以通过引用随附项目生成的 `log4net.Extension` 程序集来使用该代码。您将需要使用与此包中包含的 `log4net` 版本相同的版本。您也可以使用自己的 `log4net` 版本。在这种情况下,您将需要重新编译该程序集。第二种选择是将 `LoggerExtension` 和 `ExitLogger` 类包含在您的项目中。同时还需要引用 `log4net` 库。
历史
- 2015年1月2日:初始版本
- 2015年1月3日:修正拼写错误
- 2015年1月11日:添加了 `LogBlock`,简化了测试