Logging
Core carries no logging framework, so the logging backend is the application’s choice.
The framework writes to one place, the servlet log, and only when something fails.
The embedded server logs through its own API, and the backend on the classpath decides where both end up.
A line per request is a separate matter, written by the application through requestLogger(…).
What the framework logs
AppServlet reports four failures to the servlet log, each with the exception that caused it:
| Message | When |
|---|---|
|
A handler, a filter, or a template threw, and no |
|
The status page registered for that status threw. |
|
Decoration or writing of the response failed. |
|
The lambda given to |
A request that succeeds produces no line.
Nothing is logged at startup, and there is no debug output to switch on.
The framework calls GenericServlet.log(String, Throwable), which is part of the servlet API, so core needs no logging dependency for it.
jte has no logging either: it reports a failure by throwing.
Where the servlet log goes
The container decides where the servlet log goes, so the logger name and the level differ by server:
| Server | Logging API | Logger | Level |
|---|---|---|---|
Jetty |
slf4j |
|
WARN |
Tomcat |
JULI, which is |
|
SEVERE |
Undertow |
JBoss Logging |
|
ERROR |
ROOT in Jetty’s logger name is the context at the path /.
The server’s own lines, such as the startup banner, go through the same API under the server’s package names.
Jetty brings slf4j-api and no backend.
Without a backend on the classpath, slf4j prints one warning at startup and discards every line, the framework’s four failures included.
An application on Jetty therefore adds one of the backends below.
Logback
Logback implements the slf4j API directly, so one dependency is the whole setup:
dependencies {
implementation 'org.slf4j:slf4j-api:2.0.19'
runtimeOnly 'ch.qos.logback:logback-classic:1.6.4'
}
Logback reads logback.xml from the classpath root, which is src/main/resources/logback.xml in the build:
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} %-5level [%thread] %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="org.eclipse.jetty" level="INFO"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
The example application is set up this way.
A logback-test.xml on the test classpath takes precedence over logback.xml, so the tests can run at a quieter level.
Log4j
Log4j takes the slf4j calls through its binding for slf4j 2:
dependencies {
implementation 'org.slf4j:slf4j-api:2.0.19'
runtimeOnly 'org.apache.logging.log4j:log4j-slf4j2-impl:2.26.1'
}
The binding brings log4j-api and log4j-core with it.
Log4j reads log4j2.xml from the classpath root:
<Configuration>
<Appenders>
<Console name="CONSOLE" target="SYSTEM_OUT">
<PatternLayout pattern="%d{HH:mm:ss.SSS} %-5level [%t] %logger{36} - %msg%n"/>
</Console>
</Appenders>
<Loggers>
<Logger name="org.eclipse.jetty" level="info"/>
<Root level="info">
<AppenderRef ref="CONSOLE"/>
</Root>
</Loggers>
</Configuration>
Without a configuration file, Log4j prints errors only, so Jetty’s WARN line for a failed request does not appear.
An application that prefers the Log4j API to the slf4j API calls LogManager.getLogger(…) and declares log4j-api in place of slf4j-api.
The binding stays, because Jetty still logs through slf4j.
JDK logging
slf4j-jdk14 hands the slf4j calls to java.util.logging, which needs no library beyond the JDK:
dependencies {
implementation 'org.slf4j:slf4j-api:2.0.19'
runtimeOnly 'org.slf4j:slf4j-jdk14:2.0.19'
}
Without a configuration, java.util.logging prints INFO and above to standard error in a two-line format.
A properties file replaces that default:
handlers = java.util.logging.ConsoleHandler
.level = INFO
java.util.logging.ConsoleHandler.formatter = java.util.logging.SimpleFormatter
java.util.logging.SimpleFormatter.format = %1$tT.%1$tL %4$-7s %3$s - %5$s%6$s%n
org.eclipse.jetty.level = INFO
The JVM reads the file named by a system property:
java -Djava.util.logging.config.file=logging.properties -jar app.jar
The levels are those of java.util.logging, so slf4j’s WARN appears as WARNING and its ERROR as SEVERE.
An application that wants no slf4j call in its own code logs through System.getLogger(…) or java.util.logging.Logger, and both arrive at the same handlers.
Native image
All three backends run in a GraalVM native image, and each needs its configuration file among the included resources. A binary carries only the classpath resources the build names, so a configuration file left out is a file the backend does not find.
graalvmNative {
binaries {
main {
resources.includedPatterns.add('logback\\.xml') // or 'log4j2\\.xml', or 'logging\\.properties'
}
}
}
Logback and Log4j then read the file as they do on a JVM. The reflection Logback needs comes from GraalVM’s metadata repository, which the spider-silk plugin enables. Log4j needed no configuration beyond the resource pattern.
JDK logging differs in how the file is named.
A native binary does not read java.util.logging.config.file when it is given on the command line, so the default format stays.
Read the file from the classpath at the start of main instead:
public static void main(String[] args) throws IOException {
try (InputStream in = FlashcardApp.class.getResourceAsStream("/logging.properties")) {
LogManager.getLogManager().readConfiguration(in);
}
// ...
}
The same lines work on a JVM, so one way of loading the file serves both.
The backend changes the size of the binary. The example application was built with each backend by GraalVM CE 25, and each binary was run and answered requests with its log lines in the configured format:
| Backend | Included resource | Binary |
|---|---|---|
Logback 1.6.4 |
|
65.0MiB |
Log4j 2.26.1 |
|
68.1MiB |
JDK logging, through |
|
63.0MiB |
The example application ships the Logback row.
Tomcat
Tomcat logs through java.util.logging whichever backend the application chose.
With JDK logging as the backend, Tomcat’s lines and the application’s share one configuration, and nothing more is needed.
With Logback or Log4j, Tomcat’s lines bypass the backend and appear on standard error in the JDK’s default format.
jul-to-slf4j routes them into slf4j, and from there into the backend:
dependencies {
implementation 'org.slf4j:jul-to-slf4j:2.0.19'
}
Install the bridge before the server starts:
SLF4JBridgeHandler.removeHandlersForRootLogger();
SLF4JBridgeHandler.install();
app.server((a, port) -> new TomcatServer(a).port(port))
.start(8080);
removeHandlersForRootLogger() takes the JDK’s console handler away, so a line is not printed twice.
The servlet log then arrives at ERROR under org.apache.catalina.core.ContainerBase.[Tomcat].[localhost].[/].
Do not combine jul-to-slf4j with slf4j-jdk14: each hands its lines to the other, and a line never stops travelling.