CodeGym /Courses /Spring Boot /Structured logging in Spring Boot 4

Structured logging in Spring Boot 4

Spring Boot
Level 21, Lesson 0
Available

1. Disadvantages of plain-text logs

At the start of a project, ordinary text logs seem ideal: you open the console, see "the app started", "the courses loaded", "the port is such-and-such" — great. The problem appears not because text is bad, but because text is too "human": it is easy to read with your eyes, but hard to reliably parse into fields when you want to search, filter, and compare records not by guesswork, but by facts.

Imagine a typical real-world scenario for catalog-service. You got a complaint: "sometimes a course cannot be found by slug." You enabled more logs, messages started coming from different packages, and now you want to answer a simple question: where exactly did the error happen and with what parameters. In plain-text format, you often run into the fact that the needed bits of data are hidden inside the message string, and the message format can differ from class to class. In the end, you either scroll through the console by eye, or you start playing with regular expressions, which is especially fun when the log message changes slightly and your "pattern search" suddenly stops working.

There is also a more down-to-earth problem: you very quickly want to filter by level, by logger class, by service name, by thread, by a specific "correlation" operation ID. In plain-text, this is possible, but fragile. Any small format change means the machine stops understanding logs as well as a person does. And after all, we do not write backend systems only for people. Sometimes machines read them too — simply because there are usually more machines in production.

So that it does not sound too "enterprise", let us phrase it in plain terms: a plain-text log is when all important fields are glued into one string. A structured log is when those fields remain fields, and you do not have to "dig out" the level or logger from the text like raisins from a bun.

A small table to pin down the idea:

Task Plain-text log Structured log
Quickly understand what is happening by eye Convenient Often inconvenient, because JSON tends to be verbose
Find all records where level=ERROR Usually via grep/pattern, sometimes fragile By level field — stable
Filter by logger/package By string match By separate field
Automatically analyze logs with a program Harder, text parsing is needed Easier, because it is already JSON or a set of fields

2. Structured logging and machine-readable line

Structured logging is easy to misunderstand by accident, so let us start with an honest and simple model. Every log event has a set of data: time, level, logger name, message, sometimes an exception, sometimes a thread name, and other service fields. In plain-text format, this data turns into one "for humans" line. In structured format, it remains a structure — most often as a JSON object — and is printed as a single JSON line, ready for machine parsing.

The term machine-readable log line means: each log line is a standalone object with a fixed format that can be parsed without "guessing". If it is a JSON log, then one line is one JSON object. Yes, it looks like "a dictionary got dumped into the console", but that is exactly the value: fields did not get lost or dissolve inside the text.

It helps to imagine the path of a log, very simplified, without Logback details and without diving into configs:

flowchart LR
  %% Simplified diagram: the important thing is to understand that structured is about "how we print", not "how we write log.info"
  A["Your code: log.info(...)"] --> B["Log event (fields: time, level, logger, message, ...)"]
  B --> C["Encoder/Formatter (plain or structured)"]
  C --> D["Output: console/file"]

Pay attention to an important idea: structuredness is about the output format, not about us "writing log.info(...) differently". You create a logging event in exactly the same way, and then the "printer" prints it either as a human-readable string or as a machine-readable JSON.

A super-minimal structured log example to see the idea:

{
  "timestamp": "2026-03-19T10:15:00Z",
  "level": "INFO",
  "logger": "com.example.catalogservice.catalog.service.CourseCatalogService",
  "message": "Featured courses loaded: 4"
}

In reality, Spring Boot supports several standard structured formats, for example ECS, GELF, and Logstash, and they differ in field names and grouping. In this lecture, what matters is not a full table of fields, but understanding the key effect: level, logger, message, and time become separate fields instead of part of a "glued" string.

3. Output format instead of log.info(...)

The nicest news is this: to switch to structured logging, you usually do not need to rewrite application code. If you are already logging correctly — through an SLF4J logger, with placeholders {} — then structuring mainly changes how the log looks from the outside. In other words, we do not rewrite services and controllers; we simply tweak configuration — as is customary in Spring Boot, where most behavior is controlled by config.

