From b512ccf4bcf98b33e920933a853d6fc5f959aca9 Mon Sep 17 00:00:00 2001 From: Bram Leeda Date: Fri, 11 Nov 2022 17:31:36 +0100 Subject: [PATCH 1/3] Add new ElasticSearch Check for cluster connection Check for cluster health Returns responsetime along with success/warning/failure. Uses elasticsearch-php package - Supports both v7 and v8 as both ES versions are still supported by Elastic Signed-off-by: Bram Leeda --- src/Check/ElasticSearch.php | 102 ++++++++++++++++++++++++++++++++++++ test/ElasticSearchTest.php | 91 ++++++++++++++++++++++++++++++++ 2 files changed, 193 insertions(+) create mode 100644 src/Check/ElasticSearch.php create mode 100644 test/ElasticSearchTest.php diff --git a/src/Check/ElasticSearch.php b/src/Check/ElasticSearch.php new file mode 100644 index 0000000..c54c1b9 --- /dev/null +++ b/src/Check/ElasticSearch.php @@ -0,0 +1,102 @@ +elasticSettings = $elasticSettings; + $this->clientBuilder = $clientBuilder; + } + + public function check(): ResultInterface + { + $client = $this->getClient(); + + try { + $startTime = microtime(true); + $health = $this->getClusterHealth($client); + $responseTime = microtime(true) - $startTime; + } catch (Exception $e) { + return new Failure("Unable to connect to elasticsearch: " . $e->getMessage()); + } + + $serviceData = [ + "responseTime" => $responseTime, + ]; + + if ($health === 'green') { + return new Success("Cluster status green", $serviceData); + } + + if ($health === 'yellow') { + return new Warning("Cluster status yellow", $serviceData); + } + + return new Failure("Cluster status red", $serviceData); + } + + /** + * @return ElasticClient|ElasticsearchClient + */ + protected function getClient() + { + $this->clientBuilder->setHosts($this->elasticSettings['hosts']); + if (isset($this->elasticSettings['username'], $this->elasticSettings['password'])) { + $this->clientBuilder->setBasicAuthentication( + $this->elasticSettings['username'], + $this->elasticSettings['password'] + ); + } + + return $this->clientBuilder->build(); + } + + /** + * @param ElasticClient|ElasticsearchClient $client + * @throws Exception + */ + protected function getClusterHealth($client): string + { + if ($client instanceof ElasticsearchClient) { + $health = $client->cat()->health(); + + return $health[0]["status"] ?? "red"; + } + + if ($client instanceof ElasticClient) { + $health = $client->cat()->health(['h' => 'status'])->asString(); + + return trim($health); + } + + return 'unknown'; + } +} diff --git a/test/ElasticSearchTest.php b/test/ElasticSearchTest.php new file mode 100644 index 0000000..7f63e6b --- /dev/null +++ b/test/ElasticSearchTest.php @@ -0,0 +1,91 @@ + $expectedResult + * @throws Exception + */ + public function testElasticSearch(string $clusterStatus, string $expectedResult): void + { + if (class_exists(ElasticsearchClientBuilder::class)) { + $clientBuilder = $this->getElasticSearchClientBuilder($clusterStatus); + } elseif (class_exists(ElasticClientBuilder::class)) { + $clientBuilder = $this->getElasticClientBuilder($clusterStatus); + } else { + static::markTestSkipped("Missing elasticsearch client, unable to test ElasticSearch check"); + } + + $check = new ElasticSearch( + ['hosts' => ['127.0.0.1'], 'username' => 'user', 'password' => 'pass'], + $clientBuilder + ); + + static::assertInstanceOf($expectedResult, $check->check()); + } + + /** + * @return array{array{string, class-string}} + */ + public function healthStatusProvider(): array + { + return [ + ["green", SuccessInterface::class], + ["yellow", WarningInterface::class], + ["red", FailureInterface::class], + ["unknown", FailureInterface::class], + ]; + } + + /** + * @return ElasticsearchClientBuilder&MockBuilder + */ + private function getElasticSearchClientBuilder(string $expectedStatus): ElasticsearchClientBuilder + { + $cat = $this->createMock(ElasticsearchCat::class); + $cat->expects(self::once())->method('health')->willReturn([0 => ["status" => $expectedStatus]]); + + $client = $this->createMock(ElasticsearchClient::class); + $client->expects(self::once())->method('cat')->willReturn($cat); + + $clientBuilder = $this->createMock(ElasticsearchClientBuilder::class); + $clientBuilder->expects(self::once())->method('build')->willReturn($client); + + return $clientBuilder; + } + + private function getElasticClientBuilder(string $expectedStatus): ElasticClientBuilder + { + $response = $this->createMock(ResponseInterface::class); + $response->method('getBody')->willReturn($expectedStatus); + + $httpClient = $this->createMock(ClientInterface::class); + $httpClient->method('sendRequest')->willReturn($response); + + $clientBuilder = ElasticClientBuilder::create(); + $clientBuilder->setHttpClient($httpClient); + + return $clientBuilder; + } +} From 19581cd7cf2598c841575c5cc3fcfb545ce7f4e4 Mon Sep 17 00:00:00 2001 From: Bram Leeda Date: Fri, 11 Nov 2022 18:59:41 +0100 Subject: [PATCH 2/3] Use Guzzle instead of elasticsearch-php to fetch the elasticsearch cluster health status Signed-off-by: Bram Leeda --- src/Check/AbstractCheck.php | 2 +- src/Check/ElasticSearch.php | 68 ++++------------------- test/ElasticSearchTest.php | 68 ++++------------------- test/TestAsset/Check/TriggerUserError.php | 2 +- 4 files changed, 25 insertions(+), 115 deletions(-) diff --git a/src/Check/AbstractCheck.php b/src/Check/AbstractCheck.php index c6e6cee..68dda94 100644 --- a/src/Check/AbstractCheck.php +++ b/src/Check/AbstractCheck.php @@ -12,7 +12,7 @@ abstract class AbstractCheck implements CheckInterface /** * Explicitly set label. * - * @var string + * @var ?string */ protected $label; diff --git a/src/Check/ElasticSearch.php b/src/Check/ElasticSearch.php index c54c1b9..173cce6 100644 --- a/src/Check/ElasticSearch.php +++ b/src/Check/ElasticSearch.php @@ -2,47 +2,40 @@ namespace Laminas\Diagnostics\Check; -use Elastic\Elasticsearch\Client as ElasticClient; -use Elastic\Elasticsearch\ClientBuilder as ElasticClientBuilder; -use Elasticsearch\Client as ElasticsearchClient; -use Elasticsearch\ClientBuilder as ElasticsearchClientBuilder; use Exception; +use GuzzleHttp\ClientInterface as GuzzleClientInterface; use Laminas\Diagnostics\Result\Failure; use Laminas\Diagnostics\Result\ResultInterface; use Laminas\Diagnostics\Result\Success; use Laminas\Diagnostics\Result\Warning; +use function array_merge; use function microtime; use function trim; /** * Ensures a connection to ElasticSearch is possible and the cluster health is 'green' */ -class ElasticSearch extends AbstractCheck +class ElasticSearch extends GuzzleHttpService { - /** @var array{hosts: string[], username?: string, password?: string} */ - private array $elasticSettings; - - /** @var ElasticClientBuilder|ElasticsearchClientBuilder */ - private $clientBuilder; - /** - * @param array{hosts: string[], username?: string, password?: string} $elasticSettings - * @param ElasticClientBuilder|ElasticsearchClientBuilder $clientBuilder + * @param array $headers An array of headers used to create the request + * @param array $options An array of guzzle options used to create the request + * @param null|GuzzleClientInterface $guzzle Instance of guzzle to use */ - public function __construct(array $elasticSettings, $clientBuilder) + public function __construct(string $elasticSearchUrl, array $headers = [], array $options = [], $guzzle = null) { - $this->elasticSettings = $elasticSettings; - $this->clientBuilder = $clientBuilder; + $elasticSearchUrl .= '/_cat/health?h=status'; + + parent::__construct($elasticSearchUrl, $headers, $options, 200, null, $guzzle); } public function check(): ResultInterface { - $client = $this->getClient(); - try { $startTime = microtime(true); - $health = $this->getClusterHealth($client); + $response = $this->guzzle->send($this->request, array_merge($this->options)); + $health = trim((string) $response->getBody()); $responseTime = microtime(true) - $startTime; } catch (Exception $e) { return new Failure("Unable to connect to elasticsearch: " . $e->getMessage()); @@ -62,41 +55,4 @@ public function check(): ResultInterface return new Failure("Cluster status red", $serviceData); } - - /** - * @return ElasticClient|ElasticsearchClient - */ - protected function getClient() - { - $this->clientBuilder->setHosts($this->elasticSettings['hosts']); - if (isset($this->elasticSettings['username'], $this->elasticSettings['password'])) { - $this->clientBuilder->setBasicAuthentication( - $this->elasticSettings['username'], - $this->elasticSettings['password'] - ); - } - - return $this->clientBuilder->build(); - } - - /** - * @param ElasticClient|ElasticsearchClient $client - * @throws Exception - */ - protected function getClusterHealth($client): string - { - if ($client instanceof ElasticsearchClient) { - $health = $client->cat()->health(); - - return $health[0]["status"] ?? "red"; - } - - if ($client instanceof ElasticClient) { - $health = $client->cat()->health(['h' => 'status'])->asString(); - - return trim($health); - } - - return 'unknown'; - } } diff --git a/test/ElasticSearchTest.php b/test/ElasticSearchTest.php index 7f63e6b..c8f94c7 100644 --- a/test/ElasticSearchTest.php +++ b/test/ElasticSearchTest.php @@ -2,24 +2,18 @@ namespace LaminasTest\Diagnostics; -use Elastic\Elasticsearch\ClientBuilder as ElasticClientBuilder; -use Elasticsearch\Client as ElasticsearchClient; -use Elasticsearch\ClientBuilder as ElasticsearchClientBuilder; -use Elasticsearch\Namespaces\CatNamespace as ElasticsearchCat; use Exception; +use GuzzleHttp\Client; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\Psr7\Response; use Laminas\Diagnostics\Check\ElasticSearch; use Laminas\Diagnostics\Result\FailureInterface; use Laminas\Diagnostics\Result\ResultInterface; use Laminas\Diagnostics\Result\SuccessInterface; use Laminas\Diagnostics\Result\WarningInterface; -use PHPUnit\Framework\MockObject\MockBuilder; use PHPUnit\Framework\TestCase; -use Psr\Http\Client\ClientInterface; -use Psr\Http\Message\ResponseInterface; -use function class_exists; - -/** @covers \Laminas\Diagnostics\Check\ElasticSearch */ +/** @coversDefaultClass \Laminas\Diagnostics\Check\ElasticSearch */ class ElasticSearchTest extends TestCase { /** @@ -29,18 +23,9 @@ class ElasticSearchTest extends TestCase */ public function testElasticSearch(string $clusterStatus, string $expectedResult): void { - if (class_exists(ElasticsearchClientBuilder::class)) { - $clientBuilder = $this->getElasticSearchClientBuilder($clusterStatus); - } elseif (class_exists(ElasticClientBuilder::class)) { - $clientBuilder = $this->getElasticClientBuilder($clusterStatus); - } else { - static::markTestSkipped("Missing elasticsearch client, unable to test ElasticSearch check"); - } - - $check = new ElasticSearch( - ['hosts' => ['127.0.0.1'], 'username' => 'user', 'password' => 'pass'], - $clientBuilder - ); + $mockHandler = new MockHandler([new Response(200, [], $clusterStatus)]); + $mockClient = new Client(['handler' => $mockHandler]); + $check = new ElasticSearch('localhost:9200', [], [], $mockClient); static::assertInstanceOf($expectedResult, $check->check()); } @@ -51,41 +36,10 @@ public function testElasticSearch(string $clusterStatus, string $expectedResult) public function healthStatusProvider(): array { return [ - ["green", SuccessInterface::class], - ["yellow", WarningInterface::class], - ["red", FailureInterface::class], - ["unknown", FailureInterface::class], + ["green\n", SuccessInterface::class], + ["yellow\n", WarningInterface::class], + ["red\n", FailureInterface::class], + ["unknown\n", FailureInterface::class], ]; } - - /** - * @return ElasticsearchClientBuilder&MockBuilder - */ - private function getElasticSearchClientBuilder(string $expectedStatus): ElasticsearchClientBuilder - { - $cat = $this->createMock(ElasticsearchCat::class); - $cat->expects(self::once())->method('health')->willReturn([0 => ["status" => $expectedStatus]]); - - $client = $this->createMock(ElasticsearchClient::class); - $client->expects(self::once())->method('cat')->willReturn($cat); - - $clientBuilder = $this->createMock(ElasticsearchClientBuilder::class); - $clientBuilder->expects(self::once())->method('build')->willReturn($client); - - return $clientBuilder; - } - - private function getElasticClientBuilder(string $expectedStatus): ElasticClientBuilder - { - $response = $this->createMock(ResponseInterface::class); - $response->method('getBody')->willReturn($expectedStatus); - - $httpClient = $this->createMock(ClientInterface::class); - $httpClient->method('sendRequest')->willReturn($response); - - $clientBuilder = ElasticClientBuilder::create(); - $clientBuilder->setHttpClient($httpClient); - - return $clientBuilder; - } } diff --git a/test/TestAsset/Check/TriggerUserError.php b/test/TestAsset/Check/TriggerUserError.php index ea34f05..5a7db81 100644 --- a/test/TestAsset/Check/TriggerUserError.php +++ b/test/TestAsset/Check/TriggerUserError.php @@ -8,7 +8,7 @@ final class TriggerUserError extends AbstractCheck { - /** @var string */ + /** @var ?string */ protected $label = ''; private string $message; From 192bf3fe3463dfe8f430a777e72bdbbda988f0d7 Mon Sep 17 00:00:00 2001 From: Bram Leeda Date: Tue, 13 Dec 2022 13:49:05 +0100 Subject: [PATCH 3/3] Signed-off-by: Bram Leeda Add extra assertions for the ElasticSearch check result data --- test/ElasticSearchTest.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/test/ElasticSearchTest.php b/test/ElasticSearchTest.php index c8f94c7..5a60191 100644 --- a/test/ElasticSearchTest.php +++ b/test/ElasticSearchTest.php @@ -27,7 +27,15 @@ public function testElasticSearch(string $clusterStatus, string $expectedResult) $mockClient = new Client(['handler' => $mockHandler]); $check = new ElasticSearch('localhost:9200', [], [], $mockClient); - static::assertInstanceOf($expectedResult, $check->check()); + // Assert the ElasticSearch check converts the API response to the correct ResultInterface implementation + $checkResult = $check->check(); + static::assertInstanceOf($expectedResult, $checkResult); + + // Assert ElasticSearch check returns extra data + $resultData = $checkResult->getData(); + static::assertIsArray($resultData); + static::assertArrayHasKey('responseTime', $resultData); + static::assertIsNumeric($resultData['responseTime']); } /**