Skip to content

Logging

Hermes Frontend, Consumers and Management modules use Logback as an implementation of logging. Any option exposed by Logback can be used to configure it.

Below our production configuration is described.

Pointing to logback.xml location

Simple way to use external logback.xml file is to pass its location using JVM flag:

-Dlogback.configurationFile=/etc/hermes/logback.xml

Example logback.xml file

Sample logback.xml file that will log to file using async appender:

<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <file>/var/log/hermes/hermes-frontend.log</file>
        <encoder>
            <Pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} %-5level %logger{36} - %msg%n</Pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.FixedWindowRollingPolicy">
            <fileNamePattern>/var/log/hermes/hermes-frontend-%i.log</fileNamePattern>
            <minIndex>1</minIndex>
            <maxIndex>4</maxIndex>
        </rollingPolicy>

        <triggeringPolicy class="ch.qos.logback.core.rolling.SizeBasedTriggeringPolicy">
            <maxFileSize>100MB</maxFileSize>
        </triggeringPolicy>
    </appender>

    <appender name="ASYNC_FILE" class="ch.qos.logback.classic.AsyncAppender">
        <appender-ref ref="FILE" />
    </appender>

    <logger name="org.apache.zookeeper" level="ERROR" />

    <!-- Selector spams ERROR level messages every 100ms on IOException -->
    <logger name="org.apache.kafka.common.network.Selector" level="OFF"/>

    <logger name="kafka" level="WARN"/>

    <root level="INFO">
        <appender-ref ref="ASYNC_FILE" />
    </root>
</configuration>

This configuration is tuned for Frontend, but Consumers and Management config is more or less the same.

Structured logging

We use structured logging in Hermes modules for easier log searchability, filtering, and analysis in production environments.

Location: pl.allegro.tech.hermes.common.logging.LoggingFields

Always use constants from LoggingFields to ensure consistency across all services.

Constant Field Name Description Example Value
TOPIC_NAME topic-name Qualified topic name "my.events.topic"
SUBSCRIPTION_NAME subscription-name Qualified subscription name "my.events.topic$my-subscription"

Example usage and used patterns

Direct addKeyValue usage (single log entry)

logger.atInfo()
    .addKeyValue(SUBSCRIPTION_NAME, subscription.getQualifiedName())
    .log("Creating subscription {}", subscription.getQualifiedName());

LoggingEventBuilder usage (multiple logs, same level)

LoggingEventBuilder subscriptionLogger = logger.atInfo()
    .addKeyValue(SUBSCRIPTION_NAME, subscription.getQualifiedName());
subscriptionLogger.log("First message {}", ...);
subscriptionLogger.log("Second message {}", ...);

LoggingEventBuilder usage (multiple logs, different levels)

LoggingEventBuilder infoLogger = logger.atInfo()
    .addKeyValue(TOPIC_NAME, topic.getQualifiedName());
LoggingEventBuilder errorLogger = logger.atError()
    .addKeyValue(TOPIC_NAME, topic.getQualifiedName());

LoggingContext/MDC usage (wrap method execution)

import static pl.allegro.tech.hermes.common.logging.LoggingContext.runWithLogging;

LoggingContext.runWithLogging(
    SUBSCRIPTION_NAME,
    subscription.getQualifiedName().getQualifiedName(),
    () -> {
        // All logs in this scope automatically have SUBSCRIPTION_NAME in MDC
    });

Wrapper/Delegate usage

public class LoggingConsumer implements Consumer {
    private final Consumer delegate;
    @Override
    public void consume(Runnable signalsInterrupt) {
        LoggingContext.runWithLogging(
            SUBSCRIPTION_NAME,
            delegate.getSubscription().getQualifiedName().getQualifiedName(),
            () -> delegate.consume(signalsInterrupt));
    }
}

Wrapper method usage (preserving git blame)

// New wrapper method
private void startWithLogging(Signal start) {
    Subscription subscription = getSubscriptionFromPayload(start);
    runWithLogging(SUBSCRIPTION_NAME, subscription.getQualifiedName().getQualifiedName(),
        () -> start(start, subscription));
}
// Original method kept intact - git blame preserved
private void start(Signal start, Subscription subscription) {
    // Original implementation unchanged
}