├── CHANGELOG.md
├── LICENSE
├── README.md
├── composer.json
└── src
├── Async
├── OnFailureDefault.php
├── OnFailureInterface.php
├── OnSuccessDefault.php
└── OnSuccessInterface.php
├── Client
└── Curl.php
├── Exception
├── CloudIdParseException.php
├── CurlException.php
├── InvalidArgumentException.php
├── InvalidArrayException.php
├── InvalidIterableException.php
├── InvalidJsonException.php
├── InvalidXmlException.php
├── NoAsyncClientException.php
├── NoNodeAvailableException.php
├── NotFoundException.php
├── RuntimeException.php
├── SerializeException.php
├── TransportException.php
├── UndefinedPropertyException.php
└── UnknownContentTypeException.php
├── NodePool
├── Node.php
├── NodePoolInterface.php
├── Resurrect
│ ├── ElasticsearchResurrect.php
│ ├── NoResurrect.php
│ └── ResurrectInterface.php
├── Selector
│ ├── RoundRobin.php
│ ├── SelectorInterface.php
│ └── SelectorTrait.php
└── SimpleNodePool.php
├── OpenTelemetry.php
├── Serializer
├── CsvSerializer.php
├── JsonSerializer.php
├── NDJsonSerializer.php
├── SerializerInterface.php
├── TextSerializer.php
├── Utility.php
└── XmlSerializer.php
├── Transport.php
└── TransportBuilder.php
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Changelog
2 |
3 | ## 8.11.0 (2025-04-02)
4 |
5 | - Added Node::getLastPing and Node::getFailedPings for custom NodePool implementations with ping backoffs
6 | [#35](https://github.com/elastic/elastic-transport-php/pull/35)
7 | - Add HTTP network exception as previous exception to NoNodeAvailableException for debugging
8 | [#34](https://github.com/elastic/elastic-transport-php/pull/34)
9 | - Fixed PHP 7.4+ issue - "must not be accessed before initialization" in getLastRequest() & getLastResponse()
10 | [#33](https://github.com/elastic/elastic-transport-php/pull/33)
11 | - Fixed missing response body with guzzle psr7 streams
12 | [#30](https://github.com/elastic/elastic-transport-php/pull/30)
13 |
14 | ## 8.10.0 (2024-08-14)
15 |
16 | - Added the OpenTelemetry support
17 | [#27](https://github.com/elastic/elastic-transport-php/pull/27)
18 | - Refactored the OpenTelemetry using PSR-7 attributes
19 | [2be33cd](https://github.com/elastic/elastic-transport-php/commit/2be33cdc8be161fc7dc9a1989da5d550ffc4a230)
20 |
21 | ## 8.8.0 (2023-11-08)
22 |
23 | - Support path in host connection URI
24 | [#21](https://github.com/elastic/elastic-transport-php/pull/21)
25 |
26 | - Support userInfo in host connection URI
27 | [#22](https://github.com/elastic/elastic-transport-php/pull/22)
28 |
29 | ## 8.7.0 (2023-05-23)
30 |
31 | - Allow installation of psr/http-message v2.0
32 | [#17](https://github.com/elastic/elastic-transport-php/pull/17)
33 |
34 | ## 8.6.0 (2023-01-12)
35 |
36 | - Add full request and response to the log message context for better integration using [Clockwork](https://underground.works/clockwork/)
37 | [#13](https://github.com/elastic/elastic-transport-php/pull/13)
38 |
39 | ## 8.5.0 (2022-10-14)
40 |
41 | - Release created to be compatible with 8.5 Elastic clients
42 | - Fixed the full body message in debug() log for Transport
43 | [#11](https://github.com/elastic/elastic-transport-php/pull/11)
44 |
45 | ## 8.4.0 (2022-08-17)
46 |
47 | - Release created to be compatible with 8.4 Elastic clients
48 | - Added meta header info for Symfony HTTP client
49 | [#9](https://github.com/elastic/elastic-transport-php/pull/9)
50 | - Added composer-runtime-api v2 for InstalledVersions
51 | [#10](https://github.com/elastic/elastic-transport-php/pull/10)
52 |
53 | ## 8.3.0 (2022-06-27)
54 |
55 | - Release created to be compatible with 8.3 Elastic clients
56 |
57 | ## 8.2.0 (2022-06-22)
58 |
59 | - Release created to be compatible with 8.2 Elastic clients
60 |
61 | ## 8.1.0 (2022-04-12)
62 |
63 | - Release created to be compatible with 8.1 Elastic clients
64 |
65 | ## 8.0.1 (2022-03-30)
66 |
67 | - Support of `psr/log` v1, 2 and 3 to fix the dependency with `elasticsearch/elasticsearch`.
68 | [a413687](https://github.com/elastic/elastic-transport-php/commit/a413687ae0fcc3f949b02935731a42a301b383ad)
69 |
70 | ## 8.0.0 (2022-03-24)
71 |
72 | Finally, the 8.0.0 GA.
73 |
74 | ## 8.0.0-RC4 (2022-03-08)
75 |
76 | Added the `TransportException` to extends the `Throwable`interface.
77 |
78 | ## 8.0.0-RC3 (2022-02-26)
79 |
80 | This RC3 release introduces the `OnSuccessInterface` and `OnFailureInterface`
81 | for manage the async code with the execution of a custom function during the
82 | return of `OnSuccess` and during the execution of `OnFailure`. As default behaviour
83 | the `OnSuccessDefault` and `OnFailureDefault` does not perform any operations.
84 |
85 | ## 8.0.0-RC2 (2022-02-23)
86 |
87 | This RC2 release uses `httplug` v2.3.0 to provide a full retry async mechanism
88 | thanks to PR https://github.com/php-http/httplug/pull/168.
89 |
90 | ## 8.0.0-RC1 (2022-02-17)
91 |
92 | This is the first release candidate for 8.0.0 containing some new
93 | features and changes compared with the previous 7.x Elastic transport.
94 |
95 | ### Changes
96 |
97 | - the `ConnectionPool` namespace has been renamed in `NodePool`,
98 | consequently all the `Connection` classes has been renamed in `Node`
99 | - the previous Apache 2.0 LICENSE has been changed in [MIT](https://opensource.org/licenses/MIT)
100 |
101 | ### New features
102 |
103 | - added the usage of [HTTPlug](http://httplug.io/) library to
104 | autodiscovery [PSR-18](https://www.php-fig.org/psr/psr-18/) client
105 | and `HttpAsyncClient` interface using [Promise](https://docs.php-http.org/en/latest/components/promise.html).
106 | - added the `Trasnport::sendAsyncRequest(RequestInterface $request): Promise`
107 | to send a PSR-7 request using asynchronous request
108 | - added the `Transport::setAsyncClient(HttpAsyncClient $asyncClient)`
109 | and `Transport::getAsyncClient()` functions. If the [PSR-18](https://www.php-fig.org/psr/psr-18/)
110 | client already implements the `HttpAsyncClient` interface you
111 | don't need to use the `setAsyncClient()` function, it will discovered
112 | automatically
113 | - added the `Transport::setRetries()` function to specify the number
114 | of HTTP request retries to apply. If the HTTP failures exceed the
115 | number of retries the client generates a `NoNodeAvailableException`
116 |
117 | ## 7.16.0 (2021-12-14)
118 |
119 | Release created to be compatible with 7.16 Elastic clients
120 |
121 | ## 7.15.0 (2021-12-01)
122 |
123 | Release created to be compatible with 7.15 Elastic clients
124 |
125 | ## 7.14.0 (2021-08-03)
126 |
127 | Release created to be compatible with 7.14 Elastic clients
128 | ## 7.13.0 (2021-05-25)
129 |
130 | Release created to be compatible with 7.13 Elastic clients
131 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Copyright 2022 Elasticsearch B.V (https://www.elastic.co)
2 |
3 | Permission is hereby granted, free of charge, to any person obtaining a copy
4 | of this software and associated documentation files (the "Software"), to deal
5 | in the Software without restriction, including without limitation the rights
6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7 | copies of the Software, and to permit persons to whom the Software is
8 | furnished to do so, subject to the following conditions:
9 |
10 | The above copyright notice and this permission notice shall be included in
11 | all copies or substantial portions of the Software.
12 |
13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # HTTP transport for Elastic PHP clients
4 |
5 | [](https://github.com/elastic/elastic-transport-php/actions)
6 |
7 | This is a HTTP transport PHP library for communicate with [Elastic](https://www.elastic.co/)
8 | products, like [Elasticsearch](https://github.com/elastic/elasticsearch).
9 |
10 | It implements [PSR-7](https://www.php-fig.org/psr/psr-7/) standard for managing
11 | HTTP messages and [PSR-18](https://www.php-fig.org/psr/psr-18/) for sending HTTP requests.
12 | Moreover, it uses the [PSR-17](https://www.php-fig.org/psr/psr-17/) for building `PSR-7` objects
13 | like HTTP requests, HTTP responses, URI, etc.
14 |
15 | It uses the [HTTPlug](http://httplug.io/) library to automatic discovery a [PSR-18](https://www.php-fig.org/psr/psr-18/)
16 | client, a [PSR-17](https://www.php-fig.org/psr/psr-17/) factory and the [HttpAsyncClient](https://github.com/php-http/httplug/blob/master/src/HttpAsyncClient.php)
17 | interface with [Promise](https://docs.php-http.org/en/latest/components/promise.html) for
18 | asyncronous HTTP requestes.
19 |
20 | Starting from 9.0.0 version, if no PSR-18 clients are discovered, the library uses a
21 | default [custom HTTP client](src/Client/Curl.php) based on [cURL](https://curl.se/).
22 | This client relies on the [cURL php extension](https://www.php.net/manual/en/book.curl.php)
23 | that must be installed.
24 | Moreover, this client does **not** implement the [HttpAsyncClient](https://github.com/php-http/httplug/blob/master/src/HttpAsyncClient.php)
25 | interface, which means you won't be able to send asynchronous requests.
26 | If you need support for asynchronous requests, consider installing a PST-18 HTTP client like
27 | [Guzzle](https://github.com/guzzle/guzzle):
28 |
29 | ```bash
30 | composer require guzzlehttp/guzzle
31 | ```
32 |
33 | or [Symfony HTTP Client](https://github.com/symfony/http-client):
34 |
35 | ```bash
36 | composer require symfony/http-client
37 | ```
38 |
39 | The architecture of the Transport is flexible and customizable, you can configure it
40 | using a [PSR-18](https://www.php-fig.org/psr/psr-18/) client, a [PSR-3](https://www.php-fig.org/psr/psr-3/)
41 | logger and a custom [NodePoolInterface](src/NodePool/NodePoolInterface.php), to manage
42 | a cluster of nodes.
43 |
44 | ## Quick start
45 |
46 | The main component of this library is the [Transport](src/Transport.php) class.
47 |
48 | This class uses 3 components:
49 |
50 | - a PSR-18 client, using [ClientInterface](https://www.php-fig.org/psr/psr-18/#interfaces);
51 | - a Node pool, using [NodePoolInterface](src/NodePool/NodePoolInterface.php);
52 | - a PSR-3 logger, using [LoggerInterface](https://www.php-fig.org/psr/psr-3/#3-psrlogloggerinterface).
53 |
54 | While the `PSR-3` and `PSR-18` are well known standard in the PHP community, the `NodePoolInterface`
55 | is a new interface proposed in this library. The idea of this interface is to provide a class
56 | that is able to select a node for a list of hosts. For instance, using Elasticsearch, that is a
57 | distributed search engine, you need to manage a cluster of nodes. Each node exposes a common
58 | HTTP API and you can send the HTTP requests to one or more nodes.
59 | The `NodePoolInterface` is a component that can be used to manage the routing of the HTTP
60 | requests to the cluster node topology.
61 |
62 | In order to buid a `Transport` instance, you can use the `TransportBuilder` as follows:
63 |
64 | ```php
65 | use Elastic\Transport\TransportBuilder;
66 |
67 | $transport = TransportBuilder::create()
68 | ->setHosts(['localhost:9200'])
69 | ->build();
70 | ```
71 |
72 | This example shows how to set the transport to communicate with one node located at `localhost:9200`
73 | (e.g. Elasticsearch default port).
74 |
75 | By default, `TransportBuilder` will use the autodiscovery feature of [HTTPlug](http://httplug.io/)
76 | for the [PSR-18](https://www.php-fig.org/psr/psr-18/) client, the [SimpleNodePool](src/NodePool/SimpleNodePool.php)
77 | as `NodePoolInterface` and the [NullLogger](https://github.com/php-fig/log/blob/master/Psr/Log/NullLogger.php)
78 | as `LoggerInterface`.
79 |
80 | The `Tranport` class itself implements the [PSR-18](https://www.php-fig.org/psr/psr-18/) and the
81 | [HttpAsyncClient](https://github.com/php-http/httplug/blob/master/src/HttpAsyncClient.php) interfaces,
82 | that means you can use it to send any HTTP request using the `Tranport::sendRequest()` function
83 | as follows:
84 |
85 | ```php
86 | use Http\Discovery\Psr17FactoryDiscovery;
87 |
88 | $factory = Psr17FactoryDiscovery::findRequestFactory();
89 | $request = $factory->createRequest('GET', '/info'); // PSR-7 request
90 | $response = $transport->sendRequest($request);
91 | var_dump($response); // PSR-7 response
92 | ```
93 |
94 | The `sendRequest` function will use `$request` to send the HTTP request to the `localhost:9200`
95 | node specified in the previous example code. This behaviour can be used to specify only the URL path
96 | in the HTTP request, the host is selected at runtime using the `NodePool` implementation.
97 |
98 | **NOTE**: if you send a `$request` that contains already a host the `Transport` will
99 | use it without using the `NodePool` to select a node specified in `TransportBuilder::setHosts()`
100 | settings.
101 |
102 | For instance, the following example will send the `/info` request to `domain` and not `localhost`.
103 |
104 | ```php
105 | use Elastic\Transport\TransportBuilder;
106 |
107 | $transport = TransportBuilder::create()
108 | ->setHosts(['localhost:9200'])
109 | ->build();
110 |
111 | $request = new Request('GET', 'https://domain.com/info');
112 | $response = $transport->sendRequest($request); // the HTTP request will be sent to domain.com
113 |
114 | echo $transport->lastRequest()->getUri()->getHost(); // domain.com
115 | ```
116 |
117 | ## Asyncronous requests
118 |
119 | You can send an asyncronous HTTP request using the `Transport::sendAsyncRequest()` as follows:
120 |
121 | ```php
122 | use Http\Discovery\Psr17FactoryDiscovery;
123 |
124 | $factory = Psr17FactoryDiscovery::findRequestFactory();
125 | $request = $factory->createRequest('GET', '/info'); // PSR-7 request
126 | $promise= $transport->sendAsyncRequest($request);
127 | var_dump($promise); // Promise
128 | var_dump($promise->wait()); // PSR-7 response
129 | ```
130 |
131 | The `$promise` contains a [Promise](https://docs.php-http.org/en/latest/components/promise.html) object.
132 | A promise is an object that does not block the execution of PHP. This means the promise does not
133 | contain the HTTP response. In order to read the HTTP response you need to use the `wait()` function.
134 |
135 | Another approach to use a promise is to specify the functions to be called on success and on faliure
136 | of the HTTP request. This can achieved using the `then()` function as follows:
137 |
138 | ```php
139 | $promise->then(function (ResponseInterface $response) {
140 | // onFulfilled callback, $reponse is PSR-7
141 | echo 'The response is available';
142 |
143 | return $response;
144 | }, function (Exception $e) {
145 | // onRejected callback
146 | echo 'An error happens';
147 |
148 | throw $e;
149 | });
150 | ```
151 |
152 | For more information about the usage of Promise objetcs you can read the [documentation](https://docs.php-http.org/en/latest/components/promise.html)
153 | from HTTPlug.
154 |
155 | ## Set the number of retries
156 |
157 | You can specify the number of retries for any HTTP requests. This means if the HTTP request will fail
158 | the client will automatically try to perform another request (or more).
159 |
160 | By default, the number of retries is zero (0). If you want you can change it using the `Transport::setRetries()`
161 | function, as follows:
162 |
163 | ```php
164 | use Elastic\Transport\TransportBuilder;
165 |
166 | $transport = TransportBuilder::create()
167 | ->setHosts([
168 | '10.0.0.10:9200',
169 | '10.0.0.20:9200',
170 | '10.0.0.30:9200'
171 | ])
172 | ->build();
173 |
174 | $transport->setRetries(1);
175 | $factory = Psr17FactoryDiscovery::findRequestFactory();
176 | $request = $factory->createRequest('GET', '/info');
177 | // If a node is down, the transports retry automatically using another one
178 | $response = $transport->sendRequest($request);
179 | ```
180 |
181 | This feature can be interesting as retry mechanism especially useful if you have a cluster of nodes.
182 | You can read the following section about `Node Pool` to understand how to configure the selection
183 | of nodes in a cluster environment.
184 |
185 | ## Node Pool
186 |
187 | The `SimpleNodePool` is the default node pool algorithm used by `Tranposrt`.
188 | It uses the following default values: [RoundRobin](src/NodePool/Selector/RoundRobin.php) as
189 | `SelectorInterface` and [NoResurrect](src/NodePool/Resurrect/FalseResurrect.php) as `ResurrectInterface`.
190 |
191 | The [Round-robin](https://en.wikipedia.org/wiki/Round-robin_scheduling) algorithm select the nodes in
192 | order, from the first node in the array to the latest. When arrived to the latest nodes,
193 | it will start again from the first.
194 |
195 | \* **NOTE**: the order of the nodes is randomized at runtime to maximize the usage of all the hosts.
196 |
197 | The [NoResurrect](src/NodePool/Resurrect/FalseResurrect.php) option does not try to resurrect the
198 | node that has been marked as dead. For instance, using `Elasticsearch` you can try to
199 | resurrect a dead node using the `HEAD /` API. If you want to use this behaviour you can use the
200 | [ElasticsearchResurrect](/src/NodePool/Resurrect/ElasticsearchResurrect.php) class.
201 |
202 | ## Use a custom Selector
203 |
204 | You can specify a `SelectorInterface` implementation when you create a `NodePoolInterface` instance.
205 | For instance, imagine you implemented a `CustomSelector` and a custom `CustomResurrect` you can
206 | use it as follows:
207 |
208 | ```php
209 | use Elastic\Transport\NodePool\SimpleNodePool;
210 | use Elastic\Transport\TransportBuilder;
211 |
212 | $nodePool = new SimpleNodePool(
213 | new CustomSelector(),
214 | new CustomResurrect()
215 | );
216 |
217 | $transport = TransportBuilder::create()
218 | ->setHosts(['localhost:9200'])
219 | ->setNodePool($nodePool)
220 | ->build();
221 | ```
222 |
223 | ## Use a custom PSR-3 loggers
224 |
225 | You can specify a PSR-3 `LoggerInterface` implementation using the `TransportBuilder`.
226 | For instance, if you want to use [monolog](https://github.com/Seldaek/monolog) library
227 | you can use the following configuration:
228 |
229 | ```php
230 | use Elastic\Transport\TransportBuilder;
231 | use Monolog\Logger;
232 | use Monolog\Handler\StreamHandler;
233 |
234 | $logger = new Logger('name');
235 | $logger->pushHandler(new StreamHandler('debug.log', Logger::DEBUG));
236 |
237 | $transport = TransportBuilder::create()
238 | ->setHosts(['localhost:9200'])
239 | ->setLogger($logger)
240 | ->build();
241 | ```
242 | ## Use a custom PSR-18 clients
243 |
244 | You can specify a `PSR-18` client using the `TransportBuilder::setClient()` function.
245 | For instance, if you want to use [Symfony HTTP Client](https://symfony.com/doc/current/http_client.html)
246 | you can use the following configuration:
247 |
248 | ```php
249 | use Elastic\Transport\TransportBuilder;
250 | use Symfony\Component\HttpClient\Psr18Client;
251 |
252 | $transport = TransportBuilder::create()
253 | ->setHosts(['localhost:9200'])
254 | ->setClient(new Psr18Client)
255 | ->build();
256 | ```
257 |
258 | As mentioned in the introduction, we use the [HTTPlug](http://httplug.io/) library
259 | to automatic discovery a [PSR-18](https://www.php-fig.org/psr/psr-18/) client.
260 |
261 | You can use the `TransportBuilder::setClient()` to specify the client manually, for
262 | instance if you have multiple HTTP client library installed.
263 |
264 | By default, if the [PSR-18](https://www.php-fig.org/psr/psr-18/) client implements the
265 | [HttpAsyncClient](https://github.com/php-http/httplug/blob/master/src/HttpAsyncClient.php)
266 | it will use it when using `Transport::sendAsyncRequest()`. If you want you can override
267 | this setting using the `Transport::setAsyncClient()` function. That means you can use
268 | a [PSR-18](https://www.php-fig.org/psr/psr-18/) client for the syncronous requests and
269 | a different [HttpAsyncClient](https://github.com/php-http/httplug/blob/master/src/HttpAsyncClient.php)
270 | client for the asyncronous requests.
271 |
272 | ## OpenTelemetry
273 |
274 | Starting from v8.9.0 we introduced the support of OpenTelemetry for the HTTP send request.
275 | Right now the support is only for syncronous HTTP call.
276 |
277 | In order to enable the OpenTelemetry you need to set the ENV variable
278 | `OTEL_PHP_INSTRUMENTATION_ELASTICSEARCH_ENABLED` to true.
279 |
280 | We added the support of OpenTelemetry natively in the `Transport:sendRequest()` function.
281 | By default, the Transport create a span from a Tracer provider (e.g. Global) with the
282 | following attributes:
283 |
284 | ```
285 | http.request.method
286 | url.full
287 | server.address
288 | server.port
289 | ```
290 |
291 | We also added a `$opts` array as second optional parameter for the `Transport:sendRequest()`
292 | to pass additional attributes for OTel instrumentation.
293 |
294 | We created an [OpenTelemetry](src/OpenTelemetry.php) class to provide all the configuration.
295 |
296 | ## Copyright and License
297 |
298 | Copyright (c) [Elasticsearch B.V](https://www.elastic.co).
299 |
300 | This software is licensed under the MIT License.
301 | Read the [LICENSE](LICENSE) file for more information.
302 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elastic/transport",
3 | "type": "library",
4 | "description": "HTTP transport PHP library for Elastic products",
5 | "keywords": [
6 | "http",
7 | "transport",
8 | "elastic",
9 | "PSR-7",
10 | "PSR_17",
11 | "PSR-18"
12 | ],
13 | "license": "MIT",
14 | "require": {
15 | "php": "^8.1",
16 | "psr/http-client": "^1.0",
17 | "psr/http-factory": "^1.0",
18 | "psr/http-message": "^2.0",
19 | "psr/log": "^2.0 || ^3.0",
20 | "php-http/discovery": "^1.14",
21 | "php-http/httplug": "^2.4",
22 | "composer-runtime-api": "^2.0",
23 | "open-telemetry/api": "^1.0",
24 | "nyholm/psr7": "^1.8"
25 | },
26 | "require-dev": {
27 | "phpunit/phpunit": "^10",
28 | "phpstan/phpstan": "^2.1",
29 | "php-http/mock-client": "^1.5",
30 | "open-telemetry/sdk": "^1.0",
31 | "symfony/http-client": "^6.0"
32 | },
33 | "autoload": {
34 | "psr-4": {
35 | "Elastic\\Transport\\": "src/"
36 | }
37 | },
38 | "autoload-dev": {
39 | "psr-4": {
40 | "Elastic\\Transport\\Test\\" : "tests/"
41 | }
42 | },
43 | "scripts": {
44 | "test": [
45 | "vendor/bin/phpunit --testdox"
46 | ],
47 | "phpstan": [
48 | "vendor/bin/phpstan analyse"
49 | ]
50 | },
51 | "config": {
52 | "allow-plugins": {
53 | "php-http/discovery": true,
54 | "tbachert/spi": true
55 | }
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/Async/OnFailureDefault.php:
--------------------------------------------------------------------------------
1 |
45 | */
46 | protected array $options;
47 |
48 | /**
49 | * @param array $options
50 | */
51 | public function __construct(array $options = [])
52 | {
53 | $this->options = $options;
54 | }
55 |
56 | /**
57 | * @param array $options
58 | */
59 | public function setOptions(array $options): void
60 | {
61 | $this->options = $options;
62 | }
63 |
64 | /**
65 | * @throws CurlException
66 | */
67 | public function sendRequest(RequestInterface $request): ResponseInterface
68 | {
69 | curl_reset($this->getCurl());
70 |
71 | $headers = [];
72 | foreach ($request->getHeaders() as $name => $values) {
73 | foreach ($values as $value) {
74 | $headers[] = sprintf("%s: %s", $name, $value);
75 | }
76 | }
77 | $curlOptions = [
78 | CURLOPT_HTTP_VERSION => $this->getCurlHttpVersion($request->getProtocolVersion()),
79 | CURLOPT_CUSTOMREQUEST => $request->getMethod(),
80 | CURLOPT_CONNECTTIMEOUT => 0,
81 | CURLOPT_URL => (string) $request->getUri(),
82 | CURLOPT_NOBODY => $request->getMethod() === 'HEAD',
83 | CURLOPT_RETURNTRANSFER => true,
84 | CURLOPT_HEADER => true,
85 | CURLOPT_HTTPHEADER => $headers
86 | ];
87 |
88 | if (!in_array($request->getMethod(), self::BODYLESS_HTTP_METHODS, true)) {
89 | $curlOptions[CURLOPT_POSTFIELDS] = (string) $request->getBody();
90 | }
91 |
92 | curl_setopt_array($this->getCurl(), array_replace($curlOptions, $this->options));
93 | $response = curl_exec($this->getCurl());
94 |
95 | if ($response !== false && curl_errno($this->getCurl()) === 0) {
96 | $parse = $this->parseResponse((string) $response);
97 | return new Response(
98 | $parse['status-code'],
99 | $parse['headers'],
100 | $parse['body'],
101 | $parse['http-version'],
102 | $parse['reason-phrase']
103 | );
104 | }
105 |
106 | throw new CurlException(sprintf(
107 | "Error sending with cURL (%d): %s",
108 | curl_errno($this->getCurl()),
109 | curl_error($this->getCurl())
110 | ));
111 | }
112 |
113 | private function getCurl(): CurlHandle
114 | {
115 | if (empty($this->curl)) {
116 | $init = curl_init();
117 | if (false === $init) {
118 | throw new CurlException("I cannot execute curl initialization");
119 | }
120 | $this->curl = $init;
121 | }
122 | return $this->curl;
123 | }
124 |
125 | /**
126 | * Return cURL constant for specified HTTP version.
127 | *
128 | * @throws CurlException If unsupported version requested.
129 | */
130 | private function getCurlHttpVersion(string $version): int
131 | {
132 | switch ($version) {
133 | case '1.0':
134 | return CURL_HTTP_VERSION_1_0;
135 | case '1.1':
136 | return CURL_HTTP_VERSION_1_1;
137 | case '2.0':
138 | if (defined('CURL_HTTP_VERSION_2_0')) {
139 | return CURL_HTTP_VERSION_2_0;
140 | }
141 | throw new CurlException('libcurl 7.33 needed for HTTP 2.0 support');
142 | }
143 |
144 | return CURL_HTTP_VERSION_NONE;
145 | }
146 |
147 | /**
148 | * Parses the HTTP response from curl and
149 | * generates the start-line, headers and the body
150 | *
151 | * @see https://datatracker.ietf.org/doc/html/rfc7230#section-3
152 | *
153 | * @return array{
154 | * http-version: string, // HTTP version (e.g. if HTTP/1.1 http-version is "1.1")
155 | * status-code: int, // The status code of the response (e.g. 200)
156 | * reason-phrase: string, // The reason-phrase (e.g. OK)
157 | * headers: array, // The HTTP headers
158 | * body: string, // The body content (can be empty)
159 | * }
160 | */
161 | private function parseResponse(string $response): array
162 | {
163 | $lines = explode(self::HTTP_SPEC_CRLF, $response);
164 | $output = [
165 | 'http-version' => '',
166 | 'status-code' => 200,
167 | 'reason-phrase' => '',
168 | 'headers' => [],
169 | 'body' => ''
170 | ];
171 | foreach ($lines as $i => $line) {
172 | // status-line
173 | // @see https://datatracker.ietf.org/doc/html/rfc7230#section-3.1.2
174 | if ($i === 0) {
175 | $statusLine = explode(self::HTTP_SPEC_SP, $line, 3);
176 | $output['http-version'] = explode('/', $statusLine[0], 2)[1];
177 | $output['status-code'] = (int) $statusLine[1];
178 | $output['reason-phrase'] = $statusLine[2];
179 | continue;
180 | }
181 | // Empty line, end of headers
182 | if (empty($line)) {
183 | $output['body'] = $lines[$i+1] ?? '';
184 | break;
185 | }
186 | // Extract header name and values
187 | [$name, $value] = explode(':', $line, 2);
188 | if (!isset($output['headers'][$name])) {
189 | $output['headers'][$name]= [$value];
190 | } else {
191 | $output['headers'][$name][]= $value;
192 | }
193 | }
194 | return $output;
195 | }
196 | }
--------------------------------------------------------------------------------
/src/Exception/CloudIdParseException.php:
--------------------------------------------------------------------------------
1 | uri = Psr17FactoryDiscovery::findUriFactory()->createUri($host);
36 | }
37 |
38 | public function markAlive(bool $alive): void
39 | {
40 | $this->alive = $alive;
41 | $this->failedPings = $alive ? 0 : ($this->failedPings + 1);
42 | $this->lastPing = time();
43 | }
44 |
45 | public function isAlive(): bool
46 | {
47 | return $this->alive;
48 | }
49 |
50 | public function getUri(): UriInterface
51 | {
52 | return $this->uri;
53 | }
54 |
55 | public function getLastPing(): ?int
56 | {
57 | return $this->lastPing;
58 | }
59 |
60 | public function getFailedPings(): int
61 | {
62 | return $this->failedPings;
63 | }
64 | }
--------------------------------------------------------------------------------
/src/NodePool/NodePoolInterface.php:
--------------------------------------------------------------------------------
1 | getRequestFactory()->createRequest("HEAD", $node->getUri());
32 | try {
33 | $response = $this->getClient()->sendRequest($request);
34 | return $response->getStatusCode() === 200;
35 | } catch (Exception $e) {
36 | return false;
37 | }
38 | }
39 |
40 | public function getClient(): ClientInterface
41 | {
42 | if (empty($this->client)) {
43 | $this->client = Psr18ClientDiscovery::find();
44 | }
45 | return $this->client;
46 | }
47 |
48 | public function getRequestFactory(): RequestFactoryInterface
49 | {
50 | if (empty($this->requestFactory)) {
51 | $this->requestFactory = Psr17FactoryDiscovery::findRequestFactory();
52 | }
53 | return $this->requestFactory;
54 | }
55 | }
--------------------------------------------------------------------------------
/src/NodePool/Resurrect/NoResurrect.php:
--------------------------------------------------------------------------------
1 | nodes)) {
27 | $className = substr(__CLASS__, strrpos(__CLASS__, '\\') + 1);
28 | throw new NoNodeAvailableException(sprintf(
29 | "No node available. Please use %s::setNodes() before calling %s::nextNode().",
30 | $className,
31 | $className
32 | ));
33 | }
34 | $node = next($this->nodes);
35 | if (false === $node) {
36 | return reset($this->nodes);
37 | }
38 | return $node;
39 | }
40 | }
--------------------------------------------------------------------------------
/src/NodePool/Selector/SelectorInterface.php:
--------------------------------------------------------------------------------
1 |
24 | */
25 | protected array $nodes = [];
26 |
27 | public function setNodes(array $nodes): void
28 | {
29 | foreach ($nodes as $node) {
30 | if (!$node instanceof Node) {
31 | throw new InvalidArrayException(sprintf(
32 | "The nodes array must contain only %s objects",
33 | Node::class
34 | ));
35 | }
36 | }
37 | $this->nodes = $nodes;
38 | }
39 |
40 | public function getNodes(): array
41 | {
42 | return $this->nodes;
43 | }
44 | }
--------------------------------------------------------------------------------
/src/NodePool/SimpleNodePool.php:
--------------------------------------------------------------------------------
1 |
29 | */
30 | protected $nodes = [];
31 |
32 | /**
33 | * @var SelectorInterface
34 | */
35 | protected $selector;
36 |
37 | /**
38 | * @var ResurrectInterface
39 | */
40 | protected $resurrect;
41 |
42 | public function __construct(SelectorInterface $selector, ResurrectInterface $resurrect)
43 | {
44 | $this->selector = $selector;
45 | $this->resurrect = $resurrect;
46 | }
47 |
48 | public function setHosts(array $hosts): self
49 | {
50 | $this->nodes = [];
51 | foreach ($hosts as $host) {
52 | $this->nodes[] = new Node($host);
53 | }
54 | shuffle($this->nodes); // randomize for use different hosts on each execution
55 | $this->selector->setNodes($this->nodes);
56 |
57 | return $this;
58 | }
59 |
60 | public function nextNode(): Node
61 | {
62 | $totNodes = count($this->nodes);
63 | $dead = 0;
64 |
65 | while ($dead < $totNodes) {
66 | $next = $this->selector->nextNode();
67 | if ($next->isAlive()) {
68 | return $next;
69 | }
70 | if ($this->resurrect->ping($next)) {
71 | $next->markAlive(true);
72 | return $next;
73 | }
74 | $dead++;
75 | }
76 |
77 | throw new NoNodeAvailableException(sprintf(
78 | 'No alive nodes. All the %d nodes seem to be down.',
79 | $totNodes
80 | ));
81 | }
82 | }
--------------------------------------------------------------------------------
/src/OpenTelemetry.php:
--------------------------------------------------------------------------------
1 | getTracer(
94 | self::OTEL_TRACER_NAME,
95 | Transport::VERSION
96 | );
97 | }
98 |
99 | /**
100 | * @param array $sanitizeKeys
101 | */
102 | private static function sanitizeBody(string $body, array $sanitizeKeys): string
103 | {
104 | if (empty($body)) {
105 | return '';
106 | }
107 | $json = json_decode($body, true);
108 | if (!is_array($json)) {
109 | return '';
110 | }
111 | $patterns = array_merge(self::DEFAULT_SANITIZER_KEY_PATTERNS, $sanitizeKeys);
112 |
113 | // Convert the patterns array into a regex
114 | $regex = sprintf('/%s/', implode('|', $patterns));
115 | // Recursively traverse the array and redact the specified keys
116 | array_walk_recursive($json, function (&$value, $key) use ($regex) {
117 | if (preg_match($regex, $key, $matches)) {
118 | $value = self::REDACTED_STRING;
119 | }
120 | });
121 | return JsonSerializer::serialize($json);
122 | }
123 | }
--------------------------------------------------------------------------------
/src/Serializer/CsvSerializer.php:
--------------------------------------------------------------------------------
1 |
55 | */
56 | public static function unserialize(string $data, array $options = []): array
57 | {
58 | $result = [];
59 | foreach (explode("\n", $data) as $row) {
60 | $result[] = str_getcsv(string:$row, escape:"\\");
61 | }
62 | return $result;
63 | }
64 | }
--------------------------------------------------------------------------------
/src/Serializer/JsonSerializer.php:
--------------------------------------------------------------------------------
1 | (bool) enable/disable the removing of
31 | * null values (default is true)
32 | *
33 | * @param mixed $data
34 | */
35 | public static function serialize($data, array $options = []): string
36 | {
37 | if (empty($data)) {
38 | return '{}';
39 | }
40 | if (is_string($data)) {
41 | return $data;
42 | }
43 | try {
44 | $removeNull = $options['remove_null'] ?? true;
45 | if ($removeNull) {
46 | Utility::removeNullValue($data);
47 | }
48 | return json_encode($data, JSON_PRESERVE_ZERO_FRACTION + JSON_INVALID_UTF8_SUBSTITUTE + JSON_THROW_ON_ERROR);
49 | } catch (JsonException $e) {
50 | throw new InvalidJsonException(sprintf(
51 | "I cannot serialize to Json: %s",
52 | $e->getMessage()
53 | ));
54 | }
55 | }
56 |
57 | /**
58 | * The available options are:
59 | * 'type' => (string) specify if the output should be an array
60 | * or an object (default is array)
61 | *
62 | * @inheritdoc
63 | */
64 | public static function unserialize(string $data, array $options = [])
65 | {
66 | try {
67 | $type = $options['type'] ?? 'array';
68 | if (!in_array($type, ['object', 'array'])) {
69 | throw new UndefinedPropertyException("The unserialize 'type' option must be object or array");
70 | }
71 | return json_decode($data, $type === 'array', 512, JSON_THROW_ON_ERROR);
72 | } catch (JsonException $e) {
73 | throw new InvalidJsonException(sprintf(
74 | "Not a valid Json: %s",
75 | $e->getMessage()
76 | ));
77 | }
78 | }
79 | }
--------------------------------------------------------------------------------
/src/Serializer/NDJsonSerializer.php:
--------------------------------------------------------------------------------
1 | (bool) enable/disable the removing of
31 | * null values (default is true)
32 | *
33 | * @param array $data
34 | */
35 | public static function serialize($data, array $options = []): string
36 | {
37 | $result = '';
38 | foreach ($data as $row) {
39 | if (empty($row)) {
40 | $result .= "{}\n";
41 | continue;
42 | }
43 | $result .= JsonSerializer::serialize($row, $options) . "\n";
44 | }
45 | return $result;
46 | }
47 |
48 | /**
49 | * The available options are:
50 | * 'type' => (string) specify if the array result should contain object
51 | * or array (default is array)
52 | *
53 | * @inheritdoc
54 | */
55 | public static function unserialize(string $data, array $options = [])
56 | {
57 | $array = explode(strpos($data, "\r\n") !== false ? "\r\n" : "\n", $data);
58 | $result = [];
59 | foreach ($array as $json) {
60 | if (empty($json)) {
61 | continue;
62 | }
63 | try {
64 | $result[] = JsonSerializer::unserialize($json, $options);
65 | } catch (JsonException $e) {
66 | throw new InvalidJsonException(sprintf(
67 | "Not a valid NDJson: %s",
68 | $e->getMessage()
69 | ));
70 | }
71 | }
72 | $type = $options['type'] ?? 'array';
73 | return $type === 'array' ? $result : new ArrayObject($result);
74 | }
75 | }
--------------------------------------------------------------------------------
/src/Serializer/SerializerInterface.php:
--------------------------------------------------------------------------------
1 | $options
22 | * @return string
23 | */
24 | public static function serialize($data, array $options = []): string;
25 |
26 | /**
27 | * @param string $data
28 | * @param array $options
29 | * @return mixed
30 | */
31 | public static function unserialize(string $data, array $options = []);
32 | }
--------------------------------------------------------------------------------
/src/Serializer/TextSerializer.php:
--------------------------------------------------------------------------------
1 | &$value) {
41 | if (is_object($value) || is_array($value)) {
42 | self::removeNullValue($value);
43 | }
44 | if (null === $value) {
45 | if (is_array($data)) {
46 | unset($data[$property]);
47 | }
48 | if (is_object($data)) {
49 | unset($data->$property);
50 | }
51 | }
52 | }
53 | }
54 | }
--------------------------------------------------------------------------------
/src/Serializer/XmlSerializer.php:
--------------------------------------------------------------------------------
1 | asXML();
32 | return false === $xml ? '' : $xml;
33 | }
34 | throw new InvalidXmlException(sprintf(
35 | "Not a valid SimpleXMLElement: %s",
36 | serialize($data)
37 | ));
38 | }
39 |
40 | /**
41 | * @return SimpleXMLElement
42 | */
43 | public static function unserialize(string $data, array $options = []): SimpleXMLElement
44 | {
45 | $result = simplexml_load_string($data);
46 | if (false === $result) {
47 | $errors = libxml_get_errors();
48 | libxml_clear_errors();
49 | throw new InvalidXmlException(sprintf(
50 | "Not a valid XML: %s",
51 | serialize($errors)
52 | ));
53 | }
54 | return $result;
55 | }
56 | }
--------------------------------------------------------------------------------
/src/Transport.php:
--------------------------------------------------------------------------------
1 |
62 | */
63 | private array $headers = [];
64 | private string $user;
65 | private string $password;
66 | private ?RequestInterface $lastRequest = null;
67 | private ?ResponseInterface $lastResponse = null;
68 | private string $OSVersion;
69 | private int $retries = 0;
70 | private HttpAsyncClient $asyncClient;
71 | private OnSuccessInterface $onAsyncSuccess;
72 | private OnFailureInterface $onAsyncFailure;
73 | private TracerInterface $otelTracer;
74 |
75 | public function __construct(
76 | ClientInterface $client,
77 | NodePoolInterface $nodePool,
78 | LoggerInterface $logger
79 | ) {
80 | $this->client = $client;
81 | $this->nodePool = $nodePool;
82 | $this->logger = $logger;
83 | }
84 |
85 | public function getClient(): ClientInterface
86 | {
87 | return $this->client;
88 | }
89 |
90 | public function getNodePool(): NodePoolInterface
91 | {
92 | return $this->nodePool;
93 | }
94 |
95 | public function getLogger(): LoggerInterface
96 | {
97 | return $this->logger;
98 | }
99 |
100 | public function getOTelTracer(): TracerInterface
101 | {
102 | if (empty($this->otelTracer)) {
103 | $this->otelTracer = OpenTelemetry::getTracer(
104 | Globals::tracerProvider()
105 | );
106 | }
107 | return $this->otelTracer;
108 | }
109 |
110 | public function setOTelTracer(TracerInterface $tracer): self
111 | {
112 | $this->otelTracer = $tracer;
113 | return $this;
114 | }
115 |
116 | public function setHeader(string $name, string $value): self
117 | {
118 | $this->headers[$name] = $value;
119 | return $this;
120 | }
121 |
122 | /**
123 | * @throws InvalidArgumentException
124 | */
125 | public function setRetries(int $num): self
126 | {
127 | if ($num < 0) {
128 | throw new InvalidArgumentException('The retries number must be a positive integer');
129 | }
130 | $this->retries = $num;
131 | return $this;
132 | }
133 |
134 | public function getRetries(): int
135 | {
136 | return $this->retries;
137 | }
138 |
139 | /**
140 | * @return array
141 | */
142 | public function getHeaders(): array
143 | {
144 | return $this->headers;
145 | }
146 |
147 | public function setUserInfo(string $user, string $password = ''): self
148 | {
149 | $this->user = $user;
150 | $this->password = $password;
151 | return $this;
152 | }
153 |
154 | public function setUserAgent(string $name, string $version): self
155 | {
156 | $this->headers['User-Agent'] = sprintf(
157 | "%s/%s (%s %s; PHP %s)",
158 | $name,
159 | $version,
160 | PHP_OS,
161 | $this->getOSVersion(),
162 | phpversion()
163 | );
164 | return $this;
165 | }
166 |
167 | /**
168 | * Set the x-elastic-client-meta header
169 | *
170 | * The header format is specified by the following regex:
171 | * ^[a-z]{1,}=[a-z0-9\.\-]{1,}(?:,[a-z]{1,}=[a-z0-9\.\-]+)*$
172 | */
173 | public function setElasticMetaHeader(string $clientName, string $clientVersion, bool $async = false): self
174 | {
175 | $phpSemVersion = sprintf("%d.%d.%d", PHP_MAJOR_VERSION, PHP_MINOR_VERSION, PHP_RELEASE_VERSION);
176 | $meta = sprintf(
177 | "%s=%s,php=%s,t=%s,a=%d",
178 | $clientName,
179 | $this->purgePreReleaseTag($clientVersion),
180 | $phpSemVersion,
181 | $this->purgePreReleaseTag(self::VERSION),
182 | $async ? 1 : 0 // 0=syncronous, 1=asynchronous
183 | );
184 | $lib = $this->getClientLibraryInfo();
185 | if (!empty($lib)) {
186 | $meta .= sprintf(",%s=%s", $lib[0], $lib[1]);
187 | }
188 | $this->headers[self::ELASTIC_META_HEADER] = $meta;
189 | return $this;
190 | }
191 |
192 | /**
193 | * Remove pre-release suffix with a single 'p' letter
194 | */
195 | private function purgePreReleaseTag(string $version): string
196 | {
197 | return str_replace(['alpha', 'beta', 'snapshot', 'rc', 'pre'], 'p', strtolower($version));
198 | }
199 |
200 | public function getLastRequest(): ?RequestInterface
201 | {
202 | return $this->lastRequest;
203 | }
204 |
205 | public function getLastResponse(): ?ResponseInterface
206 | {
207 | return $this->lastResponse;
208 | }
209 |
210 | /**
211 | * Setup the headers, if not already present
212 | */
213 | private function setupHeaders(RequestInterface $request): RequestInterface
214 | {
215 | foreach ($this->headers as $name => $value) {
216 | if (!$request->hasHeader($name)) {
217 | $request = $request->withHeader($name, $value);
218 | }
219 | }
220 | return $request;
221 | }
222 |
223 | /**
224 | * Setup the user info, if not already present
225 | */
226 | private function setupUserInfo(RequestInterface $request): RequestInterface
227 | {
228 | $uri = $request->getUri();
229 | if (empty($uri->getUserInfo())) {
230 | if (isset($this->user)) {
231 | $request = $request->withUri($uri->withUserInfo($this->user, $this->password));
232 | }
233 | }
234 | return $request;
235 | }
236 |
237 | /**
238 | * Setup the connection Uri
239 | */
240 | private function setupConnectionUri(Node $node, RequestInterface $request): RequestInterface
241 | {
242 | $uri = $node->getUri();
243 | $path = $request->getUri()->getPath();
244 |
245 | $nodePath = $uri->getPath();
246 | // If the node has a path we need to use it as prefix for the existing path
247 | // @see https://github.com/elastic/elastic-transport-php/pull/20
248 | if (!empty($nodePath)) {
249 | $path = sprintf("%s/%s", rtrim($nodePath, '/'), ltrim($path,'/'));
250 | }
251 | // If the user information is not in the request, we check if it is present in the node uri
252 | // @see https://github.com/elastic/elastic-transport-php/issues/18
253 | if (empty($request->getUri()->getUserInfo()) && !empty($uri->getUserInfo())) {
254 | $userInfo = explode(':', $uri->getUserInfo());
255 | $request = $request->withUri(
256 | $request->getUri()
257 | ->withUserInfo($userInfo[0], $userInfo[1] ?? null)
258 | );
259 | }
260 | return $request->withUri(
261 | $request->getUri()
262 | ->withHost($uri->getHost())
263 | ->withPort($uri->getPort())
264 | ->withScheme($uri->getScheme())
265 | ->withPath($path)
266 | );
267 | }
268 |
269 | private function decorateRequest(RequestInterface $request): RequestInterface
270 | {
271 | $request = $this->setupHeaders($request);
272 | return $this->setupUserInfo($request);
273 | }
274 |
275 | private function logHeaders(MessageInterface $message): void
276 | {
277 | $this->logger->debug(sprintf(
278 | "Headers: %s\nBody: %s",
279 | json_encode($message->getHeaders()),
280 | (string) $message->getBody()
281 | ));
282 |
283 | $message->getBody()->rewind();
284 | }
285 |
286 | private function logRequest(string $title, RequestInterface $request): void
287 | {
288 | $this->logger->info(sprintf(
289 | "%s: %s %s",
290 | $title,
291 | $request->getMethod(),
292 | (string) $request->getUri()
293 | ), [
294 | 'request' => $request
295 | ]);
296 | $this->logHeaders($request);
297 | }
298 |
299 | private function logResponse(string $title, ResponseInterface $response, int $retry): void
300 | {
301 | $this->logger->info(sprintf(
302 | "%s (retry %d): %d",
303 | $title,
304 | $retry,
305 | $response->getStatusCode()
306 | ), [
307 | 'response' => $response,
308 | 'retry' => $retry
309 | ]);
310 | $this->logHeaders($response);
311 | }
312 |
313 | /**
314 | * @throws NoNodeAvailableException
315 | * @throws ClientExceptionInterface
316 | */
317 | public function sendRequest(RequestInterface $request): ResponseInterface
318 | {
319 | if (empty($request->getUri()->getHost())) {
320 | $node = $this->nodePool->nextNode();
321 | $request = $this->setupConnectionUri($node, $request);
322 | }
323 | $request = $this->decorateRequest($request);
324 | $this->lastRequest = $request;
325 | $this->logRequest("Request", $request);
326 |
327 | // OpenTelemetry get tracer
328 | if (getenv(OpenTelemetry::ENV_VARIABLE_ENABLED)) {
329 | $tracer = $this->getOTelTracer();
330 | }
331 |
332 | $lastNetworkException = null;
333 | $count = -1;
334 | while ($count < $this->getRetries()) {
335 | try {
336 | $count++;
337 | // OpenTelemetry span start
338 | if (!empty($tracer)) {
339 | if ($request instanceof ServerRequestInterface) {
340 | $opts = $request->getAttribute(OpenTelemetry::PSR7_OTEL_ATTRIBUTE_NAME, []);
341 | }
342 | $spanName = $opts['db.operation.name'] ?? $request->getUri()->getPath();
343 | $span = $tracer->spanBuilder($spanName)->startSpan();
344 | $span->setAttribute('http.request.method', $request->getMethod());
345 | $span->setAttribute('url.full', $this->getFullUrl($request));
346 | $span->setAttribute('server.address', $request->getUri()->getHost());
347 | $span->setAttribute('server.port', $request->getUri()->getPort());
348 | if (!empty($opts)) {
349 | $span->setAttributes($opts);
350 | }
351 | }
352 | $response = $this->client->sendRequest($request);
353 |
354 | $this->lastResponse = $response;
355 | $this->logResponse("Response", $response, $count);
356 |
357 | return $response;
358 | } catch (NetworkExceptionInterface $e) {
359 | $lastNetworkException = $e;
360 | $this->logger->error(sprintf("Retry %d: %s", $count, $e->getMessage()));
361 | if (!empty($span)) {
362 | $span->setAttribute('error.type', $e->getMessage());
363 | }
364 | if (isset($node)) {
365 | $node->markAlive(false);
366 | $node = $this->nodePool->nextNode();
367 | $request = $this->setupConnectionUri($node, $request);
368 | }
369 | } catch (ClientExceptionInterface $e) {
370 | $this->logger->error(sprintf("Retry %d: %s", $count, $e->getMessage()));
371 | if (!empty($span)) {
372 | $span->setAttribute('error.type', $e->getMessage());
373 | }
374 | throw $e;
375 | } finally {
376 | // OpenTelemetry span end
377 | if (!empty($span)) {
378 | $span->end();
379 | }
380 | }
381 | }
382 | $exceededMsg = sprintf("Exceeded maximum number of retries (%d)", $this->getRetries());
383 | $this->logger->error($exceededMsg);
384 | throw new NoNodeAvailableException($exceededMsg, 0, $lastNetworkException);
385 | }
386 |
387 | public function setAsyncClient(HttpAsyncClient $asyncClient): self
388 | {
389 | $this->asyncClient = $asyncClient;
390 | return $this;
391 | }
392 |
393 | /**
394 | * @throws NoAsyncClientException
395 | */
396 | public function getAsyncClient(): HttpAsyncClient
397 | {
398 | if (!empty($this->asyncClient)) {
399 | return $this->asyncClient;
400 | }
401 | if ($this->client instanceof HttpAsyncClient) {
402 | return $this->client;
403 | }
404 | try {
405 | $this->asyncClient = HttpAsyncClientDiscovery::find();
406 | } catch (Exception $e) {
407 | throw new NoAsyncClientException(sprintf(
408 | "I did not find any HTTP library with HttpAsyncClient interface. " .
409 | "Make sure to install a package providing \"php-http/async-client-implementation\". " .
410 | "You can also set a specific async library using %s::setAsyncClient()",
411 | self::class
412 | ));
413 | }
414 | return $this->asyncClient;
415 | }
416 |
417 | public function setAsyncOnSuccess(OnSuccessInterface $success): self
418 | {
419 | $this->onAsyncSuccess = $success;
420 | return $this;
421 | }
422 |
423 | public function getAsyncOnSuccess(): OnSuccessInterface
424 | {
425 | if (empty($this->onAsyncSuccess)) {
426 | $this->onAsyncSuccess = new OnSuccessDefault();
427 | }
428 | return $this->onAsyncSuccess;
429 | }
430 |
431 | public function setAsyncOnFailure(OnFailureInterface $failure): self
432 | {
433 | $this->onAsyncFailure = $failure;
434 | return $this;
435 | }
436 |
437 | public function getAsyncOnFailure(): OnFailureInterface
438 | {
439 | if (empty($this->onAsyncFailure)) {
440 | $this->onAsyncFailure = new OnFailureDefault();
441 | }
442 | return $this->onAsyncFailure;
443 | }
444 |
445 | /**
446 | * @throws Exception
447 | */
448 | public function sendAsyncRequest(RequestInterface $request): Promise
449 | {
450 | $client = $this->getAsyncClient();
451 | $node = null;
452 | if (empty($request->getUri()->getHost())) {
453 | $node = $this->nodePool->nextNode();
454 | $request = $this->setupConnectionUri($node, $request);
455 | }
456 | $request = $this->decorateRequest($request);
457 | $this->lastRequest = $request;
458 | $this->logRequest("Async Request", $request);
459 |
460 | $count = 0;
461 | $promise = $client->sendAsyncRequest($request);
462 |
463 | // onFulfilled callable
464 | $onFulfilled = function (ResponseInterface $response) use (&$count) {
465 | $this->lastResponse = $response;
466 | $this->logResponse("Async Response", $response, $count);
467 | return $this->getAsyncOnSuccess()->success($response, $count);
468 | };
469 |
470 | // onRejected callable
471 | $onRejected = function (Exception $e) use ($client, $request, &$count, $node) {
472 | $this->logger->error(sprintf("Retry %d: %s", $count, $e->getMessage()));
473 | $this->getAsyncOnFailure()->failure($e, $request, $count, $node ?? null);
474 | if (isset($node)) {
475 | $node->markAlive(false);
476 | $node = $this->nodePool->nextNode();
477 | $request = $this->setupConnectionUri($node, $request);
478 | }
479 | $count++;
480 | return $client->sendAsyncRequest($request);
481 | };
482 |
483 | // Add getRetries() callables using then()
484 | for ($i=0; $i < $this->getRetries(); $i++) {
485 | $promise = $promise->then($onFulfilled, $onRejected);
486 | }
487 | // Add the last getRetries()+1 callable for managing the exceeded error
488 | $promise = $promise->then($onFulfilled, function(Exception $e) use (&$count) {
489 | $exceededMsg = sprintf("Exceeded maximum number of retries (%d)", $this->getRetries());
490 | $this->logger->error(sprintf("Retry %d: %s", $count, $e->getMessage()));
491 | $this->logger->error($exceededMsg);
492 | throw new NoNodeAvailableException(sprintf("%s: %s", $exceededMsg, $e->getMessage()));
493 | });
494 | return $promise;
495 | }
496 |
497 | /**
498 | * Get the OS version using php_uname if available
499 | * otherwise it returns an empty string
500 | */
501 | private function getOSVersion(): string
502 | {
503 | if (!isset($this->OSVersion)) {
504 | $disable_functions = (string) ini_get('disable_functions');
505 | $this->OSVersion = strpos(strtolower($disable_functions), 'php_uname') !== false
506 | ? ''
507 | : php_uname("r");
508 | }
509 | return $this->OSVersion;
510 | }
511 |
512 | /**
513 | * Returns the name and the version of the Client HTTP library used
514 | * Here a list of supported libraries:
515 | * gu => guzzlehttp/guzzle
516 | * sy => symfony/http-client
517 | * ec => elastic curl client (Elastic\Transport\Client\Curl)
518 | *
519 | * @return array
520 | */
521 | private function getClientLibraryInfo(): array
522 | {
523 | $clientClass = get_class($this->client);
524 | if (false !== strpos($clientClass, 'GuzzleHttp\Client')) {
525 | return ['gu', InstalledVersions::getPrettyVersion('guzzlehttp/guzzle')];
526 | }
527 | if (false !== strpos($clientClass, 'Symfony\Component\HttpClient')) {
528 | return ['sy', InstalledVersions::getPrettyVersion('symfony/http-client')];
529 | }
530 | if (false !== strpos($clientClass, 'Elastic\Transport\Client\Curl')) {
531 | return ['ec', Transport::VERSION];
532 | }
533 | return [];
534 | }
535 |
536 | /**
537 | * Return the full URL in the format
538 | * scheme://host:port/path?query_string
539 | */
540 | private function getFullUrl(RequestInterface $request): string
541 | {
542 | $fullUrl = sprintf(
543 | "%s://%s:%s%s",
544 | $request->getUri()->getScheme(),
545 | $request->getUri()->getHost(),
546 | $request->getUri()->getPort(),
547 | $request->getUri()->getPath()
548 | );
549 | $queryString = $request->getUri()->getQuery();
550 | if (!empty($queryString)) {
551 | $fullUrl .= '?' . $queryString;
552 | }
553 | return $fullUrl;
554 | }
555 | }
556 |
--------------------------------------------------------------------------------
/src/TransportBuilder.php:
--------------------------------------------------------------------------------
1 |
38 | */
39 | protected array $hosts = [];
40 | protected TracerInterface $OTelTracer;
41 |
42 | final public function __construct()
43 | {
44 | }
45 |
46 | public static function create(): TransportBuilder
47 | {
48 | return new static();
49 | }
50 |
51 | public function setClient(ClientInterface $client): self
52 | {
53 | $this->client = $client;
54 | return $this;
55 | }
56 |
57 | public function getClient(): ClientInterface
58 | {
59 | if (empty($this->client)) {
60 | try {
61 | $this->client = Psr18ClientDiscovery::find();
62 | } catch (NotFoundException $e) {
63 | $this->client = new Curl();
64 | }
65 | }
66 | return $this->client;
67 | }
68 |
69 | public function setNodePool(NodePoolInterface $nodePool): self
70 | {
71 | $this->nodePool = $nodePool;
72 | return $this;
73 | }
74 |
75 | public function getNodePool(): NodePoolInterface
76 | {
77 | if (empty($this->nodePool)) {
78 | $this->nodePool = new SimpleNodePool(
79 | new RoundRobin(),
80 | new NoResurrect()
81 | );
82 | }
83 | return $this->nodePool;
84 | }
85 |
86 | public function setLogger(LoggerInterface $logger): self
87 | {
88 | $this->logger = $logger;
89 | return $this;
90 | }
91 |
92 | public function getLogger(): LoggerInterface
93 | {
94 | if (empty($this->logger)) {
95 | $this->logger = new NullLogger();
96 | }
97 | return $this->logger;
98 | }
99 |
100 | /**
101 | * @param array $hosts
102 | */
103 | public function setHosts(array $hosts): self
104 | {
105 | $this->hosts = $hosts;
106 | return $this;
107 | }
108 |
109 | /**
110 | * @return array
111 | */
112 | public function getHosts(): array
113 | {
114 | return $this->hosts;
115 | }
116 |
117 | public function setCloudId(string $cloudId): self
118 | {
119 | $this->hosts = [$this->parseElasticCloudId($cloudId)];
120 | return $this;
121 | }
122 |
123 | public function build(): Transport
124 | {
125 | return new Transport(
126 | $this->getClient(),
127 | $this->getNodePool()->setHosts($this->hosts),
128 | $this->getLogger()
129 | );
130 | }
131 |
132 | /**
133 | * Return the URL of Elastic Cloud from the Cloud ID
134 | *
135 | * @throws Exception\CloudIdParseException
136 | */
137 | private function parseElasticCloudId(string $cloudId): string
138 | {
139 | if (strpos($cloudId, ':') !== false) {
140 | list($name, $encoded) = explode(':', $cloudId, 2);
141 | $base64 = base64_decode($encoded, true);
142 | if ($base64 !== false && strpos($base64, '$') !== false) {
143 | list($uri, $uuids) = explode('$', $base64);
144 | return sprintf("https://%s.%s", $uuids, $uri);
145 | }
146 | }
147 | throw new Exception\CloudIdParseException(sprintf(
148 | 'Cloud ID %s is not valid',
149 | $name ?? ''
150 | ));
151 | }
152 | }
--------------------------------------------------------------------------------