JavaRush /Курси /Spring Boot /Startup logs як карта стану застосунку

Startup logs як карта стану застосунку

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

1. Startup logs як інструмент діагностики

Під час запуску Boot піднімає контекст, вебсервер і ваші runnerʼи. Тепер подивімося на цей момент інакше — як на діагностичний слід, за яким за хвилину можна зрозуміти, у якому стані насправді стартував сервіс.

Коли ви запускаєте Spring Boot-сервіс, консоль дуже швидко перетворюється на «водоспад тексту». Новачок зазвичай реагує так: або ігнорує все до рядка “Started …”, або починає панікувати від кожного INFO. Насправді startup logs — це майже готова карта стану застосунку: що піднялося, з якими налаштуваннями, у якому режимі й де виникли проблеми.

Якщо вам здається, що це «занадто багато тексту», уявіть реальну ситуацію: сервіс не запускається на машині колеги або в CI, а ви не можете поставити точку зупину. Єдиний свідок того, що відбувається, — лог. І саме стартова частина лога найчастіше відповідає на ключові запитання: чи активувалися профілі, на якому порту слухає сервер, чи не зламалася конфігурація, чи не впав контейнер через валідацію @ConfigurationProperties.

У нашому catalog-service це особливо важливо, бо проєкт config-first: багато поведінки залежить від YAML, профілів, імпортів, валідації. Тобто «не запускається» часто означає «конфігурація не така, як ви думаєте», і саме стартові логи показують, що застосунок насправді побачив під час запуску.

2. Рядок лога: читаємо одним поглядом

У більшості проєктів проблеми починаються не тому, що логів мало, а тому, що їх не вміють читати. Рядок лога в Boot за замовчуванням виглядає приблизно як «повідомлення з усіма реквізитами»: у нього є час, рівень, процес, потік, імʼя логера та текст. Якщо ви навчилися впізнавати ці частини, ви перестаєте тонути.

Приклад рядка (насправді формат може трохи відрізнятися, але зміст однаковий):

2026-03-18T10:15:32.741  INFO 24120 --- [main] c.e.c.catalog.bootstrap.StartupSummaryRunner : Courses loaded: 12

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

Фрагмент рядка Приклад Навіщо це потрібно під час діагностики
Часова мітка
2026-03-18T10:15:32.741
Ви бачите порядок подій і паузи. Іноді за однією міткою часу помітне «зависання» на старті.
Рівень
INFO
Ви відсікаєте шум. Коли сервіс упав, часто спочатку шукають ERROR, але корисні підказки бувають і в WARN.
PID
24120
Зручно, коли ви запускали кілька копій сервісу або перезапускали його через DevTools: PID змінюється.
Роздільник
---
Просто частина формату за замовчуванням; можете його не любити, але він не заважає.
Потік
[main]
Під час запуску зазвичай усе відбувається в потоці main. Коли щось «раптово» пішло в інший потік — це окремий сигнал.
Імʼя логера
c.e.c.catalog.bootstrap.StartupSummaryRunner
Головний орієнтир: хто написав повідомлення — Boot-інфраструктура чи ваш код.
Повідомлення
Courses loaded: 12
Власне зміст. Але без джерела й рівня він часто мало що дає.

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

3. Startup logs як шпаргалка для запуску

Якщо ставитися до логів як до карти, то треба заздалегідь знати, які «об’єкти на карті» ви шукаєте. У startup logs є дуже практична роль: вони швидко дають відповіді на кілька запитань, важливих для розробника в перші секунди життя сервісу. Це особливо критично для backend, тому що коли сервіс не стартував, далі вже нічого діагностувати.

Ось зручна шпаргалка «запитання → де шукати в логах». Не сприймайте це як бюрократію — це економить вам хвилини й години.

Запитання Як це зазвичай проявляється в startup logs
Застосунок узагалі стартував чи впав? Чи є фінальний рядок на кшталт Started … in … seconds або стек-трейс / APPLICATION FAILED TO START.
Які профілі активні? Рядок про активні профілі або фраза No active profile set….
На якому порту піднявся HTTP-сервер? Рядки про Tomcat / Jetty / Undertow і started on port …. (У нашому курсі — вбудований Tomcat через web starter.)
Скільки часу зайняв запуск? Зазвичай у фінальному рядку Started … in X seconds (process running for Y).
Чи піднявся Spring Context (контейнер бінів)? Коли контекст падає — буде виняток на стадії створення бінів, привʼязування конфігурації тощо. Коли піднявся — буде логіка, яка виконується після старту (наприклад, ApplicationRunner).
Де шукати наші власні «доменні» сигнали? За логером із вашого пакета, наприклад com.example.catalogservice.catalog.... Тут і потрібен StartupSummaryRunner: він робить логи «про нас», а не лише про інфраструктуру.

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

