Not another logger!
Tags: java, performance, microservices, observability
Published: 2021-03-05 13:31:26

Java is not short of logging libraries: [Log4J](https://logging.apache.org/log4j/2.x/), [SLF4J](http://www.slf4j.org/), [JBoss Logger Manager](https://github.com/jboss-logging/jboss-logmanager), [Flogger](https://google.github.io/flogger/), [Apache Commons Logger](https://commons.apache.org/proper/commons-logging/), [Logback](http://logback.qos.ch/) plus probably many more I've not heard of. There is the also much unloved `java.util.logger` (or JUL) which is part of Java since v1.4. Even with all these options I've been thinking that we need a new logger.

Step away from the pitch fork and hear me out.

Many of these libraries were created to address a particular concern, primarily performance:
* Avoiding array allocation or auto-boxing causing memory churn,
* IO performance to console or file,
* Asynchronous execution,
* Low/zero garbage.

With all these other libraries it's a really crowded space and it doesn't help with minimal microservices. Low/zero garbage would require a total re-write and if that's a concern stick with Log4J v2, but we can address the others... so I did: [`dansiviter.uk:juli`](https://juli.dansiviter.uk).

Oh no, not another logging framework I hear you groan... well, kinda, but first lets see it in action.

# Spring Clean #

It's March, so what better time to clean up your code. The current way of using JUL would typically look something like this:
```java
var log = Logger.getLogger(getClass().getName());
if (log.isLoggable(Level.INFO)) {  // <- needed to avoid array initialisation and auto-boxing
  log.log(Level.INFO, "Hello {0}", "world");  // Grrr, can't use #info(...) with params!?
}
```
This could be simplified to look like this:
```java
var log = LogProducer.log(MyLog.class);
log.hello("world");
```
What is this heresy?! Well, I've created an interface which, via annotation processing, creates an implementation that delegates to JUL but layers on the `#isLoggable(...)` call automatically to avoids the dreaded array initialisation and auto-boxing unless it's strictly needed. 

```java
@Log
public interface MyLog {
  @Message("Hello {0}")
  void hello(String str);
}
```

To verify if we get a performance hit I wrote a small JMH  benchmark comparing the two ways except forcing both auto-boxing and array initialisation in the scenarios:

```
Benchmark                                          Mode  Cnt      Score       Error  Units
LogBenchmark.legLog                               thrpt   25  27921.784 ±  1280.815  ops/s
LogBenchmark.legLog:Used memory heap              thrpt   25  26950.054 ±  8478.381     KB
LogBenchmark.newLog                               thrpt   25  40111.066 ±  1170.407  ops/s
LogBenchmark.newLog:Used memory heap              thrpt   25  25288.212 ± 10124.257     KB
```

Figures are based on my i7-1065G7 using Docker for Desktop so are purely indicative, however the new approach gives both a ~44% increase in throughput and a ~6% decrease in memory usage. I should mention that on repeat runs I did see some cases where memory usage was higher but throughput was consistently around 40% improvement.

This is great start, but we can go further.

# Asynchronicity #

Every single implementation of `java.util.logger.Handler` provided with Java is synchronous; it puts IO directly within the critical path of execution. Log4J and Logback have asynchronous implementations to decouple this and speed up the code, so why can't JUL? So, I created `uk.dansiviter.juli.AsyncHandler` to address this.

There is a concrete implementation of this which delegates to `java.util.logging.ConsoleHandler`. So I created some more JMH tests:

```
Benchmark                                               (handlerName)   Mode  Cnt      Score       Error  Units
ConsoleHandlerBenchmark.handler                        ConsoleHandler  thrpt   25  31041.836 ±  7986.031  ops/s
ConsoleHandlerBenchmark.handler:Used memory heap       ConsoleHandler  thrpt   25  37237.451 ± 15369.245     KB
ConsoleHandlerBenchmark.handler                   AsyncConsoleHandler  thrpt   25  85540.769 ±  6482.011  ops/s
ConsoleHandlerBenchmark.handler:Used memory heap  AsyncConsoleHandler  thrpt   25  41799.724 ± 16472.828     KB
```

For a ~12% increase in memory usage amount of memory usage we get a ~175% increase in throughput is great, however it's important to have some context with these results. This uses `java.util.concurrent.Flow` under the hood and it uses a default buffer size of 256. To prevent log messages being dropped backpressure is handled by blocking when the buffer is full. Therefore, buffer saturation will slow it down, hence the increased variance. However, in reality saturation will rarely occur and this will decouple IO from the log message creation improving performance. In smaller, less 'chatty' tests, massive (often incalculable) improvements are common:

```
x50 - Sync=PT0.018177S, Async=PT0S (NaN%)  // <- async so fast it didn't even register
x100 - Sync=PT0.0317765S, Async=PT0S (NaN%)  // <- ...and again
x200 - Sync=PT0.0644191S, Async=PT0.0009579S (6725.03400%)
x400 - Sync=PT0.1168272S, Async=PT0.0450558S (259.294500%)  // <- dramatic slow down
x800 - Sync=PT0.2164862S, Async=PT0.1705798S (126.912000%)
x1600 - Sync=PT0.4423355S, Async=PT0.4237862S (104.377000%)  // <- almost parity
```
See `uk.dansiviter.juli.RoughBenchmark` for code.

> :information_source: With modern containerised workloads a `FileHandler` is pretty pointless as they rarely have persistent disks to write to and generally write directly to STDERR/STDOUT or to the log aggregator directly. So I've not created an asynchronous version.

# Summary #
So, with this logging wrapper and asynchronous handlers I can have my cake an eat it:
* Cleaner code,
* Performance improvement (often huge!),
* Minimal increase in binary size (~14KB).

However, I feel I should address the title of this post. No, this isn't a new logger, but it does make the existing logger a much nicer experience. Check out the [README](https://juli.dansiviter.uk) which explains more about how it's used including exception handling.

I'm already using this with a CDI project which injects instances into the code making unit testing super-simple. I plan on seeing if this plays nicely with JPMS and GraalVM soon for even more leanness.

I'm ready for my lynching now... do your worst!