CodeGym /Kursy /Docker for Spring /Spring Boot Logs: Levels and Startup

Spring Boot Logs: Levels and Startup

Docker for Spring
Poziom 14, Lekcja 1
Dostępny

1. Useful logs in a container

When a service starts living in a container, logs suddenly become not a “nice bonus” but part of survival. Unlike running locally in an IDE, where you can click the code and set a breakpoint, in a container you usually see only symptoms: the container status, a couple of log lines, and a colleague’s puzzled look. So we do not need just any logs; we need logs that shorten the path from “it does not work” to “ah, that is why”.

The most common beginner failure looks like this: the application honestly writes something to the console, but that text answers none of the practical questions. In the spirit of “operation failed” or “start ok”. There is a log… but it does not save any time at all. And if there are many logs, they also hurt, because the important message drowned among ten thousand “well, I was just spinning in a loop a little”.

To make logs useful, it helps to keep a simple criterion in mind: every important line should answer at least three questions. What happened? With what happened (which object, id, file, sku, profile)? How did it end (success, skip, error)? It is like a receipt in a store: “what was bought” + “how much” + “total”. A receipt does not tell the whole story of your life — it records a fact. A good log does the same.

You can imagine the path of a log as a very down-to-earth chain:

flowchart LR
    A["Code in the service
log.info(...)"] --> B["Logger (SLF4J)"] B --> C["Console output
stdout/stderr"] C --> D["docker logs"] D --> E["Developer reads
and draws a conclusion"]

If the developer cannot draw a conclusion at the end of the chain, then the problem is not Docker and not a “bad environment”, but the quality of the message.

2. Logger instead of println: no snobbery

At the beginning of the journey, you really want to do it like in Java textbooks: System.out.println("hello"). It really does work and it shows the basic mechanism well: the process writes to stdout/stderr, and Docker collects it. But in a real service, println is like writing “everything is fine” on a sticky note and taping it to the monitor: it looks lively, but you cannot manage it. You need levels (INFO/WARN/ERROR), a format, the ability to turn off the chatter without rewriting code, and proper exception handling.

In Spring Boot, the basic path is almost always this: we use SLF4J (org.slf4j), and inside Boot Logback is used by default. As a result, we write code through Logger, and the infrastructure decides on its own how to print it to the console (which means — how it gets into docker logs).

The minimal logging “skeleton” in a service looks like this:

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

public class ExportService {
    // A logger is usually made static final: one instance per class, without recreating it on every call
    private static final Logger log = LoggerFactory.getLogger(ExportService.class);
}

And there is an important idea here: a logger is almost always made static final so that it is not created again on every call. It is a small detail, but it reflects the overall approach: logging is a system mechanism, not “well, we will print a couple of lines”.

Another thing worth accepting right away: logs in Spring Boot are not the place where you “write a string”. They are the place where you record an event. So from here on we will write messages with context, not novels.

3. Levels INFO, WARN, ERROR

Logging levels sound pompous, but in essence they are just an agreement on “how important is this and how bad is it”. The problem is that a beginner developer usually has two extremes. The first is logging everything with INFO, because “well, I want to see it”. The second is setting everything to ERROR, because “well, it is a problem, I am worried”. And in both cases the logs become useless: either too noisy, or everything looks like a fire, even when it is a normal situation.

For our course, it is enough to confidently use three levels:

Level When it is appropriate How it sounds in plain English
INFO an important normal event “everything is on schedule, remember the fact”
WARN a strange/suspicious situation, but the service keeps running “I do not like it, but I am alive”
ERROR the operation failed, the path is broken “this is a real error, action is needed”

If you want a more mechanical rule (like a reminder when the brain is tired), here is a simple selection scheme:

flowchart TD
    A[An event in the application] --> B{Is this an expected normal situation?}
    B -->|Yes| C{Is it important for understanding the life of the service?}
    C -->|Yes| I[INFO]
    C -->|No| S[Do not log]
    B -->|No| D{Does the service keep running?}
    D -->|Yes| W[WARN]
    D -->|No / operation failed| E[ERROR]

This scheme is useful because it lets you honestly not log part of the events. Logs are not an airplane black box where you need to record every processor breath. Logs are a diagnostics tool. If an event does not help diagnostics and does not record an important fact, it often should not go to the console.

4. Log message: context and result

When people say “make the log useful”, it sounds like a motivational poster. Let us make it concrete: usefulness usually breaks because of a lack of context. The message “export failed” is almost useless. But “export failed: file=catalog-2026-03-21.csv” is already concrete. In container diagnostics you almost always think in terms of objects: which file, which item id, which profile, which mode.

A very convenient technique is to keep the structure of the message in your head like a template:

Event → context → result.

For example, for export:

- event: export started,
- context: file name and directory,
- result: how many elements were exported and how long it took (if appropriate), or what exactly failed.

To feel the difference, let us compare “bad” and “better” in a small table:

Bad Better Why it is better
Export started Export started: file={} it is clear which file
Failed Export failed: file={} + exception there is context and a stack trace
No items Export skipped: no catalog items (WARN) it is clear that this is not a crash, but a skip

And here is what matters: in Spring Boot (through SLF4J) we usually use parameterized messages with {}. This is not a “whim” but standard practice.

// {} is an SLF4J placeholder: the value will be substituted safely and consistently
log.info("Export started: file={}", fileName);

Why is this better than concatenation?

// Concatenation makes the code noisier and forces you to build the string manually
log.info("Export started: file=" + fileName);

Parameterization makes the message standardized, readable, and, which is nice, saves extra computations when the logging level is disabled. You do not need to “sell” this optimization to yourself right now, but the pleasant bonus is that this style is the baseline for normal projects.

5. Startup logs: the first 5 seconds

Application startup is the moment when logging brings maximum benefit. It is at the beginning that configuration problems most often appear: the wrong profile, the wrong port, the wrong export directory, the wrong environment variables. And it is at startup that you usually look at container logs first. So it is useful to decide in advance: which 2–4 facts do we want to see when the service has “come up”.

Spring Boot itself writes a lot of startup logs, and that is normal. But in our project there are several domain and infrastructure things that we must mention explicitly. For example, which profiles are active (so we do not have to guess “why did it go into the database, I wanted standalone”), and which export path is actually used (especially after we started working with mounts).

The simplest training approach is to make a small component in the ops package that reacts to the “application ready” event and writes one neat line. No fanfare: one line — one fact.

Example of a minimal startup logger:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;

@Component
public class StartupLogger {
    private static final Logger log = LoggerFactory.getLogger(StartupLogger.class);

    // Runs once when Spring Boot is fully up and ready to accept requests
    @EventListener
    public void onReady(ApplicationReadyEvent event) {
        // This is a "readiness marker" in docker logs: it is immediately clear that the service reached a normal state
        log.info("Catalog service is ready to accept requests");
    }
}

Yes, this is still a “weak” log: it does not contain profiles and parameters. But it is already useful because in the container logs you see a clear marker that “we reached the ready state”. And then we can add context, but carefully: without outputting secrets and without printing “the whole world”.

If we want to log profiles, we usually use Environment. In the example below, note: we do not log the entire configuration, we log one short, safe fact.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;

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

    public static void logProfiles(Environment env) {
        // We take only the active profiles: this is safe and helps quickly understand "which mode" we started in
        String profiles = String.join(",", env.getActiveProfiles());
        log.info("Active profiles: {}", profiles); // Active profiles: standalone
    }
}

