JavaRush /Курси /Spring Boot /Логування: SLF4J і ...

Логування: SLF4J і стек Boot

Spring Boot
Рівень 20 , Лекція 1
Відкрита

1. SLF4J: пишемо логи через інтерфейс

Якщо ви колись обирали навушники, то знаєте цей біль: купили «під один розʼєм», а потім зʼясувалося, що у вас інший телефон. З логуванням усе так само: якщо ви пишете код «під конкретну бібліотеку», згодом стає складніше змінювати реалізацію, під’єднувати сторонні залежності й просто жити далі. Тому в Java-світі давно перемогла ідея: «логуй через спільний API».

SLF4J (Simple Logging Facade for Java) — це як «розетка стандарту». Вона не є «логером, який пише в консоль». Вона є спільним інтерфейсом, через який прикладний код надсилає повідомлення в систему логування, а вже конкретна реалізація (наприклад, Logback) вирішує, куди ці повідомлення потраплять: у консоль, у файл, у систему збирання логів тощо.

Важливий момент для розуміння, особливо для новачків:

- SLF4J — це API, тобто набір інтерфейсів і домовленостей, якими користується ваш код.
- Logback та інші — це двигуни, які реально записують логи кудись назовні.

Щоб не плутати терміни, можна тримати в голові таку мінітаблицю:

Шар Що це Приклад Навіщо потрібно
API (фасад) Як ми викликаємо логування з коду org.slf4j.Logger Не прив’язувати код до конкретної реалізації
Реалізація Як логування реально виконується Logback Форматування, виведення, фільтри тощо

А тепер головний практичний наслідок: у коді catalog-service ми логуємо через org.slf4j.Logger, і це вважається нормою для сервісів Spring Boot.

А тепер перенесімо цю ідею в звичайний Java-клас: як виглядає логер, який далі житиме в сервісах, runnerʼах і контролерах.

2. Logger і LoggerFactory: заводимо логер у класі

Коли ви бачите в проєкті рядок private static final Logger log = ..., це не «ритуал із секти Spring». Це просто акуратний спосіб створити в класі один об’єкт-логер і далі користуватися ним у всіх методах. Гарна новина: логер — це взагалі не «Spring-річ». Він працює в будь-якому Java-класі, незалежно від того, @Service це чи звичайний утилітний клас.

Мінімальний робочий шаблон виглядає так:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;

@Service
public class CourseCatalogService {

    // Логер створюємо один раз на клас: зазвичай його імʼя збігається з повним імʼям класу
    // static — не потрібен окремий логер для кожного об’єкта; final — не перевизначаємо його під час виконання
    private static final Logger log =
            LoggerFactory.getLogger(CourseCatalogService.class);
}

Тут є одразу кілька важливих «чому так»:

Logger — це інтерфейс SLF4J. Ми не пишемо new Logger(...) (і не можемо), бо конкретний клас логера надає вибрана реалізація.

LoggerFactory — це фабрика, яка видає логер для конкретного класу. Зазвичай логер «називається» повним імʼям класу, включно з пакетом. Це потім дуже допомагає фільтрувати повідомлення — ми ще скористаємося цим трохи пізніше, а поки просто запам’ятаймо: логер прив’язаний до імені класу.

Поле static final — майже стандарт індустрії. static, бо логер не зобов’язаний бути окремим для кожного об’єкта; final, бо він не має змінюватися, як настрій у понеділок зранку.

Правило «один клас — один логер»

Логер створюється один раз на клас і використовується всюди всередині цього класу. Не створюйте логер усередині кожного методу. Це додає шуму в код і виглядає так, ніби ви друкуєте логи «в паніці». Спокійно: один логер — і все.

Мініприклад у нашому сервісі

Припустімо, що в CourseCatalogService є пошук курсу за slug. Нам потрібен лог, який пояснює дію і показує ключовий параметр (slug).

import com.example.catalogservice.catalog.domain.CourseCard;