Let us take a small, understandable fragment from the catalog-service context. For example, our catalog service logs the fact that featured courses were loaded. The code itself is as ordinary as it gets, with no structured magic:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class CourseCatalogService {

    // SLF4J logger: the code does not "know" about JSON, it just creates log events
    private static final Logger log =
            LoggerFactory.getLogger(CourseCatalogService.class);

    public void logFeaturedCount(int count) {
        // Placeholder {} + separate argument: this passes the parameter to the logging system instead of concatenating a string
        log.info("Featured courses loaded: {}", count); // the message stays the same
    }
}

There are two important points here. First, we use the {} placeholder and pass the parameter separately, instead of concatenating strings with +. This is faster, cleaner, and works better with logging as a system. Second, there is no JSON in the code, and there should not be. That is important: structured logging should not turn your domain code into code for a log format.

If you now enable structured logging, this same log.info(...) entry will go out in structured form. So the "photo" changes, not the "model". And that is very Spring Boot-ish: behavior changes through configuration, not by rewriting ten classes.

4. Enabling structured logging in Spring Boot 4

Now we move to the most practical part: how to enable structured logging so that it actually turns on, instead of remaining just an "idea in your head". In Spring Boot 4, structured logging is enabled via configuration. We care about two properties: one controls the console, the other controls the file. Even if you are only writing to the console for now, it is important to understand that these are two independent targets: the console is usually read by a human, and files and log streams are often read by a tool.

Here is a compact table of settings so you do not mix them up:

Where we write Property What it controls
To the console (stdout) logging.structured.format.console Console output format
To a file logging.structured.format.file File output format

To get started, it is enough to enable the structured format for the console. For example, in src/main/resources/application.yaml you can add:

spring:
  application:
    name: catalog-service # service name will appear in structured fields (for example, service.name)

logging:
  structured:
    format:
      console: ecs # enable structured format for the console (stdout)

This is the minimal way to enable structured logging in the console. For catalog-service in the course, we treat ecs as the working baseline structured console: we will use it to anchor the following examples and the project’s final policy. The value ecs is the name of one of the supported formats. GELF and Logstash will also be useful, but as formats we need to recognize and read, not as a floating choice inside the project.

Notice that we are not writing logback.xml, not manually wiring strange encoders, and not diving into configuration weeds. We are doing exactly what Boot likes most: describing the desired behavior declaratively.

If you need to enable a structured format for a file, the logic will be similar — only the target changes:

logging:
  structured:
    format:
      file: ecs # enable structured format for the file (usually a log collector takes it)

We deliberately do not discuss file naming, rotation, and archiving here — that is a separate universe. For now, the main point is this: in Boot, structured logging is enabled by properties, and your Java code does not have to "know" that the logs became JSON.

Finally, one important practical thought: structured logging is not a "forever-on" option. It should be enabled where it adds value. If you turned on JSON logs in the console and suddenly felt that debugging became less convenient, that does not mean you are wrong; it is a normal effect. You just enabled the tool for the wrong task. The console is usually for eyes, and the structured format is for stable processing.

5. Comparison: plain vs structured

So that there is no feeling of "I enabled something, but I do not understand what changed", let us compare two outputs for the same log.info("Featured courses loaded: {}", count) entry from CourseCatalogService. From the code’s point of view, nothing changed. Only the way the logging event is printed changed.

An example of how such an entry may look in plain-text format:

2026-03-19T10:15:00.123 INFO  18432 --- [catalog-service]  c.e.c.c.service.CourseCatalogService : Featured courses loaded: 4

And now an imaginary structured version:

{"@timestamp":"2026-03-19T10:15:00.123Z","log.level":"INFO","log.logger":"com.example.catalogservice.catalog.service.CourseCatalogService","service.name":"catalog-service","message":"Featured courses loaded: 4"}

If you are thinking right now: "Looks worse than before," then congratulations — you are reading with your eyes correctly. A structured log often does really look worse to the eye, because it is optimized not for a person, but for stable fields. But it gains a superpower: level is the log.level field, logger is a separate field, service name is a separate field. The machine does not need to guess where the logger name is hidden in the string, and it does not need to hope that you will not change the order of parts.

Sometimes it is useful to show JSON nicely, even though in logs it is usually on one line. This is not a new format, just a visualization of what is inside:

