⚡ Hermes Logging Library for Java

High-performance logging library for Java with zero-boilerplate annotation processing, async logging via LMAX Disruptor, and comprehensive Spring Boot integration.

Java 17
LMAX Disruptor
Spring Boot
Maven
Kotlin
GraalVM
Annotation Processing
JSON Logging
Logstash
Nicholas Adamou
17 min read
🖼️
Image unavailable

Ever found yourself writing private static final Logger log = LoggerFactory.getLogger(MyClass.class); for the hundredth time and thinking, "There has to be a better way"? Or maybe you've watched your high-throughput Java app grind to a halt because logging became a bottleneck? Yeah, we've been there too.

Hermes is a modern logging library for Java 17+ that ditches the old baggage. No more boilerplate logger declarations. No more XML config files. No more lock contention killing your throughput. Instead, you get compile-time code generation, zero-allocation message formatting, and lock-free async processing via LMAX Disruptor. Named after the Greek messenger god who delivered messages at lightning speed, Hermes does exactly that for your logs.

Why Hermes?

Look, most logging frameworks were built back in the Java 6 days. They work, sure, but they weren't designed for today's cloud-native, high-throughput world. If you're running microservices that handle thousands of requests per second, or processing millions of Kafka messages per hour, traditional logging can become your bottleneck.

The Logging Performance Problem

Think about what happens every time you log something: format a message, acquire a lock, allocate strings, write to disk. Multiply that by a million events per second. Suddenly, those microseconds add up. Here's what goes wrong with traditional approaches:

  • Reflection overhead for logger field injection
  • Lock contention in multi-threaded async appenders
  • GC pressure from excessive string allocation during message formatting
  • Thread blocking when writing to I/O streams synchronously
  • Configuration complexity with XML or properties files

Zero-Boilerplate Development

Okay, let's talk about the elephant in the room: that logger declaration you copy-paste into every single class. You know the one. And you know how easy it is to forget to change the class name, right? We've all been there.

Hermes fixes this with one simple annotation:

@InjectLogger
public class UserService extends UserServiceHermesLogger {
    public void createUser(String username) {
        log.info("Creating user: {}", username);
    }
}

That's it. The @InjectLogger annotation triggers compile-time code generation that creates a base class with a protected Logger log field ready to use. What's cool about this?

  • No boilerplate — Zero. Nada. And no runtime reflection overhead either.
  • Type-safe — Catches errors at compile time, not at 3 AM in production.
  • GraalVM-friendly — Works with native images out of the box.
  • IDE-friendly — Your IDE's autocomplete just works.

Production-Ready Features

Speed is great, but you also need this thing to actually work in production, right? Hermes has you covered:

  • Structured JSON logging — Works seamlessly with ELK, Splunk, Datadog, and friends.
  • MDC support — Track requests across your distributed system.
  • Multiple appenders — Console, File, RollingFile, Async, Logstash—take your pick.
  • Flexible formatting — Customize patterns however you like.
  • Spring Boot integration — Auto-configuration via YAML. No XML nightmares.
  • Kotlin DSL — Because Kotlin developers deserve nice things too.
  • GraalVM support — Native images? Yeah, that works.

Core Architecture

Hermes uses a modular architecture split into six focused modules. You only include what you need, and the dependency boundaries are crystal clear.

System Architecture Overview

Module Dependency Structure

Hermes is organized into six distinct modules, each with specific responsibilities:

Module Breakdown

1. hermes-api - The Public Contract

Contains all public interfaces, annotations, and API surface that applications compile against. This separation enables implementation swapping without recompilation.

Key components:

  • Logger interface with level-specific methods (trace, debug, info, warn, error)
  • LoggerFactory for logger instantiation
  • @InjectLogger annotation for compile-time injection
  • LogLevel enum (TRACE, DEBUG, INFO, WARN, ERROR)
  • MDC (Mapped Diagnostic Context) for thread-local contextual data
  • Marker interface for log categorization

2. hermes-core - The Implementation Engine

Provides the concrete implementation of all logging functionality, optimized for performance and zero-allocation where possible.

Key components:

  • HermesLogger - Main logger implementation with level checks
  • LogEvent - Immutable Java 17 record capturing log data
  • MessageFormatter - Zero-allocation message formatting using ThreadLocal StringBuilder
  • Appenders: Console, File, RollingFile, Async (with Disruptor), Logstash
  • Layouts: Pattern (customizable format strings), JSON (structured logging)
  • Performance optimizations: early exit, ThreadLocal reuse, lock-free data structures

