Study Notes
Spring Boot Frameworkapplied-skills

Logging in Spring Boot

How an application keeps a written record of what it did. Why println fails, Log4j's three moving parts, the level volume-knob, SLF4J as the facade Spring Boot actually uses, configuration in application.properties, and the Actuator live dashboard. Includes microexercises and interview Q&A.

25 min read·verified Spring Boot 4.x·reviewed 2026-08-28

After this you should be able to

  • Configure useful application logs
  • Explain Spring Boot observability tools
SLF4JLog4jActuator

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.

Updated August 2026 · Read ~18 min · JAVA-301


Choose your route#

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.

Learning goal

Recall first: which request facts would help diagnose an error tomorrow? A first endpoint helps, but this page never blocks you.

  • Configure useful application logs.
  • Explain Spring Boot observability tools.
  • Choose a log level without leaking credentials.

Shared core: logs are operational evidence#

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.

Learn deeply: make a failure diagnosable#

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.

  1. Write the operator question each line answers.
  2. Use placeholders instead of string concatenation.
  3. Trigger one known error and confirm the record preserves cause and context.
Solution shape, not a script

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.

Prepare for interviews: observe without noise#

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.

Strong-answer outline

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.

Exit check#

  1. What is SLF4J’s role?
  2. Why might debug not print?
  3. Which facts must never enter logs?
  4. How do logs differ from metrics?
  5. Why narrow a debug override to one package?

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

// the big idea

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.


SECTION 1

Notebook Summary#

Copy this into a notebook. Every line is derivable from the detailed notes below.

1 · Why log#

  • Logging = keep a record of input → output → final result (SUCCESS/FAILURE).
  • No logs + prod crash → near-zero chance of finding the cause.
  • Log at every layer: Controller → Service → Repository → External API.
  • Logged: exec flow, message, object state, exceptions, class, time, thread.

2 · Ways to log (worst → best)#

WayProblem
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

3 · Log4j = 3 parts#

  • Logger → one per class, replaces SOPLs. ✎ box "MyClass" with a mouth
  • Appender → where: File / Console / JDBC / SMTP / Socket. (interface)
  • Layout → format: Simple / Pattern / HTML / XML.
  • ⭐ Logger built by factory, never new → getLogger(Class).

4 · Levels (low → high)#

  • TRACE → DEBUG → INFO → WARN → ERROR → FATAL.
  • Rule: request prints iff request level ≥ logger level.
  • ✎ ladder; a line at INFO; rungs below it greyed out
  • ⚠ Spring default = INFO → TRACE/DEBUG hidden until you enable them.
  • ⚠ SLF4J has no fatal() — only trace/debug/info/warn/error.

5 · SLF4J + Spring Boot#

  • SLF4J = facade (interface); Logback = default engine behind it.
  • Same idea as JPA → Hibernate: code to the facade, swap the engine.
  • LoggerFactory.getLogger(MyClass.class) → org.slf4j imports.
  • ⚠ Wrong import (log4j / java.util) = subtle bugs — check the package.

6 · Config (application.properties)#

  • logging.level.<pkg>=DEBUG → raise verbosity per package.
  • logging.file.name=app.log → persist to file (console = not durable).
  • Console logs are color-coded by default; killing the terminal loses them.

7 · Actuator#

  • Actuator = production dashboard: /health, /metrics, /beans, /mappings.
  • ✎ car dashboard with health + metric gauges
  • ⚠ Only /health exposed by default → add exposure.include=*.

SECTION 2

Detailed Study Notes#

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.

2.1What logging is, and why it exists#

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:

  • Data input — e.g. which username arrived in the request parameter.
  • Data output — e.g. what a called API returned.
  • Final result — e.g. SUCCESS, FAILURE, or an exception.

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.

Analogy · flight recorder

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:

Controller · log order received → Service · log validation + pricing → Repository · log DB insert/result → External API · log request/response

2.2The three ways to log — and why two of them fail#

Before frameworks, developers reached for two crude approaches. Understanding why they break down is the whole motivation for Log4j.

Attempt 1

System.out.println (SOPL)#

  • Prints to the console — a temporary stream.
  • With many SOPLs the console scrolls; older lines are gone.
  • No consistent format, no levels, no persistence.
Attempt 2

Manually writing to a file#

  • You open and close a file connection every time.
  • Forgetting to close it leaks resources / memory.
  • Hard to keep a uniform format across the codebase.

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.

2.3Log4j and its three moving parts#

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:

who

Logger#

The object your class talks to instead of using println. One per class.

where

Appender#

Writes the message to a destination — file, console, database, mail, socket.

how

Layout#

The format the appender writes in — plain text, a pattern, HTML, XML.

log.info(...) your class Logger ConsoleAppender + Layout FileAppender + Layout JDBCAppender + Layout screen app.log database
One Logger → many Appenders, each carrying a Layout → each destination.

2.4Loggers: one per class, born from a factory#

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.

Log4j — one logger per class
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.
    }
}
⭐ TRICK · why getLogger(), never new Logger()

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:

MethodUse it forExample
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--")

