24

When you have values than are expensive to compute, a common pattern you see in logging frameworks is

if (log.isDebugEnabled()) {
    String value = expensiveComputation();
    log.debug("value: {}", value);
}

Since Java 8 added lambdas, it'd be nice to do:

log.debug("value: {}", (Supplier<String>) this::expensiveComputation);

Which almost works because the logging framework will do toString() on the parameter. The problem is toString() on Supplier is the implementation in Object.

Is there a way to supply something that's evaluated lazily to Logger methods? It would almost just be a Supplier with a default toString() that calls get().

davidxxx
  • 125,838
  • 23
  • 214
  • 215
David Ehrmann
  • 7,366
  • 2
  • 31
  • 40
  • If your logging framework does `toString` then it cannot be used with lambdas (without a wrapper class for lambda). –  Nov 01 '17 at 20:43
  • 1
    "... because the logging framework will do toString() on the parameter." Why would the logging-factory do that if the parameter is of type `Supplier`? There is not need to call `toString()` on a `String` – Turing85 Nov 01 '17 at 20:43
  • 1
    which logging framework are you using? – Jochen Bedersdorfer Nov 01 '17 at 20:43
  • Also, if you use async logging, the result of `expensiveComputation` may change between the log call and actual write. And if you don't use async logging, you have a larger problem. –  Nov 01 '17 at 20:45
  • @JochenBedersdorfer slf4j – David Ehrmann Nov 01 '17 at 20:51
  • 3
    I'm surprised https://jira.qos.ch/browse/SLF4J-371 hasn't made it into slf4j yet. That would indeed be very convenient – Jochen Bedersdorfer Nov 01 '17 at 20:52
  • @JochenBedersdorfer When it does, that'd probably be the answer to this question. – David Ehrmann Nov 01 '17 at 20:54
  • log.debug("qwe", ()->{return "";}); I get such error Cannot resolve method 'debug' in 'Logger' I use lombok and @Slf4j annotation. Maven: org.projectlombok:lombok:1.18.22 (lombok-1.18.22.jar) What should I do? – Oleg Poltoratskii Mar 19 '23 at 07:40

6 Answers6

20

To pass an argument that will executed in a lazy way the String computation, you have to pass a Supplier and not a String.
The method that you invoke should have this signature :

void debug(Supplier<?> msgSupplier, Throwable t)

You could introduce this utility method in your own utility class.
But you should not need to do that as recent logging frameworks such as Log4j2 provides this feature out of the box.

For example, org.apache.logging.log4j.Logger provides overloaded methods to log that accept a Supplier.
For example :

void debug(MessageSupplier msgSupplier, Throwable t)

Logs a message (only to be constructed if the logging level is the DEBUG level) including the stack trace of the Throwable t passed as parameter. The MessageSupplier may or may not use the MessageFactory to construct the Message.

Parameters:

msgSupplier - A function, which when called, produces the desired log message.

t - the exception to log, including its stack trace.

From Log4j2 documentation :

Java 8 lambda support for lazy logging

In release 2.4, the Logger interface added support for lambda expressions. This allows client code to lazily log messages without explicitly checking if the requested log level is enabled. For example, previously you would write:

if (logger.isTraceEnabled()) {
    logger.trace("Some long-running operation returned {}", expensiveOperation());
}

With Java 8 you can achieve the same effect with a lambda expression. You no longer need to explicitly check the log level:

logger.trace("Some long-running operation returned {}", 
              () ->    expensiveOperation());
davidxxx
  • 125,838
  • 23
  • 214
  • 215
  • 4
    The very last line is what I was looking for. This should be the accepted answer. – Asaph Feb 09 '18 at 19:46
  • log.debug("qwe", ()->{return "";}); I get such error Cannot resolve method 'debug' in 'Logger' I use lombok and @Slf4j annotation. Maven: org.projectlombok:lombok:1.18.22 (lombok-1.18.22.jar) What should I do? – Oleg Poltoratskii Mar 18 '23 at 22:49
10

A small helper object will allow you to do what you want:

public class MessageSupplier {
    private Supplier<?> supplier;

    public MessageSupplier(Supplier<?> supplier) {
        this.supplier = supplier;
    }

    @Override
    public String toString() {
        return supplier.get().toString();
    }

    public static MessageSupplier msg(Supplier<?> supplier) {
        return new MessageSupplier(supplier);
    }
}

Or in Kotlin:

class MessageSupplier(supplier: Supplier<?>) {
    override fun toString() = supplier.get().toString()
}

fun msg(supplier: Supplier<?>) = MessageSupplier(supplier)

Then, with a static import of msg:

log.debug("foo: {}", msg(this::expensiveComputation));
Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277
teppic
  • 7,051
  • 1
  • 29
  • 35
  • 1
    This is a simple and effective approach with adding more dependencies. – Drakes Oct 12 '18 at 19:08
  • 1
    Nice solution. But formally there is no guarantee toString would be called only once. So there is a chance an expensive calculation would be run multiple times. That could potentially be even worse than provide to logger already built String (that could be forgot) instead of Supplier. – gdomo Nov 23 '18 at 10:03
1

Interestingly you can't even use something like this

interface LazyString { String toString(); }

as a functional interface

The only way I found so far is via anonymous classes.

Object o = new Object() { @Override public String toString() { return myExpensiveComputation(); } }; System.out.printf("%s", o);

Jochen Bedersdorfer
  • 4,093
  • 24
  • 26
0

Which almost works because the logging framework will do toString() on the parameter.

This statement isn't correct. If you step into the debug/info/whatever method, you'll find this implementation:

public void log(Level level, Supplier<String> msgSupplier) {
    if (!isLoggable(level)) {
        return;
    }
    LogRecord lr = new LogRecord(level, msgSupplier.get());
    doLog(lr);
}

If the level isn't met, the Supplier isn't even used.

Louis Wasserman
  • 191,574
  • 25
  • 345
  • 413
Mureinik
  • 297,002
  • 52
  • 306
  • 350
  • 2
    Which logging framework are you looking at? I'm looking at slf4j for the API and Logback for the implementation. – David Ehrmann Nov 01 '17 at 20:48
  • 1
    This snippet is taken from `java.util.logging.Logger`. I'll see if I can dig up logback's implementation – Mureinik Nov 01 '17 at 21:03
0

For java.util.logging and Java 8+ you can also use this lazy and handy notation:

LOGGER.fine(() -> "Message1: "  + longComputation1() + ". Message2: " + longComputation2());

longComputation1() and longComputation2() will be called lazy - i.e. only when needed.

adaslaw
  • 170
  • 11
0

Since you revealed you use SLF4J which still does not support this at the core, this might come in handy if you code in Kotlin (which you can with JDK 8) :

fun Logger.info( provider: () -> String ) { if (this.isInfoEnabled) this.info(provider.invoke())
fun Logger.debug( provider: () -> String ) { if (this.isDebugEnabled) this.debug(provider.invoke())
fun Logger.trace( provider: () -> String ) { if (this.isTraceEnabled) this.trace(provider.invoke()) 

Usage:

log.trace { "State dump: " + expensiveLongSerialisation(state) }

Source: My Gist :) https://gist.github.com/OndraZizka/a7381b8cd86f734bc3b6bf9e528a01ad

Ondra Žižka
  • 43,948
  • 41
  • 217
  • 277