Трейсы в Spring Boot 3 с использованием Zipkin и Kafka в качестве транспорта
Рассказываем об использовании трейсов в ежедневной работе компании. Автор статьи – главный разработчик управления информационных систем розничного блока «АльфаСтрахование» Никита Носов.
Предисловие
Страховая компания «АльфаСтрахование», в которой я работаю, продает страховые продукты не только в офисах, но и через сеть страховых агентов. Агенты могут оформлять полисы, используя REST API. Этот API разрабатывает и поддерживает команда, в которой мне посчастливилось трудиться.
Статья содержит контекст и технику, поэтому если Вас интересует только техническая часть, можно смело переходить к разделу «Пререквизиты».
Статья не содержит описания Zipkin и концепции трейсов. Прочитать про Zipkin можно на официальном сайте.
Контекст
Немного о том, как продается страховой продукт
Основными операциями при продаже являются:
• Расчет стоимости страхового продукта;
• Сохранение данных страхового полиса в core-системах СК;
• Оплата;
• Печать документов.
Специфика оформления предполагает выполнение вышеуказанных операций с временными задержками. К примеру, рассчитав страховой продукт, мы даем клиенту время на принятие решения. В случае положительного решения, на основании расчета производится сохранение, оплата и т. д. Мы используем микросервисную архитектуру, разделяя операции по сервисам, ответственным за них.
Стоит отметить, что каждый такой сервис в свою очередь обращается в десятки других core-сервисов. Основной фреймворк, используемый для написания таких сервисов — Spring Boot.
Когда что-то идет не так
Вышеописанные цепочки вызовов порой возвращают HTTP 500, после чего стартует классический процесс разбора — что же пошло не так. Стандарт нашей компании предполагает наличие логов и трейсов, таким образом добраться до причины ошибки не представляет особого труда. Далее с этим пониманием в системы вносятся исправления (или производятся консультации конечного потребителя) .
Сервисов много, Zipkin — один
Для трассировки приложений используется Zipkin. Транспортом является Kafka. Такой подход считается общепринятым в рамках организации. Наше светлое будущее в аспекте трейсов выглядит следующим образом: имея трейсы со всех сервисов, мы можем легко отследить цепочки вызовов и с минимальными временными затратами понять, что пошло не так (если все-таки что-то пошло не так).
Пререквизиты
Итак, что мы имеем на входе:
• Java 17;
• Сервис, написанный на Spring Boot 3.1.2;
• Корпоративный Zipkin, принимающий трейсы через транспорт Kafka (в т. ч. тестовый стенд);
• Корпоративная Kafka с топиком, в который следует писать трейсы (в т. ч. тестовый топик на тестовых серверах);
• Потребность в автоконфигурации (т. к. сервисов много, не хотелось бы писать реализацию в каждом) — будет рассмотрена на втором шаге.
Шаг 1. Реализация на базе существующего сервиса
В данном разделе (как и в последующих) мы не будем погружаться в детали реализации существующего сервиса. Достаточно будет вышеуказанных вводных, которые сформируют техническое окружение, в котором мы будем находиться.
Задача первого шага
Добавить конфигурации, с помощью которых сервис сможет отправлять трейсы в Zipkin с транспортом Kafka. Предусмотреть возможность собирать трейсы внутри приложения, т. е. не только между сервисами, но и внутри, между компонентами одного сервиса.
Реализация первого шага
Dependencies
Для реализации нам потребуются следующие зависимости. Стоит отметить, что при написании сервиса был использован parent org. springframework. boot:spring-boot-starter-parent:3.1.2, из которого все версии подтянулись сами. Ниже они указаны для справки.
Properties
Потребуются для настройки подключения к топику Kafka, в который будем отправлять трейсы. Вынесем их в отдельную ветку конфигураций. Получим нечто похожее на:
Что в конечном счете будет заполняться из фрагмента application. yaml:
@Test: CustomTracingProperties должны собирать свойства из application. yaml
Kafka sender
Вручную конфигурируем KafkaSender. Потребуется указать действительный способ аутентификации в Kafka. В данном примере используется:
• security. protocol=SASL_plaintext;
• sasl. mechanism=SCRAM-SHA-256.
Зная все вводные, собираем конфигурацию:
Пример выше показывает, как с помощью KafkaSender. newBuilder() мы заполняем нужные для подключения к Kafka свойства, в т. ч. используя CustomTracingProperties и константы. Константы в примере записаны в виде литералов, чтоб было нагляднее.
Примечание. Строка с брокерами разбирается в лист таким образом:
@Test: KafkaSender должен быть корректно сконфигурирован
Observation
Для того, чтобы иметь возможность смотреть трейсы взаимодействия компонентов внутри одного сервиса, также добавим конфигурацию:
Таким образом, аннотируя методы (или классы) с помощью @Observed, получим трейсы конкретного метода (или всех методов класса).
Параметр name в аннотации @Observed служит для определения имени наблюдаемого объекта. Тест данной конфигурации рассмотрим позднее, в этой же статье.
Management
В Spring Boot 3 трейсы были вынесены в Micrometer Tracing, про это не следует забывать. Я добавил такие настройки в application. yaml:
Проверим результат и переходим к стартеру
Реализовав функциональность и убедившись в том, что трейсы сервиса доступны в Zipkin, соберем все в автоконфигурацию. Вероятно, Вам покажется странным что на данном шаге не приведены результаты. Они обязательно будут приведены позже, когда будем тестировать автоконфигурацию.
Шаг 2. Создаем автоконфигурацию
Задача второго шага
Создать автоконфигурацию, которая может быть подключена к готовому сервису, написанному на Spring Boot, как отдельная библиотека (стартер).
Реализация второго шага
Создадим новый Spring Boot проект, добавим класс MyTracingAutoConfiguration. class. Перенесем в проект все конфигурации и зависимости, описанные выше. Сделаем так, чтобы конфигурация включалась при management. tracing. enabled=true.
Нижеуказанную зависимость сохраним, но добавим ей provided. Данное действие указывает на то, что actuator у нас и так присутствует в classpath сервиса, к которому будем подключать стартер.
Момент включения автоконфигурации
Хочу обратить Ваше внимание на то, что конфигурацию мы будем производить до ZipkinAutoConfiguration. class во избежание конфликта бинов (restTemplateSender vs. kafkaSender). Сделаем это с помощью аннотации @AutoConfigureBefore.
Автоконфигурация
В примере выше мы обозначили следующие условия:
• @ConditionalOnProperty — говорит о том, что конфигурация будет включаться, если существует property management. tracing. enabled в значении true;
• @AutoConfigureBefore(ZipkinAutoConfiguration. class) — говорит о том, что обсудили в разделе “Предварительные действия” (см. выше) .
Далее мы импортируем вышеописанные конфигурации:
• KafkaSenderConfiguration. class;
• ObservedAspectConfiguration. class.
Регистрируем автоконфигурацию
Чтобы это действительно было автоконфигурацией, нам нужно добавить файл /src/main/resources/META-INF/spring/org. springframework. boot. autoconfigure. AutoConfiguration. imports с указателем на класс MyTracingAutoConfiguration (указываем каноническое имя класса автоконфигурации).
Некоторые тесты
Ниже приведены тесты, которые, по моему мнению, важно указать в статье, т. к. они наглядно демонстрируют работу приложения.
@SpringBootConfiguration в тестовом окружении
Для тестирования автоконфигурации я добавлю класс TestConfiguration. class, аннотированный @SpringBootConfiguration в тестовый пакет, в котором размещаются тесты автоконфигурации. Подробнее про это можно посмотреть в этом видео.
Как тестировать @Observed?
Тест поможет понять, каким образом мы используем аннотацию @Observed и получаем трейсы на уровне взаимодействия компонентов приложения.
Шаг 1. Потребуется добавить зависимость
Шаг 2. Создадим тестовую аннотацию @EnableTestObservation, которая подготовит контекст для теста
Подготовка тесового контекста заключается в добавлении конфигурационного бина observationRegistry (TestObservationRegistry из зависимости, которую добавили для тестирования). Также импортируем нашу ObservedAspectConfiguration и добавляем аннотацию @AutoConfigureObservability.
Мы делаем отдельную аннотацию для того, чтобы переиспользовать ее в нескольких тестах.
Шаг 3. Напишем тест и аннотируем его @EnableTestObservation
В тесте мы проверили, что при вызове метода foo() тестового компонента someClass, observation стартовала и завершилась.
Протестируем условие включения/игнорирования конфигурации
@Test: Конфигурация должна включаться при наличии management. tracing. enabled=true
@Test: Конфигурация НЕ должна включаться при отсутствии management. tracing. enabled=false
Проверяем работоспособность
Создадим новый проект, в pom. xml добавим зависимости, указанные в статье + автоконфигурацию, написанную ранее. Также добавим lombok для удобства. Далее добавим application. yaml, укажем необходимые нам properties:
Добавим сервис FooService для проверки работоспособности аннотации @Observed:
Напишем простейший контроллер:
Запустим приложение и отправим запрос
curl http://localhost:8080/foo $ bar
Проверим логи. В application. yaml Вы могли увидеть, что уровень логов для org. apache. kafka. clients. NetworkClient: debug. Данная настройка указана для того, чтобы убедиться, что сообщение в Kafka было отправлено. Убедимся, что запрос был:
2023-09-22T21:31:43.270+03:00 DEBUG [demo-tracing-app,,] 16342 --- [emo-tracing-app] org.apache.kafka.clients.NetworkClient: [Producer clientId=demo-tracing-app] Sending produce request with header RequestHeader … etc.
Продолжаем изучать логи. Найдем traceId (650ddd8f171924cbfc5d355d33fb9d9b), по которому будем искать трейс в Zipkin:
2023-09-22T21:31:43.260+03:00 INFO [demo-tracing-app, 650ddd8f171924cbfc5d355d33fb9d9b, c2987710f3440cd1] 15011 --- [nio-8080-exec-1] c. e.d. t.application. service. FooService: this is an internalFoo log
Найдем трейс в Zipkin:
На картинке выше показан трейс, содержащий 2 интервала (span) : родительский (parent) и дочерний (тот самый, заданный с помощью @Observed(name = «observation-name»)) .
Наступило ли наше светлое будущее?
На самом деле только частично. Да, трейсы и возможность быстрого их подключения у нас есть, но представленная конфигурация добавляет их для конкретного приложения. Она не позволяет собрать их воедино в единую цепочку вызовов (чтобы под одним traceId увидеть span’ы из разных приложений).
Иными словами: если поднять 2 приложения, подключив к ним стартер, при этом одно будет вызывать другое, мы сможем увидеть 2 разных traceId: 1го и 2го соответственно.
Конфигурация, позволяющая увязать все воедино — наша следующая цель, которую только предстоит реализовать. Уважаемый читатель, если у Вас есть предложения по этому поводу — буду благодарен им в комментариях.
Заключение
Мы рассмотрели вариант добавления трейсов в Spring Boot 3 приложение с возможностью последующего переиспользования этой функциональности. Трейсы передаются в Zipkin через Kafka. Функциональность также позволяет добавлять трейсы межкомпонентного взаимодействия внутри приложения.