Conditional logging with minimal cyclomatic complexity

前端 未结 12 1847
情歌与酒
情歌与酒 2020-11-27 10:58

After reading \"What’s your/a good limit for cyclomatic complexity?\", I realize many of my colleagues were quite annoyed with this new QA policy on our project: no more 10

相关标签:
12条回答
  • 2020-11-27 11:27

    Here is an elegant solution using ternary expression

    logger.info(logger.isInfoEnabled() ? "Log Statement goes here..." : null);

    0 讨论(0)
  • 2020-11-27 11:32

    In Python you pass the formatted values as parameters to the logging function. String formatting is only applied if logging is enabled. There's still the overhead of a function call, but that's minuscule compared to formatting.

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

    You can do something like this for any language with variadic arguments (C/C++, C#/Java, etc).


    This isn't really intended for when the arguments are difficult to retrieve, but for when formatting them to strings is expensive. For example, if your code already has a list of numbers in it, you might want to log that list for debugging. Executing mylist.toString() will take a while to no benefit, as the result will be thrown away. So you pass mylist as a parameter to the logging function, and let it handle string formatting. That way, formatting will only be performed if needed.


    Since the OP's question specifically mentions Java, here's how the above can be used:

    I must insist that the problem is not 'formatting' related, but 'argument evaluation' related (evaluation that can be very costly to do, just before calling a method which will do nothing)

    The trick is to have objects that will not perform expensive computations until absolutely needed. This is easy in languages like Smalltalk or Python that support lambdas and closures, but is still doable in Java with a bit of imagination.

    Say you have a function get_everything(). It will retrieve every object from your database into a list. You don't want to call this if the result will be discarded, obviously. So instead of using a call to that function directly, you define an inner class called 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());
        }
    }
    

    In this code, the call to getEverything() is wrapped so that it won't actually be executed until it's needed. The logging function will execute toString() on its parameters only if debugging is enabled. That way, your code will suffer only the overhead of a function call instead of the full getEverything() call.

    0 讨论(0)
  • 2020-11-27 11:33

    Pass the log level to the logger and let it decide whether or not to write the log statement:

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

    UPDATE: Ah, I see that you want to conditionally create the log string without a conditional statement. Presumably at runtime rather than compile time.

    I'll just say that the way we've solved this is to put the formatting code in the logger class so that the formatting only takes place if the level passes. Very similar to a built-in sprintf. For example:

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

    That should meet your criteria.

    0 讨论(0)
  • 2020-11-27 11:34

    With current logging frameworks, the question is moot

    Current logging frameworks like slf4j or log4j 2 don't require guard statements in most cases. They use a parameterized log statement so that an event can be logged unconditionally, but message formatting only occurs if the event is enabled. Message construction is performed as needed by the logger, rather than pre-emptively by the application.

    If you have to use an antique logging library, you can read on to get more background and a way to retrofit the old library with parameterized messages.

    Are guard statements really adding complexity?

    Consider excluding logging guards statements from the cyclomatic complexity calculation.

    It could be argued that, due to their predictable form, conditional logging checks really don't contribute to the complexity of the code.

    Inflexible metrics can make an otherwise good programmer turn bad. Be careful!

    Assuming that your tools for calculating complexity can't be tailored to that degree, the following approach may offer a work-around.

    The need for conditional logging

    I assume that your guard statements were introduced because you had code like this:

    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;
    }
    

    In Java, each of the log statements creates a new StringBuilder, and invokes the toString() method on each object concatenated to the string. These toString() methods, in turn, are likely to create StringBuilder instances of their own, and invoke the toString() methods of their members, and so on, across a potentially large object graph. (Before Java 5, it was even more expensive, since StringBuffer was used, and all of its operations are synchronized.)

    This can be relatively costly, especially if the log statement is in some heavily-executed code path. And, written as above, that expensive message formatting occurs even if the logger is bound to discard the result because the log level is too high.

    This leads to the introduction of guard statements of the form:

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

    With this guard, the evaluation of arguments d and w and the string concatenation is performed only when necessary.

    A solution for simple, efficient logging

    However, if the logger (or a wrapper that you write around your chosen logging package) takes a formatter and arguments for the formatter, the message construction can be delayed until it is certain that it will be used, while eliminating the guard statements and their cyclomatic complexity.

    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;
      }
    
    }
    

    Now, none of the cascading toString() calls with their buffer allocations will occur unless they are necessary! This effectively eliminates the performance hit that led to the guard statements. One small penalty, in Java, would be auto-boxing of any primitive type arguments you pass to the logger.

    The code doing the logging is arguably even cleaner than ever, since untidy string concatenation is gone. It can be even cleaner if the format strings are externalized (using a ResourceBundle), which could also assist in maintenance or localization of the software.

    Further enhancements

    Also note that, in Java, a MessageFormat object could be used in place of a "format" String, which gives you additional capabilities such as a choice format to handle cardinal numbers more neatly. Another alternative would be to implement your own formatting capability that invokes some interface that you define for "evaluation", rather than the basic toString() method.

    0 讨论(0)
  • 2020-11-27 11:34

    Maybe this is too simple, but what about using the "extract method" refactoring around the guard clause? Your example code of this:

    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
    }
    

    Becomes this:

    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 ... */ }
    
    0 讨论(0)
  • Consider a logging util function ...

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

    Then make the call with a "closure" round the expensive evaluation that you want to avoid.

    debugUtil(“We got a %s”, new Object() {
           @Override String toString() { 
           // only evaluated if the debug statement is executed
               return expensiveCallToGetSomeValue().toString;
           }
        }
    );
    
    0 讨论(0)
提交回复
热议问题