1. Endpoint є, але HTTP повертає 404
Зазвичай новачок стикається з Actuator так: ставить залежність, запускає сервіс, відкриває браузер, натискає /actuator/health — і все працює. Далі, за інерцією, він пробує /actuator/info або /actuator/env і отримує 404. Після цього починається магічне мислення: «Напевно, я неправильно написав YAML», «Напевно, треба додати анотацію», «Напевно, Spring мене не любить».
Насправді магії тут немає. У Actuator є два різні питання, які легко переплутати. Перше — «де живуть endpointи?», тобто який у них спільний префікс (base-path). Друге — «які endpointи взагалі дозволено показувати через HTTP?», тобто exposure policy. І ось це важливо: endpoint може бути увімкнений усередині застосунку, але не експонований назовні через HTTP. Тому URL ніби «логічно існує», але зовні вам чесно відповідають: дивитися тут немає на що.
Зараз важливо навчитися розрізняти ці ситуації по-дорослому, а не методом «потрясти компʼютер — може, запрацює».
Давайте одразу закріпимо це на типовому симптомі. Ви запускаєте catalog-service і робите запит:
# Перевіряємо, що endpoint `info` справді доступний через HTTP
curl -i http://localhost:8080/actuator/info
# HTTP/1.1 404
Це якраз той випадок, коли легко переплутати вміст endpointа з його видимістю у вебі: info може бути зібраний нормально, але сам endpoint ще не опублікований через HTTP.
404 тут не означає, що Actuator «не встановився». Найчастіше це означає, що endpoint не потрапив до списку HTTP‑exposure і/або ви помилилися базовим шляхом. Тому зараз розберемо, як відрізняти ці ситуації спокійно, а не через «потрясти компʼютер, може, запрацює».
2. base-path: складання URL для Actuator endpointів
Перше, що варто зробити для спокою, — зрозуміти, як формується шлях. У Spring Boot діє таке правило: URL Actuator endpointа = base-path + endpoint id. За замовчуванням base-path — це /actuator, а id — наприклад health або info. Тому й виходить знайоме /actuator/health. Це саме домовленість за замовчуванням для web-застосунків, і вона описана в довідковій документації.
Можна запамʼятати так: Actuator — це «службова папка», а всередині неї — «файли-endpointи».
Ось проста формула для servlet-застосунку, як у нас:
http://localhost:8080 + {context-path?} + {management base-path} + /{endpoint id}
Саме тому початківець часто робить «майже правильно», але все одно отримує 404: він змінює base-path, а потім продовжує ходити за старою адресою.
Явно фіксуємо base-path у конфігу проєкту
Так, /actuator — це дефолт. Але в навчальному проєкті корисно зробити його явним, щоб у вас не було відчуття, ніби «воно десь там саме». У catalog-service це можна зафіксувати в application.yaml.
Файл: src/main/resources/application.yaml
management:
endpoints:
web:
# Загальний префікс для всіх web Actuator endpointів
base-path: "/actuator"
Якщо ви залишите все як є, без цього налаштування, поведінка буде такою самою. Але тепер будь-яка людина, відкривши YAML, одразу побачить: «Ага, службові endpointи живуть під /actuator».
Зміна префікса
Іноді /actuator справді зайнятий під щось своє, або ви хочете більш «нейтральний» префікс, наприклад /manage. Тоді змінюємо management.endpoints.web.base-path.
Файл: src/main/resources/application-local.yaml
management:
endpoints:
web:
# Переносимо Actuator з /actuator на /manage
base-path: "/manage"
Після цього health буде доступний уже так:
GET /manage/health
І ось тут дуже типова пастка: розробник змінює base-path на /manage, але продовжує відкривати /actuator/health, отримує 404 і думає, що «Actuator зламався». Ні, він просто переїхав.
Нюанс про context-path
Є тонкість, яка спливає, коли ви використовуєте server.servlet.context-path. Якщо management port не винесений окремо, то base-path рахується відносно контекстного шляху застосунку. Тобто якщо ви раптом зробите server.servlet.context-path: "/catalog", то Actuator опиниться за адресою /catalog/actuator/health. Це нормальна логіка: усе живе всередині одного web-застосунку.
3. Exposure policy: endpoint існує, але «невидимий» через HTTP
Тепер — найважливіше. В Actuator шлях (base-path) відповідає на питання «де», а exposure — на питання «кому можна». І Boot робить дуже консервативну річ: за замовчуванням він експонує назовні лише health. Не health і info, не «все корисне», а саме тільки health. Це прямо описано в документації, і причина проста: багато endpointів легко розкривають зайве.
Якщо ви не налаштовували exposure, то майже напевно побачите лише /actuator/health.
Це дуже змінює мислення. Раніше ви думали: «Усе є, просто я не знаю URL». Тепер ви думаєте: «URL я знаю, але його можуть не дозволити до показу».
Щоб не плутатися, тримайте в голові такий пайплайн:
flowchart TD
A[Endpoint існує в застосунку] --> B[Endpoint увімкнений / доступний]
B --> C{"Дозволено HTTP-exposure"}
C -- ні --> D[Через HTTP: 404 / не видно в discovery]
C -- так --> E[Через HTTP: доступний під base-path/id]
Ключовий момент: наявність endpointа в застосунку не дорівнює доступності через HTTP.
Налаштування exposure у Boot
Boot дає два властивості, і ми зараз говоримо лише про web/HTTP‑експонування:
management.endpoints.web.exposure.include
management.endpoints.web.exposure.exclude
Сенс простий: include — «дозволити показувати ось ці endpoint id», exclude — «заборонити показувати ось ці endpoint id». І важливо: exclude сильніший за include, тобто заборона перемагає дозвіл.
4. include і exclude: відкрити потрібне і не відкрити зайвого
Тут хочеться почати з філософії. Actuator — дуже корисна річ, але в нього є небезпечний режим «ой, я відкрив усе». В інженерному світі це зазвичай називають «пʼятничний реліз», бо потім вихідні ви проводите з логами. Тому правило курсу просте: у local/dev можна дати собі більше видимості, а в prod — тримати мінімальний набір.
Найпростіший список дозволених endpointів: відкриємо health і info
Припустімо, ми хочемо, щоб info справді був доступний через HTTP, а не лише існував у теорії. Тоді в application-local.yaml (або в базовому application.yaml, якщо хочете всюди) додаємо include.
Файл: src/main/resources/application-local.yaml
management:
endpoints:
web:
exposure:
# Дозволяємо через HTTP лише вибрані endpoint id
include: "health,info"
Тепер info буде доступний через HTTP під /actuator/info (або під вашим base-path, якщо ви його змінювали).
Важливо: include — це не URL, а саме id endpointів. Ми пишемо health,info, а не /actuator/health,/actuator/info.
Розширений local/dev allowlist: додамо env і configprops
У наступній частині курсу ми будемо використовувати env і configprops як «рентген» для конфігурації: env допомагає зрозуміти, яке значення перемогло і з якого джерела, а configprops показує, у що реально забіндилися ваші @ConfigurationProperties. Але щоб до них узагалі дістатися через HTTP, їх треба експонувати.
Файл: src/main/resources/application-local.yaml
management:
endpoints:
web:
exposure:
# Додатково відкриваємо діагностичні endpointи для локального налагодження
include: "health,info,env,configprops"
Зараз ми не обговорюємо вміст env і configprops, але вже фіксуємо правильний важіль керування: увімкнення та вимкнення через HTTP робиться саме через exposure.
Варіант для продакшена: залишити лише мінімум
Для prod зазвичай достатньо залишити health і info. І це зручно тримати в application-prod.yaml.
Файл: src/main/resources/application-prod.yaml
management:
endpoints:
web:
exposure:
# У продакшені залишаємо мінімальну діагностичну поверхню
include: "health,info"
Так у prod у вас залишається мінімальна діагностична поверхня, але ви не роздаєте назовні «рентген конфігурації». А якщо пізніше зʼявиться Security-шар, ви вже починатимете не з хаосу, а з акуратного мінімуму.
*: відкрити все
Boot підтримує * як вибір «усі endpointи». Це зручно для локального налагодження, але страшно як дефолт. І є ще один практичний нюанс: у YAML символ * має спеціальне значення, тому його потрібно обовʼязково брати в лапки, інакше YAML‑парсер спробує інтерпретувати його як YAML alias, і ви отримаєте помилку рівня «чому життя таке несправедливе».
Коректний приклад — як демонстрація механіки, а не як рекомендація для prod:
management:
endpoints:
web:
exposure:
# Відкриваємо всі endpointи (лише для локального налагодження)
include: "*"
# Але ці два — точно не віддаємо через HTTP
exclude: "env,configprops"
Тут ми робимо кумедний трюк: «відкрити все, але ці два — точно ні». Це хороший спосіб побачити пріоритет: exclude перекриває include.
Якщо говорити людською мовою, include: "*" — це як видати всім перепустку до серверної «про всяк випадок». Так, вам так зручніше. Але потім виявляється, що перепусткою скористалися не лише ви, а й хтось дуже допитливий.
Маленька таблиця за властивостями
| Що налаштовуємо | Властивість | Приклад | Навіщо вам це |
|---|---|---|---|
| Де живуть endpointи | management.endpoints.web.base-path | "/actuator" або |
Щоб усі службові URL були під єдиним префіксом. |
| Що видно через HTTP | management.endpoints.web.exposure.include | "health,info" | Дозволяємо через HTTP рівно потрібний набір. |
| Що точно не видно через HTTP | management.endpoints.web.exposure.exclude | "env,configprops" | Забороняємо навіть якщо потрапили до include. |
5. Перевірка exposure через /actuator
Якщо ви не хочете гадати, відкрився endpoint чи ні, найпростіший спосіб — запитати кореневий Actuator endpoint, тобто сам base-path без конкретного id. Зазвичай він повертає JSON із посиланнями _links на те, що реально експоновано. Це зручно: ви не перебираєте вручну 20 URL-адрес, а дивитеся на «меню ресторану» і вибираєте те, що є в наявності.
Запускаємо catalog-service з профілем local і перевіряємо:
# Запитуємо кореневий Actuator endpoint, щоб побачити, що реально експоновано
curl -s http://localhost:8080/actuator
Якщо ви увімкнули лише health, то побачите приблизно таку ідею:
{
"_links": {
"self": { "href": "http://localhost:8080/actuator" },
"health": { "href": "http://localhost:8080/actuator/health" }
}
}
Якщо ви додали info, env, configprops до include, то в _links зʼявляться і вони. Це найпростіша швидка перевірка для політики exposure: не треба читати логи й гадати, достатньо подивитися на список посилань.
Важливе для нашого курсу
У catalog-service ми багато уваги приділяли конфігурації: YAML, profiles, precedence, @ConfigurationProperties. І тепер Actuator стає інструментом, який дозволяє перевіряти це на працюючому застосунку, а не лише «в голові». Але щоб цей інструмент був безпечним, ми маємо вміти керувати тим, що видно через HTTP у різних профілях. Це і є нормальна доросла конфігурація: не «увімкнув усе і забув», а «у local — зручно, у prod — мінімально».
Важлива ремарка про безпеку
Boot прямо рекомендує: перш ніж розширювати include, переконайтеся, що ви не віддаєте чутливої інформації, і що endpointи захищені або мережею, або чимось на кшталт Spring Security. У нашому курсі ми свідомо не тягнемо Security, тому головний інструмент безпеки у нас — вузьке exposure за профілями і здоровий глузд.
6. Типові помилки під час налаштування base-path, include і exclude
У цій темі є кілька граблів, на які наступають навіть люди з досвідом — просто тому, що «в голові здавалося простіше». Якщо впіймати їх зараз, ви заощадите собі багато часу на налагодженні: «чому 404, я ж точно все зробив».
Помилка №1: плутати base-path та exposure.
Дуже поширена історія: ви змінюєте management.endpoints.web.base-path на "/manage", а потім продовжуєте ходити на /actuator/health і отримуєте 404. Або навпаки: ви знаєте, що endpoint має бути за /actuator/info, але не додали його до exposure.include, і знову отримуєте 404. Лікується це одним правилом: спочатку перевіряєте, який base-path, потім — список _links на кореневому endpointі /actuator (або /manage).
Помилка №2: писати include: * без лапок у YAML.
У YAML зірочка — не «просто символ». Це частина синтаксису, і без лапок конфіг може навіть не розпарситися. Правильний запис — include: "*". Це окремий випадок, який Spring підкреслює в документації, бо помилка надто часта.
Помилка №3: сподіватися, що exclude — це «рекомендація», а не правило.
Іноді здається, що якщо ви вказали include: "*", то «все вже точно відкрите». Але exclude має пріоритет і може викинути endpoint із exposure. Це хороша новина, бо дозволяє робити широкий include в локальному налагодженні та точково закривати небезпечне. Але це й пастка, якщо ви забули, що десь залишили exclude: "info" і тепер дивуєтеся, чому info недоступний.
Помилка №4: однаково широкий exposure в усіх профілях.
Найнебезпечніший анти‑патерн — зробити один конфіг, де include: "*", і закомітити його «бо так зручно». Зручно буде рівно до першого моменту, коли сервіс стане доступним не лише вам. У нашому курсі нормальна стратегія така: local/dev — ширше, prod — вужче. Це не параноя, а звичайна дисципліна.
Помилка №5: конфіг написаний правильно, але лежить не там, або активний не той профіль.
Це вже більш «життєва» помилка: ви написали все в application-local.yaml, але запустили сервіс без профілю, і дивуєтеся, чому доступний лише health. Тут допомагає ваша ж інфраструктура з попередніх модулів: перевіряйте активні профілі в логах старту і, якщо потрібно, задавайте spring.profiles.active=local тим способом, яким ви вже вмієте: env var, system property, CLI args.
ПЕРЕЙДІТЬ В ПОВНУ ВЕРСІЮ