3. hermes-processor - Compile-Time Code Generation

Annotation processor that generates logger base classes during compilation, eliminating runtime reflection overhead.

How it works:

  1. Detects @InjectLogger annotation during compilation
  2. Generates a base class (e.g., UserServiceHermesLogger) with protected Logger log field
  3. User class extends the generated base class to access the logger
  4. No runtime overhead—all code generation happens at compile time

4. hermes-spring-boot-starter - Spring Boot Integration

Auto-configuration module providing seamless Spring Boot integration with property binding and lifecycle management.

Features:

  • @EnableAutoConfiguration support
  • HermesProperties for YAML/properties binding
  • Actuator health indicator for monitoring
  • Automatic logger configuration from application.yml

5. hermes-kotlin - Idiomatic Kotlin Extensions

Kotlin DSL providing idiomatic syntax for Kotlin projects using extension functions and builders.

Features:

  • MyClass::class.logger - Extension property for logger creation
  • Lazy evaluation: log.info { "Expensive $computation" }
  • MDC DSL: withMDC("key" to "value") { ... }
  • Structured logging builders with type-safe key-value pairs

6. hermes-examples - Reference Implementations

Working examples demonstrating various Hermes features and integration patterns.

High-Performance Async Logging with LMAX Disruptor

Here's the thing: most async logging uses lock-based queues. They work okay for moderate loads, but when you're pushing millions of logs per second, those locks become a serious bottleneck. Thread contention, context switching—it all adds up.

Hermes uses the LMAX Disruptor instead. If you haven't heard of it, it's the same tech that powers financial trading systems where nanoseconds matter. It's lock-free, wait-free, and absurdly fast.

Async Logging Flow

The asynchronous logging process is designed to minimize application thread latency while maximizing throughput:

Performance Characteristics

Application Thread (Publisher)

  • Latency: ~100 nanoseconds (wait-free publish)
  • Non-blocking: Returns immediately after publishing to ring buffer
  • No locks: Uses CAS (Compare-And-Swap) operations

Background Thread (Consumer)

  • Batching: Processes multiple events together, amortizing overhead
  • Sequential I/O: Writes are coalesced for better disk throughput
  • Single consumer: Eliminates coordination overhead

Throughput Comparison

  • ArrayBlockingQueue (lock-based)

    • Throughput: ~1M logs/sec
    • Latency: ~5-10 µs
  • LMAX Disruptor (lock-free)

    • Throughput: 10M+ logs/sec
    • Latency: ~0.1 µs

Ring Buffer Mechanics

The ring buffer is pre-allocated with a fixed size (power of 2, typically 1024-8192 slots). This design provides several advantages:

  1. Memory locality: Contiguous memory improves CPU cache utilization
  2. No allocation: Slots are reused, eliminating GC pressure
  3. Fast modulo: Ring wrapping uses bitwise AND instead of expensive modulo operation
  4. Wait-free: Publishers never wait for consumers (unless buffer full)

Zero-Allocation Message Formatting

Every time traditional logging formats a message, it creates new String objects. One log message? No big deal. A million per second? Now you've got a garbage collection nightmare.

Hermes fixes this by reusing ThreadLocal StringBuilder instances. After the initial allocation, it's zero-copy formatting all the way down.

Performance Impact

Before (Traditional Approach)

// Creates 3 new String objects per call
log.info("User " + username + " logged in at " + timestamp);
  • Allocations: 3+ String objects, intermediate char arrays
  • GC pressure: Continuous young generation collections
  • Throughput impact: GC pauses affect latency

After (Hermes Zero-Allocation)

// Zero allocations using ThreadLocal StringBuilder reuse
log.info("User {} logged in at {}", username, timestamp);
  • Allocations: Zero (after first call per thread)
  • GC pressure: Minimal—only final String and LogEvent allocated
  • Throughput: No GC pauses from message formatting

Implementation Details

public class MessageFormatter {
    // ThreadLocal ensures each thread has its own StringBuilder
    private static final ThreadLocal<StringBuilder> BUFFER =
        ThreadLocal.withInitial(() -> new StringBuilder(256));

