Всем привет! Иногда внутренний мониторинг не даёт полной картины, что все работает как надо. И полезно сделать внешний пинг и посмотреть, действительно ли нужный проект доступен.
Сегодня мы расскажем, как решали эту задачу для себя, и выложим код, который вы сможете применить для простого мониторинга своих проектов. И да, мы знаем про существование специализированных сервисов для решения этой задачи, но всегда веселее написать свой скрипт.
В конце статьи вы найдете ссылки на GitHub, где код нашего внешнего мониторинга доступности размещен под Open Source лицензией Apache 2.0. Будем рады, если он вам пригодится.
У нас в облаке проекты можно разворачивать в двух регионах - в Москве и в Варшаве. И хотелось иметь возможность автоматически мониторить стабильность работы сетевой инфраструктуры. Для этого, мой коллега, Иван Шихалдин написал на Java+SpringBoot скрипт, и развернул в Amvera Cloud парочку небольших приложений для проведения healthcheck’ов и отправки мониторинговых сообщений в наш мониторинговый телеграм-канал.
Что такое Amvera
Amvera - облако для простого развертывания IT-приложений, предоставляющее бонусом:
Автоматизацию деплоя. Делаете push в привязанный git (или перетягиваете файлы в интерфейсе) и сервис сам все настроит и запустит. Это проще настройки VPS;
Бесплатный внешний домен с HTTPS;
Встроенные бэкапы, алерты, мониторинг, логирование, проксирование до нейронок и еще множество вещей, упрощающих жизнь разработчика.
Приветственные 111 рублей на баланс для тестов;
Первое приложение поднимает вебсервер и слушает эндпойнт healthcheck’a. Второе приложение в цикле пингует этот эндпойнт и проверяет статус ответа. Если статус ответа изменяется, то оно отправляет предупреждение в группу телеграм, плюс логирует все свои обращения к эндпойнту.
Мы используем разную инфраструктуру в кластере Москвы и в кластере Варшавы. Поэтому нам показалась хорошей идея, что тестер из Москвы будет проверять доступность эндпойнта в Варшаве, а тестер из Варшавы - доступность эндпойнта в регионе Москва. Таким образом, мы оперативно можем узнать о возникших проблемах с работой клиентских приложений или сетевой доступностью, при этом вероятность, что проблема возникнет сразу в двух регионах на различной физической инфраструктуре небольшая и получается довольно стабильный тест.
Для эндпойнта мы использовали стартеры - spring-boot-starter-web для запуска вебсервера, и spring-boot-starter-security, чтобы прикрыть эндпойнт фильтром на проверку API Key:
// Проверяем, есть ли заголовок "X-API-KEY" String requestApiKey = request.getHeader("X-API-KEY"); // Если ключ отсутствует или не совпадает с ожидаемым if (requestApiKey == null || !requestApiKey.equals(apiKey)) { // Если запрос идет к /api/v1/healthcheck, и ключ неверный, возвращаем 401 Unauthorized // Иначе, если это другой незащищенный эндпоинт, пропускаем дальше (если нужно) if (request.getRequestURI().startsWith("/api/v1/")) { response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Unauthorized response.getWriter().write("Invalid API Key"); return; } } else { // Если ключ валиден, создаем объект аутентификации и помещаем его в SecurityContext // AuthorityUtils.NO_AUTHORITIES означает, что у пользователя нет специфических ролей Authentication authentication = new ApiKeyAuthentication(requestApiKey, AuthorityUtils.NO_AUTHORITIES); SecurityContextHolder.getContext().setAuthentication(authentication); } // Передаем запрос дальше по цепочке фильтров filterChain.doFilter(request, response);
Сам API ключ хранится в переменной окружения в виде строки. Конфигурация секьюрити получилась такая:
@Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) // Отключаем CSRF для API (не используем сессии) .sessionManagement(sm -> sm.sessionCreationPolicy(org.springframework.security.config.http.SessionCreationPolicy.STATELESS)) // Важно для API-ключей/токенов .authorizeHttpRequests(authorize -> authorize // Разрешаем /api/v1/healthcheck только с валидным API-ключом .requestMatchers( "/api/v1/healthcheck", "/api/v1/ping").authenticated() // Все остальные запросы разрешаем без аутентификации .anyRequest().permitAll() ) // Добавляем наш фильтр перед стандартным UsernamePasswordAuthenticationFilter .addFilterBefore(apiKeyAuthFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); }
Сам рест-контроллер получился очень простым:
@RestController @RequestMapping("/api/v1/") public class HealthCheckController { @GetMapping("/healthcheck") public String index() { return "health OK"; } @GetMapping("/ping") public String ping() { return "pong"; } }
Теперь предлагаю заглянуть в содержание тестера. В конфиге gradle практически то же самое, я убрал web-стартер и добавил библиотеку telegrambots в зависимости, чтобы мы могли отправлять уведомления в наш мониторинговый телеграм-чат.
Для этого в TelegramService мы создаем внутренний класс TelegramSender, расширяющий DefaultAbsSender для передачи туда токена бота:
private static class TelegramSender extends DefaultAbsSender { protected TelegramSender(DefaultBotOptions options, String botToken) { super(options, botToken); } }
Затем мы инжектируем его через конструктор. Токен бота и id чатов, куда будем слать уведомления, задаем через переменные окружения и application.yml:
private final TelegramSender telegramSender; public TelegramService(@Value("${app.telegram.token}") String telegramBotToken, @Value("${app.telegram.chat}") String telegramBotChat, @Value("${app.amvera.alerts.chat}") String amveraAlertsChat){ this.telegramBotToken = telegramBotToken; this.telegramBotChat = telegramBotChat; this.amveraAlertsChat = amveraAlertsChat; this.telegramSender = new TelegramSender(new DefaultBotOptions(), telegramBotToken); log.info("Bot is initialized"); }
Сам этот сервис содержит всего один метод - для отправки уведомления в телеграм чат:
public void sendMessage(String messageText){ var message = new SendMessage(); message.setChatId(telegramBotChat); message.setText(messageText); try{ telegramSender.execute(message); log.info("Message sended: {}", messageText); }catch (TelegramApiException e){ log.error(e.getMessage()); } if(amveraAlertsChat!=null&&amveraAlertsChat.isEmpty()==false){ //отправим в чат алертов уведомление message.setChatId(amveraAlertsChat); message.setText(messageText); try{ telegramSender.execute(message); log.info("Alert sended: {}", messageText); }catch (TelegramApiException e){ log.error("Chat id {} not found",amveraAlertsChat); log.error(e.getMessage()); } } }
Также наш тестер содержит еще один сервис HealthCheckService, в котором мы по расписанию опрашиваем наш эндпойнт и анализируем его ответы. Используем аннотацию @Scheduled(fixedRate = 1*60000L, initialDelay = 5000L), чтобы наш тестирующий метод запускался раз в минуту (60000 миллисекунд), с задержкой 5 секунд (5000мс) перед стартом, чтобы программа успевала загрузиться:
@Scheduled(fixedRate = 1*60000L, initialDelay = 5000L) public void ping() { //error messages every 5 minutes // log.info("Starting ping..."); HttpClient client = HttpClient.newBuilder().build(); // Запрос к /api/v1/ping var response = sendHealthCheckRequest(client, pingURL); if(response == null){ log.error("PING NULL response"); if(status != 0) { telegramService.sendMessage("%s - не удается установить соединение".formatted(goal)); } status = 0; } else { if (response.statusCode()==200) { log.info("PING {}",response.statusCode()); } else{ log.warn("PING {}",response.statusCode()); } if(status != response.statusCode()){ if (response.statusCode()==200) { telegramService.sendMessage("%s OK".formatted(goal)); } else{ telegramService.sendMessage("%s сбоит : %s" .formatted(goal,response.statusCode())); } } status = response.statusCode(); } }
В данном случае GOAL это направление тестирования - Москва или Варшава, по сути, просто строка с текстом из переменных окружения, которая информационно будет передаваться в телеграм-алерт. Тут мы рассматриваем три состояния - эндпойнт вернул статус-код 200, отправляем статус “Все ОК”, или же вернул отличный от 200 статус - отправляем статус с ошибкой, или же мы просто не смогли установить соединение (например, сетевые проблемы). Тогда так и пишем в телеграм - не смогли установить соединение.
Сама отправка запроса на эндпойнт, результат которой мы анализировали выше, выглядит так:
private HttpResponse<String> sendHealthCheckRequest(HttpClient client, String url) { HttpResponse<String> response; try { HttpRequest request = HttpRequest.newBuilder() .uri(URI.create(url)) // Указываем URL .header("X-API-KEY", API_KEY) // Добавляем HTTP-заголовок с API-ключом .GET() // Указываем HTTP-метод GET .build(); response = client.send(request, HttpResponse.BodyHandlers.ofString()); log.debug("ANSWER {} {} {}", response.statusCode(), response.body(), url); } catch (IOException e) { // log.error("IOException when HTTP Request to {} [{}] {}", url, e.getMessage(),e); log.error("IOException when HTTP Request to {} [{}]", url, e.getMessage()); response = null; } catch (InterruptedException e) { log.error("InterruptedException when HTTP Request to {} [{}] {}", url, e.getMessage(),e); response = null; } return response; }
Здесь мы дергаем ручку эндпойнта (URL тоже берется из переменных окружения), в заголовке передаем X-API-KEY и вызываем метод GET нашего HTTP запроса.
Мое повествование уже подходит к концу. Подведем итоги. Мы по расписанию вызываем перекрестно между регионами эндпойнты специальным тестером. И в случае изменения статуса ответа, возвращаемого эндпойнтом, шлем или не шлем уведомления в наши рабочие чаты. А дальше уже начинаем разбираться по логам и метрикам, что же случилось с сетевой доступностью.
Код нашего решения для пинга доступен по ссылкам на GitHub 1 и 2 под Open Source лицензией Apache 2.0.
Надеюсь, код поможет осущесвлять мониторинг ваших проектов. А лучшая благодарность, это развертывание пингатора в нашем облаке) Для этого в репозитории уже есть все конфигурационные файлы. Достаточно заменить эндпоинты и сделать git push amvera master.