This level of detail at startup is already enough so that you do not have to play guessing games in Docker logs.

6. Business export logs: start, skip, success

Exporting a catalog to a file is our very “container-like” scenario: it touches the file system, depends on the export directory, and is easy to break because of paths or permissions. So it is the ideal place to show the “correct” set of logs: operation start, non-standard situation without a crash, successful completion, and an error with an exception.

Let us start with the beginning of the operation. It is important for us to record the file name (context), because later the student will look for this file on the host machine or check the mount.

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

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

    public void export(String fileName) {
        // Important: record the context (which file) so we can later match the log with the real result on the file system
        log.info("Export started: file={}", fileName);
    }
}

Now let us add a “non-standard but not fatal” situation: there is nothing to export. This is not an application error, the service is alive, but the event is useful to see, because otherwise the user will wait for the file and think “it did not appear, everything is broken”.

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

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

    public void exportIfAny(int itemsCount) {
        if (itemsCount == 0) {
            // WARN: an atypical situation, but the service keeps living
            log.warn("Export skipped: no catalog items");
            return;
        }
        // INFO: an important fact about the normal course of the operation
        log.info("Export will proceed: items={}", itemsCount);
    }
}

Pay attention to the meaning of the level: WARN here is not “oh no, fire”, but “the situation is suspicious/atypical”. For a teaching service, this is a normal model: the user requested export, but the catalog is empty — we warn.

Successful completion is also an important fact. But even here there is no need to turn the log into an “export dissertation”. Briefly: which file and how many items.

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

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

    public void finish(String fileName, int itemsCount) {
        // On success, record the minimum: where we wrote and how many we exported
        log.info("Export finished: file={}, items={}", fileName, itemsCount);
    }
}

