github linkedin
Lazy logging with SLF4J
2021-12-23

Today I want to show you a way how to implement lazy logging with SLF4J 1.x. Newer log libraries like Log4j2 support lazy logging with lambdas like this:

LOGGER.info("Expensive operation: {}", () -> expensiveOperation());

SLF4J 1.x doesn’t support this, trying this gives you a compiler error.

But we can leverage a behaviour of the logging frameworks: they call .toString() on the parameter objects. So let’s define a method like this:

static Object lazy(Callable<?> callable) {
    return new Object() {
        @Override
        public String toString() {
            try {
                Object result = callable.call();
                if (result == null) {
                    return "null";
                }

                return result.toString();
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
    };
}

This method takes a Callable, which is a SAM interface and can be used in a lambda expression. It then returns a new anonymous class with the toString() method overridden. This toString() calls the given Callable and delegates the toString() method to the returned object (and adds some null checks and exception handling).

The lazy method can then be used like this:

LOGGER.info("Expensive operation: {}", lazy(() -> expensiveOperation()));

It looks like the Log4j2 version, only the lazy method call is added.

Let’s test in a real program:

public static void main(String[] args) throws InterruptedException {
    LOGGER.info("Started");
    LOGGER.info("Expensive operation: {}", lazy(() -> expensiveOperation()));
    LOGGER.info("Stopped");
}

where expensiveOperation() is defined like this:

private static String expensiveOperation() throws InterruptedException {
    Thread.sleep(2000);
    return "foo";
}

When run, it prints

2021-12-23T17:33:57.719+01:00 [main] INFO  de.mkammerer.skeleton.Main - Started
2021-12-23T17:33:57.722+01:00 [main] INFO  de.mkammerer.skeleton.Main - Expensive operation: foo
2021-12-23T17:33:59.723+01:00 [main] INFO  de.mkammerer.skeleton.Main - Stopped

The “Stopped” message appears 2 seconds after the “Started” message, as the expensive operation is called and the thread slept for 2 seconds. Let’s change the logging level from the expensiveOperation() from INFO to TRACE. The logging backend is configured to only log DEBUG and higher, hence the log message is suppressed:

LOGGER.trace("Expensive operation: {}", lazy(() -> expensiveOperation()));

And run it again:

2021-12-23T17:35:56.678+01:00 [main] INFO  de.mkammerer.skeleton.Main - Started
2021-12-23T17:35:56.681+01:00 [main] INFO  de.mkammerer.skeleton.Main - Stopped

Now the “Stopped” message appears immediately after the “Started” message, which proves that expensiveMethod() has not been called and that the lazy method works as expected.


Back to posts