Перейти к основному содержимому

Подключение http клиентов в сервис Ensi

Микросервисная архитектура подразумевает обмен сообщениями между сервисами. Основным способом общения сервисов Ensi между собой являются http-вызовы, поэтому http-клиент является очень важной частью сервиса и имеет некоторые настройки, о которых необходимо знать.

Конфигурирование http клиента (GuzzleHttp) происходит в файле app/Providers/OpenApiClientsServiceProvider.php.

Глобальные настройки http клиента

В методе configureHandler происходит следующее:

$stack = new HandlerStack(Utils::chooseHandler());

$stack->push(Middleware::httpErrors(new BodySummarizer()), 'http_errors');
$stack->push(Middleware::redirect(), 'allow_redirects');
$stack->push(Middleware::prepareBody(), 'prepare_body');
if (!config('ganesha.disable_middleware', false)) {
$stack->push($this->configureGaneshaMiddleware());
}

$stack->push(new PropagateInitialEventLaravelGuzzleMiddleware());

if (config('app.debug')) {
$stack->push($this->configureLoggerMiddleware(), 'logger');
}

Т.е. на т.н. GuzzleHandler навешиваются middleware:

  • httpErrors смотрит на код ответа и кидает исключение для 4 и 5 кодов
  • redirect обеспечивает поддержку редиректов
  • prepareBody добавляет в запрос стандартные заголовки вроде Content-Type и Content-Length
  • GaneshaMiddleware - Circuit breaker
  • PropagateInitialEventLaravelGuzzleMiddleware добавляет в запрос заголовок трассировки, содержащий информацию о том кто и где инициировал этот запрос
  • configureLoggerMiddleware - добавляет логирование исходящих запросов с помощью GuzzleHttp\Middleware::log(), это очень полезно при локальной разработке
private function configureLoggerMiddleware(): callable
{
$logger = logger()->channel('http_client');
$format = "{req_headers}\n{req_body}\n\n{res_headers}\n{res_body}\n\n";
$formatter = new MessageFormatter($format);

return Middleware::log($logger, $formatter, 'debug');
}

Регистрация http клиентов к сервисам ensi

После подключения пакет клиента необходимо добавить всего два кусочка кода.

Первый - регистрация клиента в DI контейнере:

$this->registerService(
handler: $handler,
domain: 'catalog',
serviceName: 'offers',
configurationClassName: OffersClientProvider::$configuration,
apisClassNames: OffersClientProvider::$apis
);

Здесь handler - это вышеописанный GuzzleHandler, к которому уже применены глобальные настройки. domain - это не dns домен, а бизнес-домен, по сути просто первая часть имени внешнего сервиса. serviceName - код сервиса клиент к которому мы регистрируем, т.е. вторая часть имени сервиса. Класс OffersClientProvider всегда есть в сгенерированных клиентах, он называется по-разному в разных клиентах.

Второй - регистрация переменных конфигурации в файле config/openapi-clients.php:

return [
'catalog' => [
'offers' => [
'base_uri' => env('CATALOG_OFFERS_SERVICE_HOST') . "/api/v1",
],
],
];

Здесь catalog и offers - это вышеописанные domain и serviceName. base_uri содержит адрес сервиса, а точнее берёт его из env переменной формата {DOMAIN}_{SERVICE}_SERVICE_HOST. В эту переменную следует передавать адрес с указанием схемы, например:

CATALOG_OFFERS_SERVICE_HOST=http://catalog-offers.ensi.127.0.0.1.nip.io

Регистрация http-клиентов к внешним сервисам

Иногда возникает необходимость работать не только через наши автосгенерированные клиенты, но и обращаться к другим внешним системам (например логистические системы (СДЭК), процессинговые системы (Сбер), внешние crm, erp, wms и т.д.). В таком случае у нас нет возможности сгенерировать к ним готовый клиент и тут есть 2 пути:

  1. Это полноценная внешняя система с приличным объемом взаимодействия, различных DTO и эндпоинтов, которые логически стоит разделить на разные классы Api. Или система, взаимодействие с которой может идти из разных систем-потребителей. Например, внешняя crm, интеграция с которой нужна и на bff и в customers. Или внешняя 3pl система, через которую мы получаем данные по разным сущностям, заказам, остаткам и т.д. В таком случае стоит выделить отдельный composer-пакет под sdk к этой внешней системе (подробнее об этом ниже).
  2. Это небольшая внешняя система, буквально с 1-3 эндпоинтами, запросы в которую будут гарантированно делаться только из одного сервиса. В таком случае клиент можно не выделять в отдельный пакет, а реализовывать непосредственно внутри сервиса-потребителя. Примером такого взаимодействия может быть oms - Сбер.

Независимо от выбранного способа, клиент к внешней системе должен использовать тот же объект GuzzleHandler, описанный выше. Соответственно должен проходить регистрацию в том же app/Providers/OpenApiClientsServiceProvider.php (исключением могут быть ситуации, когда вы хотите использовать готовый пакет для взаимодействия с внешней системой, но он не даёт возможности произвести настройку).

Небольшой клиент внутри сервиса-потребителя

В случае реализации клиента внутри сервиса-потребителя, вы можете создать отдельный метод в app/Providers/OpenApiClientsServiceProvider.php и проводить там регистрацию, примерно так:

$this->app->when(MyHttpClient::class)
->needs(GuzzleHttp\ClientInterface::class)
->give(fn () => new GuzzleHttp\Client([
'handler' => $handler,
'base_uri' => $baseUri,
// other client options
]));

Шаблон для sdk-пакетов

Для того чтобы сделать свой новый sdk-пакет, можно использовать шаблон. Он написан таким образом, что регистрация и взаимодействие с ним в коде сервиса-потребителя выглядит абсолютно аналогично, как в автосгенерированным клиентом.

Последовательность шагов для работы с sdk из шаблона:

  1. Создаём репозиторий в своём проекте под sdk к нужной внешней системе
  2. Клонируем себе шаблон
git clone git@gitlab.com:greensight/ensi/templates/sdk-template.git <sdk-repo-name>
cd <sdk-repo-name>
rm -rf .git && git init
git remote add origin git@gitlab.com:<my>/<project>/<sdk-repo-name>.git
  1. Переименовываем/удаляем все заглушки:
  • В README.md
  • В composer.json
  • Переименовываем BackendServiceClientProvider в ваш {ServiveName}ClientProvider. В этом файле есть статическая переменная $apis, в которую необходимо вручную добавлять создаваемые вами классы Api
  • Заменяем ExampleApi, ExampleRequest, ExampleResponse на свои классы, например ProductsApi, SearchProductsRequest, SearchProductsResponse. Вы можете выделять столько {MyName}Api, сколько вам логически требуется, но каждый из них необходимо указывать в {ServiveName}ClientProvider::$apis
  • Меняем во всех файлах namespace с Ensi\BackendServiceClient\* на ваш {Project}\{ServiveName}Client\*
  1. Пушим изменения
git add . && git commit -m "Initial commit" && git push -u origin master
  1. После этого подключаем sdk в сервис-потребитель точно так же, как автогенерируемый клиент (описано выше)