And finally — an error. Here it is critical not just to write “failed”, but to add context and pass the exception so that the logs contain a stack trace. In container diagnostics, this is often the only chance to understand the root cause.

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;

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

    public void handleError(String fileName, IOException ex) {
        // Important: pass ex as a separate parameter — then the stack trace will appear in the log
        log.error("Export failed: file={}", fileName, ex);
    }
}

Such a line usually gives you both clear text and the full error stack. That means: when the container is “alive, sort of”, but export does not create a file — you will not guess, you will see exactly what went wrong.

7. Errors and log configuration

Error logging: important and unnecessary

Errors are the “most expensive” part of logging, because that is where we most often look for the truth. But with errors it is easy to make two typical mistakes. The first is to log an error without the exception, leaving only ex.getMessage(). The second is to catch an exception, log it, and… continue as if nothing happened, creating a semi-working application that later takes revenge with strange symptoms.

The rule is fairly simple: if you catch an exception and consider it important, you almost always should log it with the exception object so that the stack is preserved. It looks like this:

log.error("Export failed: file={}", fileName, ex);

But this is usually worse, because the stack is lost (or will be too poor):

log.error("Export failed: file={}, message={}", fileName, ex.getMessage());

The second important part is not to log unnecessary things and not to log dangerous things. For example, it is very tempting to print the datasource URL “just to check” at startup. But if you log the URL together with the password (or token), you yourself will create a security hole. In a learning project we deliberately keep discipline: logs should help diagnostics, but not become a “confidentiality leak”.

If you still need context, look for a safe option. For example, you can log the fact “we are working in postgres mode”, but do not log the password, do not log the full connection string with secrets, and do not log the contents of files that we export.

Logging level through configuration

The nicest side of a logger, compared to println, is that you can control message detail through configuration. That is, you do not rewrite the code “let me add 20 more lines for debugging”, but change the logging level for the duration of the diagnosis.

In Spring Boot this is usually done via logging.level.* in application.yml. For our project, a sensible baseline is to keep the normal level at INFO for our code.

spring:
  application:
    # The application name often appears in logs/metrics and helps distinguish services in infrastructure
    name: catalog-service

logging:
  level:
    # Baseline log level for our code: we see important facts, but do not drown in noise
    com.example.catalog: INFO

If something is “too noisy”, you can lower the level precisely for a specific package or class. Conversely, if you are temporarily debugging export and want more details, you raise the level for com.example.catalog.export (for example, to DEBUG) — and then switch it back, without leaving “lifetime debug” in the code.

It is important to remember discipline here: the logging level is a lever, not a crutch. If you need to log an important fact in normal mode, it should be on INFO/WARN/ERROR. And if it is “temporary debugging to understand”, then it is a candidate for a lower level and enabling through config. We do not make the service permanently chatty, otherwise any real signal will be lost.

8. Typical mistakes

Mistake #1: “Logs for the sake of logs” — messages without context.
Beginners often write something like export started and consider the task done. But without the file name and the result, you will not be able to connect the message with the real effect on the file system later. It is better to make messages that record the operation object: file, items, id, sku.

Mistake #2: turning everything into ERROR because “I am worried”.
If an empty catalog is logged as ERROR, you create a false sense of a crash. After a couple of weeks, such ERROR entries stop being read, like “false alarms”. The normal scale is simple: INFO — important facts, WARN — strange, but we live, ERROR — the operation path is broken.

Mistake #3: log an error without the exception and lose the stack.
The phrase “Export failed” without a stack trace is almost like “I feel bad” without describing the symptoms. In a container you cannot always quickly jump into a debugger, so the exception stack is often the main source of truth. Use log.error("...", ex), not just ex.getMessage().

Mistake #4: printing secrets and sensitive parameters “for convenience”.
It is very tempting to output passwords, tokens, or the full connection string into the log “for a minute”. But logs often live longer than your minute and fall into the wrong hands faster than you can say “oops”. Keep only safe facts in logs: profiles, mode, file name, entity identifiers — without secrets.

Mistake #5: noisy logs as the permanent default.
If a service writes ten lines for every HTTP request and one more for every catalog item, the logs turn into background noise that nobody reads. A useful log is not the one that is “frequent”, but the one that “gives meaning”. Let important events be noticeable, not drowned in constant chatter.

1
Zadanie
Docker for Spring, poziom 14, lekcja 1
Niedostępne
Znaczące `INFO`, `WARN` i `ERROR`
Znaczące `INFO`, `WARN` i `ERROR`
1
Zadanie
Docker for Spring, poziom 14, lekcja 1
Niedostępne
Startowy log aplikacji
Startowy log aplikacji
Komentarze
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION