9
0
0
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
Назад

Spring Boot Actuator под капотом: лепим свою ручку к швейцарскому ножу

Время чтения 36 минут
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники
9
0
0
Нет времени читать?
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники

Стандартный Actuator — это коробка с 12 отвертками. Вроде бы всё есть, пока не прилетает задача:

«А покажите-ка мне, сколько активных веб-сокет-сессий висит на нашем чате с нейросетью? И желательно по каждому пользователю отдельно. И прямо сейчас, без деплоя».

И вы такой: «Э-э-э…»

Потому что /metrics такого не даст. /health — тем более. А в голову лезут варианты:

  1. Написать отдельный REST-контроллер. А потом забыть его закрыть от внешнего мира.
  2. Залезть в лог и руками посчитать сессии. К утру понедельника.

Или третий вариант: свой эндпойнт для Actuator.

Spring Boot Actuator под капотом: лепим свою ручку к швейцарскому ножу

Как это выглядит в реальной жизни, а не в красивых презентациях

В моем чате с Ollama есть WebSocket-соединения. Пользователи задают вопросы нейросети, она отвечает. И тут кто-то жалуется: «Тормозит». Вопрос: это сеть тормозит или сессий накопилось столько, что сервер задыхается?

Хочется простого ответа — без логов и дебаггера.

Пишем класс. Обратите внимание: всё через аннотации Actuator, никаких @RestController или @GetMapping


java
@Component
@Endpoint(id = "websockets")
public class WebSocketActuatorEndpoint {
    private final SimpUserRegistry userRegistry;
    private final SimpSessionRegistry sessionRegistry;
    public WebSocketActuatorEndpoint(SimpUserRegistry userRegistry,
                                     SimpSessionRegistry sessionRegistry) {
     this.userRegistry = userRegistry;
     this.sessionRegistry = sessionRegistry;
}
    @ReadOperation
public Map<String, Object> getStats() {
     // Собираем статистику по сессиям вручную, потому что стандартные метрики не умеют
     Set<SimpUser> users = userRegistry.getUsers();
     int totalSessions = users.stream()
         .mapToInt(user -> user.getSessions().size())
         .sum();
    
        Map<String, Integer> sessionsPerUser = new HashMap<>();
     for (SimpUser user : users) {
         sessionsPerUser.put(user.getName(), user.getSessions().size());
     }
    
        Map<String, Object> result = new LinkedHashMap<>();
     result.put("totalUsers", users.size());
     result.put("totalSessions", totalSessions);
     result.put("sessionsPerUser", sessionsPerUser);
     result.put("timestamp", Instant.now().toString());
    
        // А вот сюрприз: если сессий больше сотни, можно добавить warning
     if (totalSessions > 100) {
         result.put("warning", "Многовато сессий для одного инстанса, пора горизонтально масштабироваться");
     }
    
        return result;
}
    @ReadOperation
public String getHealthCheck() {
     // А это уже хак: можно сделать несколько ReadOperation, если параметры разные
     long sessionCount = userRegistry.getUsers().stream()
         .flatMap(user -> user.getSessions().stream())
         .count();
    
        if (sessionCount > 200) {
         return "CRITICAL: too many websocket sessions";
     }
     return "OK";
}
    @WriteOperation
public void disconnectUser(String username) {
     // Операция записи: выкидываем пользователя из веб-сокетов через актуатор
     SimpUser user = userRegistry.getUser(username);
     if (user != null) {
         user.getSessions().forEach(session -> session.close(CloseStatus.GOING_AWAY));
     }
}
    @DeleteOperation
public void clearAllSessions() {
     // Когда всё совсем плохо, сбросить все сессии разом
     userRegistry.getUsers().forEach(user ->
            user.getSessions().forEach(session -> session.close(CloseStatus.GOING_AWAY))
     );
}
}

 

Теперь по адресу /actuator/websockets при GET-запросе я вижу:


json
{
  "totalUsers": 42,
  "totalSessions": 127,
  "sessionsPerUser": {
"alexey": 1,
"pavel": 3,
"guest_123": 5
  },
  "timestamp": "2025-01-15T14:23:00Z",
  "warning": "Многовато сессий для одного инстанса, пора горизонтально масштабироваться"
}

 

Без отдельного контроллера и правки security config. И без мучений, кстати.

Тонкости, о которых молчат в ютуб-туториалах

Первое. Зачем три аннотации: @ReadOperation, @WriteOperation, @DeleteOperation?

