Logback is a Java logging library that writes our log messages to the console, to files and to other outputs. Our code does not call Logback itself. It calls the SLF4J API, and Logback writes the messages.
We use Logback to see what our app does at runtime, such as which member borrowed a book or why a return failed, and to keep those messages in log files.
The following example sets up Logback with one Maven dependency, one logback.xml file and a logger in the LoanService class of a small library app.
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.6.5</version>
</dependency>
<configuration>
<appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender">
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
</configuration>
private static final Logger log = LoggerFactory.getLogger(LoanService.class);
log.debug("Checking book '{}'", book); // not printed, DEBUG is below INFO
log.info("Book '{}' borrowed by {}", book, member); // printed
log.warn("Book '{}' is already borrowed", book); // printed
log.error("Cannot return '{}': no loan found", book); // printed
11:53:31.407 [main] INFO c.h.demo.logback.library.LoanService - Book 'Dune' borrowed by Lokesh
11:53:31.408 [main] WARN c.h.demo.logback.library.LoanService - Book 'Dune' is already borrowed
11:53:31.408 [main] ERROR c.h.demo.logback.library.LoanService - Cannot return 'Emma': no loan found
The Logger and LoggerFactory classes come from the org.slf4j package. Notice that the DEBUG line is missing because the root logger’s level is INFO, so Logback prints only INFO, WARN and ERROR messages.
Next, we look at how Logback finds its configuration and how log levels work, and we write log files that roll over by date and size. Then we add a user name to every line with MDC, make logging asynchronous, switch settings per environment, use Logback in Spring Boot and fix the most common errors.
1. What Is Logback?
Many Java projects use Logback as the library that does the logging behind SLF4J, and Spring Boot uses it by default, too.
SLF4J (Simple Logging Facade for Java) is only a set of interfaces. Because our code depends only on this facade, we can change the logging library later without changing our code. We can also set up SLF4J with other logging libraries, such as Log4j 2.
A Logback setup uses three jar files. We add only logback-classic, and Maven downloads the other two jars as transitive dependencies.
| Jar | What it contains |
|---|---|
| logback-classic | The Logback Logger that implements the SLF4J API, and the level and pattern features |
| logback-core | Appenders, encoders and the XML configuration reader, shared by all Logback modules |
| slf4j-api | The interfaces our code calls. logback-classic 1.6.5 depends on slf4j-api 2.0.19 |
To use the newest SLF4J release, we can also declare slf4j-api 2.0.20 in the pom.xml.
Logback 1.6.x is the current stable series and needs Java 11 or later. The Logback team now calls the 1.5.x series legacy, and the 1.2.x to 1.4.x series are end-of-life.
1.1. Logger, Appender and Encoder
Three terms appear in every Logback file.
- A logger is the object our code calls, such as log.info().
- An appender is an output, such as the console or a file.
- An encoder turns a log event into text, using a pattern.
One log call goes through the level check first and then through the appenders.

1.2. Placeholders Instead of String Concatenation
We pass values to a log call with {} placeholders because Logback fills the placeholders only when it prints the line. With the + operator, Java builds the string every time, even for a DEBUG line that Logback skips.
log.debug("Checking book " + book); // builds the string even when DEBUG is off
log.debug("Checking book '{}'", book); // builds nothing when DEBUG is off
log.error("Cannot return '{}'", book, ex); // last argument is an exception: the stack trace is printed
2. How Logback Finds Its Configuration
At startup, Logback searches for a configuration in a fixed order. Logback uses the first configuration it finds and ignores the rest.
- The file named in the system property logback.configurationFile, for example -Dlogback.configurationFile=config/logback.xml. The value can be a classpath resource, a file path or a URL.
- The file logback-test.xml on the classpath.
- The file logback.xml on the classpath.
- When no file is found, Logback uses its built-in default configuration.
In a Maven project, we put logback.xml in src/main/resources and, if we want different settings for tests, logback-test.xml in src/test/resources. Maven then puts the test file on the classpath only when tests run and leaves it out of the jar.
Without any file, Logback logs everything at DEBUG level to the console, as in the output of our example when no logback.xml is on the classpath.
11:53:31.904 [main] INFO com.howtodoinjava.demo.logback.LogbackDemo -- Library opened
11:53:31.907 [main] DEBUG com.howtodoinjava.demo.logback.library.CatalogService -- Searching for 'dune'
11:53:31.938 [main] INFO com.howtodoinjava.demo.logback.library.LoanService -- Book 'Dune' borrowed by Lokesh
11:53:31.938 [main] WARN com.howtodoinjava.demo.logback.library.LoanService -- Book 'Dune' is already borrowed
The built-in default output has full class names and the — separator. When our output looks like this, Logback did not find our file, and the usual causes are a typo in the file name or a file in the wrong folder.
2.1. Checking What Logback Loaded
Logback records its own startup messages as status messages and prints them whenever the file has an error or a warning. With debug=”true” on the configuration element, Logback prints them on every start.
<configuration debug="true">
...
<root level="INFO">
<appender-ref ref="CONSOL"/> <!-- typo: no appender named CONSOL -->
</root>
</configuration>
11:52:42,585 |-INFO in ch.qos.logback.classic.LoggerContext[default] - Found resource [logback-examples/broken.xml] at [file:/tmp/logback-tutorial-build/target/classes/logback-examples/broken.xml]
11:52:42,930 |-WARN in ch.qos.logback.core.model.processor.AppenderModelHandler - Appender named [CONSOLE] not referenced. Skipping further processing.
11:52:42,942 |-WARN in ch.qos.logback.core.model.processor.AppenderRefModelHandler - Appender named [CONSOL] could not be found. Skipping attachment to Logger[ROOT].
11:52:42,969 |-WARN in Logger[com.howtodoinjava.demo.logback.LogbackDemo] - No appenders present in context [default] for logger [com.howtodoinjava.demo.logback.LogbackDemo].
The Found resource line shows which file Logback loaded, and the WARN lines point to the typo. The application keeps running, but it logs nothing because the root logger has no appender.
3. Log Levels and the Logger Hierarchy
Every log message has a log level that shows how important it is, and every logger has a minimum level that it prints. A logger prints a message only when the message level is the same as or higher than the logger’s level. Logback has five levels, from lowest to highest.
| Level | Typical use |
|---|---|
| TRACE | Very detailed steps, for example each loop pass |
| DEBUG | Values that help while developing |
| INFO | Normal events, for example “book borrowed” |
| WARN | Something unexpected that the application handled |
| ERROR | An operation that failed |
Two more values exist only for configuration. ALL turns on every level, and OFF turns a logger off.
3.1. Setting a Level per Package or Class
Logger names follow the package names, so the loggers form a tree in which, for example, com.howtodoinjava.demo.logback.library is the parent of com.howtodoinjava.demo.logback.library.LoanService.
The root logger is the parent of all loggers. A logger without its own level inherits the level of its nearest parent that has one.
We set levels with logger elements. In the next file, every class in the library package logs DEBUG, except CatalogService, which logs only WARN and above.
<logger name="com.howtodoinjava.demo.logback.library" level="DEBUG"/>
<logger name="com.howtodoinjava.demo.logback.library.CatalogService" level="WARN"/>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
INFO c.h.demo.logback.LogbackDemo - Library opened
DEBUG c.h.demo.logback.library.LoanService - Checking book 'Dune'
INFO c.h.demo.logback.library.LoanService - Book 'Dune' borrowed by Lokesh
WARN c.h.demo.logback.library.LoanService - Book 'Dune' is already borrowed
ERROR c.h.demo.logback.library.LoanService - Cannot return 'Emma': no loan found
LoanService now prints DEBUG lines, whereas the INFO line Found 1 books from CatalogService is gone. LogbackDemo is outside the library package, so it inherits the root level INFO.
3.2. Additivity and Duplicate Log Lines
A log event first goes to the appenders of its own logger, and then Logback also sends it to the appenders of every parent logger, up to root. This behavior is called additivity, and it is on by default, so attaching the same appender to a logger and to root is a common mistake.
<logger name="com.howtodoinjava.demo.logback.library" level="DEBUG">
<appender-ref ref="CONSOLE"/>
</logger>
<root level="INFO">
<appender-ref ref="CONSOLE"/>
</root>
INFO c.h.demo.logback.LogbackDemo - Library opened
DEBUG c.h.demo.logback.library.LoanService - Checking book 'Dune'
DEBUG c.h.demo.logback.library.LoanService - Checking book 'Dune'
INFO c.h.demo.logback.library.LoanService - Book 'Dune' borrowed by Lokesh
INFO c.h.demo.logback.library.LoanService - Book 'Dune' borrowed by Lokesh
Each LoanService line appears twice, because one copy comes from the library logger and one from root. LogbackDemo is not in the library package, so its line appears once.
The root level INFO does not stop the DEBUG line either. Only the level of the logger where the event starts counts, and the level of a parent logger does not filter the event again.

