JavaRush /Курсы /Java Server /Logging stack: SLF4J

Logging stack: SLF4J и Logback

Java Server
21 уровень , 1 лекция
Открыта

1. Две части logging stack

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

Когда говорят “подключи логирование”, новичок часто представляет что-то вроде: «ну это ещё один способ печатать текст». На самом деле logging stack — это маленькая система, где у каждого участника своя работа. И хорошая новость: она не сложная, если не пытаться изучить всё сразу, как будто сдаёшь экзамен по “Логам и страданиям”.

В нашем курсе logging stack будет состоять из двух ролей. Первая роль — это единый API в коде, через который мы пишем сообщения (так, чтобы потом можно было поменять “движок” логов и не переписывать проект). Вторая роль — это реальная реализация, которая решает, куда эти сообщения попадут (в консоль, файл и т.п.) и как будет выглядеть каждая строка лога.

Давайте посмотрим на эту систему как на “трубу”, по которой ваши сообщения идут наружу:

flowchart LR
  A["Ваш Java-код
log.info(...)"] --> B["SLF4J API
(facade)"] B --> C["Logback
(implementation)"] C --> D["Appender
Console"] D --> E["stderr / terminal diagnostics"]

Важная мысль: в коде приложения мы хотим видеть только SLF4J, а не логбековские классы. 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("Catalog search started, query={}", 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 started");
    }
}

Почему 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-коде, но важная часть logging stack — конфигурация. И тут опять хорошая новость: мы не будем хардкодить уровни логирования и формат сообщений внутри классов. Это быстро превращает код в “конфиг-кашку” и делает изменения неудобными.

Logback по умолчанию ищет конфигурацию в classpath. Самый привычный вариант — файл logback.xml, который лежит в корне ресурсов. В нашем проекте это означает путь src/main/resources/logback.xml. Он попадёт в classpath при запуске через Gradle, и Logback автоматически его подхватит.

Этот файл описывает именно диагностический канал. Пользовательский CLI-вывод можно оставлять отдельно, чтобы результат команды и технические сообщения не спорили за одно и то же место в консоли.

Покажу в виде “куда положить файл”:

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

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

Почему это место важно? Потому что ресурсы — это часть артефакта приложения. Вы собираете jar, запускаете его, и настройки логов едут вместе с ним. Это базовая инженерная привычка: конфигурация должна жить рядом с приложением, а не быть спрятанной в IDE-настройках или в вашем личном “волшебном” файле где-то на диске.

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

Logback умеет много, но мы — в bridge-курсе, и нам нужна минимально полезная конфигурация. Обычно достаточно консольного 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 started

Суперважная мысль: конфиг можно менять без правки 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 такой подход особенно удобен в client-фазе: когда вы отлаживаете catalog details, вы можете временно дать DEBUG только catalog.client или catalog.service, не трогая остальные пакеты.

10. Logging stack в 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("Catalog search started, query={}", query);

        // Здесь реальный вызов клиента и обработка результата

        // Симметричное сообщение "закончили" помогает видеть длительность/границы операции в потоке логов
        log.info("Catalog search finished, query={}", 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("Application started");
        // Разбор args и запуск нужного режима (не забывайте: если логов будет мало, отлаживать будет больно)
    }
}

Что важно в этом примере. Мы не делаем “универсальный логгер” в common, мы не тащим Logback-классы в код, и мы не пытаемся решать уровни через if-ы в 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 лишняя зависимость. В учебном проекте лучше сразу приучаться к чистоте: одна реализация — один предсказуемый вывод. Иначе часть логов может пойти “не туда” или формат неожиданно поменяется.

Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