Actuator: base-path и include/ exclude

Spring Boot
22 уровень , 2 лекция
Открыта

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" или
"/manage"
Чтобы все служебные 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.

1
Задача
Spring Boot, 22 уровень, 2 лекция
Недоступна
Перенос Actuator под путь `/manage`
Перенос Actuator под путь `/manage`
1
Задача
Spring Boot, 22 уровень, 2 лекция
Недоступна
Разный HTTP-exposure для `local` и `prod`
Разный HTTP-exposure для `local` и `prod`
Комментарии
ЧТОБЫ ПОСМОТРЕТЬ ВСЕ КОММЕНТАРИИ ИЛИ ОСТАВИТЬ КОММЕНТАРИЙ,
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