Appearance
Logging Foundations: SLF4J, Logback and the Logger Hierarchy
Logging means recording important events as an application runs (requests, errors, warnings, debugging information) so developers can monitor it and find out what it did. When a user reports a problem with "create user", the log shows how far the code got and what it saw.
Chapter 15 showed that traces give the path of a request, while logs give the details. Before collecting logs from many services (chapter 19), this chapter and the next two look at how logging works inside one application.
The pieces
- SLF4J (Simple Logging Facade for Java) is an API only: interfaces, no implementation. As in the facade design pattern, your code talks to SLF4J and does not depend on the library underneath.
- An implementation does the work: Logback (Spring Boot's default) or Log4j2 (Log4j version 2).
- The implementation hands each log event to appenders. An appender decides the destination: console, file, database, Kafka, a cloud service.
- Log aggregation tools then read from those destinations to search and display logs.
Step 1: log something
No extra dependency is needed:
java
@RestController
public class PaymentController {
Logger log = LoggerFactory.getLogger(PaymentController.class); // org.slf4j
@GetMapping("/payments")
public String getPayments() {
log.info("fetch the payments successfully");
return "successfully fetched all payments";
}
}GET /payments prints on the console:
text
2025-... INFO 12345 --- [payment-service] [nio-8080-exec-1] c.e.payment.PaymentController : fetch the payments successfullyThree questions follow: where do the libraries come from, how does getLogger work, and how did the line reach the console with no configuration?
The dependencies
Every Spring Boot starter (spring-boot-starter-web included) depends on spring-boot-starter, which depends on spring-boot-starter-logging. That brings:
slf4j-api.jar: the API;logback-classic.jarandlogback-core.jar: the implementation.
Switching to Log4j2
Adding Log4j2's starter is not enough:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>Now two SLF4J implementations are on the classpath. SLF4J warns at startup and simply uses the first one it finds:
text
SLF4J(W): Class path contains multiple SLF4J providers.
SLF4J(W): Found provider [ch.qos.logback.classic.spi.LogbackServiceProvider@4ca8195f]
SLF4J(W): Found provider [org.apache.logging.slf4j.SLF4JServiceProvider@65e579dc]
SLF4J(I): Actual provider is of type [ch.qos.logback.classic.spi.LogbackServiceProvider@4ca8195f]Internally it collects the providers into a list and takes get(0), so here you got Logback, not the Log4j2 you added. It is only a warning, so the build succeeds, but which library you get depends on classpath order. Exclude the default logging starter when you switch:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId> <!-- brings Logback -->
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>How getLogger works
LoggerFactory.getLogger(PaymentController.class):
LoggerFactory(SLF4J) finds the implementation to use, from the libraries on the classpath.- It asks that implementation's
ILoggerFactory: in Logback, theLoggerContext(Log4j2 has its own equivalent). - The
LoggerContextkeeps a cache,Map<String, Logger>, keyed by logger name. If a logger with that name exists, it is returned; otherwise one is created and cached.
You receive an org.slf4j.Logger; behind it is a ch.qos.logback.classic.Logger. Ask twice for the same name and you get the same object.
The logger object
A Logback logger has four important fields: name, level, parent, and children.
Name
Any string: a class name, a package name, anything. One name means one logger object. The convention is one logger per class, named after the class: getLogger(PaymentController.class) creates the logger com.example.payment.PaymentController.
Level
There are five levels, from most to least severe:
ERROR > WARN > INFO > DEBUG > TRACE
Every logger has a level, and every log statement has one too, set by the method you call (log.error, log.info...). A logger prints a statement only if the statement's level is the same as or more severe than the logger's level.
java
log.error("error log");
log.warn("warning log");
log.info("info log");
log.debug("debug log");
log.trace("trace log");| Logger level | Printed |
|---|---|
| INFO (the default) | error, warning, info |
| DEBUG | error, warning, info, debug |
| WARN | error, warning |
Set a logger's level in application.properties by its name:
properties
logging.level.com.example.payment.PaymentController=DEBUGParent and children: the hierarchy
Loggers form a tree based on their dotted names. Creating com.example.payment.PaymentController also creates its ancestors:
The ROOT logger is the ancestor of every logger. A debugger on a Logback logger shows the chain: PaymentController's parent is com.example.payment, whose parent is com.example, then com, then ROOT.
The hierarchy gives three behaviors.
1. Level inheritance. A logger with no level of its own uses its nearest ancestor's. With a thousand classes, you do not set a thousand levels; set one on a package:
properties
logging.level.com=DEBUG # every logger under com, unless overriddenPaymentController now prints debug lines too.
2. Overrides. A specific child can still have its own level:
properties
logging.level.com=DEBUG
logging.level.com.example.payment.PaymentController=WARNEverything under com is at DEBUG, except PaymentController, which prints only error and warning.
3. Accepted events travel upward. The level check happens once, at the logger where you logged:
- Rejected (below the logger's level): discarded. Nothing else happens.
- Accepted: the event is passed to that logger's appenders, then to its parent's, then its grandparent's, all the way up to ROOT. The ancestors' levels are not checked again: once accepted, an event reaches every appender on the way up.
This upward flow is called additivity; it is on by default and can be turned off per logger.
Suppose com.example has a file appender and ROOT a console appender. PaymentController (level INFO) logs log.info(...):
The event is accepted at PaymentController and written to the file and to the console, even though com.example's own level is WARN.
So how did the first example reach the console?
PaymentController's logger had no appender, nor did com.example.payment, com.example or com. The accepted event travelled up to ROOT, and Spring Boot configures ROOT with a console appender by default. That is the only reason any log appears at all.
Next: Chapter 17 opens up appenders: configuring console and file appenders yourself, log patterns, rolling log files, and which logger gets which appender.
Check yourself
- What is SLF4J, and why does code depend on it rather than on Logback?
- Which jars does a Spring Boot starter bring for logging?
- You add
spring-boot-starter-log4j2without excluding anything. What happens, and how do you fix it? - What does
LoggerFactory.getLogger(...)do, step by step? - What is the naming convention for loggers?
- List the five levels from most to least severe.
- A logger is at WARN. Which of
error,warn,info,debug,traceprint? - What does
logging.level.com=DEBUGdo tocom.example.payment.PaymentController? - How do you keep
comat DEBUG but one class at WARN? - After a child logger accepts an event, are its ancestors' levels checked?
- What is an appender?
- Why do logs appear on the console without any appender configuration?
Answers
- The Simple Logging Facade for Java: an API without implementation. Code written against it works with any implementation (Logback, Log4j2).
slf4j-api,logback-classicandlogback-core, throughspring-boot-starter-logging.- Two SLF4J providers are on the classpath; SLF4J warns and uses the first it finds (often Logback). Exclude
spring-boot-starter-loggingfrom the starters. LoggerFactoryfinds the implementation on the classpath, asks itsILoggerFactory(Logback'sLoggerContext) for the logger by name, which returns the cached logger or creates and caches a new one.- One logger per class, named after the class (
getLogger(MyClass.class)). - ERROR, WARN, INFO, DEBUG, TRACE.
errorandwarn.- It inherits DEBUG (unless it has its own level), so debug statements print.
logging.level.com=DEBUGandlogging.level.<that class>=WARN.- No. The level is checked once; the event then goes to every appender up to ROOT.
- The component that decides where a log event goes: console, file, database, Kafka...
- The accepted event travels up to ROOT, which Spring Boot gives a console appender by default.