JavaRush /Курси /Spring Boot /Логування в бекенд-сервісі замість

Логування в бекенд-сервісі замість println

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

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 поля, які справді пояснюють стан, і вивести їх. «Стиснути контекст» — одна з головних цілей логування.

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