JavaRush /Курси /Java Server /Стек логування: SLF4J

Стек логування: SLF4J і Logback

Java Server
Рівень 21 , Лекція 1
Відкрита

1. Дві частини стека логування

Ми вже розділили користувацьке виведення і діагностику. Тепер нам потрібен інструмент саме для діагностичного каналу: із рівнями, форматом і налаштуваннями, щоб усе не скочувалося назад до нового println().

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

У нашому курсі стек логування складатиметься з двох ролей. Перша роль — це єдиний API в коді, через який ми пишемо повідомлення, щоб потім можна було змінити «двигун» логів і не переписувати проєкт. Друга роль — це конкретна реалізація, яка вирішує, куди ці повідомлення потраплять — у консоль, файл тощо — і як виглядатиме кожен рядок логу.

Давайте подивимося на цю систему як на «трубу», якою ваші повідомлення виходять назовні:

flowchart LR
  A["Ваш Java-код
log.info(...)"] --> B["SLF4J API
(фасад)"] B --> C["Logback
(реалізація)"] C --> D["Appender
Консоль"] D --> E["stderr / діагностика термінала"]

Важлива думка: у коді застосунку ми хочемо бачити тільки SLF4J, а не класи Logback. Logback має жити «під капотом» і не змішуватися з бізнес-кодом. Це як із розеткою: вам важливо, що в стіні стандартна розетка, а не те, як саме електростанція виробляє енергію.

2. SLF4J: єдиний фасад

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

Навіщо потрібен фасад? Тому що ви не хочете, щоб ваш проєкт був «привʼязаний» до конкретної реалізації логування. Сьогодні ви використовуєте Logback (і в курсі це зафіксовано), завтра в іншій компанії буде Log4j2 або щось інше. Якщо код завʼязаний на реалізацію, змінювати його боляче. Якщо код завʼязаний на SLF4J, можна обійтися без переписування сотень класів.

Мінімальний «правильний» імпорт, який нас цікавить:

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

І типовий шаблон у класі:

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

public class CatalogService {
    // Логер створюємо один раз на клас, а не в кожному методі
    private static final Logger log = LoggerFactory.getLogger(CatalogService.class);

    public void search(String query) {
        // {} — заповнювач SLF4J: рядок збиратиметься лише тоді, коли рівень логування увімкнено
        log.info("Пошук у каталозі розпочато, запит={}", query);
    }
}

Зверніть увагу на три деталі. По-перше, ми не створюємо Logger «на льоту» всередині методу — він ніби «прикріплений» до класу. По-друге, ми пишемо через log.info(...), а не через System.out.println(...). По-третє, ми використовуємо заповнювач {} — це невелика магія заради того, щоб не склеювати рядки вручну й не платити за конкатенацію, коли рівень логування вимкнено.

Поки тримаємо в голові просте правило: у коді застосунку — тільки SLF4J. Це одна з тих звичок, які потім дуже допомагають, коли ви прийдете в Spring-проєкти та побачите рівно такий самий підхід.

3. Logback: реалізація логування

SLF4J сам по собі нічого не друкує. Він схожий на пульт керування, який має бути підключений до телевізора. Якщо телевізора немає — ви натискаєте кнопки, а в кімнаті просто стає трохи сумніше. Тому нам потрібна реалізація — Logback.

Logback відповідає за практичні питання: куди писати повідомлення (у консоль), як форматувати рядок, які рівні ввімкнені за замовчуванням. І найприємніше: усе це налаштовується поза кодом, у XML-конфігу, який лежить у ресурсах проєкту.

У нашому курсі ми використовуємо Logback не тому, що він «найкращий у світі» (у світі Java це небезпечна фраза), а тому, що він популярний, стабільний і чудово дружить із SLF4J. Плюс багато бібліотек уже логують через SLF4J, а отже наші налаштування автоматично впливають і на логи бібліотек.

Щоб картина остаточно склалася, можна сказати це людською мовою: ми пишемо в SLF4J, а Logback читає це й реально виводить.

4. Gradle: SLF4J і Logback

Тут буде трохи інженерної дисципліни, але без неї логування іноді перетворюється на загадку: «я пишу log.info(...), а де мої повідомлення?». Відповідь часто криється в залежностях.

У ReadLater Starter нам потрібен SLF4J як API на етапі компіляції, адже ми імпортуємо Logger і LoggerFactory, тому його зазвичай підключають через implementation. А Logback — це runtime-реалізація; за змістом йому достатньо бути на classpath під час запуску, тому його часто кладуть у runtimeOnly. Це не абсолютне правило, але добрий стиль: ви явно показуєте, що застосунок компілюється проти SLF4J, а Logback — окрема реалізація під час запуску.

Приклад фрагмента build.gradle.kts як орієнтир:

dependencies {
    // SLF4J — це API, з ним компілюється код (Logger/LoggerFactory)
    implementation("org.slf4j:slf4j-api:2.0.17")

    // Logback — реалізація, потрібна на етапі запуску (runtime classpath)
    runtimeOnly("ch.qos.logback:logback-classic:1.5.32")
    // Винесено окремо, щоб було явно видно: classic підтягує core, але ми свідомо фіксуємо версію
    runtimeOnly("ch.qos.logback:logback-core:1.5.32")
}

Якщо у вас Logback оголошено як implementation, проєкт теж працюватиме. Але runtimeOnly точніше відображає ідею «фасад у коді — реалізація під час запуску».

І ще один нюанс. Якщо раптом ви підключите кілька реалізацій одночасно, наприклад Logback та іншу, SLF4J почне скаржитися на «кілька провайдерів». Ми так не робимо, але корисно знати: іноді такі попередження — не «страшна помилка», а сигнал, що в classpath зайве.

5. Logger і LoggerFactory

Коли ви пишете System.out.println, ви напряму звертаєтеся до одного глобального потоку виведення. У Logger підхід інший: він як «мікрофон», привʼязаний до конкретного джерела. Джерело зазвичай — це клас або пакет, і це дуже зручно: потім за іменем логера можна фільтрувати та вмикати або вимикати деталізацію.

Типовий шаблон, який ми використовуватимемо по всьому проєкту, виглядає так:

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

public class ReadLaterApplication {
    private static final Logger log = LoggerFactory.getLogger(ReadLaterApplication.class);

    public void run(String[] args) {
        log.info("ReadLater запущено");
    }
}

Чому private static final? Тому що логер не має створюватися багато разів, він не змінюється і не залежить від стану обʼєкта. Вам не потрібно 50 логерів на 50 екземплярів класу — достатньо одного на клас.

Чому LoggerFactory.getLogger(SomeClass.class), а не getLogger("якийсь рядок")? Можна й рядком, але привʼязка до класу працює як самодокументування: читаєте лог і одразу бачите джерело повідомлення. Це економить нерви, а нерви, як відомо, не відновлюються; принаймні так каже кожен розробник після третього продакшн-інциденту.

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

6. Підключення Logback через SLF4J

Зараз буде коротка магія без магії. Питання: ви написали log.info(...). Як SLF4J розуміє, хто має це обробити? Відповідь: він шукає провайдера (implementation) у classpath. У нашому випадку цим провайдером є Logback.

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

Якщо реалізацій кілька, SLF4J теж попередить. Типовий сценарій для новачка: ви випадково підключили зайву залежність, і в консолі бачите повідомлення на кшталт «Multiple SLF4J providers were found». Це не кінець світу, але це знак, що час прибрати зайве із залежностей.

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

7. logback.xml у src/main/resources

До цього моменту ми говорили переважно про Java-код, але важлива частина стека логування — конфігурація. І тут знову добра новина: ми не будемо жорстко вшивати рівні логування та формат повідомлень усередині класів. Це швидко перетворює код на «конфіг-кашу» і робить зміни незручними.

Logback за замовчуванням шукає конфігурацію в classpath. Найзвичніший варіант — файл logback.xml, який лежить у корені ресурсів. У нашому проєкті це означає шлях src/main/resources/logback.xml. Він потрапить у classpath під час запуску через Gradle, і Logback автоматично його підхопить.

Цей файл описує саме діагностичний канал. Користувацьке CLI-виведення можна залишити окремо, щоб результат команди та технічні повідомлення не змагалися за одне й те саме місце в консолі.

Покажу у вигляді «куди покласти файл»:

Файл: src/main/resources/logback.xml

<!-- тут буде конфігурація -->

Чому це місце важливе? Тому що ресурси — це частина артефакту застосунку. Ви збираєте jar, запускаєте його, і налаштування логів їдуть разом із ним. Це базова інженерна звичка: конфігурація має жити поруч із застосунком, а не бути схованою в IDE-налаштуваннях чи у вашому особистому «магічному» файлі десь на диску.

8. Мінімальний logback.xml

Logback уміє багато, але ми — у перехідному курсі, і нам потрібна мінімально корисна конфігурація. Зазвичай достатньо консольного appender-а, зрозумілого формату рядків і одного рівня за замовчуванням.

Візьмемо компактний стартовий конфіг, який одразу надсилає діагностику в stderr:

Файл: src/main/resources/logback.xml

<configuration>
    <!-- Appender — це "куди писати". Тут пишемо в stderr -->
    <appender name="STDERR" class="ch.qos.logback.core.ConsoleAppender">
        <target>System.err</target>
        <encoder>
            <!-- Pattern — це "як виглядає один рядок логу" -->
            <pattern>%d{HH:mm:ss.SSS} %-5level %logger - %msg%n</pattern>
        </encoder>
    </appender>

    <!-- root level — рівень за замовчуванням для всіх логерів -->
    <root level="INFO">
        <!-- Підключаємо appender до root, інакше його не використовуватимуть -->
        <appender-ref ref="STDERR"/>
    </root>
</configuration>

Тепер розберімо терміни максимально приземлено.

Appender — це «куди писати». Тут пишемо в stderr, щоб діагностичний потік не змішувався з користувацьким stdout.

Pattern — це «як виглядає один рядок». Тут ми виводимо час, рівень, імʼя логера, зазвичай клас, повідомлення і переведення рядка.

root level — це рівень за замовчуванням для всіх логерів, якщо ви окремо нічого не налаштували.

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

12:10:03.512 INFO  com.example.readlater.app.ReadLaterApplication - ReadLater запущено

Найважливіше: конфіг можна змінювати без правки Java-коду. Наприклад, якщо вам потрібно більше деталей, можна тимчасово поставити DEBUG на root — і одразу побачите більше повідомлень. Але в наступній лекції ми окремо поговоримо про те, чому DEBUG на root — це іноді як увімкнути пожежну сигналізацію просто тому, що вам нудно.

Тоді stdout залишається вільним для користувацького виведення з CLI-режимів, а діагностика йде своїм шляхом.

9. root level і рівні пакетів

У реальному проєкті вам часто потрібна асиметрія. Наприклад, модуль каталогу (catalog) хочеться бачити докладніше — там зовнішні виклики, таймаути, статуси, — а все інше залишити на спокійному INFO, щоб консоль не перетворилася на «кулемет логів».

Logback дає змогу налаштувати рівень для конкретного пакета:

<!-- Вмикаємо DEBUG лише для конкретного пакета -->
<logger name="com.example.readlater.catalog" level="DEBUG"/>

І при цьому залишити root на INFO. Тоді від catalog ви отримаєте і DEBUG, і INFO, а від інших пакетів — лише INFO і вище.

Повний приклад — покажу фрагмент, який додається в logback.xml:

Файл: src/main/resources/logback.xml (фрагмент)

<!-- Детальні логи лише для каталогу -->
<logger name="com.example.readlater.catalog" level="DEBUG"/>

<!-- Для всього іншого залишаємо більш "тихий" рівень -->
<root level="INFO">
    <appender-ref ref="STDERR"/>
</root>

Чому це важливо для новачка? Тому що це дисципліна: «підкрутити деталізацію там, де болить», замість того щоб «увімкнути все всюди» і потім потонути в шумі.

У ReadLater Starter такий підхід особливо зручний на етапі роботи з клієнтом: коли ви розбираєтеся з деталями каталогу, можете тимчасово дати DEBUG лише catalog.client або catalog.service, не чіпаючи інші пакети.

10. Стек логування в ReadLater Starter

Тепер зберімо все в єдиний, упізнаваний шматок проєкту. Нехай у нас є CatalogService, який викликає клієнт і повертає результати. Ми хочемо, щоб service чесно писав «ключові події», але не перетворювався на друкарський верстат.

Наприклад, так:

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

public class CatalogService {
    // Логер на клас: за ним зручно фільтрувати логи в конфігурації
    private static final Logger log = LoggerFactory.getLogger(CatalogService.class);

    public void search(String query) {
        // Важливо логувати вхідні параметри (у розумних межах), щоб потім можна було відтворити ситуацію
        log.info("Пошук у каталозі розпочато, запит={}", query);

        // Тут реальний виклик клієнта й обробка результату

        // Симетричне повідомлення "завершено" допомагає бачити тривалість і межі операції в потоці логів
        log.info("Пошук у каталозі завершено, запит={}", query);
    }
}

І в точці входу ми логуємо старт режиму:

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

public class ReadLaterApplication {
    // У логах буде видно імʼя класу — це майже завжди те, що потрібно на старті
    private static final Logger log = LoggerFactory.getLogger(ReadLaterApplication.class);

    public void run(String[] args) {
        log.info("Застосунок запущено");
        // Розбір args і запуск потрібного режиму (не забувайте: коли логів буде замало, розбиратися буде боляче)
    }
}

Що важливо тут. Ми не робимо «універсальний логер» у common, ми не тягнемо класи Logback у код і ми не намагаємося вирішувати рівні через умовні перевірки в Java. Java-код повідомляє факти та контекст, а Logback вирішує, як це показувати.

Якщо ви зараз увімкнете logback.xml, запустите застосунок через Gradle і виконаєте якусь команду, ви отримаєте нормальні, рівні рядки логів. І найприємніше — ви зможете домовлятися про стиль повідомлень, тому що всі повідомлення йдуть через один і той самий механізм.

11. Типові помилки під час налаштування SLF4J + Logback

Помилка №1: писати в коді на класах Logback замість SLF4J.
Іноді хочеться «підлезти ближче до заліза» і імпортувати щось із ch.qos.logback.*. На практиці це одразу привʼязує ваш код до реалізації та ламає ідею фасаду. У застосунку тримайте імпорти лише org.slf4j.Logger і org.slf4j.LoggerFactory, а Logback залишайте в залежностях і logback.xml.

Помилка №2: створювати Logger всередині кожного методу.
Код на кшталт Logger log = LoggerFactory.getLogger(...) на початку кожного методу виглядає безпечно, але це зайвий шум і погана звичка. Логер має бути полем класу, зазвичай private static final. Так код простіше читати, і ви точно не забудете, що логер є.

Помилка №3: покласти logback.xml не туди й потім «шукати баг у Java».
Logback шукає конфіг у classpath. Якщо ви поклали файл, наприклад, у src/main/java або в корінь репозиторію, він просто не потрапить у ресурси, і Logback його не побачить. Правильне місце — src/main/resources/logback.xml. Це звучить як дрібниця, але саме такі дрібниці найчастіше й зʼїдають час.

Помилка №4: підключити SLF4J, але забути реалізацію (Logback), і дивуватися, що логів немає.
SLF4J без реалізації — це фасад без будинку. Застосунок компілюється, бо API є, але під час запуску повідомлення можуть зникнути в «NOP». Перевіряйте, що в залежностях є logback-classic і що він справді потрапив у runtime classpath.

Помилка №5: випадково підключити дві реалізації та ігнорувати попередження.
Якщо ви побачили попередження про кілька SLF4J providers, це майже завжди означає, що в classpath зайва залежність. У навчальному проєкті краще одразу звикати до чистоти: одна реалізація — один передбачуваний вивід. Інакше частина логів може піти «не туди» або формат несподівано зміниться.

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