Actuator разделяет операции по принципу REST:

  • @ReadOperation — GET, ничего не меняет.
  • @WriteOperation — POST, меняет состояние.
  • @DeleteOperation — DELETE, удаляет что-то.

Можно переопределить HTTP-метод через @Selector, но обычно это не нужно.

Второе. Разные методы с одинаковой аннотацией не работают.

Вот этот код вызовет ошибку при старте:


java
@ReadOperation
public Map<String, Object> getStats() { ... }
 
@ReadOperation
public String getSimpleStats() { ... } // ОШИБКА! Actuator не поймет, какой вызывать

 

Actuator не смотрит на имена методов, только на типы параметров. Если параметров нет, считает, что это один и тот же эндпойнт.

Это можно обойти, если добавить параметр-дискриминатор:


java
@ReadOperation
public Map<String, Object> getStats(@Selector String mode) {
if ("full".equals(mode)) {
     return fullStats();
}
return simpleStats();
}

 

И вызывать /actuator/websockets/full.

Третье. Безопасность. Свой эндпойнт наследует настройки Actuator.

Если вы настроили:


yaml
management:
  endpoints:
web:
   exposure:
     include: "*"

 

Ваш эндпойнт будет доступен всем, даже если он удаляет все веб-сокеты.

Защититься можно либо через management.endpoints.web.exposure.exclude, либо через Spring Security:


java
@Endpoint(id = "websockets", enableByDefault = false) // не включается автоматически

 

И потом в конфиге вручную разрешаете доступ только нужным ролям.

Пример: кеш, который давно надо было посмотреть

У меня в проекте был кеш с результатами работы Ollama. Он хранил ответы на повторяющиеся вопросы — экономил деньги на GPU. Но я понятия не имел, что у него внутри.

Написал эндпойнт:


java
@Endpoint(id = "cache")
@Component
public class CacheEndpoint {
private final CacheManager cacheManager;
    public CacheEndpoint(CacheManager cacheManager) {
     this.cacheManager = cacheManager;
}
    @ReadOperation
public Map<String, Object> cacheStats() {
     Cache cache = cacheManager.getCache("ollama-responses");
     if (cache == null) {
         return Map.of("error", "Cache not found");
     }
    
        // Через нативный доступ к библиотеке кеша (Caffeine в моем случае)
     CaffeineCache caffeineCache = (CaffeineCache) cache;
     com.github.benmanes.caffeine.cache.Cache<Object, Object> nativeCache =
            caffeineCache.getNativeCache();
    
        return Map.of(
         "size", nativeCache.estimatedSize(),
         "hitRate", String.format("%.2f%%", cacheHits * 100.0 / (cacheHits + cacheMisses)),
         "evictions", evictionsCount
     );
}
    @WriteOperation
public void evictAll() {
     cacheManager.getCache("ollama-responses").clear();
}
}

 

Теперь я могу в любой момент дернуть /actuator/cache и увидеть: кеш на 87%, hit rate — 94%, вытеснений почти нет. Кеш работает — я спокоен.

А если кеш набивался мусором, просто отправлял POST /actuator/cache, и он очищался. 

Когда пилить свой эндпойнт — святое дело, а когда — нет

Кастомный эндпойнт нужен, если:

  • данные уже есть в бинах Spring, но нет готового API;
  • доступ к управлению приложением «только для админа»;
  • стандартные метрики не затрагивают бизнес-логику;
  • лень писать полноценный REST-контроллер с документацией.

Не нужно ничего придумывать, если:

  • данные можно получить через /metrics с кастомным счетчиком (это чище);
  • логика требует сложной авторизации (роли, разные права);
  • вы планируете отдавать эти данные на фронтенд (тогда обычный @RestController понятнее).

Что в итоге? 

Свой эндпойнт Actuator — это:

  •  @Component + @Endpoint(id = "...");
  •  @ReadOperation, @WriteOperation, @DeleteOperation;
  • доступ по /actuator/your-id;
  • те же права, что и у стандартных эндпойнтов;
  • хороший выбор для админских «заглянуть одним глазком».

Не надо городить велосипед с отдельным портом, аутентификацией и документацией. Всё уже есть. 

После того как я выкатил эндпойнт для веб-сокетов, прод перестал быть для меня черным ящиком. Я перестал гадать, сколько людей онлайн. Перестал угадывать, не пора ли добавить еще один под. Я просто открываю браузер, ввожу /actuator/websockets и вижу цифры — цивилизация!

Комментарии0
Тоже интересно
Комментировать
Поделиться
Скопировать ссылку
Telegram
WhatsApp
Vkontakte
Одноклассники