简体   繁体   中英

How to factor out conditional log checks in Java 8 & SLF4J?

I am trying to factor out if (log.isInfoEnabled()) or if (log.isWarnEnabled()) statements so I am thinking to create an interface in Java 8 like below however I am not sure if I could run into any problems?

public interface Logging {

    Logger log_ = LoggerFactory.getLogger(Thread.currentThread().getContextClassLoader().getClass());

    default void logInfo(String... messages) {
        if (log_.isInfoEnabled()) {
            String meg = String.join("log message: ", messages)
            log_.info(msg);
        }
    }
}


public class Hello implements Logging {

    //Imagine this function is called lot of times like imagine
    // an infinite loop calling this function
    public void doSomething(String name) {
        if (name.equals("hello")) {
            logInfo("What class name does the log print here ? ")  
        }
    }

     //Imagine this function is called lot of times like imagine
    // an infinite loop calling this function
   public void doSomething2(String name) {
        if (name.equals("hello2")) {
            logInfo("What class name does the log print here ? ")  
        }
    }

    //Imagine this function is called lot of times like imagine
    // an infinite loop calling this function
    public void doSomething3(String name) {
        if (name.equals("hello3")) {
            logInfo("What class name does the log print here ? ")  
        }
    }
}

vs

public class Hello {

    Logger log_ = LoggerFactory.getLogger(Hello.class);

    public void doSomething(String name) {
        if (name.equals("hello")) {
            if (log_.isInfoEnabled()) { // Don't want to have this code everywhere
                logInfo("What class name does the log print here ? ") 
            }
        }
    }
}

Are these two equivalent? Any problems if I go with the Logging interface above?

I am trying to factor out if (log.isInfoEnabled()) or if (log.isWarnEnabled()) statements

I think you didn't understand how and why the if (log.isLevelXXXEnabled()){...} pattern is used.

What you are writing here :

public void doSomething(String name) {
    if (name.equals("hello")) {
        if (log_.isInfoEnabled()) { // Don't want to have this code everywhere
            logInfo("What class name does the log print here ? ") 
        }
    }
}

has really no need to be enclosed by :

if (log_.isInfoEnabled()){

Why ?
Because you pass a String to log that doesn't require any computation : "What class name does the log print here ?" to your logger.
Besides, loggers are already designed to write the log only if the actual level of the logger matches with the requested log level.

Now, suppose you passed an object with a expensive toString() method.
The logger invokes indeed the toString() method of the passed object to log when it performs actually the log .
But in this case, using a check before logging is still useless.

Suppose myObject is a variable referring to an instance of class with a toString() performing multiple computations or simply a collection of objects. This is helpless :

if (log_.isInfoEnabled()) { 
    logInfo(myObject); 
}

as the logger implementation will perform myObject.toString() only if the effective log level of the logger matches with the requested level for logging.

Using this check log pattern makes sense only when you have to perform a relatively expensive computation for the passed object.
Generally, it is when you perform yourself a computation and you provide it as parameter to the log operation.
For example :

if (log_.isInfoEnabled()) { 
    log_.info("name :" + myPerson.getName() + ", order history :" + myPerson.getOrders()); 
}

Here it makes sense not because you spare 4 concatenations but because myPerson.getOrders() is a collection with potential hundreds elements and invoking toString() on it should be done only if you really log.

Now how do you think design a generic method that could do this specific check and any other specific check for you ?
It is not possible.
In fact, what you try to do is possible but will only repeat what the logger libraries already do for you.
The specific case where you should do the check because it is expensive has to be performed by you for each specific case.

Now according to preferences, you can of course replace this :

if (log_.isInfoEnabled()) { 
    log_.info("name :" + myPerson.getName() + ", order history :" + myPerson.getOrders()); 
}

by this other syntax :

log_.info("name : {}, order history : {}", myPerson.getName(), myPerson.getOrders()); 

that relies on the

public void info(String format, Object... arguments);

method of the org.slf4j.Logger class.

Using if (log_.isInfoEnabled()) { or public void info(String format, Object... arguments); doesn't change really the problem as you should always think when using the way to spare potential computation.

The guard is (should be) primarily used to prevent costly operations when no need arises - just for debugging.

So the following would be the correct pattern:

    if (log_.isInfoEnabled()) {
        String meg = String.join("log message: ", f(), g(), h);
        log_.info(msg);
    }

default void logInfo(Supplier<String> messageSupplier) {
    if (log_.isInfoEnabled()) {
        log_.info(messageSupplier.get());
    }
}

logInfo(() -> String.join("log message: ", f(), g(), h));

However you risk that this method is used for the simple logging cases, turning it in a costly extra function call logInfo , and that lambda.

I think your approach, even if it works, is pointless. The log method already did this work.

The reason because we use the log guard condition is about string concatenation.

Here is the issue:

log.debug("This is the case where " + varToBeChecked);

In this case you have to use the guard condition:

if (log.isDebugEnabled()) {
    log.debug("This is the case where " + varToBeChecked);
}  

This not only avoids the execution of the log statement, but also the string concatenation, that will be done in the previos example.

So if you have just a fixed string with no concatenation you have no reasons to use the guard condition:

log.info("Method starts here");

In your proposal, the interface is not protecting you from this issue, so you are going to add complexity without solving the issue.

The solution to this log staff could be a change of the log interface:

log.debug("My log message for %s of run", myVarToBeLogged);

In this case we could avoids the string concatenation, but I'm not sure it is the same performance of using the guard condition.

The logging methods only output values to log when a specific logging level is enabled. The only case where manually checking the logging level makes sense is when creating the message to log is expensive. Say, you have to retrieve a lazy loaded field from the database or walk through a collection with many items. In that case it makes sense to create an interface to simulate a mixin and use functional programming as in:

public interface Logging {

    Logger getLogger();

    default void logInfo(Supplier<String> msg){
        Logger logger = getLogger();
        if (logger.isInfoEnabled()){
            logger.info(msg.get());
        }
    }

}

public class Something implements Logging {

    public static Logger LOG = LoggerFactory.getLogger(Something.class);

    @Override
    public Logger getLogger() {
        return LOG;
    }

    public void doSomething(){
        logInfo(this::createExpensiveMessage); 
    }

    private String createExpensiveMessage(){
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // Nobody cares 
        }

        return "Something happened";
   }

}

You don't need guards if you use paramterized messages and the parameters are direct references to objects or primitive values.

https://www.slf4j.org/faq.html#logging_performance

This is a parameterized message: log.debug("Person is {}", person);

Guards are still needed if logging parameters use some kind of processing to get to the object we want to log. What slf4j deferres is only the call to object's toString() method, but not the computations that are performed to get the object itself. This guard is reasonable to put here:

if (log.isdebugEnabled()) {
 log.debug("Value {} is not valid", obj.connectToDBAndRetrieveValue());
}

So having a custom log wrapper to encapsulate the guard logic might be handy. And also, it will shift unit testing of this condition from client code to the concrete wrapper class what would be much easier and cleaner to do.

As a side note, actually this kind of wrapper is not needed for INFO level since INFO is usually enabled in production. Unless this is not your case.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM