JavaRush /Курси /Spring Boot /Структуроване логування

Структуроване логування в Spring Boot 4

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

1. Недоліки plain-text логів

На старті проєкту звичайні текстові логи здаються ідеальними: відкрили консоль — побачили «застосунок запустився», «курси завантажилися», «порт такий-то» — і краса. Проблема з’являється не тому, що текст поганий, а тому, що він занадто «людський»: очима його читати легко, а стабільно розбирати на поля — складно. Особливо коли ви хочете шукати, фільтрувати й порівнювати записи не навмання, а за фактами.

Уявіть типовий життєвий сценарій catalog-service. Ви отримали скаргу: «іноді не знаходиться курс за slug». Ви ввімкнули більше логів, з різних пакетів посипалися повідомлення, і вам треба відповісти на просте запитання: де саме виникла помилка та з якими параметрами. У plain-text форматі потрібні дані часто сховані всередині повідомлення, а формат записів може відрізнятися від класу до класу. У результаті ви або на око гортаєте консоль, або починаєте бавитися з регулярними виразами. Особливо «весело», коли повідомлення трохи змінилося, а пошук за шаблоном раптом перестав працювати.

Є й більш приземлена проблема: ви дуже швидко хочете фільтрувати за рівнем, класом-логером, іменем сервісу, потоком, конкретним кореляційним ідентифікатором операції. У plain-text це можливо, але крихко. Будь-яка дрібна зміна формату призводить до того, що машина перестає розуміти логи так само добре, як людина. А ми ж пишемо бекенд не лише для людей. Інколи його читають і машини — просто тому, що їх у продакшені зазвичай більше.

Щоб не звучало надто «enterprise», сформулюємо простіше: plain-text лог — це коли всі важливі поля склеєні в один рядок. Structured лог — це коли ці поля залишаються полями, і вам не потрібно «виколупувати» рівень або логер із тексту, наче родзинки з булочки.

Невелика таблиця, щоб зафіксувати сенс на пальцях:

Завдання Plain-text лог Structured лог
Швидко зрозуміти очима, що відбувається Зручно Часто незручно, бо JSON виходить багатослівним
Знайти всі записи, де level=ERROR Зазвичай через grep/шаблон, іноді крихко За полем level — стабільно
Фільтрувати за логером/пакетом За збігом рядка За окремим полем
Автоматично аналізувати логи програмою Складніше, потрібен парсинг тексту Простіше, бо це вже JSON або набір полів

2. Structured logging і машиночитаний рядок лога

Structured logging легко випадково зрозуміти неправильно, тож почнімо з чесної й простої моделі. У кожної log event є набір даних: час, рівень, імʼя логера, повідомлення, іноді виняток, іноді імʼя потоку та інші службові поля. У plain-text форматі ці дані перетворюються на один рядок «для людини». У structured форматі вони залишаються структурою — найчастіше у вигляді JSON-об’єкта — і друкуються як один рядок JSON, придатний для машинного парсингу.

Термін машиночитаний рядок лога означає: кожен рядок лога — це самостійний об’єкт фіксованого формату, який можна розібрати без вгадувань. Якщо це JSON-лог, то один рядок — один JSON-об’єкт. Так, це виглядає так, ніби «в консоль вилили словник», але саме в цьому й цінність: поля не загубилися й не розчинилися всередині тексту.

Корисно уявити собі шлях лога — дуже спрощено, без деталей Logback і без занурення в конфіги:

flowchart LR
  %% Спрощена схема: важливо зрозуміти, що structured — це про «як друкуємо», а не про «як пишемо log.info»
  A["Ваш код: log.info(...)"] --> B["Подія логування (поля: time, level, logger, message, ...)"]
  B --> C["Енкодер/форматувальник (plain або structured)"]
  C --> D["Вивід: консоль/файл"]

Зверніть увагу на важливу думку: структурованість — це про формат виводу, а не про те, що ми «по-іншому пишемо log.info(...)». Ви створюєте подію логування так само, а ось «принтер» друкує її або як рядок, зрозумілий людині, або як машиночитаний JSON.

Приклад надмінімального structured-лога, щоб побачити ідею:

{
  "timestamp": "2026-03-19T10:15:00Z",
  "level": "INFO",
  "logger": "com.example.catalogservice.catalog.service.CourseCatalogService",
  "message": "Завантажено рекомендовані курси: 4"
}

У реальності Spring Boot підтримує кілька стандартних структурованих форматів, наприклад ECS, GELF і Logstash, і вони відрізняються назвами полів та їхнім групуванням. У цій лекції нам важлива не таблиця всіх полів, а ключовий ефект: рівень, логер, повідомлення й час стають окремими полями, а не частиною «склеєного» рядка.

3. Формат виводу замість log.info(...)

Найприємніша новина: щоб перейти на structured logging, вам зазвичай не потрібно переписувати прикладний код. Якщо ви вже логуєте правильно — через SLF4J-логер, із плейсхолдерами {} — то структуризація змінює передусім те, як лог виглядатиме назовні. Тобто ми не переписуємо сервіси й контролери, а акуратно підкручуємо конфігурацію — як і годиться в Spring Boot, де більша частина поведінки керується конфігурацією.

