最小循环复杂度的条件日志记录

在阅读了“ 你的循环复杂度上限是多少?”之后,我意识到我的许多同事对我们项目中的这个新的 质量保证策略非常恼火: 每个函数不再有10个 循环复杂度

意义: 不超过10个“ if”、“ else”、“ try”、“ catch”等代码工作流分支语句。对。正如我在“ 您测试私有方法吗?”中解释的那样,这样的政策有许多好的副作用。

但是: 在我们的(200人-7年)项目的开始,我们很高兴地记录(不,我们不能轻易地委托给某种类型的“ 面向侧面的程序设计”日志方法)。

myLogger.info("A String");
myLogger.fine("A more complicated String");
...

当我们的系统的第一个版本启动时,我们经历了巨大的内存问题,不是因为日志记录(有一次关闭了) ,而是因为 日志参数(字符串) ,它总是被计算,然后传递给‘ info ()’或‘ fine ()’函数,只是发现日志记录的级别是‘ OFF’,并且没有发生日志记录!

所以 QA 又回来了,督促我们的程序员做条件日志记录。

if(myLogger.isLoggable(Level.INFO) { myLogger.info("A String");
if(myLogger.isLoggable(Level.FINE) { myLogger.fine("A more complicated String");
...

但是现在,由于每个函数的“不能移动”10个循环复杂度级别的限制,他们认为他们在函数中放入的各种日志都是一种负担,因为每个“ if (isLoggable ())”都被算作 + 1的循环复杂度!

因此,如果一个函数有8个“如果”,“否则”等等,在一个紧密耦合的不容易共享的算法,和3个关键的日志操作... 他们打破了限制,即使条件日志可能不是该函数的 真的部分说的复杂性..。

你会怎么处理这种情况?
我已经在我的项目中看到了一些有趣的代码演进(由于那个“冲突”) ,但是我只是想先听听你的想法。


谢谢你的回答。
我必须坚持认为,这个问题与“格式化”无关,而是与“参数评估”有关(评估可能是非常昂贵的,就在调用一个什么都不做的方法之前)
所以当我在“ a String”上面写的时候,我实际上指的是 a function () ,a function ()返回一个字符串,调用一个复杂的方法来收集和计算所有类型的日志数据,然后由日志记录器显示... ... 或者不显示(因此问题就出现了,使用条件日志记录的 义务,因此实际问题就是人为地增加“循环复杂度”... ...)

我现在得到的“ 变数函数”点由你们中的一些人提出(谢谢约翰)。
注意: 在 java6中的一个快速测试显示,我的 Varargs 函数 varargs 函数在被调用之前确实计算了它的参数,所以它不能被应用于函数调用,但是可以应用于‘ Log 撷取器对象’(或‘ function wrapper’) ,在这个对象上,toString ()只有在需要的时候才会被调用。知道了。

我现在已经把我在这个话题上的经验贴出来了。
我会把它放在那里直到下周二投票,然后我会从你们的答案中选择一个。
再次感谢您的建议:)

26372 次浏览

在 C 或 C + + 中,我会使用预处理器代替条件日志的 if 语句。

将日志级别传递给日志记录器,让它决定是否写入日志语句:

//if(myLogger.isLoggable(Level.INFO) {myLogger.info("A String");
myLogger.info(Level.INFO,"A String");

更新: 啊,我看到您想要有条件地创建日志字符串而不使用 If判断语句。大概是在运行时而不是编译时。

我只想说,我们解决这个问题的方法是将格式化代码放在 logger 类中,这样格式化只有在级别通过时才会发生。非常类似于内置的 sprintf。例如:

myLogger.info(Level.INFO,"A String %d",some_number);

这应该符合你的标准。

对于当前的日志框架来说,这个问题毫无意义

当前的日志框架,如 slf4j 或 log4j 2,在大多数情况下不需要守护语句。它们使用参数化日志语句,以便可以无条件地记录事件,但只有在启用事件时才会进行消息格式化。记录器根据需要执行消息构造,而不是由应用程序先发制人地执行。

如果您不得不使用一个古老的日志库,那么您可以继续阅读以获得更多的背景信息,以及使用参数化消息改造旧的日志库的方法。

警卫语句真的增加了复杂性吗?

考虑将日志保护语句排除在循环复杂度计算之外。

可以说,由于其可预测的形式,条件日志检查实际上并没有增加代码的复杂性。

缺乏灵活性的度量标准会使一个本来很好的程序员变坏!

假设计算复杂性的工具不能达到这种程度,下面的方法可以提供一个解决方案。

需要条件日志记录

我假设你的警卫语句被引入是因为你有这样的代码:

private static final Logger log = Logger.getLogger(MyClass.class);


Connection connect(Widget w, Dongle d, Dongle alt)
throws ConnectionException
{
log.debug("Attempting connection of dongle " + d + " to widget " + w);
Connection c;
try {
c = w.connect(d);
} catch(ConnectionException ex) {
log.warn("Connection failed; attempting alternate dongle " + d, ex);
c = w.connect(alt);
}
log.debug("Connection succeeded: " + c);
return c;
}

在 Java 中,每个日志语句创建一个新的 StringBuilder,并在连接到字符串的每个对象上调用 toString()方法。反过来,这些 toString()方法可能会创建自己的 StringBuilder实例,并在可能很大的对象图中调用其成员的 toString()方法,依此类推。(在 Java5之前,它甚至更昂贵,因为使用了 StringBuffer,而且它的所有操作都是同步的。)

这可能代价相对较高,特别是如果日志语句处于某个高度执行的代码路径中。并且,如上所述,即使由于日志级别太高,日志记录器必然会丢弃结果,也会发生这种代价高昂的消息格式化。

这导致采用警卫声明的形式:

  if (log.isDebugEnabled())
log.debug("Attempting connection of dongle " + d + " to widget " + w);

有了这个保护,只有在必要时才会执行参数 dw的计算以及字符串串联。

一种简单、高效的日志记录解决方案

然而,如果日志记录器(或者你在选择的日志包中编写的包装器)接受了一个格式化程序和格式化程序的参数,那么消息构造可能会被延迟,直到确定它会被使用,同时消除了 Guard 语句和它们的循环复杂度。

public final class FormatLogger
{


private final Logger log;


public FormatLogger(Logger log)
{
this.log = log;
}


public void debug(String formatter, Object... args)
{
log(Level.DEBUG, formatter, args);
}


… &c. for info, warn; also add overloads to log an exception …


public void log(Level level, String formatter, Object... args)
{
if (log.isEnabled(level)) {
/*
* Only now is the message constructed, and each "arg"
* evaluated by having its toString() method invoked.
*/
log.log(level, String.format(formatter, args));
}
}


}


class MyClass
{


private static final FormatLogger log =
new FormatLogger(Logger.getLogger(MyClass.class));


Connection connect(Widget w, Dongle d, Dongle alt)
throws ConnectionException
{
log.debug("Attempting connection of dongle %s to widget %s.", d, w);
Connection c;
try {
c = w.connect(d);
} catch(ConnectionException ex) {
log.warn("Connection failed; attempting alternate dongle %s.", d);
c = w.connect(alt);
}
log.debug("Connection succeeded: %s", c);
return c;
}


}

现在,不会发生带有缓冲区分配的级联 toString()调用,除非他们是必要的!这有效地消除了导致守卫语句的性能损失。在 Java 中,一个小小的缺点是将传递给日志记录器的任何基本类型参数自动装箱。

执行日志记录的代码可以说比以往任何时候都要干净,因为不整洁的字符串串联已经消失了。如果格式字符串是外部化的(使用 ResourceBundle) ,那么它甚至可以更干净,这也有助于软件的维护或本地化。

进一步改进

还要注意的是,在 Java 中,可以使用 MessageFormat对象来代替“格式化”String,后者为您提供了额外的功能,比如更灵活地处理基数的选择格式。另一种选择是实现您自己的格式化功能,调用您为“求值”定义的某个接口,而不是基本的 toString()方法。

尽管我非常讨厌 C/C + + 中的宏,但是在工作中,我们为 if 部分定义了 # 定义,如果 false 忽略(不计算)下面的表达式,但是如果 true 返回一个流,可以使用’< <’操作符将数据通过管道传送到这个流。 像这样:

LOGGER(LEVEL_INFO) << "A String";

我假设这将消除额外的“复杂性”,您的工具看到,也消除了任何计算字符串,或任何表达式被记录,如果没有达到的水平。

在支持将 lambda 表达式或代码块作为参数的语言中,解决这个问题的一种方法是将 lambda 表达式或代码块作为参数赋予日志记录方法。它可以计算配置,并且只有在需要时才实际调用/执行提供的 lambda/代码块。 不过还没试过。

理论上这是可能的。由于性能问题,我不想在生产中使用它,因为我希望大量使用 lamdas/代码块进行日志记录。

但是一如既往: 如果有疑问,测试它并测量对 CPU 负载和内存的影响。

在 Python 中,将格式化的值作为参数传递给日志记录函数。只有在启用日志记录时才应用字符串格式设置。函数调用的开销仍然存在,但与格式化相比,这个开销微不足道。

log.info ("a = %s, b = %s", a, b)

您可以对任何具有可变参数的语言(C/C + + 、 C #/Java 等)执行类似的操作。


当参数很难检索时,这并不是真正用于检索,而是用于将它们格式化为字符串的开销很大时。例如,如果代码中已经有一个数字列表,则可能需要记录该列表以便进行调试。执行 mylist.toString()将需要一段时间,没有好处,因为结果将被抛弃。因此,可以将 mylist作为参数传递给日志记录函数,并让它处理字符串格式化。这样,只有在需要时才执行格式化。


由于 OP 的问题特别提到了 Java,以下是如何使用上述内容:

我必须坚持认为,这个问题与“格式化”无关,而是与“参数评估”有关(评估可能是非常昂贵的,就在调用一个什么都不做的方法之前)

诀窍在于拥有在绝对需要之前不会执行昂贵计算的对象。这在支持 lambdas 和闭包的 Smalltalk 或 Python 等语言中很容易实现,但在 Java 中仍然可以实现,只需要一点想象力。

假设有一个函数 get_everything()。它将把数据库中的每个对象检索到一个列表中。显然,如果结果将被丢弃,您不希望调用这个函数。因此,您不需要直接使用对该函数的调用,而是定义一个名为 LazyGetEverything的内部类:

public class MainClass {
private class LazyGetEverything {
@Override
public String toString() {
return getEverything().toString();
}
}


private Object getEverything() {
/* returns what you want to .toString() in the inner class */
}


public void logEverything() {
log.info(new LazyGetEverything());
}
}

在这段代码中,对 getEverything()的调用是封装的,因此在需要它之前不会实际执行它。只有在启用调试时,日志记录函数才会对其参数执行 toString()。这样,您的代码将只承受函数调用的开销,而不是完整的 getEverything()调用。

谢谢你们所有的回答! 你们太棒了:)

现在,我的反馈并不像你的那样直截了当:

是的,对于 一个项目(就像“在单个生产平台上部署和运行一个程序”) ,我想你可以对我进行所有的技术方面的研究:

  • 专用的“日志检索器”对象,只需要调用 toString ()即可传递给 Logger 包装器
  • 与日志 可变参数函数(或纯 Object []数组)一起使用

就是这样,正如“ John Millikin”和“ erickson”所解释的那样。

然而,这个问题迫使我们思考一下: “我们当初究竟为什么要伐木?”
我们的项目实际上是30个不同的项目(每个项目5到10人) ,部署在不同的生产平台上,具有异步通信需求和中央总线架构。
问题中描述的简单日志记录对于每个项目 一开始(5年前)来说都很好,但是从那时起,我们必须加快步伐。输入 KPI

我们不要求日志记录器记录任何东西,而是要求自动创建的对象(称为 KPI)注册事件。这是一个简单的调用(mykpi. i.am 向你发送信号) ,不需要附加条件(这解决了“人为增加循环复杂度”的问题)。

那个 KPI 对象知道谁在调用它,而且因为它从应用程序的开始就运行,所以它能够检索我们以前在日志记录时现场计算的大量数据。
另外,可以独立监视 KPI 对象,并在单独的发布总线上根据需要计算/发布其信息。
这样,每个客户都可以要求得到他真正想要的信息(比如,“我的流程已经开始了吗? 如果是的话,从什么时候开始的?”?’) ,而不是寻找正确的日志文件,并抓取一个神秘的字符串..。

事实上,这个问题‘我们为什么要首先进行伐木?’?让我们意识到我们不仅仅是为了程序员和他的单元测试或集成测试而写日志,而是为了更广泛的社区,包括一些最终客户端本身。我们的“报告”机制必须是集中的、异步的、全天候的。

KPI 机制的具体情况超出了这个问题的范围。可以说,它的正确校准是目前为止,毫无疑问,我们面临的最复杂的非功能性问题。它仍然时不时地把系统带到它的膝盖上!然而,适当校准,它是一个救生员。

再次感谢你的建议。当简单的日志记录仍然存在时,我们将在系统的某些部分考虑它们。
但是这个问题的另一个观点是在一个更大更复杂的背景下向你们解释一个特定的问题。
希望你喜欢。我可能会问一个关于 KPI 的问题(信不信由你,这个问题到目前为止还没有出现在 SOF 的任何问题中!)下周晚些时候。

我将把这个答案留到下周二投票,然后我会选择一个答案(显然不是这个;)

也许这太简单了,但是围绕着守卫子句使用“提取方法”重构怎么样呢?您的示例代码如下:

public void Example()
{
if(myLogger.isLoggable(Level.INFO))
myLogger.info("A String");
if(myLogger.isLoggable(Level.FINE))
myLogger.fine("A more complicated String");
// +1 for each test and log message
}

变成这样:

public void Example()
{
_LogInfo();
_LogFine();
// +0 for each test and log message
}


private void _LogInfo()
{
if(!myLogger.isLoggable(Level.INFO))
return;


// Do your complex argument calculations/evaluations only when needed.
}


private void _LogFine(){ /* Ditto ... */ }

下面是一个使用三元表达式的优雅解决方案

Info (logger.isInfoEnable () ? “ Log Statement goes here...”: null) ;

alt text
(来源: Scala-lang.org)

Scala 有一个标注 @ elidential (),它允许您移除带有编译器标志的方法。

使用 scala REPL:

C: > scala

欢迎使用 Scala 版本2.8.0. final (JavaHotSpot (TM)64-BitServerVM,Java1。 6.0 _ 16). 键入表达式以求值。 Type: help 获取更多信息。

Scala > import scala.annotation.elitable Import scala.annotation.elitable

Scala > import scala.annotation.elitable. _ Import scala.annotation.el敌

Scala >@elitable (FINE) def logDebug (arg: String) = println (arg)

LogDebug: (arg: String) Unit

Scala > logDebug (“ test”)

Scala >

用 elide-beloset

C: > scala-Xelide-低于0

欢迎使用 Scala 版本2.8.0. final (JavaHotSpot (TM)64-BitServerVM,Java1。 6.0 _ 16). 键入表达式以求值。 Type: help 获取更多信息。

Scala > import scala.annotation.elitable Import scala.annotation.elitable

Scala > import scala.annotation.elitable. _ Import scala.annotation.el敌

Scala >@elitable (FINE) def logDebug (arg: String) = println (arg)

LogDebug: (arg: String) Unit

Scala > logDebug (“ test”)

测试

Scala >

参见 Scala 断言定义

条件日志记录是有害的,它会给你的代码增加不必要的混乱。

你应该总是把你有的对象发送给记录器:

Logger logger = ...
logger.log(Level.DEBUG,"The foo is {0} and the bar is {1}",new Object[]{foo, bar});

然后有一个 java.util.log。使用 MessageFormat 将 foo 和 bar 压平到要输出的字符串中的格式化程序。只有在日志记录器和处理程序将在该级别进行日志记录时才会调用它。

为了增加乐趣,你可以使用某种表达式语言,以便能够很好地控制如何格式化已记录的对象(toString 可能并不总是有用)。

考虑一个日志 util 函数..。

void debugUtil(String s, Object… args) {
if (LOG.isDebugEnabled())
LOG.debug(s, args);
}
);

然后在你想要避免的昂贵的评估周围做一个“结束”的呼叫。

debugUtil(“We got a %s”, new Object() {
@Override String toString() {
// only evaluated if the debug statement is executed
return expensiveCallToGetSomeValue().toString;
}
}
);