public CourseCard findBySlug(String slug) {
    // {} — плейсхолдер SLF4J: параметри передаються окремими аргументами, без конкатенації рядків
    log.info("Пошук курсу за slug={}", slug);
    return repository.findBySlug(slug);
}

Зверніть увагу на {} — це не «магія Spring», а параметризоване повідомлення SLF4J. Ми розберемо його докладніше трохи далі, але вже зараз можна запамʼятати просту думку: не склеюємо рядки через +, а використовуємо {}.

Логер у контролері: обережно з INFO

Логер можна заводити і в @RestController. Але логувати кожен запит на INFO — погана ідея: привіт, нескінченний шум. На старті логер у контролері потрібен рідко: частіше корисніше логувати бізнес-події в сервісі та стартове зведення у runnerʼі. Але технічно шаблон той самий:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class CourseCatalogController {

    // Той самий принцип: один логер на клас, щоб логи були стабільно «прив’язані» до контролера
    private static final Logger log =
            LoggerFactory.getLogger(CourseCatalogController.class);
}

3. Spring Boot: Logback, мости і формат

У Spring Boot є дуже приємна річ: якщо ви підключили «нормальний» starter (наприклад, spring-boot-starter-webmvc), то логування у вас вже працює, навіть якщо ви нічого не налаштовували. Це важливо методично: вам не треба починати кожен проєкт із «а давайте тиждень обирати бібліотеку логування». Boot каже: «Ось дефолт, він адекватний. Пишіть код».

У нашому курсі це особливо важливо, бо catalog-service — навчальний шаблон. Ми хочемо, щоб він стартував «з коробки» і логував «як прийнято», без зайвої творчості.

Звідки береться Logback

Spring Boot за замовчуванням приносить стек логування через залежність spring-boot-starter-logging, яка зазвичай підтягуюється транзитивно, тобто «приходить сама», разом з іншими starterʼами.

У результаті у вас зʼявляються, якщо спростити, такі ролі в системі:

Компонент Роль Що дає нам як розробникам
slf4j-api спільний API ми пишемо log.info(...), не думаючи про конкретний двигун
Logback реалізація виведення в консоль, формат, фільтрація, кольорові рівні під час розробки
мости (bridges) «перекладачі» навіть якщо бібліотека логує не через SLF4J, її повідомлення можна звести до єдиного стилю

Про мости варто сказати кілька слів, щоб не виникало відчуття «що за зоопарк».

У світі Java-бібліотек частина коду може писати логи через java.util.logging (JUL), хтось історично — через Log4j, хтось — через ще щось. Boot намагається зробити так, щоб ви в підсумку бачили єдиний потік логів, а не п’ять різних форматів одночасно. Тому він часто підтягує «перекладачів», які перенаправляють різні logging API до SLF4J, а вже потім усе пише Logback.

Якщо коротко й по-людськи: Spring Boot робить так, щоб усі в проєкті «розмовляли» через SLF4J, а друкував це Logback.

Формат рядка логу за замовчуванням

Ви побачите в консолі рядки приблизно такого вигляду (приклад умовний, формат може трохи відрізнятися залежно від налаштувань середовища):

2026-03-19T09:10:02.531  INFO 12345 --- [main] c.e.c.catalog.service.CourseCatalogService : Пошук курсу за slug=spring-boot

Навіть якщо ви поки не вмієте «читати» такі рядки як книгу, уже корисно помітити кілька речей: у повідомлення є час, рівень, потік, імʼя логера (зазвичай клас) і текст. Це робить логи діагностичним інструментом, а не «просто текстом у консолі».

І найважливіший висновок для нашої лекції: щоб отримати такий лог, вам не треба писати XML-конфігурацію, не треба під’єднувати Logback вручну і не треба нічого «ініціалізувати». Вам достатньо написати Logger log = LoggerFactory.getLogger(...) і викликати log.info(...).

