具有最小圈复杂度的条件logging
在阅读完“ 你对圆形复杂性有什么限制? ”之后,我发现很多同事对我们项目的这个新的质量保证政策非常恼火:每个function不会超过十个循环复杂性。
含义:不超过10个“if”,“else”,“try”,“catch”等代码工作stream分支语句。 对。 正如我在“ 你testing私有方法吗? ',这样的政策有很多好的副作用。
但是:在我们(200人 – 7年)的项目开始时,我们愉快地进行了日志logging(不,我们不能轻易将它委托给日志的某种“ 面向方面编程 ”方法)。
myLogger.info("A String"); myLogger.fine("A more complicated String"); ...
当我们系统的第一个版本上线时,我们遇到了很大的内存问题,而不是由于日志logging(这是closures的),但是由于总是计算的日志参数 (string),然后传递给'info()'或'fine()'函数,只能发现日志级别为“OFF”,并且没有发生日志logging!
所以QA回来了,并且敦促我们的程序员做条件logging。 总是。
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个“if”,“else”等等,在一个紧密耦合的不容易共享的algorithm中,以及3个关键的日志动作……即使条件日志可能不是真的该function的复杂性的一部分…
你将如何处理这种情况?
在我的项目中,我已经看到了一些有趣的编码演变(由于这个“冲突”),但是我只想把你的想法放在第一位。
谢谢你所有的答案。
我必须坚持认为,问题不是“格式化”相关的,而是“论证评估”相关的(评估在做一个什么都不会做的方法之前可能是非常昂贵的)
所以当写上面的“一个string”时,我实际上是指一个函数(),用一个函数()返回一个string,并调用一个复杂的方法来收集和计算logging器显示的所有types的日志数据。或不(因此问题和使用条件日志的义务 ,因此人为地增加“圈复杂性”的实际问题…)
我现在得到你们中的一些人提出的“ 可变函数”的一点(谢谢约翰)。
注意:在java6中的一个快速testing表明,我的varargs函数在被调用之前确实评估了它的参数,所以它不能被应用于函数调用,而是被应用于“日志search器对象”(或者“函数包装器”),其中toString )只会在需要的时候被调用。 得到它了。
现在我已经发表了关于这个话题的经验。
我会把它留在下个星期二投票,然后我会select你的答案之一。
再次,谢谢你的所有build议:)
在Python中,将格式化的值作为parameter passing给日志loggingfunction。 string格式仅适用于启用日志logging的情况。 还有一个函数调用的开销,但与格式相比,这是微不足道的。
log.info ("a = %s, b = %s", a, b)
你可以使用可变参数(C / C ++,C#/ Java等)为任何语言做这样的事情。
当参数难以检索时,这并不是真正的目的,而是在将它们格式化为string时代价昂贵。 例如,如果您的代码已经有一个数字列表,您可能需要logging该列表进行debugging。 执行mylist.toString()
将需要一段时间没有好处,因为结果将被丢弃。 所以你把mylist
作为parameter passing给日志函数,让它处理string格式。 这样,格式化将只在需要的时候执行。
由于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()
的调用被包装起来,以至于在需要的时候才会真正执行。 只有启用了debuggingfunction,日志loggingfunction才会在其参数上执行toString()
。 这样,你的代码将只受到函数调用的开销,而不是完整的getEverything()
调用。
在目前的日志框架下,这个问题是没有意义的
当前的日志框架如slf4j或log4j 2在大多数情况下不需要警戒语句。 它们使用参数化的日志语句,以便可以无条件地logging事件,但只有在启用事件时才会发生消息格式化。 logging器根据需要执行消息构造,而不是由应用先发制人。
如果您必须使用古色古香的日志库,则可以继续阅读以获取更多背景信息,以及使用参数化消息改造旧库的方法。
守卫声明真的增加了复杂性吗?
考虑从圈复杂度计算中排除日志警卫语句。
可以认为,由于其可预测的forms,有条件的日志logging检查实际上不会影响代码的复杂性。
不灵活的指标可以使一个好的程序员变坏。 小心!
假设你的复杂度计算工具不能适应这个程度,下面的方法可能会提供一个解决方法。
需要有条件的日志logging
我假设你的警惕声明是因为你有这样的代码而引入的:
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中,每个日志语句创build一个新的StringBuilder
,并在每个连接到string的对象上调用toString()
方法。 这些toString()
方法反过来可能会创build自己的StringBuilder
实例,并在可能的大对象图上调用其成员的toString()
方法等等。 (在Java 5之前,它更加昂贵,因为使用了StringBuffer
,并且它的所有操作都是同步的。)
这可能是相对昂贵的,特别是如果日志语句是在一些严格执行的代码path。 而且,如上所述,即使logging器由于日志级别过高而被放弃结果,也会发生昂贵的消息格式化。
这导致forms的守卫声明的引入:
if (log.isDebugEnabled()) log.debug("Attempting connection of dongle " + d + " to widget " + w);
有了这个警卫,参数d
和w
的评估和string连接只有在必要时才会执行。
简单高效的日志logging解决scheme
但是,如果logging器(或者您在所选日志logging程序包中编写的包装器)为格式化程序提供格式化程序和参数,则可以延迟消息构造,直到确定它将被使用,同时消除守护语句及其语句圈复杂度。
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中一个小的惩罚就是自动装箱你传递给logging器的任何原始types参数。
执行日志logging的代码可以说比以前更干净,因为不整齐的string连接不见了。 如果格式化string是外部化的(使用ResourceBundle
),它可以更清洁,这也可以帮助维护或本地化软件。
进一步增强
还要注意的是,在Java中,可以使用MessageFormat
对象来代替“格式” String
,从而为您提供更多的function,例如select格式来更加巧妙地处理基数。 另一种方法是实现自己的格式化function,调用一些你为“评估”定义的接口,而不是基本的toString()
方法。
在支持lambdaexpression式或代码块作为参数的语言中,解决这个问题的一个办法就是给日志logging方法。 那人可以评估configuration,只有在需要的时候实际调用/执行提供的lambda / code块。 尽pipe如此,还没有尝试过。
理论上这是可能的。 由于性能问题,我不希望在生产中使用它,因为我希望大量使用lamdas /代码块进行日志logging。
但是一如既往:如果有疑问,testing它并测量对CPU负载和内存的影响。
在C或C ++中,我会使用预处理器而不是条件日志logging的if语句。
谢谢你所有的答案! 你们摇滚:)
现在我的反馈并不像你的那样直截了当:
是的,对于一个项目 (例如“在单个生产平台上部署和自行运行的一个程序”),我想你可以对我所有的技术:
- 专用的“日志提取器”对象,可以传递给一个logging器包装只调用toString()是必要的
- 与日志可变参数 (或一个普通的Object []数组)一起使用!)
就像@John Millikin和@erickson所解释的那样。
然而,这个问题迫使我们想一点“为什么我们首先login?
我们的项目实际上是30个不同的项目(每个5-10人)部署在各个生产平台上,具有asynchronous通信需求和中央总线架构。
每个项目开始时 (5年前)问题中所描述的简单日志logging都是正确的,但从那时起,我们必须加强。 inputKPI 。
我们要求一个自动创build的对象(称为KPI)来注册事件,而不是要求logging器logging任何事情。 这是一个简单的调用(myKPI.I_am_signaling_myself_to_you()),并不需要是有条件的(这解决了“人为增加圈复杂度”问题)。
该KPI对象知道是谁调用它,并且自从他从应用程序的开始运行以来,他能够检索我们在login时以前计算的大量数据。
此外,KPI对象可以独立监控,并根据需要在单独和独立的出版物总线上计算/发布其信息。
这样,每个客户端都可以询问他真正想要的信息(比如,“我的stream程是否已经开始,如果是的话,从什么时候开始?”),而不是寻找正确的日志文件,并为一个神秘的string寻找…
事实上,“为什么我们首先login?”这个问题。 使我们意识到我们不仅仅是为程序员和他的单元或集成testing,而是为了更广泛的社区,包括一些最终客户本身。 我们的“报告”机制必须是集中的,asynchronous的,24/7。
该KPI机制的具体是超出了这个问题的范围。 只要说它正确的校准就是我们所面临的最复杂的非function性问题。 它仍然不时地使系统在膝盖上! 正确校准,但它是一个生命的救星。
再次感谢您的所有build议。 当简单的日志logging仍然存在时,我们会考虑他们的系统的某些部分。
但是这个问题的另一个问题是在一个更大更复杂的背景下向你说明一个具体的问题。
希望你喜欢它。 我可能会在下周晚些时候提出一个关于KPI的问题(不pipe是否相信SOF都是有问题的!)。
我将把这个答案留给下周二投票,然后我会select一个答案(显然不是这个答案))
也许这太简单了,但是如何使用“提取方法”重构守卫子句呢? 你的示例代码如下:
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 ... */ }
将日志级别传递给logging器,并决定是否写入日志语句:
//if(myLogger.isLoggable(Level.INFO) {myLogger.info("A String"); myLogger.info(Level.INFO,"A String");
更新:啊,我看到你想有条件地创build没有条件语句的日志string。 推测在运行时而不是编译时间。
我只是说,我们解决这个问题的方法是把格式化代码放到logging器类中,这样格式化只有在关卡通过时才会发生。 非常类似于内置的sprintf。 例如:
myLogger.info(Level.INFO,"A String %d",some_number);
那应该符合你的标准。
替代文字http://www.scala-lang.org/sites/default/files/newsflash_logo.png
Scala有一个annontation @elidable() ,它允许你用编译器标志去除方法。
随着Scala REPL:
C:>斯卡拉
欢迎来到Scala 2.8.0.final(Java HotSpot(TM)64位服务器虚拟机,Java 1. 6.0_16)。 键入expression式来评估它们。 键入:帮助以获取更多信息。
scala> import scala.annotation.elidable import scala.annotation.elidable
scala> import scala.annotation.elidable._ import scala.annotation.elidable._
scala> @elidable(FINE)def logDebug(arg:String)= println(arg)
logDebug:(arg:String)单位
scala> logDebug(“testing”)
斯卡拉>
随着elide-beloset
C:> scala -Xelide-低于0
欢迎来到Scala 2.8.0.final(Java HotSpot(TM)64位服务器虚拟机,Java 1. 6.0_16)。 键入expression式来评估它们。 键入:帮助以获取更多信息。
scala> import scala.annotation.elidable import scala.annotation.elidable
scala> import scala.annotation.elidable._ import scala.annotation.elidable._
scala> @elidable(FINE)def logDebug(arg:String)= println(arg)
logDebug:(arg:String)单位
scala> logDebug(“testing”)
testing
斯卡拉>
另请参见Scala声明定义
条件日志是邪恶的。 它给你的代码增加了不必要的混乱。
您应该始终将所需的对象发送给logging器:
Logger logger = ... logger.log(Level.DEBUG,"The foo is {0} and the bar is {1}",new Object[]{foo, bar});
然后有一个java.util.logging.Formatter,它使用MessageFormat将foo和bar压扁成要输出的string。 只有当logging器和处理程序将在该级别login时才会调用它。
为了增加乐趣,可以使用某种expression式语言,以便能够很好地控制如何格式化logging的对象(toString可能并不总是有用)。
就像我讨厌C / C ++中的macros一样,在工作中,我们有#define for if部分,如果false忽略(不计算)下面的expression式,但是如果true返回一个stream,可以使用' <<“运算符。 喜欢这个:
LOGGER(LEVEL_INFO) << "A String";
我认为这样可以消除你的工具看到的额外“复杂性”,并且消除了string的任何计算,或者在没有达到级别时logging任何expression式。
这是一个使用三元expression式的优雅解决scheme
logger.info(logger.isInfoEnabled()?“Log Statement go here …”:null);
考虑一个loggingutilfunction…
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; } } );