Давайте візьмемо невеликий, зрозумілий фрагмент із контексту catalog-service. Наприклад, наш сервіс каталогу логує факт завантаження featured-курсів. Код при цьому максимально звичайний, без жодної structured-магії:

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

public class CourseCatalogService {

    // SLF4J-логер: код не "знає" про JSON, він просто створює log-події
    private static final Logger log =
            LoggerFactory.getLogger(CourseCatalogService.class);

    public void logFeaturedCount(int count) {
        // Плейсхолдер {} + окремий аргумент: так параметр передається системі логування, а не склеюється в рядок
        log.info("Завантажено рекомендовані курси: {}", count); // повідомлення залишиться тим самим
    }
}

Тут важливо не прогледіти два моменти. По-перше, ми використовуємо плейсхолдер {} і передаємо параметр окремо, а не склеюємо рядок через +. Це і швидше, і чистіше, і краще дружить із системою логування. По-друге, у коді немає й не повинно бути жодного JSON. Це принципово: structured logging не має перетворювати ваш доменний код на код під лог-формат.

Якщо ви зараз увімкнете structured logging, цей самий запис log.info(...) піде назовні вже в структурованому вигляді. Тобто зміниться «фотографія», а не «модель». І це дуже в дусі Boot: поведінка змінюється через конфігурацію, а не через переписування десяти класів.

4. Увімкнення structured logging у Spring Boot 4

Тепер переходимо до найпрактичнішого: як увімкнути structured logging так, щоб він справді запрацював, а не залишився «ідеєю в голові». У Spring Boot 4 structured logging вмикається конфігурацією. Нас цікавлять дві властивості: одна відповідає за консоль, інша — за файл. Навіть якщо ви поки що пишете лише в консоль, важливо розуміти, що це два незалежні напрями: консоль зазвичай читає людина, а файли й потоки логів часто читає інструмент.

Ось компактна табличка за налаштуваннями, щоб не плутати їх:

Куди пишемо Властивість Чим керує
У консоль (stdout) logging.structured.format.console Формат виводу в консоль
У файл logging.structured.format.file Формат виводу у файл

Для старту нам достатньо увімкнути structured-формат для консолі. Наприклад, у src/main/resources/application.yaml можна додати:

spring:
  application:
    name: catalog-service # імʼя сервісу потрапить у structured-поля (наприклад, service.name)

logging:
  structured:
    format:
      console: ecs # вмикаємо structured-формат для консолі (stdout)

Це мінімальне увімкнення structured logging у консоль. Для catalog-service у курсі вважаємо ecs робочою базовою точкою відліку для structured-консолі. Саме на ній фіксуємо подальші приклади та фінальну політику проєкту. Значення ecs — це назва одного з підтримуваних форматів. GELF і Logstash нам ще знадобляться, але вже як формати, які потрібно вміти впізнавати й читати, а не як плаваючий вибір усередині проєкту.

Зверніть увагу: ми не пишемо logback.xml, не підключаємо дивні енкодери вручну й не ліземо в нетрі конфігурації. Ми робимо рівно те, що Boot любить найбільше: описуємо бажану поведінку декларативно.

Якщо вам потрібно увімкнути structured-формат для файлу, логіка буде аналогічною — змінюється лише місце призначення:

logging:
  structured:
    format:
      file: ecs # вмикаємо structured-формат для файлу (зазвичай його забирає збирач логів)

Ми свідомо не обговорюємо тут налаштування імені файлу, ротацію й архівування — це окремий всесвіт. Зараз фіксуємо головне: у Boot structured logging вмикається властивостями, і ваш Java-код при цьому не зобовʼязаний «знати», що логи стали JSON.

І нарешті, важлива практична думка: structured logging — це не «вічно увімкнена опція». Його потрібно вмикати там, де він дає цінність. Якщо ви увімкнули JSON-логи в консоль і раптом відчули, що дебажити стало незручно, це не означає, що ви помилилися. Просто ви ввімкнули інструмент не під ту задачу. Консоль зазвичай для очей, structured-формат — для стабільної обробки.

5. Порівняння: plain vs structured

Щоб не було відчуття «я щось увімкнув, але не розумію, що змінилося», давайте порівняємо два виводи для одного й того самого запису log.info("Завантажено рекомендовані курси: {}", count) з CourseCatalogService. З точки зору коду нічого не змінилося. Змінився лише спосіб друку події логування.

Приклад того, як подібний запис може виглядати в plain-text форматі:

2026-03-19T10:15:00.123 INFO  18432 --- [catalog-service]  c.e.c.c.service.CourseCatalogService : Завантажено рекомендовані курси: 4

А тепер умовний structured-варіант:

{"@timestamp":"2026-03-19T10:15:00.123Z","log.level":"INFO","log.logger":"com.example.catalogservice.catalog.service.CourseCatalogService","service.name":"catalog-service","message":"Завантажено рекомендовані курси: 4"}