4. Параметризовані повідомлення: {} замість конкатенації

У новачків є природна звичка: «щоб вивести значення, треба склеїти рядок». У логах це виглядає так:

// Поганий стиль: рядок збирається вручну, і легко почати «нарощувати» повідомлення до нечитабельності
log.info("Кількість рекомендованих курсів=" + featured.size());

Працює? Так. Гарно? Не дуже. Масштабується? Як табуретка в ролі літака: наче сидіти можна, але летіти страшно.

SLF4J пропонує нормальний стиль: шаблон повідомлення + параметри. Параметри вставляються на місця {}.

// Гарний стиль: подія + ключовий факт окремим параметром
log.info("Рекомендовані курси підготовлено, кількість={}", featured.size());

Чому це краще, окрім естетики?

По-перше, повідомлення стає читабельнішим. Оком простіше вхопити «що сталося», а потім — «які значення».

По-друге, це зменшує спокусу напхати в лог півоб’єкта через конкатенацію. Ви починаєте обирати конкретні значення: count, slug, track.

По-третє, у параметризованих логів є оптимізація: якщо рівень логування вимкнено (наприклад, DEBUG), то часто рядок не буде збиратися взагалі. Для новачка це не головний аргумент, але приємний: ви не платите продуктивністю за «невидимі» логи.

Приклад: логуємо результат без «логів заради логів»

Візьмімо наш StartupSummaryRunner. Ми хочемо повідомити два факти: які профілі активні та скільки курсів завантажено. Не треба друкувати весь CatalogProperties цілком — це буде і шумно, і небезпечно, і марно.

import java.util.Arrays;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.env.Environment;

public void logStartupSummary(Environment env, int coursesCount) {
    // Профілі — короткий і корисний контекст: одразу видно, яка конфігурація реально піднялася
    log.info("Активні профілі: {}", Arrays.toString(env.getActiveProfiles()));

    // Ще один сухий факт: скільки сутностей вдалося завантажити на старті
    log.info("Курсів завантажено: {}", coursesCount);
}

Тут {} допомагає нам тримати повідомлення короткими й стабільними: «ключ = значення».

Кілька {} поспіль — це нормально

Іноді потрібно вивести два-три параметри. Це нормально, доки ви не перетворюєте лог на роман.

// Нормально виводити 2–3 ключові параметри, якщо це допомагає зрозуміти стан системи
log.info("Каталог завантажено, курсів={}, режимОбслуговування={}",
        properties.courses().size(),
        properties.maintenanceMode());

Подивіться, як виходить компактно: подія (Catalog loaded) і два ключові факти.

Але одного факту log.info(...) замало. У якийсь момент стає важливим уже не тільки те, як написати повідомлення, а й які з них тримати перед очима постійно, а які вмикати лише для розслідування.

5. Винятки в логах: як зберегти контекст і stack trace

Коли застосунок падає, найдорожча річ у логах — це stack trace (трасування стека). Це та сама простиня, яку спочатку хочеться закрити, а потім виявляється, що без неї ви не розумієте, де саме все зламалося. Тому «правильне логування винятку» — це не пафос, а буквально ваш шанс не перетворити діагностику на ворожіння на кавовій гущі.

Класична помилка новачка виглядає так: спіймали виняток і залогували лише текст.

catch (RuntimeException ex) {
    // Поганий стиль: у лог потрапляє лише текст, а stack trace втрачається
    log.error("Помилка пошуку курсу: " + ex.getMessage());
    throw ex;
}

Проблема тут не в + (хоча й у ньому теж), а в тому, що ви викинули головне: stack trace. Текст ex.getMessage() часто буває коротким, іноді взагалі null, і майже ніколи не каже, де саме зламалося.

Правильний базовий стиль у SLF4J: повідомлення + контекст + об’єкт винятку останнім аргументом.

