Skip to content

Releases: merlimat/slog

v0.10.0

Choose a tag to compare

@merlimat merlimat released this 11 Jun 00:05
cd934a5

What's Changed

A performance and correctness pass over the logging hot paths β€” every change validated with JMH benchmarks and regression tests.

Fixes

  • Fix stale-level races in the Log4j2 cached level scheme: the generation and effective level are packed into a single word, and generation bumps are atomic (#10)
  • Route through AsyncLogger in full-async mode (AsyncLoggerContextSelector) instead of calling LoggerConfig directly, which silently ran appender I/O on the application thread (#12)
  • Pool log events only when Log4j2 thread-locals are enabled: with thread-locals disabled (the web-app default), AsyncLoggerConfig enqueues events by reference and a reused event could cross-contaminate records (#13)
  • Guard the pooled event against reentrant logging (e.g. an attr supplier that logs), which could corrupt the in-flight outer event (#16)

Performance

  • Measure timed() durations with a monotonic clock: allocation-free, immune to wall-clock adjustments, ~20% faster (#11)
  • Save and restore only the touched MDC keys in the SLF4J handler instead of copying the whole MDC map on every structured event (#14)
  • Flatten logger context into a single array at logger-build time: +14% throughput and βˆ’60% allocation on context-bearing events (#15)
  • Skip per-thread event pooling on virtual threads, where pool memory would scale with the number of live threads (#17)
  • Store event attrs in a single interleaved array: 80 β†’ 64 B/op on the fluent path (#18)
  • Clear the pooled event right after emit, so the last event's throwable and attr values are not pinned on idle threads (#19)

Full Changelog: v0.9.9...v0.10.0

v0.9.9

Choose a tag to compare

@merlimat merlimat released this 28 Apr 23:39
c6410ba

What's Changed

  • Treat null Logger as a no-op in LoggerBuilder.ctx() and Event.ctx() to avoid NullPointerException when callers pass an optional context logger that may be unset (#7).

Full Changelog: v0.9.8...v0.9.9

v0.9.8

Choose a tag to compare

@merlimat merlimat released this 25 Apr 04:02

Bug Fixes

  • Fix MDC propagation under the Log4j2 backend. Log4j2Logger.buildContextData was building the event's ContextData from only slog's own context attrs and event attrs, ignoring the caller thread's ThreadContext. Layouts using %X{key} and appenders that read event.getContextData().getValue(key) saw nothing β€” even when the thread had MDC entries set when the log call ran. Fixed by merging ThreadContext.getImmutableContext() into the event's StringMap before slog's own attrs (which override on key collision). Fast path is preserved β€” when MDC is empty the only added work is one ThreadContext.isEmpty() check, no allocation. (#6)

New Features

  • Add Event.ctx(Logger other) so a static or shared logger can inherit another logger's context for a single event, avoiding the per-call allocation of a child logger. Ordering mirrors LoggerBuilder.ctx: inherited attrs first, then this logger's own context, then per-event attrs; multiple ctx() calls append in invocation order. Allocation-free in the common static-logger case via AttrChain.withPrefix short-circuits. (#5)

    static final Logger log = Logger.get(MyService.class);
    
    void handle(Logger requestLog, String msgId) {
        log.info()
                .ctx(requestLog)            // adopt request-scoped context
                .attr("msgId", msgId)
                .log("processed");
    }

Full Changelog: v0.9.7...v0.9.8

v0.9.7

Choose a tag to compare

@merlimat merlimat released this 14 Apr 15:34

Bug Fixes

  • Fix NPE in ContextMapLookup when a ${ctx:key} lookup resolves against a slog event with no context attributes β€” Log4j2Logger.buildContextData returned null for the no-context case, which caused ContextMapLookup.lookup() to NPE on event.getContextData().getValue(key). This showed up under RoutingAppender, whose route key pattern is evaluated per event via StrSubstitutor. Fixed by returning ContextDataFactory.emptyFrozenContextData() instead, courtesy of @nodece in #3.

Dependencies

  • Bump log4j-core from 2.25.3 to 2.25.4 (#2)

Full Changelog: v0.9.6...v0.9.7

v0.9.6

Choose a tag to compare

@merlimat merlimat released this 10 Apr 19:09

Bug Fixes

  • Fix NPE in JsonTemplateLayout when using NDC resolver β€” MutableLogEvent.clear() nulls the context stack, and since slog bypasses ThreadContext, the stack stayed null. Log4j2's ThreadContextStackResolver does not null-check it, causing an NPE with templates like EcsLayout.json. Fixed by setting the context stack to EMPTY_STACK after clear().

v0.9.5

Choose a tag to compare

@merlimat merlimat released this 03 Apr 16:10

What's New

Rate Limiting

  • Added onceEvery(int) and onceEvery(Duration) rate limiting to the Event API, allowing log events to be throttled to avoid flooding

Security

  • Added security policy for vulnerability reporting
  • Upgraded Jackson to 2.21.1 to fix GHSA-72hv-8253-57qq
  • Added explicit permissions to CI and Release workflows

Other

  • Added Google Flogger to benchmarks
  • Added installation instructions to README
  • Added Dependabot configuration

Full Changelog: v0.9.4...v0.9.5

v0.9.4

Choose a tag to compare

@merlimat merlimat released this 02 Apr 21:55

What's New

Performance optimizations

  • Cached log level with generation counter β€” isXxxEnabled() checks now use a VarHandle.getOpaque() generation-counter scheme, avoiding calls into the Log4j2 hierarchy on every check. Disabled path is 2.4–2.8Γ— faster than native Log4j2/SLF4J.
  • Zero-alloc simple emit path β€” MutableLogEvent, message, and context map are pooled in ThreadLocals. Simple enabled path allocates 0 B/op.
  • Native Object values in context data β€” attribute values are passed as Object to Log4j2's StringMap, avoiding unnecessary toString() conversion at emit time.
  • Eliminate LogRecord from production β€” fields are passed directly through the emit() signature; LogRecord and TestLogger moved to the test source set.
  • Parallel arrays in EventImpl β€” fluent event attributes use inline String[]/Object[] arrays instead of ArrayList<Attr>, reducing allocation from 144 β†’ 80 B/op (14Γ— less garbage than SLF4J fluent).

API improvements

  • ThrowingSupplier<T> β€” new functional interface replacing java.util.function.Supplier for attribute and message suppliers. Suppliers may now throw checked exceptions; failures are caught and the exception message is recorded as the attribute value, so a failing supplier never crashes the application.

Benchmark results (vs native framework calls)

Path slog Log4j2 SLF4J
Disabled (level check) 1,005 ops/ΞΌs 413 360
Simple enabled 24.6 ops/ΞΌs 14.6 11.2
Fluent enabled (3 attrs) 13.2 ops/ΞΌs 6.4 (positional) 4.2 (fluent)
Simple alloc 0 B/op 24 B/op 24 B/op
Fluent alloc 80 B/op 40 B/op 1,104 B/op

v0.9.3

Choose a tag to compare

@merlimat merlimat released this 02 Apr 06:44

What's New in v0.9.3

πŸš€ Performance Optimizations

Major overhaul of the Log4j2 emit path, achieving 1.8x faster simple logging and 2x faster fluent structured logging compared to v0.9.2:

  • Zero-allocation emit path β€” pool MutableLogEvent and SortedArrayStringMap in ThreadLocals, reusing them across log calls instead of allocating on every emit
  • Bypass ThreadContext entirely β€” structured attributes are set directly on the log event's context data, eliminating ThreadContext.put()/clear()/restore() overhead
  • Remove redundant timestamp β€” eliminated double clock.instant() call (one in slog, one in Log4j2) by letting Log4j2 handle timestamping via clock.millis()
  • Skip context save/restore β€” when there are no structured attributes, skip the context data pipeline entirely

Benchmark Results (ops/ΞΌs, higher is better)

Enabled path (INFO call):

Benchmark ops/ΞΌs
slog Simple 24.3
SLF4J Simple 13.9
Log4j2 Simple 13.6
slog Fluent (3 attrs) 9.5
slog Fluent+Ctx 7.8
Log4j2 Positional 7.4
SLF4J Positional 6.2
SLF4J Fluent 4.1

πŸ“¦ API & Library Changes

  • Logger is now a pure interface β€” implementation moved to package-private classes
  • Primitive attr() overloads β€” attr(String, int), attr(String, long), etc. to avoid autoboxing when the log level is disabled
  • Supplier<?> support β€” lazy attribute evaluation with attr(String, Supplier<?>) and log(Supplier<String>)
  • JMH benchmark module β€” ./gradlew :benchmark:jmh to run benchmarks; supports async-profiler flame graphs

πŸ“– Documentation

  • Added Performance section to README with charts, comparison summary, and benchmark instructions
  • Added Lombok @CustomLog integration instructions
  • Added package-level Javadoc for io.github.merlimat.slog
  • Fixed Javadoc version extraction from release tag

Full Changelog: v0.9.2...v0.9.3

v0.9.2

Choose a tag to compare

@merlimat merlimat released this 28 Mar 05:50

What's Changed

New Features

  • Add Consumer<Event> overload for deferred logging of expensive messages β€” avoids constructing the log event when the level is disabled (068760d)

Bug Fixes

  • Fix caller location resolution in Log4j2 output β€” stack frames now correctly point to the caller instead of the library (d5c44ef)

Internal

  • Make isEnabled(Level) private and use it consistently in all logging methods (cf53900)

v0.9.1

Choose a tag to compare

@merlimat merlimat released this 24 Mar 20:37

slog v0.9.1

Changes since v0.9.0

  • No hard runtime dependencies β€” SLF4J is now compileOnly like Log4j2. slog auto-discovers whichever backend is on the classpath; throws IllegalStateException if neither is found
  • slf4j-simple support β€” new Slf4jSimpleHandler inlines attrs into the log message since slf4j-simple doesn't render MDC
  • Fix all javadoc warnings β€” proper @param/@return tags on all public methods
  • Upgrade jackson-databind to 2.19.0 (test dependency, CVE fix)

Maven

<dependency>
    <groupId>io.github.merlimat.slog</groupId>
    <artifactId>slog</artifactId>
    <version>0.9.1</version>
</dependency>

Gradle

implementation("io.github.merlimat.slog:slog:0.9.1")