Якщо ви зараз думаєте: «Виглядає гірше, ніж раніше», — вітаю, ви правильно читаєте очима. Structured лог часто справді гірше читається очима, бо оптимізований не під людину, а під стабільні поля. Зате в нього з’являється суперсила: рівень — це поле log.level, логер — окреме поле, імʼя сервісу — окреме поле. Машина не повинна вгадувати, де саме в рядку сховане імʼя логера, і не повинна сподіватися, що ви не зміните порядок частин.

Іноді корисно показати JSON гарно, хоча в логах він зазвичай в один рядок. Це не новий формат, а просто візуалізація того, що всередині:

{
  "@timestamp": "2026-03-19T10:15:00.123Z",
  "log.level": "INFO",
  "service.name": "catalog-service",
  "message": "Завантажено рекомендовані курси: 4"
}

І тут добре видно, заради чого все це й затівалося: якщо вам потрібно швидко відфільтрувати всі INFO від нашого сервісу, ви не парсите рядок. Ви дивитеся на поля.

6. Де корисний structured logging

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

Але є й зворотний бік. Коли ви запускаєте catalog-service в IDE і просто читаєте консоль, JSON може перетворити простий дебаг на читання бухгалтерського звіту. Вам хочеться побачити короткий рядок, а ви бачите об’єкт із полями. Це нормально. Structured logging не скасовує цінності plain-text виводу, він просто розв’язує інше завдання.

Дуже важливо зафіксувати ще одну думку, щоб не було магічних очікувань: structured logging не робить лог «розумним» автоматично. Якщо ви написали погане повідомлення, воно залишиться поганим — просто в JSON. Ось приклад поганого лога, який формально structured, але практично марний:

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

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

    public void run() {
        // Проблема не у форматі, а в сенсі: за таким логом незрозуміло, що саме «Запущено»
        log.info("Запущено"); // занадто мало змісту
    }
}

А ось більш корисний варіант — той самий рівень, той самий логер, але повідомлення вже несе конкретний сигнал:

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

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

    public void run(int coursesCount) {
        // Тут є конкретний факт + параметр передається окремо через {}, що коректно для логування
        log.info("Каталог запущено. Завантажено курсів: {}", coursesCount); // зрозумілий сигнал
    }
}

Structured-формат не рятує від дисципліни: не варто логувати величезні об’єкти цілком лише тому, що JSON нібито «для цього». Якщо ви почнете друкувати весь список курсів у лог, ви отримаєте гігантський шум, і жодна структурованість не поверне з нього корисний сигнал.

І так — structured-формат теж не скасовує базового правила безпеки. Якщо ви залогували токен, то ви залогували токен. Він буде не менш небезпечним, просто гарно упакованим у JSON.

7. Типові помилки під час увімкнення structured logging

Structured logging — штука корисна, але новачкам він часто «стріляє в ногу» не тому, що технологія погана, а тому, що очікування від неї не збігаються з реальністю. Якщо заздалегідь знати типові помилки, можна зекономити купу часу, а заодно не перетворити логи на дивний артефакт, який ніхто не хоче читати.

Помилка № 1: увімкнути structured logging і очікувати «красиву консоль».
Structured-логи часто виглядають у консолі гірше, ніж plain-text, і це нормально. Вони не зобовʼязані бути зручними для очей. Перед увімкненням варто чесно відповісти собі: ви зараз читаєте лог очима чи вам важливі поля для стабільного розбору. Якщо мета — швидко дебажити вручну, plain-text іноді виграє.

Помилка № 2: думати, що JSON-форма замінює хороший текст повідомлення.
Якщо повідомлення «ні про що» або надто розпливчасте, structured-формат лише запакує цю розпливчастість у JSON. Сенс усе одно залишається в повідомленні. Завантаження... і Запущено майже завжди гірші, ніж Каталог запущено. Завантажено курсів: 12 — незалежно від формату.

Помилка № 3: логувати великі об’єкти цілком, бо «раз уже JSON».
Друкувати цілий список курсів, цілу конфігурацію або всю відповідь сервера «для зручності» — дуже спокусливо. Але це народжує шум, збільшує обсяг логів і робить реальні проблеми менш помітними. Краще логувати агрегати й факти: кількість, slug, прапорці, важливі рішення, а не всі дані світу.

Помилка № 4: змішати різні стилі логів без чіткого правила.
Наприклад, частина середовищ пише plain-text, частина — structured, а частина — і те, і інше впереміш, бо конфігурація розʼїхалася. У результаті ви не впевнені, що саме побачите під час запуску. Логи мають бути передбачуваними: або тут plain, або тут structured, і це закріплено конфігом, а не пам’яттю розробника.

Помилка № 5: переплутати structured logging із налаштуванням рівнів.
Structured logging — це не «DEBUG замість INFO» і не «більше логів». Це форма виводу. Можна писати structured-логи на INFO, можна на DEBUG, можна на ERROR. Рівні вирішують, скільки подій ви пишете; structured-формат вирішує, як ці події виглядають і наскільки легко їх розбирати за полями.

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