Matt Gale

08 Sep 2023

Application and Webserver Logging in Spring Boot 3.1

What you want:

  1. Application logs for Spring Boot 3.1 (using the default Logback via SLF4J)
  2. Webserver access logs (using the default embedded Tomcat 10)
  3. All logs going to the same sink (locally, that’s STDOUT)
  4. A very optional explanation of how logging in Spring Boot works and why we have hoops to jump through.

The minimal configuration you need:

  1. dependencies
  2. a configuration file
  3. a bean

Dependencies

In your pom.xml:

<dependencies>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-access</artifactId>
        <version>1.4.11</version>
    </dependency>
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-core</artifactId>
        <version>1.4.11</version>
    </dependency>
</dependencies>

A Configuration File

In src/main/resources/ create a logback-access.xml.

This example is very close to the default pattern from Spring Boot:

<configuration>
	<appender name="STDOUT" class="ch.qos.logback.core.ConsoleAppender">
		<encoder class="ch.qos.logback.access.PatternLayoutEncoder">
			<pattern>%t{yyyy-MM-dd'T'HH:mm:ss.SSSXXX}  INFO %-5.5(0) --- [%15.15I] %-40.40(org.apache.tomcat) : %requestURL %statusCode</pattern>
		</encoder>
	</appender>

	<appender-ref ref="STDOUT" />
</configuration>

A Bean

@Configuration
public class AppConfig {

    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory() {
        TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
        LogbackValve logbackValve = new LogbackValve();
        logbackValve.setFilename("logback-access.xml");
        tomcatServletWebServerFactory.addContextValves(logbackValve);
        return tomcatServletWebServerFactory;
    }
}

With this together, Tomcat access logs will log alongside your application.

What even is all of this? An explanation of how logging in Spring Boot works

Logging frameworks

To refresh, java has a TON of logging frameworks. To name a few popular ones:

  • java.util.logging (aka JUL)
  • Log4j
  • Logback
  • Log4j2

Which one you pick is up to you, what’s important here is how Spring Boot detects what you want and uses it.

SLF4J (Simple Logging Facade) is a popular abstraction library over the most used logging frameworks- it allows you (or in this case, Spring) to interact with a facade instead of the underlying logging library, so you can pick whichever logging library appeals to you at no additional development effort.

However: Log4j2 has diverged from the contract SLF4J expects, and without an adapter SLF4J can’t service Log4j2.

A small detail to mention: the authors of SLF4J are also the authors of Logback, and so the usage of them together is often assumed, unless otherwise stated.

Logging in Spring

To make configuration easier, Spring has created a library called spring-jcl (Jakarta Commons Logging) that searches your classpath and attempts to automatically configure your logging based on what it finds. spring-jcl has a preferred order to which logging frameworks it will select depending on what it finds, but the Spring Boot starter is set up to use Logback via SLF4J. You can use any logger you’d like though, you just have to configure it.

If you use lombok, you can use its logging annotations to choose which logger you’d like use in any given class. For example, in the default config for Spring Boot, you can use @Slf4j to use Logback. If you’re configured for Log4j via SLF4J, you could use @Slf4j or use @Log4j if you’d like to use the bare logger.

As an aside, recall that because SLF4J and Logback are often synonymous, there’s no need for a @Logback helper from lombok- @Slf4j would usually use Logback anyway!

Logging in Tomcat

Because we embed Tomcat in our applications now, it’s easy to forget that Tomcat is a web server with a servlet container, so it’s actions and concerns are very different than our application code.

When you visit any URL path under the spring property server.servlet.context-path, Tomcat will forward that request to your application container (otherwise returning a Tomcat default 404 page).

Tomcat’s access logs, when enabled, by default go to a file. So to take control over those logs to send them where we want them, we need to be able to configure Tomcat. During the embedded Tomcat creation, you can pass it a logging pipeline (a series of org.apache.catalina.Valve s) to handle your logs, and in our case, direct the flow of logs to an destination/appender we can configure and control.

Sticking with the default Logback we have in Spring Boot, a project exists from the Logback team called logback-access that comes with a valve (ch.qos.logback.access.tomcat.LogbackValve) we can use to direct the log stream. A quirk to be aware of is that while logback-access might have many classnames identical to ones in logback-core, their behavior is different and they are not interchangeable: logback-access is more HTTP sepecific and more restrictive. The documentation for logback-access is here.

As mentioned in bean configuration, we have to tell Tomcat which Logback configuration to use. Remember that Tomcat is running as its own server with its own `ClassLoader- so it doesn’t share the singletons created by the logging frameworks that our Spring Boot app has. Tomcat has to instantiate its own instance of the logger and push logs through that.

Why use Logback directly in Tomcat? Shouldn’t we use SLF4J?

It seems you can do this, though I don’t have the patience to try it, so here we enter conjecture territory:

It may be possible to use the jul-to-slf4j bridge. The motivation behind this project is that you have a dependency that is using JUL that you can’t migrate. The bridge gives you a handler that you configure/install so JUL sends its logs to the bridge to then send logs to SLF4J. Tomcat however is running in a different ClassLoader context- so you can’t make this change in Spring Boot and expect Tomcat to just inherit it. You’d have to figure out how to provide this configuration to Tomcat to have it configure the handler itself. My thinking here is that the point of SLF4J is to allow us to find a drop-in replacement at runtime for whichever logging framework we want. Tomcat doesn’t work the same way that Spring does- it’s not meant to support whatever you want at runtime. Therefore, give it the appropriate Valve for the logging framework you want, and you’re off to the races.

Lastly, it’s my opinion that we shouldn’t work too hard to have access logs show up identically alongside our application logs. In the production cases I’ve been involved with, during log collection, logs from different sources are collected and filtered differently. Most of the time, we filter away Tomcat access logs because they aren’t relevant to our troubleshooting, except in very specfic circumstances. We tend to look at access logs for specific reasons and specific times, so having a turely unified view of all logs isn’t what we truely want.