[!] fatal() exists in Log4j but not in the SLF4J API you'll actually use in Spring Boot — see §2.5 and §2.8.

2.5Appenders: where the message goes#

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.

AppenderDestinationTypical use
ConsoleAppenderConsoleReal-time monitoring while developing/debugging.
FileAppenderA fileLong-term storage for auditing, compliance, later review.
JDBCAppenderDatabaseStructured logs you can query, report on, and analyse.
SMTPAppenderEmail [+]Instant alert to admins on a critical condition (outage).
SocketAppenderRemote server [+]Central log aggregation for distributed systems.

2.6Layouts: how the message is formatted#

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.

SimpleLayout

Type + message#

The minimum — just the level and the text. Good for quick, human reading.

PatternLayout

A pattern you define#

Track several properties together: time, date, line, file, thread, class.

HTMLLayout

An HTML table#

Log file opens in a browser as a formatted, colour-coded table.

XMLLayout

Structured XML#

Machine-readable records for tools that ingest logs.

A PatternLayout conversion pattern [+]
%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

2.7Logging levels: the volume knob#

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:

// the 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.

TRACE DEBUG INFO WARN ERROR / FATAL threshold = INFO below the line: dropped at/above: printed increasing severity
Level ladder — the threshold decides which rungs actually emit.
⚠ PITFALL · why your debug lines "don't work"

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.

2.8SLF4J: the facade Spring Boot actually uses#

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.

Analogy · SLF4J is to logging what JPA is to Hibernate

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.

your code SLF4J (facade / API) Logback ✓ Log4j java.util.logging Spring default
SLF4J facade with interchangeable engines — Logback is Spring Boot's default.
SLF4J logger in a Spring Boot class
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";
    }
}
⚠ PITFALL · watch the imports, and no fatal()

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.

2.9Configuration in application.properties#

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.

application.properties
# 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
  • Color-coded console logs are on by default in Spring Boot — levels are tinted so errors jump out.
  • Console logs do not persist. Kill the terminal and they're gone. To make logs sustain over time you must write them to a file or database — which is exactly what logging.file.name turns on.
💡 TIP · per-package levels are the real skill

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.

2.10Spring Boot Actuator: the live dashboard#

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.

Analogy · a car dashboard

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.

/actuator/health

Health#

Overall up/down status for the app and its dependencies.

/actuator/metrics

Metrics#

Vital signs — memory, CPU load, request counts.

/actuator/beans

Beans / Mappings#

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:

HelloWorldController.java
@RestController
public class HelloWorldController {

    @GetMapping("/helloworld")
    public String getMethodName() {
        return new String("Hello World!");
    }
}
⚠ PITFALL · only /health is exposed by default

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:

management.endpoints.web.exposure.include=*

Exposing everything is fine locally; in real production you expose a curated subset, not *. [+]

2.11Interview & practice Q&A#

Q1 Why not just use System.out.println for logging?

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.

Q2 What are the three components of Log4j?

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.

Q3 A logger is set to WARN. Which of debug/info/warn/error print?

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.

Q4 What is SLF4J, and how does it relate to Logback and Log4j?

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.

Q5 Why is the Logger created with getLogger() instead of new?

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.

Q6 My debug logs aren't appearing. What's the likely cause?

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.

Q7 Console logs disappear when I restart. How do I keep them?

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.

Q8 What does Spring Boot Actuator do, and why is /beans blocked at first?

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).

2.12Key takeaways#

  • Logs are the record you read after something breaks — without them, a prod failure is a black box.
  • Frameworks beat SOPL and manual file-writing because they add levels, uniform format, and persistence.
  • Log4j = Logger (who) → Appender (where) → Layout (how).
  • A message prints only when its level ≥ the logger's threshold; Spring's default is INFO.
  • Spring Boot gives you SLF4J + Logback free; tune it from application.properties, no code changes.
  • Actuator is the live dashboard — but only /health is exposed until you say otherwise.
logging = flight recorder Logger · Appender · Layout level ≥ threshold prints SLF4J facade → Logback getLogger = factory no fatal() in SLF4J default = INFO file to persist Actuator = dashboard expose.include=*

SECTION 3

Microexercises#

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.

3.1Build it — a logging controller + config#

// your task

Write a Spring Boot REST controller with an SLF4J logger, and the application.properties needed to actually see its debug output.

Requirements

  • Declare a static final SLF4J logger named after the class.
  • Map GET /status; inside it, log one line at each of debug, info, and error.
  • In config: keep the app quiet at INFO globally, but show DEBUG for package com.crio, and persist to logs/app.log.

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.

Show the full walkthrough

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).

StatusController.java
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.

application.properties
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.

3.2Diagnose it — the code that won't behave#

// your task

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.

OrderController.java
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";
    }
}
logging.level.root=INFO

Answer these: Does the file compile? If it ran, which of (a)(b)(c) would print?

Show the full walkthrough

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.

OrderController.java — fixed
    @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";
    }
# reveal the debug line when you need it
logging.level.com.crio=DEBUG

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.

getLogger(Class), org.slf4j no log.fatal() → use error() debug hidden at INFO logging.level.<pkg>=DEBUG logging.file.name to persist