    public static String format(String pattern, Object... args) {
        StringBuilder sb = BUFFER.get();
        sb.setLength(0); // Clear but don't deallocate

        int argIndex = 0;
        int patternLength = pattern.length();

        for (int i = 0; i < patternLength; i++) {
            char c = pattern.charAt(i);
            if (c == '{' && i + 1 < patternLength && pattern.charAt(i + 1) == '}') {
                // Placeholder found
                if (argIndex < args.length) {
                    sb.append(args[argIndex++]);
                }
                i++; // Skip the '}'
            } else {
                sb.append(c);
            }
        }

        return sb.toString(); // Only allocation
    }
}

Spring Boot Integration

If you're using Spring Boot (and let's be honest, you probably are), Hermes plays nice right out of the box. No complex setup, no XML files to wrestle with—just add the starter and configure via YAML.

Configuration Example

# application.yml
hermes:
  # Root log level and package-specific levels
  level:
    root: INFO
    packages:
      io.github.dotbrains: DEBUG
      com.myapp.service: TRACE
      com.myapp.repository: DEBUG

  # Log output pattern (supports multiple format directives)
  pattern: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"

  # Async logging configuration
  async:
    enabled: true
    queue-size: 2048 # Ring buffer size (must be power of 2)

Pattern Format Directives

  • %d{pattern} - Timestamp (e.g., 2026-01-10 11:45:30.123)
  • %thread - Thread name (e.g., http-nio-8080-exec-1)
  • %level - Log level (e.g., INFO)
  • %-5level - Left-aligned level, 5 chars (e.g., INFO )
  • %logger{length} - Logger name, abbreviated (e.g., c.e.s.UserService)
  • %msg - Log message (e.g., User created successfully)
  • %n - Line separator (platform-specific)
  • %mdc{key} - MDC value (e.g., req-12345)
  • %exception - Exception stack trace (full stack trace)

Structured JSON Logging

If you're shipping logs to ELK, Splunk, Datadog, or any modern observability platform, you need JSON. Not because it's trendy, but because it's the only way to make your logs actually queryable and useful.

Hermes has built-in JSON formatting that's optimized for these platforms:

JSON Output Example

{
  "timestamp": "2026-01-10T04:58:51.123Z",
  "level": "INFO",
  "logger": "com.example.UserService",
  "thread": "http-nio-8080-exec-1",
  "threadId": "42",
  "message": "User john.doe created successfully",
  "mdc": {
    "requestId": "req-789",
    "userId": "12345",
    "sessionId": "sess-abc"
  }
}

Logstash Integration

Hermes includes a dedicated LogstashAppender for direct TCP streaming to Logstash:

LogstashAppender logstash = new LogstashAppender(
    "logstash",
    "logstash.example.com",  // Logstash host
    5000                     // TCP input port
);
logstash.start();

Logstash Configuration:

input {
  tcp {
    port => 5000
    codec => json
  }
}

filter {
  # Add additional fields, parse timestamps, etc.
  mutate {
    add_field => { "environment" => "production" }
  }
}

output {
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "hermes-logs-%{+YYYY.MM.dd}"
  }
}

Kotlin DSL

Writing Kotlin? You shouldn't have to suffer through a Java logging API. Hermes includes a dedicated Kotlin module with extension functions and DSL builders that feel natural in Kotlin code.

Kotlin Extensions Overview

import io.github.dotbrains.kotlin.*

class UserService {
    // Extension property for logger creation
    private val log = UserService::class.logger

    fun createUser(userId: String, username: String) {
        // Lazy evaluation - only computed if DEBUG enabled
        log.debug { "Creating user with expensive data: ${expensiveOperation()}" }

        // MDC with automatic cleanup
        withMDC("userId" to userId, "username" to username) {
            log.info { "Processing user creation" }
            // MDC is automatically cleared after block
        }

        // Structured logging DSL
        log.infoWith {
            "message" to "User created successfully"
            "userId" to userId
            "username" to username
            "timestamp" to System.currentTimeMillis()
            "environment" to System.getenv("ENV")
        }
    }

    // Exception logging with Kotlin syntax
    fun handleError(userId: String, error: Exception) {
        log.error(error) { "Failed to process user: $userId" }
    }
}

Lazy Evaluation Benefits

Kotlin's lambda syntax enables true lazy evaluation—expensive operations are only executed if the log level is enabled:

// ❌ BAD: expensiveOperation() always called, even if DEBUG disabled
log.debug("Result: ${expensiveOperation()}")