The usual fix is to send the library events to their own file and set additivity=”false”.
<appender name="LOANS_FILE" class="ch.qos.logback.core.FileAppender">
<file>logs/loans.log</file>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
<logger name="com.howtodoinjava.demo.logback.library" level="DEBUG" additivity="false">
<appender-ref ref="LOANS_FILE"/>
</logger>
INFO c.h.demo.logback.LogbackDemo - Library opened
11:53:35.451 [main] DEBUG c.h.d.logback.library.CatalogService - Searching for 'dune'
11:53:35.461 [main] DEBUG c.h.demo.logback.library.LoanService - Checking book 'Dune'
11:53:35.461 [main] INFO c.h.demo.logback.library.LoanService - Book 'Dune' borrowed by Lokesh
11:53:35.467 [main] ERROR c.h.demo.logback.library.LoanService - Cannot return 'Emma': no loan found
4. Appenders for the Console and Log Files
An appender decides where a line goes. One logger can have several appenders, and several loggers can share one appender. Logback includes appenders for the console, files, sockets, syslog and more, but three of them cover most apps.
| Appender | Class | Writes to |
|---|---|---|
| Console | ch.qos.logback.core.ConsoleAppender | System.out, or System.err with target set to System.err |
| File | ch.qos.logback.core.FileAppender | One file that keeps growing |
| Rolling file | ch.qos.logback.core.rolling.RollingFileAppender | One active file plus archives, by date and size |
We saw the ConsoleAppender in the intro and the FileAppender in section 3.2. The Logback console appender can also print colored output.
4.1. RollingFileAppender With SizeAndTimeBasedRollingPolicy
A single log file keeps growing until the disk is full. Rolling (or rollover) means that Logback closes the current file, renames it into an archive and starts a new file. The SizeAndTimeBasedRollingPolicy rolls over when the date changes and when the file reaches a size limit.
<appender name="ROLLING" class="ch.qos.logback.core.rolling.RollingFileAppender">
<file>logs/library.log</file>
<rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
<!-- a new archive every day, and whenever the file reaches maxFileSize -->
<fileNamePattern>logs/library-%d{yyyy-MM-dd}.%i.log.gz</fileNamePattern>
<maxFileSize>1KB</maxFileSize> <!-- use 10MB or more in real applications -->
<maxHistory>30</maxHistory> <!-- keep 30 days of archives -->
<totalSizeCap>1GB</totalSizeCap> <!-- delete the oldest archives above 1GB -->
</rollingPolicy>
<encoder>
<pattern>%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n</pattern>
</encoder>
</appender>
Each setting of the rolling policy controls one part of the rollover.
| Setting | Meaning |
|---|---|
| file | The active log file. The application always writes to this file |
| fileNamePattern | Archive names. %d{yyyy-MM-dd} starts a new archive each day. %i numbers the archives within one day. The .gz ending makes Logback compress each archive |
| maxFileSize | Roll over when the active file reaches this size |
| maxHistory | Keep this many days of archives, and delete older ones |
| totalSizeCap | Delete the oldest archives when all archives together pass this size |
We set maxFileSize to 1KB to see the rollover after a few lines, and with 40 borrowed books, our run left four compressed archives and one active file.
library-2026-10-04.0.log.gz
library-2026-10-04.1.log.gz
library-2026-10-04.2.log.gz
library-2026-10-04.3.log.gz
library.log
Logback compresses archives in a background thread, so the application does not wait for the compression. A RollingFileAppender can also roll over by time only, with a TimeBasedRollingPolicy.
Many examples define a property such as LOG_ROOT for the log folder and reuse it in file and fileNamePattern. We write the folder name in the path itself, as logs/library.log, and both styles produce the same files.
5. Patterns and MDC
The pattern in an encoder is a template for one line, built from conversion words (placeholders that start with %, such as %msg for the message). Our intro pattern uses six conversion words.
| Conversion word | Prints | Example |
|---|---|---|
| %d{HH:mm:ss.SSS} | Time in the given format | 11:53:31.407 |
| %thread | Name of the thread that logged | main |
| %-5level | Level, padded to 5 characters | INFO |
| %logger{36} | Logger name, shortened to about 36 characters | c.h.demo.logback.library.LoanService |
| %msg | The message with the placeholders filled | Book ‘Dune’ borrowed by Lokesh |
| %n | Line break |
Logback has many more conversion words, and other layouts, such as the Logback HTMLLayout, write HTML or XML instead of plain text.
5.1. Adding a User Name to Every Line With MDC
In a web application, many users send requests at the same time, so the log lines of all users get mixed in the log file and we cannot tell which line belongs to which user.
MDC (Mapped Diagnostic Context) is a small map of values per thread, and every log line from that thread can print those values. We put a value at the start of a request and remove it at the end.
MDC.put("member", "Lokesh");
try {
loans.borrow("Lokesh", "Dune"); // every line from this thread now has member=Lokesh
} finally {
MDC.remove("member"); // threads are reused, so always clean up
}
The %X{member} conversion word prints the value, and after :-, we can give a default for lines without the value.
<pattern>%d{HH:mm:ss.SSS} %-5level [%X{member:-guest}] %logger{20} - %msg%n</pattern>
11:53:38.055 INFO [guest] c.h.d.l.LogbackDemo - Library opened
11:53:38.062 INFO [Lokesh] c.h.d.l.l.LoanService - Book 'Dune' borrowed by Lokesh
11:53:38.062 WARN [Lokesh] c.h.d.l.l.LoanService - Book 'Dune' is already borrowed
11:53:38.062 ERROR [Lokesh] c.h.d.l.l.LoanService - Cannot return 'Emma': no loan found
Our code logged the first line before MDC.put(), so the line shows the default guest. With %logger{20}, Logback shortens the package names to their first letters.
In a web app, we set the MDC value in a servlet filter, so every request gets it. To hide values such as card numbers in the output, we can mask sensitive data with Logback.
6. AsyncAppender for Faster Logging
A file appender writes on the thread that calls log.info(), so a slow disk slows down our request threads. The AsyncAppender puts each event into a queue in memory and returns at once, and a background thread takes the events from the queue and passes them to the real appender.
<appender name="ASYNC" class="ch.qos.logback.classic.AsyncAppender">
<queueSize>1024</queueSize> <!-- default 256 -->
<discardingThreshold>0</discardingThreshold> <!-- 0 = never drop INFO and lower -->
<neverBlock>false</neverBlock> <!-- wait when the queue is full -->
<appender-ref ref="FILE"/>
</appender>
<root level="INFO">
<appender-ref ref="ASYNC"/>
</root>
Before we use AsyncAppender in production, we check three defaults.
- By default, when the queue is 80% full, AsyncAppender drops TRACE, DEBUG and INFO events. A discardingThreshold of 0 keeps every event.
- With neverBlock set to false, a full queue makes the logging thread wait. With true, AsyncAppender drops the event and never blocks.
- The caller data, such as the line number for %line, is not copied by default. Set includeCallerData to true when the pattern needs it.
Events still in the queue are lost if the JVM stops before the queue is empty. So we stop the Logback context when the application ends, which empties the queue and closes all appenders.
((LoggerContext) LoggerFactory.getILoggerFactory()).stop();
Spring Boot stops the context for us when the application shuts down. In a standalone application, a shutdownHook element in logback.xml stops the context when the JVM exits.
7. Different Settings per Environment
We often want DEBUG lines on a laptop and only warnings in production, and Logback can switch parts of one file with a condition element and an if/then/else block. The condition element needs Logback 1.5.20 or later.
The next file checks the property APP_ENV. Logback looks for the property first in the file and in the system properties, and after that in the OS environment variables.
<condition class="ch.qos.logback.core.boolex.PropertyEqualityCondition">
<key>APP_ENV</key>
<value>prod</value>
</condition>
<if>
<then>
<root level="WARN">
<appender-ref ref="CONSOLE"/>
</root>
</then>
<else>
<root level="DEBUG">
<appender-ref ref="CONSOLE"/>
</root>
</else>
</if>
WARN c.h.demo.logback.library.LoanService - Book 'Dune' is already borrowed
ERROR c.h.demo.logback.library.LoanService - Cannot return 'Emma': no loan found
Without APP_ENV, the same file prints all DEBUG lines. For more complex rules, the ExpressionPropertyCondition class accepts expressions such as isDefined(“APP_ENV”) && propertyEquals(“APP_ENV”, “prod”). Logback has more condition classes for conditional configuration.
Older tutorials write the condition as Java code, as in if condition=’property(“APP_ENV”).equals(“prod”)’, with the Janino library. Logback 1.5.37 removed that form for security reasons, so an old file with that form now behaves as described in section 9.3.
A second option is a separate file per environment, whose name we pass with -Dlogback.configurationFile, as described in section 2.
8. Logback in Spring Boot
Spring Boot uses Logback by default because every Spring Boot starter adds it through the spring-boot-starter-logging dependency. Spring Boot 4.1.1 uses Logback 1.5.38 and SLF4J 2.0.18.
For simple needs, we do not write any XML, because we can set levels, files and patterns with the logging properties in application.properties.
When we need XML, we name the file logback-spring.xml instead of logback.xml, so Spring Boot loads the file itself after the Spring environment is ready. The name logback-spring.xml lets us use two extra tags.
- The springProfile tag turns parts of the file on or off by Spring profile.
- The springProperty tag reads a value from application.properties.
<configuration>
<include resource="org/springframework/boot/logging/logback/base.xml"/>
<springProfile name="prod">
<logger name="com.howtodoinjava" level="WARN"/>
</springProfile>
<springProfile name="!prod">
<logger name="com.howtodoinjava" level="DEBUG"/>
</springProfile>
</configuration>
The base.xml include keeps Spring Boot’s default console and file output. A complete Spring Boot setup for profile-specific logging uses the same tags.
9. Logback FAQs
9.1. Why Does SLF4J Log Nothing After I Add the Dependency?
SLF4J logs nothing because it has no provider (that is, the library that does the real logging, such as Logback). With only slf4j-api on the classpath, SLF4J prints a No SLF4J providers warning and drops every log line.
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Defaulting to no-operation (NOP) logger implementation
SLF4J(W): See https://www.slf4j.org/codes.html#noProviders for further details.
The same warning appears with an old logback-classic 1.2.x and SLF4J 2.x, because SLF4J 2.x ignores old 1.7-style bindings.
SLF4J(W): No SLF4J providers were found.
SLF4J(W): Class path contains SLF4J bindings targeting slf4j-api versions 1.7.x or earlier.
SLF4J(W): Ignoring binding found at [jar:file:/root/.m2/repository/ch/qos/logback/logback-classic/1.2.13/logback-classic-1.2.13.jar!/org/slf4j/impl/StaticLoggerBinder.class]
The fix in both cases is logback-classic 1.6.5, or any version from 1.3 on, and mvn dependency:tree shows which versions Maven picked.
9.2. What If Two Logging Libraries Are on the Classpath?
SLF4J picks one provider and prints a warning. This happens when a library brings its own SLF4J provider, such as slf4j-simple or log4j-slf4j2-impl.
SLF4J(W): Class path contains multiple SLF4J providers.
SLF4J(W): Found provider [ch.qos.logback.classic.spi.LogbackServiceProvider@7530d0a]
SLF4J(W): Found provider [org.slf4j.simple.SimpleServiceProvider@27bc2616]
SLF4J(W): See https://www.slf4j.org/codes.html#multiple_bindings for an explanation.
SLF4J(I): Actual provider is of type [ch.qos.logback.classic.spi.LogbackServiceProvider@7530d0a]
We keep one provider and remove the other with a Maven exclusion, because SLF4J is only the facade and needs one logging library behind it.
9.3. Why Does an Old logback.xml With if Conditions Print Nothing?
Since 1.5.37, Logback skips both the then and the else part of an if condition=’…’ block, although this form worked up to Logback 1.5.36. When the root appenders are inside the block, the application prints nothing at all, and the status messages show the reason.
11:52:48,771 |-WARN in ch.qos.logback.core.model.processor.conditional.IfModelHandler - The 'condition' attribute in <if> element is deprecated and slated for removal. Use <condition> element instead.
11:52:48,786 |-WARN in ch.qos.logback.core.model.processor.AppenderModelHandler - Appender named [CONSOLE] not referenced. Skipping further processing.
We rewrite the block with a condition element, as in section 7. Spring Boot 4.1.1 uses Logback 1.5.38, so Spring Boot applications are affected too.
9.4. What Does a Parsing Error in logback.xml Look Like?
Logback prints an XML_PARSING error, because the file must be well-formed XML and a missing closing tag stops the whole configuration.
12:08:01,715 |-ERROR in ch.qos.logback.core.joran.event.SaxEventRecorder@3cd1f1c8 - XML_PARSING - Parsing fatal error on line 2 and column 1
The line and column numbers show where the XML parser stopped reading, which is often one line after the real mistake.
9.5. Can Logback Reload logback.xml Without a Restart?
Yes. With scan=”true”, Logback checks the file for changes and reloads it when it changes. By default, Logback checks once per minute, and the scanPeriod attribute changes the interval.
<configuration scan="true" scanPeriod="30 seconds">
...
</configuration>
If the changed file has an XML error, Logback keeps using the last working configuration.
10. Conclusion
A working Logback setup needs logback-classic and one logback.xml file, while our code calls the SLF4J Logger and uses {} placeholders. The logger elements set a level per package or class, and each event also goes to the appenders of the parent loggers unless we set additivity=”false”. For production, a RollingFileAppender with SizeAndTimeBasedRollingPolicy keeps the log files small.
MDC adds request data to every line, and AsyncAppender writes to disk on a background thread, so a slow disk does not slow down request threads.
When something looks wrong, the Logback status messages show which file Logback loaded and what failed. For other setups, we can start from ready-made logback.xml examples.
The example project runs each configuration file with mvn test. The class LogbackDemo takes the file name as an argument, and the JUnit test LogbackConfigTest checks the output of each file.
11. References
- Logback Manual: Configuration
- Logback Manual: Architecture
- Logback Manual: RollingFileAppender
- Logback Manual: AsyncAppender and SiftingAppender
- Logback Manual: MDC
- Logback News and Release Notes
- SLF4J Error Codes
- Spring Boot Reference: Logging
Happy Learning !!