4. Boot і наші логи

Під час запуску Spring Boot пише багато інфраструктурних повідомлень: автоконфігурація, вебсервер, контекст, завантаження ресурсів. Це нормально: Boot чесно розповідає, що відбувається. Проблема виникає, коли ви не відокремлюєте їх від логів свого застосунку, і все в голові перетворюється на суцільне полотно.

Найпростіше відрізняти їх за імʼям логера (колонка «logger name» у рядку). У Boot-інфраструктури частіше будуть імена, що починаються з org.springframework... або org.apache.catalina... (для Tomcat). У наших логів — com.example.catalogservice.... Це як відрізняти оголошення в аеропорту (інфраструктура) від голосу вашого друга, який кричить у слухавку: «Я вже в зоні видачі багажу!» (ваш застосунок). І те, і інше корисне, але розвʼязує різні задачі.

Велику користь дає те, що ви заздалегідь домовилися про структуру пакетів (ми це зробили раніше в курсі) і налаштували рівні за пакетами (лекція 3). Тоді можна зробити так, щоб ваш пакет com.example.catalogservice.catalog логував докладніше, а org.springframework залишався на спокійному рівні INFO (або навіть WARN, коли ви вже впевнені). Ви не «вимикаєте Spring» — ви просто робите свій сигнал помітним.

5. Happy path: нормальний старт catalog-service

Давайте тепер подивимося на «хороший старт», коли все вдалося. Це важливо: коли ви знаєте, як виглядає норма, ви швидше помічаєте відхилення. Нижче приклад фрагмента логів; він умовний, але за змістом дуже близький до реальності.

# Тут важливо побачити «маяки»: Starting..., активний профіль, старт Tomcat і фінальний Started...
2026-03-18T10:15:31.102  INFO 24120 --- [main] c.e.c.CatalogServiceApplication : Запуск CatalogServiceApplication з використанням Java 25
2026-03-18T10:15:31.110  INFO 24120 --- [main] c.e.c.CatalogServiceApplication : Активовано 1 профіль: "local"
2026-03-18T10:15:31.780  INFO 24120 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat ініціалізовано з портом 8080
2026-03-18T10:15:32.120  INFO 24120 --- [main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat запущено на порту 8080 (http)
2026-03-18T10:15:32.130  INFO 24120 --- [main] o.s.web.servlet.DispatcherServlet        : Ініціалізація сервлета 'dispatcherServlet'
2026-03-18T10:15:32.620  INFO 24120 --- [main] c.e.c.c.b.StartupSummaryRunner            : Активні профілі: [local]
2026-03-18T10:15:32.621  INFO 24120 --- [main] c.e.c.c.b.StartupSummaryRunner            : Завантажено курси: 12
2026-03-18T10:15:32.740  INFO 24120 --- [main] c.e.c.CatalogServiceApplication          : CatalogServiceApplication запущено за 1.73 секунди

Тепер — як читати це по-дорослому, без істерики й без пропуску важливого.

Перший рядок зазвичай відповідає на прості запитання: чи стартували ми взагалі, який це застосунок, яка версія Java, який клас є точкою входу. Це корисно, коли ви запускаєте кілька сервісів або коли в CI лог перемішаний, і хочеться зрозуміти: це взагалі той проєкт?

Далі йде рядок про активні профілі. Це один із найважливіших сигналів для config-first сервісу. Коли ви очікували local, а там “default” — не треба одразу шукати баг у коді. Спочатку треба зрозуміти, чому профіль не активувався. Зазвичай відповідь десь поруч: ви не передали --spring.profiles.active=local, не виставили змінну середовища або IDE запускає не ту конфігурацію.

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

Потім зʼявляється ініціалізація DispatcherServlet. Це «серце» Spring MVC. Тут зазвичай усе добре, але коли ви бачите помилки мапінгу або конфлікт маршрутів, це часто проявляється саме тут.

І нарешті — наші два рядки від StartupSummaryRunner. Важливо, що вони приходять уже після підняття контексту й запуску інфраструктури. Вони відповідають на «доменні» запитання: який профіль реально активний і скільки даних каталогу завантажилося. Це вже не інфраструктурна правда, а правда нашого застосунку.

Коли ви привчите себе регулярно читати такий happy path — хоча б перші два тижні навчання, — у вас зʼявиться дуже корисна навичка: ви помічатимете проблему ще до того, як відкриєте браузер і побачите “connection refused”.

6. StartupSummaryRunner: зведення запуску

Spring Boot дає хороші загальні логи, але вони нічого не знають про ваш домен. Boot не може здогадатися, що вам важлива кількість курсів або чи ввімкнено maintenanceMode. Тому в нормальних сервісах майже завжди є невелике «зведення запуску» від застосунку. Ми вже знайомилися з ApplicationRunner раніше в курсі, а сьогодні робимо його частиною культури логування.

У catalog-service ми хочемо, щоб після запуску було зрозуміло щонайменше таке: які профілі активні, скільки курсів завантажилося із CatalogProperties, чи ввімкнені ключові прапорці та який порт використовується. Головне правило: зведення має бути коротким — це не «дамп усього світу», а кілька рядків, які читаються за 3 секунди.

Нижче — компактний варіант StartupSummaryRunner. Я спеціально показую фрагментами, щоб код не виглядав стіною тексту (стіни ми й так сьогодні читаємо — у логах).

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

public class StartupSummaryRunner {
    // Логер робимо static final: один на клас, без зайвих алокацій і сюрпризів у рантаймі
    private static final Logger log =
            // Використовуємо поточний клас, щоб у логах було зрозуміле імʼя логера
            LoggerFactory.getLogger(StartupSummaryRunner.class);
}

Тепер додамо залежності, які потрібні саме для startup-зведення: Environment для профілів і CatalogProperties для доменних даних. Зверніть увагу: залежності надходять через конструктор — це наш базовий стиль курсу.

import org.springframework.core.env.Environment;

public StartupSummaryRunner(Environment environment,
                            CatalogProperties properties) {
    // Environment потрібен, щоб читати активні профілі й властивості (зокрема server.port)
    this.environment = environment;
    // CatalogProperties — доменна конфігурація, із неї беремо «що реально завантажилося»
    this.properties = properties;
}

І нарешті — сам run(...). У реальному проєкті клас буде трохи довшим через анотації та поля, але центральна ідея ось така: логуємо не «все», а кілька фактів.

import java.util.Arrays;

// ActiveProfiles повертає масив — зручно перетворити його на читабельний рядок
log.info("Активні профілі: {}",
        Arrays.toString(environment.getActiveProfiles()));

// {} — це плейсхолдери SLF4J: форматування буде виконано лише за потрібного рівня логування
log.info("Каталог завантажено: title='{}', courses={}",
        properties.title(),
        properties.courses().size());

Зверніть увагу на стиль повідомлень. Ми не пишемо «started ok» або «done». Повідомлення має відповідати на запитання «що сталося» і «що вийшло в результаті». Тут «що сталося» — завантаження каталогу, а «що вийшло» — кількість курсів і назва каталогу.

Порт можна дістати з Environment (так, це рядок, і це нормально на цьому рівні: ми не будуємо тут окрему типобезпечну модель для порту). При цьому корисно задати значення за замовчуванням.

// Беремо server.port із конфігурації; коли його не задано — вважаємо, що буде значення за замовчуванням 8080
String port = environment.getProperty("server.port", "8080");
log.info("Налаштований HTTP-порт: {}", port);

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

// Явно логуємо ключовий прапорець: за ним легко зрозуміти, чому сервіс «поводиться дивно»
log.info("maintenanceMode={}", properties.maintenanceMode());

І саме тут зʼявляється важлива архітектурна гігієна: коли startup summary починає розростатися до 20 рядків — це не «стало інформативніше», це стало важче читати. У продакшені такі логи швидко перетворюються на шум, тому що ніхто не здатен утримати увагу на 200 рядках на старті.

7. Поломки запуску: читання WARN/ERROR

Помилки запуску рідко виглядають як «упало ось тут». Зазвичай це стек-трейс на 200 рядків, і десь усередині заховано одне коротке “Caused by”, яке й є справжньою причиною. Завдання розробника — не переписувати проєкт заново від відчаю, а методично дістатися до кореневої причини. І тут знову рятує розуміння структури startup logs.

Один із найтиповіших сценаріїв — порт зайнято. Ви запускаєте сервіс, і він падає через те, що 8080 уже зайнятий іншою копією застосунку (або взагалі чим завгодно). У логах це виглядає приблизно так: ви побачите ERROR і десь поруч BindException. Важливо, що проблема не в Spring, не в конфігурації бінів і не в контролерах — проблема «на рівні ОС»: не можна зайняти порт.

# Шукайте ключову причину: BindException майже завжди означає «порт уже зайнято»
... ERROR ... TomcatWebServer : Unable to start embedded Tomcat
... Caused by: java.net.BindException: Address already in use

Це як намагатися поставити машину на місце, де вже стоїть інша машина. Можна скільки завгодно змінювати колір бампера, але місце від цього не звільниться.

Друга дуже часта історія в нашому курсі — зламана конфігурація. Після 18-го дня в нас є валідація конфігурації, і це правильно: «битий» конфіг має валити запуск. Але новачка це лякає: «я ж нічого не змінював, чому все впало?». Логи в такому випадку зазвичай доволі чесні: вони прямо пишуть, яка властивість не пройшла перевірку або не вдалося її привʼязати.

Ви можете побачити, наприклад, що maxFeaturedCount невалідний або title порожній. І ключовий момент — не шукати проблему в контролері, тому що до контролера сервіс навіть не дійшов.

Третя категорія — помилки wiring (не знайшли бін, кілька кандидатів, циклічна залежність). Тут логи зазвичай містять фрази на кшталт “Consider defining a bean of type …” або “required a bean of type … that could not be found”. Для читання таких помилок дуже допомагає вміння відрізняти «наш пакет» від пакета Spring: часто в стеку видно, який саме ваш клас запросив залежність.

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

Лог як історія

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

На практиці це виглядає так: ви спочатку знаходите початок запуску (зазвичай за повідомленням Starting ...), потім поглядом відмічаєте ключові маяки — активні профілі, запуск вебсервера, фінальний рядок Started ..., а між ними дивитеся, чи немає WARN або дивних пауз. Коли застосунок упав, ви шукаєте APPLICATION FAILED TO START і далі переходите до Caused by: — але не до першого-ліпшого, а до того, що ближче до початку ланцюжка причин.

Ще одна корисна звичка — тримати в голові, що ми очікуємо побачити в happy path. Саме тому ми розбирали нормальний старт вище. Коли ви очікуєте рядок про активний профіль local, але його немає — це вже відхилення. Коли очікуєте Courses loaded: 12, а бачите Courses loaded: 0 — застосунок стартував, але доменний стан дивний, і це привід реагувати (хоча б WARN).

8. Типові помилки під час роботи зі startup logs

Помилка №1: дивитися лише на останній рядок і ігнорувати попередження.
Коли сервіс падає, внизу часто величезний стек-трейс, і рука тягнеться читати останні 5 рядків. Але справжня підказка нерідко зʼявляється раніше: у WARN, у повідомленні про профіль, у рядку “Failed to bind properties…”. Коли ви читаєте лог як послідовність подій, ви швидше знайдете першопричину й менше будете «лікувати симптоми».

Помилка №2: плутати логи Boot-інфраструктури з логами застосунку.
Новачок бачить “Tomcat started” і думає, що це зробив його код. Або бачить DispatcherServlet і намагається «лагодити сервлет». Набагато корисніше дивитися на імʼя логера: коли це org.springframework..., це інфраструктура. Коли це com.example.catalogservice..., це вже ваш домен, і саме там ви контролюєте повідомлення.

Помилка №3: робити startup summary у кількох місцях.
Іноді хочеться «про всяк випадок» залогувати профілі в main(), потім у @PostConstruct, потім у ApplicationRunner. У підсумку в логах три однакові рядки, і вони не допомагають, а дратують. Краще тримати одне джерело істини — один StartupSummaryRunner, який стабільно друкує коротке зведення.

Помилка №4: перетворювати зведення запуску на дамп усього обʼєкта конфігурації.
Друкувати log.info("CatalogProperties={}", properties) здається зручним, але зазвичай це або нечитабельно, або небезпечно, або й те й інше. Хороше зведення — це 2–5 рядків із числами, прапорцями та кількома ключовими значеннями. Усе інше або шум, або потенційний ризик (а про безпеку логів ми ще поговоримо в наступній лекції).

Помилка №5: вмикати глобальний DEBUG і радіти: «тепер я все бачу».
Глобальний DEBUG часто дає не знання, а ілюзію знання: ви бачите мільйон рядків від фреймворку й перестаєте бачити свій сигнал. Набагато продуктивніше налаштовувати рівні точково за пакетами й підсилювати логування лише там, де це справді потрібно для діагностики запуску.

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