// ✅ GOOD: expensiveOperation() only called if DEBUG enabled
log.debug { "Result: ${expensiveOperation()}" }

GraalVM Native Image Support

Hermes includes GraalVM native-image metadata for ahead-of-time compilation to native executables, eliminating the need for manual reflection configuration.

Native Image Metadata

Hermes includes the following metadata in hermes-core/src/main/resources/META-INF/native-image/:

reflect-config.json - Reflection hints

[
  {
    "name": "io.github.dotbrains.core.HermesLoggerProvider",
    "allPublicMethods": true
  }
]

resource-config.json - Resource patterns

{
  "resources": {
    "includes": [
      { "pattern": "META-INF/services/.*" },
      { "pattern": "application.ya?ml" }
    ]
  }
}

Building Native Images

# Maven with GraalVM native plugin
mvn -Pnative native:compile

# Gradle with GraalVM native plugin
./gradlew nativeCompile

# Direct native-image invocation
native-image --no-fallback -jar myapp.jar

Performance Benchmarks and Metrics

Hermes is designed for high-performance scenarios. Here are the expected performance characteristics:

Latency Measurements

  • Disabled log statement: ~1 ns (level check only, early exit)
  • Enabled sync log (console): ~1-5 µs (message formatting + I/O)
  • Enabled async log (Disruptor): ~100 ns (wait-free publish to ring buffer)
  • JSON formatting: ~2-10 µs (object construction + serialization)
  • File write (buffered): ~5-20 µs (depends on I/O buffer size)

Throughput Comparison

  • Sync console logging: ~1M logs/sec (single thread, pattern layout)
  • Async file logging: ~10M+ logs/sec (multiple threads, Disruptor, buffered I/O)
  • JSON structured logging: ~500K logs/sec (single thread, JSON layout)

Memory Efficiency

Before (Traditional Logging)

  • String allocation per log message
  • Intermediate object creation during formatting
  • GC pressure from short-lived objects

After (Hermes Optimization)

  • ThreadLocal StringBuilder reuse (zero allocation after warmup)
  • Immutable LogEvent records (compact memory layout)
  • Pre-allocated ring buffer (no allocation during logging)

Use Cases and Deployment Scenarios

Hermes excels in various production scenarios where logging performance and developer experience matter:

High-Throughput Microservices

Scenario: REST API handling 10K+ requests/second with detailed request tracing.

Solution:

  • Async appender with Disruptor for non-blocking logging
  • MDC for request correlation (requestId, userId, sessionId)
  • JSON layout for structured logs sent to ELK stack

Benefits:

  • Application threads return immediately (sub-microsecond logging)
  • Request tracing across distributed services via MDC propagation
  • Efficient log querying in Elasticsearch using structured fields

Data Processing Pipelines

Scenario: Apache Kafka consumer processing millions of messages per hour with detailed progress tracking.

Solution:

  • Rolling file appender with size-based rotation
  • Zero-allocation message formatting to minimize GC pauses
  • Conditional debug logging for problematic message inspection

Benefits:

  • Minimal overhead on message processing throughput
  • Automatic log rotation prevents disk exhaustion
  • Debug logging enabled dynamically without restarts

Spring Boot Enterprise Applications

Scenario: Monolithic Spring Boot application with complex business logic requiring comprehensive audit logging.

Solution:

  • Spring Boot starter for auto-configuration
  • YAML-based configuration for environment-specific settings
  • Multiple appenders (console for dev, file + Logstash for production)
  • MDC populated via Spring interceptors for request tracking

Benefits:

  • Zero-boilerplate logger injection with @InjectLogger
  • Environment-specific configuration without code changes
  • Seamless integration with Spring Boot Actuator

Kotlin-Based Applications

Scenario: Modern Kotlin microservice requiring idiomatic logging syntax.

Solution:

  • Kotlin DSL with lazy evaluation and structured logging builders
  • Extension functions for logger creation
  • MDC DSL for contextual data

Benefits:

  • Lazy evaluation prevents unnecessary computation
  • Type-safe structured logging with DSL builders
  • Cleaner code with Kotlin-idiomatic syntax

Technical Highlights

What Sets Hermes Apart

1. Compile-Time Logger Injection

  • Zero runtime overhead through annotation processing
  • Type-safe generated base classes
  • GraalVM-friendly (no reflection configuration)

