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'а с его web-видимостью: 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‑приложений, и оно описано в reference docs.
Можно запомнить так: 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 — держать минимальный набор.
Самый базовый allowlist: откроем 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 или нет, самый простой способ — запросить root 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 появятся и они. Это самый простой smoke check для 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.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