Week 13

Observability introduction

Logging in Spring Boot

Structured Logging

Shipping Logs to the Cloud

Debugging with Logs

Health & Metrics

Appendix: Alerting (optional)

Practice

Assignment

Backend Track

Your app already logs

Every time you start your Spring Boot application, it prints dozens of lines: the banner, "Started Application in 3.2 seconds". That output is not System.out — it comes from a real logging system that has been running under your feet since Week 4. Spring Boot uses SLF4J (the logging interface your code talks to) with Logback (the engine that formats and delivers the lines). You get both for free with every starter - there is nothing to install.

This chapter teaches you to use that system on purpose: write your own log lines, give each one the right importance level, and control what appears — per package — from application.yaml.

Watch: Intro to Logging by Grafana

The following video is a great refresher to logging

https://www.youtube.com/watch?v=TLnH7efQNd0

In our track, we will focus only on Application logging.

Writing your first log line

Get a logger at the top of a class, then call it wherever something worth recording happens:

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

@Service
public class TaskService {

    private static final Logger log = LoggerFactory.getLogger(TaskService.class);

    public Task createTask(NewTaskRequest request) {
        log.info("Creating task with title={}", request.title());
        // ...
    }
}

Two details matter here:

log.info("User {} created task {}", userId, taskId);      // ✅ do this
log.info("User " + userId + " created task " + taskId);  // ❌ avoid

Placeholders are cleaner to read, and the message is only assembled if that log line is actually enabled — concatenation does the string work even when nobody is listening.

Shortcut with Lombok

writing that private static final Logger log = … line in every class gets repetitive. Since you already use Lombok, you can replace it with the @Slf4j annotation on the class — Lombok generates the exact same field, named log, for you:

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class TaskService {

    public Task createTask(NewTaskRequest request) {
        log.info("Creating task with title={}", request.title());
    }
}

No import of Logger/LoggerFactory, no field declaration — just log, ready to use. Every example in this chapter uses the plain version so you can see what is really happening, but @Slf4j is the form you will write in practice.

<aside> ⚠️

System.out.println still works, but it is invisible to everything you learn this week: no level, no timestamp, no class name, no way to switch it off per package. From now on, if it is worth printing, it is worth logging.

</aside>

Levels: how loud is this message?

Every log line has a level — a statement of how much attention it deserves:

The level does two jobs at once. It tells the reader how serious the line is — and it lets you filter: a logging system shows only lines at or above a chosen level. Set the level to INFO and every DEBUG line vanishes without touching the code.

image.png

<aside> 💭

Some systems also use the "CRITICAL" log level. As you can imagine, it is above "ERROR" and requires immediate action.

</aside>

Controlling levels in application.yaml

Here is the payoff of named loggers. Try this experiment in your own project. Set the root level to DEBUG:

logging:
  level:
    root: DEBUG

Start the app and watch the flood: hundreds of lines from Spring's own internals — bean creation, request mapping, connection pool chatter. Your own log lines are in there somewhere, drowning.

<aside> 💭

More logging is not always better logging.

</aside>

Now express what you actually want — quiet framework, talkative you:

logging:
  level:
    root: INFO
    net.hackyourfuture.myapp: DEBUG

Loggers are named after classes, and classes live in packages — so you can set a level for any package prefix. Framework code stays at INFO, while every class under your own package speaks at DEBUG. Replace net.hackyourfuture.myapp with your project's base package.

<aside> 💡

This is a dial you will turn in real jobs: production usually runs at INFO, and when something needs investigating, you raise one specific package to DEBUG — not the whole world.

</aside>

Logging exceptions properly

When you log an error, pass the exception object as the last argument, after the placeholders — with no {} for it:

try {
    importFile(filename);
} catch (IOException e) {
    log.error("Failed to import file {}", filename, e);
}

Logback prints the full stack trace under your message. Compare that with log.error("Failed: " + e) — which throws the stack trace away and keeps only the exception's one-line summary. In the next chapters, that stack trace is the single most valuable thing in your logs; never truncate it.

What to log and what never to log

Good log lines share a shape: event + context. "Task created" is useless on its own; "Task 42 created by user 7" lets you follow a story. Log the moments that matter: important business events, decisions your code takes, everything odd, and every failure.

And some things must never appear in a log — no matter the level:

log.info("Login attempt: email={} password={}", email, password);  // ❌ never
log.info("Login attempt for userId={}", userId);                   // ✅ an ID is enough

<aside> ❗

Logs are stored, copied, and read by many people and systems — they are one of the most common places sensitive data leaks. Under the GDPR, personal data in logs is a real legal problem for a company, not just bad style. When in doubt, log an ID, never the data itself. This is the same discipline you practised when handling sensitive data in Week 10 — now applied to log lines.

</aside>

Extra resources


The HackYourFuture curriculum is licensed under CC BY-NC-SA 4.0 *https://hackyourfuture.net/*

CC BY-NC-SA 4.0 Icons

Built with ❤️ by the HackYourFuture community · Thank you, contributors

Found a mistake or have a suggestion? Let us know in the feedback form.