2. Lock-Free Async Processing

  • LMAX Disruptor for 10-100x better throughput than lock-based queues
  • Wait-free publish semantics
  • Single consumer pattern eliminates coordination overhead

3. Zero-Allocation Formatting

  • ThreadLocal StringBuilder reuse
  • Early exit for disabled log levels
  • Minimal GC pressure

4. Modern Java Features

  • Java 17 records for immutable, compact LogEvent
  • ServiceLoader pattern for decoupled architecture
  • Text blocks for readable configuration

5. Comprehensive Integration

  • Spring Boot auto-configuration
  • Kotlin DSL extensions
  • GraalVM Native Image metadata
  • Logstash direct streaming

How Does Hermes Stack Up?

You're probably wondering: "How does this compare to what I'm already using?" Fair question. Here's the honest breakdown:

FeatureSLF4J + LogbackLog4j2Hermes
Logger InjectionManual or LombokManual@InjectLogger annotation
ConfigurationXML/GroovyXML/JSON/YAMLYAML with defaults
Async LoggingVia appenders (lock-based)Lock-free (Disruptor)Built-in LMAX Disruptor
Message FormattingString concatenationThreadLocal reuseZero-allocation
Spring Boot IntegrationExternal startersExternal startersNative auto-config
Kotlin SupportJava API onlyJava API onlyDedicated Kotlin DSL
GraalVM Native ImageManual configurationManual configurationIncluded metadata
Java Version8+8+17+ (modern features)
JSON LoggingVia Jackson/GsonBuilt-inOptimized built-in

Getting Started

Ready to give it a spin? Here's how to get Hermes into your project.

Maven Setup

<dependencies>
    <!-- Core API -->
    <dependency>
        <groupId>io.github.dotbrains</groupId>
        <artifactId>hermes-api</artifactId>
        <version>1.0.0</version>
    </dependency>

    <!-- Annotation processor (for @InjectLogger) -->
    <dependency>
        <groupId>io.github.dotbrains</groupId>
        <artifactId>hermes-processor</artifactId>
        <version>1.0.0</version>
        <scope>provided</scope>
    </dependency>

    <!-- Core implementation -->
    <dependency>
        <groupId>io.github.dotbrains</groupId>
        <artifactId>hermes-core</artifactId>
        <version>1.0.0</version>
        <scope>runtime</scope>
    </dependency>
</dependencies>

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <annotationProcessorPaths>
                    <path>
                        <groupId>io.github.dotbrains</groupId>
                        <artifactId>hermes-processor</artifactId>
                        <version>1.0.0</version>
                    </path>
                </annotationProcessorPaths>
            </configuration>
        </plugin>
    </plugins>
</build>

Spring Boot Quick Start

1. Add the starter dependency:

<dependency>
    <groupId>io.github.dotbrains</groupId>
    <artifactId>hermes-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

2. Configure in application.yml:

hermes:
  level:
    root: INFO
    packages:
      com.example: DEBUG
  async:
    enabled: true
    queue-size: 2048

3. Use in your code:

@InjectLogger
@Service
public class UserService extends UserServiceHermesLogger {
    public void createUser(String username) {
        log.info("Creating user: {}", username);
    }
}

Tech Stack

Core Technologies:

  • Java 17 (Records, Text Blocks, Modern JVM)
  • LMAX Disruptor (Lock-free async processing)
  • ServiceLoader SPI (Decoupled architecture)
  • Annotation Processing (Compile-time code generation)

Integration & Frameworks:

  • Spring Boot (Auto-configuration)
  • Kotlin (DSL extensions)
  • Maven/Gradle (Build system)
  • GraalVM (Native image compilation)

Testing & Quality:

  • JUnit 5 (Unit testing)
  • Mockito (Mocking framework)
  • GitHub Actions (CI/CD)
  • Maven Central (Artifact distribution)

The Bottom Line

Hermes brings Java logging into the modern era. By leveraging Java 17 features, LMAX Disruptor, and compile-time code generation, it delivers the performance you need without the boilerplate you hate.

Whether you're building microservices that need to log millions of events per second, data pipelines processing Kafka streams, or enterprise Spring Boot apps, Hermes has your back. Fast, flexible, and actually enjoyable to use—that's the goal.

Want to learn more? Check out the official docs or dive into the GitHub repo. And yes, contributions are welcome!

If you liked this project.

You will love these as well.