SENDLINE open source
open source / logging4s / benchmarks

Benchmarks

A JMH harness in benchmarks/ logs the same mid-size event (eight fields — every scalar type, a list, and one nested object) through each backend, routed to a discarding sink so only CPU cost is measured. Structured values are built with the same jsoniter codec everywhere, except the two console rows, which contrast the two derivation paths.

Every row is logging4s itself, wired to a different backend/encoder — this compares our own backend choices against each other, not logging4s against other logging libraries (there is no third-party baseline here).

bash
sbt "benchmarks/Jmh/run -f 1 -wi 5 -i 12 .*BackendsBench.*"

Measured on 6.1 as throughput — operations per second, higher is better — and as the bytes allocated per record (-prof gc), with 6.0 alongside (single fork, one developer machine — rough ballpark, not authoritative; run it on your own hardware):

Config ops/s bytes per record on 6.0
log4j2 + JsonTemplateLayout (nested) ~705k ~4.2 KB ~7.6 KB
console + fromEncoders (jsoniter) ~663k ~4.3 KB ~8.6 KB
logback + Logging4sEncoder ~643k ~5.3 KB ~10.3 KB
console + derives ~606k ~6.0 KB ~11.0 KB
log4j2 (stringified message) ~510k ~3.7 KB ~6.1 KB
logback + LogstashEncoder ~494k ~3.9 KB ~6.2 KB

Throughput on this machine is rough: error bars of ±5–7% within a run, and noticeably wider swings between runs. Read it as orders of magnitude, not a precise ranking. The bytes per record are stable to the byte between runs, and are the better measure of what a change did.

Takeaways:

  • Throughput spans roughly ~490k–705k ops/s. log4j2 with JsonTemplateLayout leads; the two configurations that hand the JSON to a stock encoder or layout trail.
  • 6.1 allocates 38–49% fewer bytes per record than 6.0 on every configuration: the message text and the JSON line are each built in one presized buffer and escaped in place, small value lists are scanned rather than hashed, and logback renders a value's JSON only when an encoder writes it.
  • The encoding path matters as much as the backend: on the same console backend, fromEncoders with a codec allocates ~28% less than derives, which assembles both JSON and plain from Mirror.
  • Logging4sEncoder (~643k) runs ~1.3× LogstashEncoder (~494k) — it builds the JSON in a single pass, straight from the event, with no Jackson round-trip. It allocates more per record, because it builds the line as a String and then encodes it, where LogstashEncoder writes through Jackson's recycled buffers.

The disabled-level path

A second harness measures what a suppressed record costs — a debug call under a logger set to INFO:

bash
sbt "benchmarks/Jmh/run -f 1 .*LevelGateBench.*"
Call shape ops/s on 6.1 on 6.0
interpolator, level off (debug"event $e") ~779M ~791M
direct call with a lazy value, level off ~793M ~387M
direct call with a pre-rendered value, level off (2.x behaviour) ~757M ~1.5M
interpolator, level on ~444k ~409k
direct call, level on ~476k ~406k

In 2.x a suppressed debug still rendered every value to JSON at the call site, so it cost roughly a quarter of an actual log write. 3.0 made values render lazily (~250× cheaper), and the interpolator skipped the call entirely when the level was off. Since 6.1 every call does — a direct call checks the level before it evaluates its arguments, so even a value pre-rendered at the call site is never built, and the three suppressed shapes cost the same. With the level on, the guard costs nothing measurable.

The two disabled rows are JIT-optimistic in absolute terms: with nothing escaping, escape analysis removes the wrapper allocations outright, so ~1.3 ns/op is a floor rather than a field measurement. The orders of magnitude and the ordering are the signal; the enabled rows are ordinary measured work.

built from README.md at v7.0.0 edit this page on GitHub