1. Роль логів у бекенд-сервісі
Швидкий цикл зворотного зв’язку корисний, поки ви поруч із застосунком і можете одразу перезапустити його з IDE. Але у сервісу є ще одне життя: він працює сам по собі, а розбиратися з ним доводиться вже постфактум. У цей момент потрібен не ще один println, а постійний діагностичний канал — логи.
Коли ви пишете консольний застосунок, здається, що найприродніший спосіб дізнатися «що відбувається» — поставити точку зупинки й подивитися на змінні. У сервісі все швидко ускладнюється: запити надходять паралельно, сервіс працює довго, а проблема часто проявляється саме тоді, коли ви… вже пішли пити чай. Дебагер у цей момент не допомагає: він корисний, коли ви поруч і все контролюєте. Сервіс же — штука самостійна.
Уявіть типову ситуацію: catalog-service запущено не з IDE, а як звичайний процес (нехай навіть локально). Він стартував, попрацював кілька годин, хтось відкрив /api/catalog/featured, а потім раптом «щось стало дивно»: на сторінці порожньо, хоча курси точно є. Ви не зможете «повернутися назад у часі» й подивитися стан у момент, коли він став дивним. Зате ви майже завжди зможете відкрити логи й побачити ланцюжок подій: сервіс стартував, скільки курсів завантажив, у якому режимі працював, які важливі гілки виконував.
Саме в цьому й полягає головна роль логів: вони фіксують історію життя сервісу. Лог можна уявити як «чорну скриньку» літака, тільки без драматичної музики на тлі. Дебагер — це мікроскоп у руках лікаря. Логи — це медична картка пацієнта, яку читають, коли пацієнт уже вдома, а ви не можете знову «увімкнути» той самий момент.
2. System.out.println і його пастки
System.out.println справді здається зручним. Він завжди під рукою, не потребує імпортів, можна нашвидкуруч додати кілька рядків і відразу побачити, що відбувається всередині методу. Для навчальних задач це нормально: ви вивчаєте Java і вам важливо швидко побачити результат. Але щойно ви пишете бекенд-сервіс, println починає працювати проти вас, бо він не про діагностику, а про «швидко щось вивести».
Головна проблема навіть не в тому, що println «поганий» (він не поганий, він просто не для цього). Проблема в тому, що println не дає вам нормальних інструментів керування: ви не можете легко відрізняти важливі повідомлення від другорядних, фільтрувати виведення для конкретного пакета, вмикати й вимикати деталізацію без правок коду, додавати системний контекст (час, потік, джерело) єдиним чином. У підсумку сервіс перетворюється на «балакучого сусіда»: він говорить багато, але незрозуміло, чи по суті.
Давайте порівняємо це більш предметно:
| Характеристика | System.out.println | Логування (logging) |
|---|---|---|
| «Важливість» повідомлення | Немає: це просто рядок | Є рівні важливості — або хоча б сама ідея «важливо / неважливо» |
| Джерело повідомлення | Його доводиться вписувати вручну | Зазвичай видно, який клас це написав |
| Формат | Як ви склали рядок, так і буде | Є єдиний формат рядка логу: час, рівень, потік тощо |
| Керування шумом | Майже відсутнє | Можна «приглушувати» або «посилювати» деталі без правок бізнес-коду |
| Робота в багатопоточності | Повідомлення легко перемішуються | Зазвичай хоча б видно потік, і можна групувати за джерелом |
| Життєвий цикл | Часто залишається в коді «назавжди» | Логи — частина дисципліни, їх пишуть свідомо |
І ще одна тонкість: println дуже легко перетворюється на «саморобний протокол», який ламається від будь-якого рефакторингу. Ви пишете System.out.println("here 1"), а через тиждень забуваєте, що таке «here 1», і починаєте археологію в репозиторії. Так, археологія — корисна навичка, але краще нехай це буде факультатив, а не щоденна рутина.
Мініприклад «як зазвичай роблять на старті»:
public void start() {
// Погана звичка: println не дає рівня важливості й не додає контекст автоматично
System.out.println("запущено"); // запущено
}
Виведення буде, але воно не відповідає на питання «хто запустився? що саме запустилося? це нормально чи помилка? коли саме?». Для живого сервісу цього замало.
3. Подія, контекст і важливість у логах
Щоб лог був корисним, він має бути не «потоком думок розробника», а описом подій сервісу. Хороше лог-повідомлення зазвичай робить три речі: називає подію, додає контекст і показує важливість. Саме тому логування — це дисципліна: ви не просто друкуєте текст, а залишаєте слід, який потім хтось (у тому числі й ви через місяць) читатиме й намагатиметься зрозуміти, що сталося.
Подія — це відповідь на питання «що сталося?». Контекст — це «з якими значеннями / у якому режимі?». Важливість — це «наскільки це серйозно?». Навіть якщо ви поки не дуже впевнено відчуваєте рівні важливості (ми ще будемо розвивати їх далі), сама ідея важливості відразу робить лог читабельнішим: різні події мають звучати по-різному.
Поки вважайте log просто робочим об’єктом логування, а {} — способом підставити значення без склеювання рядків. Тут нам важлива сама форма повідомлення: подія й факт.
Подивіться на різницю повідомлень:
// Погано: нічого не говорить про подію
log.info("готово");
// Краще: говорить, що саме зроблено, і показує результат
// {} — плейсхолдери; значення передаються окремо, без конкатенації рядків
log.info("Featured courses prepared, count={}", featured.size());
Навіть якщо ви не знаєте нічого про внутрішню кухню логування, ви вже відчуваєте: друге повідомлення можна читати як частину історії, перше — як випадкову репліку.
Ще один важливий момент: лог не має переказувати кожен рядок реалізації. Якщо метод робить 12 кроків, а ви залогуєте всі 12 на одному рівні, ви отримаєте «роман у 12 томах», який ніхто не дочитає. У логах хочеться бачити опорні точки: початок важливого процесу, підсумок важливого процесу, підозрілий стан, помилку.
Саме тому в catalog-service доречні повідомлення на кшталт «каталог завантажено, курсів стільки-то» й «featured-добірку сформовано», а ось «зараз я роблю stream().filter()» — недоречне. Stream — це деталь реалізації, а не подія домену.
4. Що логувати в catalog-service
Коли ви починаєте логувати, найчастіша помилка — або логувати все підряд («про всяк випадок»), або не логувати майже нічого («потім розберемося»). У навчальному проєкті хочеться знайти золоту середину: залишити достатньо сигналів, щоб сервіс можна було пояснити й діагностувати, але не перетворювати логи на білий шум.
У catalog-service дуже природна перша точка логування — старт і завантаження даних каталогу з конфігурації. У нас дані надходять із CatalogProperties, тож під час старту корисно підтвердити, що конфіг справді прочитано й зв’язано так, як ми очікували. Це не «зайва балаканина», а перевірка здоров’я застосунку на ранній стадії: якщо курсів раптом 0, це важливий сигнал.
Ще одна добра точка — формування featured-добірки. Це прикладна, доменна штука: вона залежить від даних і налаштувань, і за нею легко помітити підозрілий стан. Наприклад, якщо maxFeaturedCount=4, а в результаті 0 — це не обов’язково помилка, але точно привід насторожитися.
Нижче — маленький фрагмент із сервісу, де лог «стискає» важливу подію до одного рядка, а не переказує реалізацію:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
@Service
public class CourseCatalogService {
// Логер прив’язано до класу: за ним у логах видно джерело повідомлення
private static final Logger log = LoggerFactory.getLogger(CourseCatalogService.class);
public void logFeaturedPrepared(int limit, int actualCount) {
// Фіксуємо подію домену та важливий контекст: ліміт і фактичну кількість
log.info("Підбірку featured-курсів підготовлено, ліміт={}, кількість={}", limit, actualCount);
}
}
Тут лог не розповідає, як саме ви відбирали курси, але фіксує факт: «підбірка готова» — і показує два числа, які справді допомагають у діагностиці.
Ще один приклад — режим обслуговування. У нас це конфігураційний прапорець (maintenanceMode). Навіть якщо зараз він впливає на поведінку мінімально, сам по собі режим — важливий контекст. Ідея проста: якщо сервіс працює в режимі обслуговування, то дивна поведінка (наприклад, порожні відповіді) може бути очікуваною. Тоді один лог на старті економить вам півгодини розслідування.
5. Де ставити логи в шарах сервісу
У сервісі є шари: веб-шар (controller), прикладний шар (service), шар доступу до даних (repository), а також bootstrap-код запуску. Якщо логувати хаотично, легко отримати три однакові повідомлення на одну подію: controller написав «починаю», service написав «роблю», repository написав «роблю», а потім усе це повторилося ще раз під час обробки винятку. У підсумку ви не допомогли діагностиці, а просто помножили шум.
Корисна модель мислення така: логування — це як дорожні знаки. Якщо поставити знак «Обережно, поворот» кожні два метри, водій перестане його помічати. Якщо поставити його там, де справді небезпечно, він працюватиме. Тому логувати варто в місцях, які дійсно важливі для розуміння системи: межі сценаріїв, підсумкові результати, нештатні стани.
Нижче — проста схема, де зазвичай «живуть» корисні логи в нашому проєкті:
flowchart TD
A["Запуск / bootstrap"] --> B[Шар сервісу]
B --> C[Шар репозиторію]
D[Веб-шар] --> B
A:::good -->|"Підсумок старту, кількість, режими"| L1["Логи"]
B:::good -->|"Доменна подія: підготували featured"| L1
D:::meh -->|"Логи лише за наявності явної причини"| L1
C:::meh -->|"Обачно: легко створити шум"| L1
classDef good fill:#e7ffe7,stroke:#2f7d32,stroke-width:1px;
classDef meh fill:#fff7e6,stroke:#b26a00,stroke-width:1px;
Bootstrap і service-шар — добрі кандидати на перші осмислені логи в catalog-service. Controller-шар і repository-шар потребують обережності: там легко почати «логувати кожну дрібницю», особливо якщо хочеться бачити кожен запит. Але на цьому рівні нам важливіше зрозуміти стан сервісу, а не перетворити лог на трекер кожного руху.
Ще один критерій, де зупинитися: лог має бути зрозумілий без читання класу цілком. Якщо повідомлення звучить як «step=3 reached», воно зрозуміле тільки автору — і то перші два дні. Якщо ж повідомлення звучить як «Каталог завантажено, курсів=12», його зрозуміє будь-хто, хто знає домен.
6. Рефакторинг: println → лог
Перехід від println до логування краще робити не революцією в усьому проєкті, а маленькими, зрозумілими кроками. Найправильніший перший крок — взяти місце, де ви вже виводили щось у консоль (наприклад, startup summary), і перевести це на лог. Так ви не змінюєте бізнес-поведінку, не додаєте нову фічу, а просто робите виведення діагностичної інформації дорослішим.
Спочатку подивімося, як виглядає наївна версія (і так, усі ми так робили — навіть ті, хто тепер удає, що ні):
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class StartupSummaryRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) {
// Наївний варіант: повідомлення піде напряму в stdout і випаде із загальної logging-системи
System.out.println("Підсумок старту каталогу підготовлено"); // Підсумок старту каталогу підготовлено
}
}
Повідомлення з’явиться, але воно ніяк не пов’язане із системою логування. Далі ми замінюємо його на лог-повідомлення, яке вже буде частиною загальної картини старту:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
@Component
public class StartupSummaryRunner implements ApplicationRunner {
// SLF4J-логер: далі форматуванням, рівнями та виведенням керує конфігурація логування
private static final Logger log = LoggerFactory.getLogger(StartupSummaryRunner.class);
@Override
public void run(ApplicationArguments args) {
// Тепер повідомлення потрапляє до загального потоку логів застосунку
log.info("Підсумок старту каталогу підготовлено");
}
}
Зверніть увагу: ми не ускладнили світ, ми просто змінили канал виведення. Але цей маленький крок дає велику користь: тепер повідомлення проходить через logging-систему, а отже, оформлюється так само, як і решта виведення Spring Boot. Його можна буде фільтрувати, і воно не виглядатиме так, ніби його «приклеїли скотчем» до консолі.
На цьому кроці достатньо побачити сам перехід stdout → logging. Повноцінний startup summary — це вже окреме завдання: там важливі профілі, порт і доменні показники, а не сам факт повідомлення.
7. Типові помилки під час переходу на логи
Коли ви починаєте логувати, хочеться або перелогувати все (і отримати шум), або логувати так само беззмістовно, як раніше друкували в консоль. Це нормальна стадія зростання: спочатку ми пишемо багато, потім вчимося писати по суті. Нижче — кілька помилок, які трапляються майже у всіх початківців і через які логи швидко перестають допомагати.
Помилка №1: залишати System.out.println у сервісах і runner-класах «бо так швидше».
println справді швидше написати, але він майже завжди залишається в коді довше, ніж ви планували. У результаті частина інформації живе в логах, частина — у випадкових виведеннях, і ви читаєте два різні «світи». Якщо повідомлення важливе — нехай це буде лог-повідомленням. Якщо ні — найімовірніше, його взагалі не має бути.
Помилка №2: писати повідомлення виду here, ok, done, step1.
Такі повідомлення створюють ілюзію контролю («я бачу, що код дійшов сюди»), але не допомагають зрозуміти стан сервісу. Через тиждень ви побачите done і не пригадаєте, що саме було «done». Хороший лог називає подію людськими словами й додає вимірюваний результат: courses=12, maintenanceMode=true, limit=4.
Помилка №3: логувати реалізацію, а не подію.
Якщо ви пишете лог перед кожним рядком, ви перетворюєте лог на «коментарі, які друкуються». Це не діагностика, а серіал «Як я робив stream().filter()». Лог має описувати початок і кінець важливого кроку, а не копіювати внутрішню кухню методу.
Помилка №4: логувати все на одному рівні «просто щоб було видно».
Початківці часто роблять усе info, бо «так видно в консолі». Але тоді у вас не буде різниці між важливою подією і фоновою деталлю. Навіть якщо ви поки не дуже впевнені в рівнях, корисно хоча б інтуїтивно розділяти: «важливий підсумок» і «деталь для діагностики».
Помилка №5: друкувати повний об’єкт “про всяк випадок”.
Фрази на кшталт log.info("properties={}", properties) виглядають привабливо, але зазвичай створюють величезні рядки, які складно читати. Набагато корисніше вибрати 1–3 поля, які справді пояснюють стан, і вивести їх. «Стиснути контекст» — одна з головних цілей логування.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