Java · Spring Boot · Session 6
How an application keeps a written record of what it did — so that when an order silently vanishes in production, you can read back the story instead of guessing. From Log4j internals to SLF4J, configuration, and the Actuator dashboard.
Problem: when a service fails in production, a useful record must explain what happened without exposing secrets. Spend 25 minutes configuring evidence or 10 minutes revising the reasoning.
Recall first: which request facts would help diagnose an error tomorrow? A first endpoint helps, but this page never blocks you.
Spring Boot uses SLF4J as the application-facing logging API and configures a backend. Levels filter volume, structured context connects events, and actuator exposes selected operational signals.
Closest confusion: a log line explains an event; a metric measures a changing value; a trace follows one request across boundaries. They complement rather than replace one another.
Failure trace: a debug line is not missing when the effective level is INFO. Lower the narrow package level while diagnosing, rather than turning every dependency noisy.
Decision rule: log stable identifiers and decisions at appropriate levels, omit credentials and sensitive payloads, and make production verbosity a configuration choice.
Version note: reviewed with Spring Boot 4.x material on 2026-08-28; confirm actuator exposure policies for your deployment.
Mission (20 minutes): add request-scoped order logging to a small endpoint, then change one package level at runtime or configuration. Likely files: controller, service, and application.properties.
Acceptance checks: an INFO line names the order id; debug detail appears only when enabled for your package; no authorization header or token is logged.
Keep request identifiers near the boundary and domain decisions in the service. Let configuration set the environment’s volume.
Transfer: add a timing metric proposal and explain why it should not be a log on every request.
Two-minute summary: SLF4J is the facade, the backend emits records, levels control volume, and observability needs logs, metrics, and traces for different questions.
Explain in 60 seconds: why is println inadequate for a deployed service?
Primary question: choose levels for normal completion, a recoverable validation issue, and a failed payment. Follow-ups: what should never be logged, and how do you enable debug safely?
Debug exercise: a production incident has no diagnostic line because debug is filtered. Propose the narrowest configuration change and rollback plan.
Connect each level to an operator decision, distinguish logs from metrics and traces, then show that security and cost constrain verbosity.
Timed mock: design the first five fields for an order-failure log in 90 seconds.
Learn next: Use configure useful application logs as the base, then add structured logs in Logging: SLF4J, Logback & Log4j. Interview next: Use configure useful application logs as the base, then add structured logs in Logging: SLF4J, Logback & Log4j.
Continue this path · Switch paths from this topic · Review first: first endpoint
Logging is your application's flight recorder. A logging framework routes each message through three parts — a Logger (who is speaking), an Appender (where it's written), and a Layout (how it's formatted) — filtered by a level so noise stays out of production. Spring Boot ships this for free: SLF4J + Logback, tuned from application.properties, no code changes required.
| Way | Problem |
|---|---|
| SOPL (println) | temporary, scrolls off console, no format |
| Write to file (manual) | open/close conn, leaks, no uniform format |
| Framework (Log4j) | solves all of the above → use this |
A customer clicks Place order. The screen says nothing is wrong. But the order never reaches the kitchen, never charges the card, never appears in the database. There is no red error box, no stack trace on their screen — the request just evaporated somewhere between the controller, the service, the repository, and the payment gateway. You have one question and no way to answer it: what actually happened? That silence is the problem logging exists to solve.
Logging is the practice of writing down what your program did while it ran. Each log line is a small note the code leaves behind — what came in, what went out, and how it ended. Concretely, you keep track of three things:
Beyond bare debugging, logs let you monitor the same application across environments — Dev, QA, Stage, Prod — each of which may misbehave differently. A well-logged program also records its execution flow, the state of important objects, exceptions, the class and thread that ran, and the time each event occurred. Together those notes are a documented history of what the system did.
A plane's black box is ignored on every normal flight. Its entire value shows up in the one minute after something goes wrong: it replays exactly what happened, in order, so investigators don't have to guess. Logs are that black box for your application — useless until the day they're the only thing you have.
The order-processing failure from the hook is hard precisely because the flow crosses several components. You log at each one so the trail is unbroken:
Before frameworks, developers reached for two crude approaches. Understanding why they break down is the whole motivation for Log4j.
A logging framework is a library that does all of this correctly for you. It manages the file or database connection, enforces one format, adds severity levels, and can persist your logs — solving every problem above at once. That library is what the rest of this lesson is about.
Log4j is a fast, highly configurable logging framework for Java. Three properties make it the classic choice: it's designed for speed and minimal overhead (safe in high-traffic apps), it's configured from external XML/properties files so you change logging behaviour without touching code, and it uses a hierarchical model that categorises and filters messages by package or class name.
Every log message flows through the same three components. Learn these three words and Log4j stops being mysterious:
The object your class talks to instead of using println. One per class.
Writes the message to a destination — file, console, database, mail, socket.
The format the appender writes in — plain text, a pattern, HTML, XML.
A Logger is the object that turns Log4j on inside a single Java class. You create exactly one, as a static field named after the class, and you call its methods instead of System.out.println. Because it's static, all instances of the class share the one logger, and it carries the class name so every line it emits is automatically attributed.
public class YourClassName {
// named after the class → messages are auto-tagged with the class name
static Logger log = Logger.getLogger(YourClassName.class.getName());
public static void main(String[] args) {
// your logic goes here, using log.info(...) etc.
}
}
You never write new Logger() — you ask a factory method (getLogger) for one. That's the Factory design pattern: it hides construction, gives one central place to control how loggers are made, and keeps your class loosely coupled to the logging library. If an interviewer asks "where have you used a design pattern in real code?", this is a clean, honest answer.
The Logger exposes one method per severity. Choosing the right one is what makes logs filterable later:
| Method | Use it for | Example |
|---|---|---|
| debug() | Fine-grained detail, useful only while developing. | log.debug("--debug--") |
| info() | Normal progress of the application. | log.info("--info--") |
| warn() | Something faulty or unexpected, but survivable. | log.warn("--warn--") |
| error() | An error the system can still continue past. | log.error("--error--") |
| fatal() | Critical failure that crashes the application. | log.fatal("--fatal--") |
An Appender is the component that writes a log message to an external destination. Appender is an interface, which is why Log4j can offer many interchangeable implementations — you pick the destination by picking the appender, without changing your logging calls.
| Appender | Destination | Typical use |
|---|---|---|
| ConsoleAppender | Console | Real-time monitoring while developing/debugging. |
| FileAppender | A file | Long-term storage for auditing, compliance, later review. |
| JDBCAppender | Database | Structured logs you can query, report on, and analyse. |
| SMTPAppender | Instant alert to admins on a critical condition (outage). | |
| SocketAppender | Remote server | Central log aggregation for distributed systems. |
A Layout specifies the format in which the appender writes each log statement. Same message, different dress: a Layout decides whether a line is bare text or a rich, parseable record.
The minimum — just the level and the text. Good for quick, human reading.
Track several properties together: time, date, line, file, thread, class.
Log file opens in a browser as a formatted, colour-coded table.
Machine-readable records for tools that ingest logs.
%d{yyyy-MM-dd HH:mm:ss} [%t] %-5p %c{1} - %m%n
%d date/time %t thread %-5p level (left-padded to 5)
%c{1} logger (last segment) %m message %n newline
A level is a severity rank that decides whether a message is actually emitted. Each logger is configured with a threshold, and Log4j applies one rule:
A logging request of level p is enabled if and only if p is higher than or equal to the level configured on its logger. Anything below the threshold is silently dropped.
So if a logger is set to INFO, then INFO, WARN, ERROR (and FATAL) print, while DEBUG and TRACE are ignored. Raise the threshold in production to cut noise; lower it to DEBUG when hunting a bug.
Spring Boot's default root level is INFO, so TRACE and DEBUG calls run but print nothing. Beginners assume the logger is broken. It isn't — the threshold is filtering them out. You must explicitly lower the level (§2.9) to see them.
SLF4J (Simple Logging Facade for Java) is an abstraction that sits in front of a real logging engine. You write your code against SLF4J's interface; at runtime an actual implementation — Logback, Log4j, or java.util.logging — does the work. Spring Boot bundles logging in by default: the SLF4J API with Logback as the engine.
You already know this shape. You code to the JPA interface and Hibernate implements it underneath; swapping the provider doesn't touch your code. SLF4J is the same contract for logging: program to the facade, and you can switch the engine to Log4j later without rewriting a single log call. A facade is a universal remote — one set of buttons, any brand of device behind it.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
public class LogController {
// SLF4J factory — note the org.slf4j import, not org.apache.log4j
private static final Logger log = LoggerFactory.getLogger(LogController.class);
@GetMapping("/logs")
public String logs() {
log.trace("trace — hidden at default level");
log.debug("debug — hidden at default level");
log.info("info — visible");
log.warn("warn — visible");
log.error("error — visible");
return "check your console";
}
}
Two traps live here. First, IDEs will happily import a different Logger (from org.apache.log4j or java.util.logging) — always confirm it's org.slf4j. Second, the SLF4J Logger has no fatal() method; its levels are trace/debug/info/warn/error only. Calling log.fatal(...) won't compile.
The point of a framework is that you tune behaviour without editing code. In Spring Boot that happens in application.properties. Two everyday needs: reveal debug logs, and make logs survive a terminal restart.
# raise verbosity for your own package only
logging.level.com.crio=DEBUG
# keep the rest of the world quiet
logging.level.root=INFO
# persist logs to a file (console alone is not durable) [+]
logging.file.name=logs/app.log
logging.level.<package> lets you crank one noisy subsystem up to DEBUG while everything else stays at INFO. In an interview, "I'd raise the level on just the failing package in prod rather than debug-logging the whole app" signals you understand the hierarchical model from §2.3.
Logs tell you what happened. But when orders in production suddenly turn slow, you also want to know what's happening right now — memory, CPU, database health — without attaching a debugger. Spring Boot Actuator adds production-ready endpoints that report your app's health and metrics on demand.
Actuator is a dashboard bolted onto your running application. Each endpoint is a gauge, metrics are the vital signs (memory, CPU, request counts), and health checks are the overall status light for the app and its dependencies. You read the dashboard instead of opening the hood.
Overall up/down status for the app and its dependencies.
Vital signs — memory, CPU load, request counts.
Every Spring bean and every URL mapping the app registered.
Add the actuator dependency at start.spring.io, then a controller like the one taught in the session:
@RestController
public class HelloWorldController {
@GetMapping("/helloworld")
public String getMethodName() {
return new String("Hello World!");
}
}
Hit /actuator/beans or /actuator/mappings straight away and you'll get nothing — by default Actuator exposes only /health over the web, on purpose, to prevent leaking internals. Open the rest explicitly:
SOPL output is temporary console text with no levels, no consistent format, and no persistence — a busy app scrolls the early lines out of view, and nothing survives after the process ends. A framework gives you severity levels, a uniform layout, and durable destinations (file/DB) while managing the connection for you.
Logger (who logs — one per class, used instead of println), Appender (where it's written — console, file, JDBC, SMTP, socket), and Layout (how it's formatted — simple, pattern, HTML, XML). A message flows Logger → Appender → Layout → destination.
Only warn and error (and fatal). The rule is: a request prints iff its level is greater than or equal to the logger's level. DEBUG and INFO are below WARN, so they're dropped.
SLF4J is a facade — an abstraction you code against. The actual logging is done by an engine behind it: Logback (Spring Boot's default), Log4j, or java.util.logging. It's the same pattern as JPA → Hibernate: swap the implementation without changing your code.
It's the Factory design pattern. A factory method centralises and abstracts object creation, keeps the class loosely coupled from the logging library, and lets the framework decide how loggers are built and cached — so you don't manage that yourself.
The effective level is too high. Spring Boot defaults to INFO, which suppresses DEBUG and TRACE. Set logging.level.<your.package>=DEBUG in application.properties to reveal them — the logger was never broken.
Console output isn't durable. Write logs to a file (or database) instead — in Spring Boot, set logging.file.name (or logging.file.path). With Log4j directly you'd attach a FileAppender or JDBCAppender.
Actuator exposes production-ready endpoints (health, metrics, beans, mappings) so you can monitor a running app. By default only /health is web-exposed for security; enable the rest with management.endpoints.web.exposure.include=* (and expose a narrow subset in real production).
Reading proves you recognise these ideas. These two drills prove you can use them — one to build, one to diagnose. Attempt each before opening the walkthrough.
Write a Spring Boot REST controller with an SLF4J logger, and the application.properties needed to actually see its debug output.
Requirements
Expected result: with that config, hitting /status prints all three lines to the console and to logs/app.log. Remove the com.crio line and the debug entry disappears.
Step 1 — get the logger from the factory. Never new a logger; ask LoggerFactory.getLogger and pass the class so lines are auto-tagged. Confirm the import is org.slf4j.
Step 2 — log at three levels. Use debug/info/error — not fatal (SLF4J has no such method).
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.*;
@RestController
public class StatusController {
private static final Logger log = LoggerFactory.getLogger(StatusController.class);
@GetMapping("/status")
public String status() {
log.debug("debug: computing status");
log.info("info: status requested");
log.error("error: sample error line");
return "ok";
}
}
Step 3 — configure levels and a file. Debug is hidden at the default INFO threshold, so raise it for your package only, and add a file so logs persist.
logging.level.root=INFO
logging.level.com.crio=DEBUG
logging.file.name=logs/app.log
Why: the per-package level threads §2.7 (level ≥ threshold) and §2.9 (config, not code) together — you changed what's visible without touching a single log statement.
A teammate says "half my logs vanish and the file won't even compile." Here's the class and config. Predict the exact failure, name the root cause, and fix both issues.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@RestController
public class OrderController {
private static final Logger log = LoggerFactory.getLogger(OrderController.class);
@GetMapping("/order")
public String order() {
log.debug("validating order"); // (a)
log.info("order accepted"); // (b)
log.fatal("payment gateway down"); // (c)
return "done";
}
}
Answer these: Does the file compile? If it ran, which of (a)(b)(c) would print?
Prediction first. The file does not compile. Line (c) log.fatal(...) is a compile error: "cannot find symbol — method fatal(String)". The SLF4J Logger exposes only trace/debug/info/warn/error — fatal belongs to Log4j, not SLF4J.
Root cause #1 — wrong API assumption. The teammate reached for a Log4j level on an SLF4J logger. Map "critical" onto error (optionally with a marker), the highest SLF4J level.
Root cause #2 — the "vanishing" logs. Once it compiles, at root=INFO line (a) debug is below the threshold and is dropped; only (b) info (and the fixed error) print. Nothing is broken — the level filter is doing its job.
@GetMapping("/order")
public String order() {
log.debug("validating order"); // hidden until level lowered
log.info("order accepted"); // prints at INFO
log.error("payment gateway down"); // was fatal() — use error()
return "done";
}
Why: the compile error drills the SLF4J-vs-Log4j level list (§2.8), and the missing debug line drills the threshold rule (§2.7) — the two mistakes that eat the most time in practice.