{
  "@timestamp": "2026-03-19T10:15:00.123Z",
  "log.level": "INFO",
  "service.name": "catalog-service",
  "message": "Featured courses loaded: 4"
}

And here you can clearly see why all of this exists: if you need to quickly filter all INFO records from our service, you do not parse a string. You look at fields.

6. Where structured logging is useful

With structured logging, it is easy to fall into the trap of "if it is modern, I will turn it on everywhere." But logging is not a fashion show. It either helps you diagnose a system or gets in the way. The structured format is especially useful where logs must be stable input data for processing: for search, aggregation, statistics building, and field-based analysis. This can be local too, if you want to quickly select records by fields, and in environments where logs are collected and analyzed somewhere else.

But there is also the other side. When you run catalog-service in the IDE and just read the console, JSON can turn simple debugging into reading an accounting report. You want to see a short line, and instead you get an object with fields. That is normal. Structured logging does not cancel the value of plain-text output; it just solves a different problem.

It is very important to pin down one more thought so that there are no magical expectations: structured logging does not make a log "smart" automatically. If you wrote a bad message, it will stay bad — just in JSON. Here is an example of a bad log that is formally structured but practically useless:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StartupSummaryRunner {
    private static final Logger log = LoggerFactory.getLogger(StartupSummaryRunner.class);

    public void run() {
        // The problem is not the format, but the meaning: with such a log, it is unclear what exactly "Started" means
        log.info("Started"); // too little meaning
    }
}

And here is a more useful version — the same level, the same logger, but the message now carries a concrete signal:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class StartupSummaryRunner {
    private static final Logger log = LoggerFactory.getLogger(StartupSummaryRunner.class);

    public void run(int coursesCount) {
        // Here there is a concrete fact + the parameter is passed separately via {}, which is correct for logging
        log.info("Catalog started. Courses loaded: {}", coursesCount); // clear signal
    }
}

The structured format does not save you from discipline: do not log huge objects in full just because JSON is supposedly "for that". If you start printing the entire list of courses to the log, you will get huge noise, and no amount of structure will bring useful signal back out of it.

And yes — the structured format does not cancel the basic security rule either. If you logged a token, then you logged a token. It will be no less dangerous, just nicely wrapped in JSON.

7. Common mistakes when enabling structured logging

Structured logging is useful, but beginners often "shoot themselves in the foot" with it not because the technology is bad, but because their expectations do not match reality. If you know the common mistakes in advance, you can save yourself a lot of time and avoid turning logs into a strange artifact that nobody wants to read.

Mistake #1: enabling structured logging and expecting a "pretty console."
Structured logs often look worse in the console than plain-text logs, and that is normal. They are not required to be pleasant to the eye. Before turning them on, ask yourself honestly: are you reading the log with your eyes right now, or do you need fields for stable parsing? If the goal is quick manual debugging, plain-text sometimes wins.

Mistake #2: thinking that the JSON form replaces a good message text.
If the message is "about nothing" or too vague, the structured format merely packages that vagueness into JSON. The meaning still lives in the message. Loading... and Started are almost always worse than Catalog started. Courses loaded: 12 — regardless of format.

Mistake #3: logging large objects in full because "it is JSON already."
Printing the entire course list, the entire configuration, or the entire response "for convenience" is very tempting. But it creates noise, increases log volume, and makes real problems less visible. It is better to log aggregates and facts: count, slug, flags, important decisions, not all the data in the world.

Mistake #4: mixing different logging styles without a clear rule.
For example, part of the environments writes plain-text, part writes structured, and part writes both mixed together because the configuration drifted apart. In the end, you are not sure what exactly you will see when you run the app. Logs should be predictable: either plain here or structured here, and that should be fixed in config, not in a developer's memory.

Mistake #5: confusing structured logging with level settings.
Structured logging is not "DEBUG instead of INFO" and not "more logs." It is an output form. You can write structured logs at INFO, at DEBUG, or at ERROR. Levels decide how many events you write; structured format decides how those events look and how easy they are to parse by fields.

1
Task
Spring Boot, level 21, lesson 0
Locked
Structured console logging in ECS
Structured console logging in ECS
1
Task
Spring Boot, level 21, lesson 0
Locked
The same `INFO` log in plain and structured modes
The same `INFO` log in plain and structured modes
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION