Подключение 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 пути:
- Это полноценная внешняя система с приличным объемом взаимодействия, различных DTO и эндпоинтов, которые логически стоит разделить на разные классы Api. Или система, взаимодействие с которой может идти из разных систем-потребителей. Например, внешняя crm, интеграция с которой нужна и на bff и в customers. Или внешняя 3pl система, через которую мы получаем данные по разным сущностям, заказам, остаткам и т.д. В таком случае стоит выделить отдельный composer-пакет под sdk к этой внешней системе (подробнее об этом ниже).
- Это небольшая внешняя система, буквально с 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 из шаблона:
- Создаём репозиторий в своём проекте под sdk к нужной внешней системе
- Клонируем себе шаблон
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
- Переименовываем/удаляем все заглушки:
- В
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\*
- Пушим изменения
git add . && git commit -m "Initial commit" && git push -u origin master
- После этого подключаем sdk в сервис-потребитель точно так же, как автогенерируемый клиент (описано выше)