public CourseCard safeFindBySlug(String slug) {
    try {
        // Прагнемо виконати основну операцію як звичайно
        return repository.findBySlug(slug);
    } catch (RuntimeException ex) {
        // У повідомлення додаємо контекст (slug), а виняток передаємо останнім аргументом — так потрапить stack trace
        log.error("Помилка пошуку курсу, slug={}", slug, ex);
        throw ex;
    }
}

Що тут відбувається:

- Повідомлення пояснює подію: «Помилка пошуку курсу».
- Контекст показує важливий параметр: slug.
- ex передано останнім аргументом, і тому в лог потрапить stack trace.

Значення для catalog-service

Наш сервіс поки лише для читання і без бази, але навіть у такому проєкті винятки трапляються: зламана конфігурація, невалідний slug, помилка під час завантаження списку курсів, випадковий NullPointerException у логіці фільтрації. Якщо ви логуєте виняток правильно, то швидко зрозумієте, «яке місце в коді винне», навіть якщо проблему можна відтворити лише на машині колеги.

І ще один важливий нюанс, який варто запамʼятати вже зараз: не треба логувати виняток у кожному шарі підряд. Якщо контролер, сервіс і репозиторій усі троє залогували один і той самий ex, ви отримаєте три однакові stack trace і гортатимете їх, як «день бабака». Ми докладно говоритимемо про шум і дублікати пізніше, але мінімальне правило можна прийняти вже зараз: логуйтесь виняток там, де у вас зʼявляється найкращий контекст і де ви справді ухвалюєте рішення, що це помилка.

6. Типові помилки під час логування

У цій темі найчастіше ламаються не на «як написати імпорт», а на звичках із консольних застосунків. І це нормально: майже всі ми спочатку пишемо println, потім учимося писати логи, потім учимося писати логи так, щоб їх не хотілося видалити. Нижче — помилки, які варто відловлювати у себе відразу, поки проєкт маленький.

Помилка № 1: використовувати не той Logger.
В IDE легко випадково імпортувати java.util.logging.Logger, бо він теж називається Logger, і мозок каже: «Ну логер і логер». У нашому Boot-проєкті базова лінія — org.slf4j.Logger. Якщо ви оберете JUL-логер, то почнете жити в паралельній реальності з іншим API і іншим поведінкою.

Помилка № 2: створювати логер усередині методу.
Іноді трапляється код виду Logger log = LoggerFactory.getLogger(getClass()) просто всередині findBySlug. Працювати буде, але виглядає так, ніби ви щоразу наново купуєте мікрофон, щоб сказати одне слово. Логер створюється один раз на клас: private static final Logger log = ....

Помилка № 3: логувати «done», «ok», «here».
Такі повідомлення дають ілюзію «я щось бачу в консолі», але не відповідають на жодне діагностичне запитання. Гарний лог майже завжди називає подію і показує один-два факти: slug, count, maintenanceMode, profiles. Тоді за місяць ви зможете читати логи без телепатії.

Помилка № 4: втрачати stack trace, логуючи лише ex.getMessage().
Ця помилка особливо підступна: здається, що ви «все залогували», адже в логові є слово «failed». Але без stack trace ви не знаєте, де саме впало і який шлях до цього привів. Правильна форма: log.error("...", ex) або log.error("..., slug={}", slug, ex).

Помилка № 5: думати, що «для логів потрібен окремий конфіг-файл, інакше не можна».
У Spring Boot базовий стек уже є і вже налаштований. На рівні цієї лекції вам не потрібно писати logback.xml і не потрібно налаштовувати appenders. Завдання зараз простіше: почати логувати через SLF4J і звикнути до акуратного формату повідомлень. Коли у вас зʼявиться реальна причина ускладнювати конфігурацію, ви зробите це усвідомлено, а не «тому що так у статті було».

Коментарі
ЩОБ ПОДИВИТИСЯ ВСІ КОМЕНТАРІ АБО ЗАЛИШИТИ КОМЕНТАР,
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