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 і звикнути до акуратного формату повідомлень. Коли у вас зʼявиться реальна причина ускладнювати конфігурацію, ви зробите це усвідомлено, а не «тому що так у статті було».
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