Notes on logback plus slf4j

Russell Bateman
February 2025
last update:

Distilling logging using logback and slf4j down to its essence...

I have used the logback method of logging most over the years I've written Java in Tomcat applications, Apache NiFi custom processors and controllers, as well as other projects.

Except for going a little wild on pom.xml below and explaining console versus file logging, I have kept this to as small amount of code as possible in order not to confuse, but still work.

I'm far from being the brightest Crayola® out of the box. log4j always confused me. I have had better luck with logback. I never liked Java native logging. So, of all I have attempted to document about logging (I have other notes on Java Hot Chocolate), this is the only page of notes I feel sure of.

The most relevant lines in my examples below are highlighted. These are a whole, that is, a single project. Ensure the highlighted elements are in your own code with the greatest variability in your production and test code.

What's happening?

In Foo.bar() we make one TRACE and one DEBUG logging call. Playing around with the highlighted lines in logback-test.xml, we see how the logging behavior is changed. Meanwhile, pom.xml shows how to get the necessary software.*

project/pom.xml:

This ensures that we get logback-classic, but that will also ensure pulling in logback-core.jar and slf4j-api.jar.*

<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
                             http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.windofkeltia.tryit</groupId>
  <artifactId>tryit</artifactId>
  <version>1.0.0-SNAPSHOT</version>
  <name>tryit</name>
  <description>Try out some examples</description>

  <properties>
    <logback-version>1.2.10</logback-version>
    <maven-compiler-version>3.13.0</maven-compiler-version>
    <junit.version>4.13.2</junit.version>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>21</maven.compiler.source>
    <maven.compiler.target>21</maven.compiler.target>
  </properties>

  <dependencies>
    <dependency>
      <groupId>ch.qos.logback</groupId>
      <artifactId>logback-classic</artifactId>
      <version>${logback-version}</version>
    </dependency>
    <dependency>
    <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>${junit.version}</version>
    </dependency>
    <dependency>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-compiler-plugin</artifactId>
      <version>${maven-compiler.version}</version>
    </dependency>
  </dependencies>

  <build>
    <sourceDirectory>src</sourceDirectory>
    <testSourceDirectory>src/test</testSourceDirectory>
    <resources>
      <resource>
        <directory>src</directory>
        <excludes>
          <exclude>**/*.java</exclude>
        </excludes>
      </resource>
    </resources>
    <plugins>
      <plugin>
        <artifactId>maven-compiler-plugin</artifactId>
        <version>${maven-compiler.version}</version>
        <configuration>
          <source>21</source>
          <target>21</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
</project>
project/src/main/java/com/windofkeltia/observe_logging/Foo.java:

Here we make calls to place statements in the (on-screen) log as method bar() executes.

package com.windofkeltia.observe_logging;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class Foo
{
  private static final Logger logger = LoggerFactory.getLogger( Foo.class );

  public void bar()
  {
    logger.trace( "Calling bar()" );
    logger.debug( "Debugging bar()" );
  }
}
project/src/test/java/com/windofkeltia/observe_logging/FooTest.java:

The simplest JUnit test possible. It only creates an instance of class Foo, then calls its lone method, bar(), which will try to log statements.

package com.windofkeltia.observe_logging;

import org.junit.Test;

public class FooTest
{
  @Test
  public void test()
  {
    Foo foo = new Foo();
    foo.bar();
  }
}

Output from JUnit test when log level configured for TRACE...

100  [main] TRACE c.w.o.Foo.bar:16 - Calling bar()
101  [main] DEBUG c.w.o.Foo.bar:17 - Debugging bar()

project/src/test/resources/logback-test.xml:

We could do this to a file, but here we set up the console as if the log file. The highlighted lines (actually, the first one highlighted) is what determines the level (INFO, DEBUG, TRACE, etc.) that reaches the log. As configured here, only the DEBUG will go through and not the TRACE call.

<configuration>
  <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
      <pattern>
        %-4relative [%thread] %-5level %logger{5}.%method:%line - %message%n
      </pattern>
      <!-- %relative outputs the number of milliseconds elapsed since beginning
              of execution
          %logger{n] where {n} is the abbreviation (number) of or referring
              to a "scheme", see http://logback.qos.ch/manual/layouts.html
        -->
    </encoder>
  </appender>

  <logger name="com.windofkeltia" level="DEBUG" additivity="false">
    <appender-ref ref="CONSOLE" />
  </logger>

  <root level="INFO">
    <appender-ref ref="CONSOLE" />
  </root>
</configuration>
project/src/main/resources/logback.xml:

For your production version of the logback configuration file above, I suggest this name. Obviously, you would ship it to use level INFO or perhaps ERROR only. Your end users would, upon your suggestion, reach in and change that in consulation with you at dire times ( ☹ ).

Also obviously, you'd need to define a rolling file appender.

Don't be too confused by ${catalina.base}/logs/catalina.out. This is how you get your logger to use Tomcat's log file (/opt/tomcat/logs/catalina.out).

Last, you can merge these two configuration files which you might wish to do in the case of logback-test.xml if you want even your JUnit test logging to reach a log file. In that case, you will want to add to (the highlighted lines) both appender references,
<appender-ref ref="CONSOLE" /> and <appender-ref ref="FILE" />.

<configuration>
  <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
    <file>${catalina.base}/logs/catalina.out</file>
    <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
      <!--
        For daily rollover, use 'app_%d.log'.
        For hourly rollover, use 'app_%d{yyyy-MM-dd_HH}.log'.
        To GZIP rolled files, replace '.log' with '.log.gz'.
        To ZIP rolled files, replace '.log' with '.log.zip'.
      -->
      <fileNamePattern>
        ${org.apache.nifi.bootstrap.config.log.dir}/nifi-app_%d{yyyy-MM-dd_HH}.%i.log
      </fileNamePattern>
      <!-- keep up to 100Mb of log and 30 log files worth of history -->
      <maxFileSize>100MB</maxFileSize>
      <maxHistory>30</maxHistory>
    </rollingPolicy>
    <immediateFlush>true</immediateFlush>
    <append>true</append>
    <encoder class="ch.qos.logback.classic.encoder.PatternLayoutEncoder">
      <pattern>
        %-4relative [%thread] %-5level %logger{5}.%method:%line - %message%n
      </pattern>
    </encoder>
  </appender>

  <logger name="com.windofkeltia" level="DEBUG" additivity="false">
    <appender-ref ref="FILE" />
  </logger>

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

For other log file destinations, consider these samples:

Tomcat, but local IDE testing <file>${base.folder}/logs/project.log</file>
Tomcat <file>${catalina.base.folder}/logs/project.log</file>

Note(s)

* Sadly, you must not wildly dump a bunch of JAR references into the Maven dependencies. log4j, slf4j and logback JARs are not always inter-compatible. Sometimes, you can shut off logging completely by having the wrong set (usually too many, etc.).