diff --git a/CHANGELOG.md b/CHANGELOG.md index d3fb85e5..d0b906b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ * BC Break: Removed non-PSR-3 methods to add records, all the `add*` (e.g. `addWarning`) methods as well as `emerg`, `crit`, `err` and `warn` * BC Break: The record timezone is now set per Logger instance and not statically anymore * BC Break: There is no more default handler configured on empty Logger instances + * BC Break: ElasticSearchHandler renamed to ElasticaHandler * BC Break: Various handler-specific breaks, see [UPGRADE.md] for details * Added scalar type hints and return hints in all the places it was possible. Switched strict_types on for more reliability. * Added DateTimeImmutable support, all record datetime are now immutable, and will toString/json serialize with the correct date format, including microseconds (unless disabled) @@ -13,6 +14,7 @@ * Added SendGridHandler to use the SendGrid API to send emails * Added LogmaticHandler to use the Logmatic.io API to store log records * Added SqsHandler to send log records to an AWS SQS queue + * Added ElasticsearchHandler to send records via the official ES library. Elastica users should now use ElasticaHandler instead of ElasticSearchHandler * Added NoopHandler which is similar to the NullHandle but does not prevent the bubbling of log records to handlers further down the configuration, useful for temporarily disabling a handler in configuration files * Added ProcessHandler to write log output to the STDIN of a given process * Added HostnameProcessor that adds the machine's hostname to log records diff --git a/UPGRADE.md b/UPGRADE.md index 9949fa18..7dc49ecc 100644 --- a/UPGRADE.md +++ b/UPGRADE.md @@ -57,3 +57,8 @@ #### HipChatHandler - Removed HipChat API v1 support + +#### ElasticSearchHandler + +- As support for the official Elasticsearch library was added, the former ElasticSearchHandler has been + renamed to ElasticaHandler and the new one added as ElasticsearchHandler. diff --git a/composer.json b/composer.json index a7b2aaf8..2545c164 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "jakub-onderka/php-parallel-lint": "^0.9", "predis/predis": "^1.1", "phpspec/prophecy": "^1.6.1", + "elasticsearch/elasticsearch": "^6.0", "rollbar/rollbar": "^1.3" }, "suggest": { @@ -36,6 +37,7 @@ "sentry/sentry": "Allow sending log messages to a Sentry server", "doctrine/couchdb": "Allow sending log messages to a CouchDB server", "ruflin/elastica": "Allow sending log messages to an Elastic Search server", + "elasticsearch/elasticsearch": "Allow sending log messages to an Elasticsearch server via official client", "php-amqplib/php-amqplib": "Allow sending log messages to an AMQP server using php-amqplib", "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", "ext-mongodb": "Allow sending log messages to a MongoDB server (via driver)", diff --git a/doc/02-handlers-formatters-processors.md b/doc/02-handlers-formatters-processors.md index 55af6967..16900110 100644 --- a/doc/02-handlers-formatters-processors.md +++ b/doc/02-handlers-formatters-processors.md @@ -81,7 +81,7 @@ [Mongo](http://pecl.php.net/package/mongo) extension connection. - [_CouchDBHandler_](../src/Monolog/Handler/CouchDBHandler.php): Logs records to a CouchDB server. - [_DoctrineCouchDBHandler_](../src/Monolog/Handler/DoctrineCouchDBHandler.php): Logs records to a CouchDB server via the Doctrine CouchDB ODM. -- [_ElasticSearchHandler_](../src/Monolog/Handler/ElasticSearchHandler.php): Logs records to an Elastic Search server. +- [_ElasticsearchHandler_](../src/Monolog/Handler/ElasticsearchHandler.php): Logs records to an Elasticsearch server. - [_DynamoDbHandler_](../src/Monolog/Handler/DynamoDbHandler.php): Logs records to a DynamoDB table with the [AWS SDK](https://github.com/aws/aws-sdk-php). ### Wrappers / Special Handlers diff --git a/src/Monolog/Formatter/ElasticaFormatter.php b/src/Monolog/Formatter/ElasticaFormatter.php index a6354f50..128e07ab 100644 --- a/src/Monolog/Formatter/ElasticaFormatter.php +++ b/src/Monolog/Formatter/ElasticaFormatter.php @@ -65,6 +65,8 @@ class ElasticaFormatter extends NormalizerFormatter /** * Convert a log message into an Elastica Document + * @param array $record + * @return Document */ protected function getDocument(array $record): Document { diff --git a/src/Monolog/Formatter/ElasticsearchFormatter.php b/src/Monolog/Formatter/ElasticsearchFormatter.php new file mode 100644 index 00000000..1667f2cc --- /dev/null +++ b/src/Monolog/Formatter/ElasticsearchFormatter.php @@ -0,0 +1,89 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use DateTime; + +/** + * Format a log message into an Elasticsearch record + * + * @author Avtandil Kikabidze + */ +class ElasticsearchFormatter extends NormalizerFormatter +{ + /** + * @var string Elasticsearch index name + */ + protected $index; + + /** + * @var string Elasticsearch record type + */ + protected $type; + + /** + * @param string $index Elasticsearch index name + * @param string $type Elasticsearch record type + */ + public function __construct(string $index, string $type) + { + // Elasticsearch requires an ISO 8601 format date with optional millisecond precision. + parent::__construct(DateTime::ISO8601); + + $this->index = $index; + $this->type = $type; + } + + /** + * {@inheritdoc} + */ + public function format(array $record) + { + $record = parent::format($record); + + return $this->getDocument($record); + } + + /** + * Getter index + * + * @return string + */ + public function getIndex(): string + { + return $this->index; + } + + /** + * Getter type + * + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * Convert a log message into an Elasticsearch record + * + * @param array $record Log message + * @return array + */ + protected function getDocument(array $record): array + { + $record['_index'] = $this->index; + $record['_type'] = $this->type; + + return $record; + } +} diff --git a/src/Monolog/Handler/ElasticSearchHandler.php b/src/Monolog/Handler/ElasticaHandler.php similarity index 93% rename from src/Monolog/Handler/ElasticSearchHandler.php rename to src/Monolog/Handler/ElasticaHandler.php index a0ec2350..4a444b85 100644 --- a/src/Monolog/Handler/ElasticSearchHandler.php +++ b/src/Monolog/Handler/ElasticaHandler.php @@ -27,13 +27,13 @@ use Elastica\Exception\ExceptionInterface; * 'index' => 'elastic_index_name', * 'type' => 'elastic_doc_type', * ); - * $handler = new ElasticSearchHandler($client, $options); + * $handler = new ElasticaHandler($client, $options); * $log = new Logger('application'); * $log->pushHandler($handler); * * @author Jelle Vink */ -class ElasticSearchHandler extends AbstractProcessingHandler +class ElasticaHandler extends AbstractProcessingHandler { /** * @var Client @@ -81,8 +81,7 @@ class ElasticSearchHandler extends AbstractProcessingHandler if ($formatter instanceof ElasticaFormatter) { return parent::setFormatter($formatter); } - - throw new \InvalidArgumentException('ElasticSearchHandler is only compatible with ElasticaFormatter'); + throw new \InvalidArgumentException('ElasticaHandler is only compatible with ElasticaFormatter'); } public function getOptions(): array diff --git a/src/Monolog/Handler/ElasticsearchHandler.php b/src/Monolog/Handler/ElasticsearchHandler.php new file mode 100644 index 00000000..7d5d3a87 --- /dev/null +++ b/src/Monolog/Handler/ElasticsearchHandler.php @@ -0,0 +1,158 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Elasticsearch\Client; +use Elasticsearch\Common\Exceptions\RuntimeException as ElasticsearchRuntimeException; +use InvalidArgumentException; +use Monolog\Formatter\ElasticsearchFormatter; +use Monolog\Formatter\FormatterInterface; +use Monolog\Logger; +use RuntimeException; +use Throwable; + +/** + * Elasticsearch handler + * + * @link https://www.elastic.co/guide/en/elasticsearch/client/php-api/current/index.html + * + * Simple usage example: + * + * $client = \Elasticsearch\ClientBuilder::create() + * ->setHosts($hosts) + * ->build(); + * + * $options = array( + * 'index' => 'elastic_index_name', + * 'type' => 'elastic_doc_type', + * ); + * $handler = new ElasticsearchHandler($client, $options); + * $log = new Logger('application'); + * $log->pushHandler($handler); + * + * @author Avtandil Kikabidze + */ +class ElasticsearchHandler extends AbstractProcessingHandler +{ + /** + * @var Client + */ + protected $client; + + /** + * @var array Handler config options + */ + protected $options = []; + + /** + * @param Client $client Elasticsearch Client object + * @param array $options Handler configuration + * @param string|int $level The minimum logging level at which this handler will be triggered + * @param bool $bubble Whether the messages that are handled can bubble up the stack or not + */ + public function __construct(Client $client, array $options = [], $level = Logger::DEBUG, bool $bubble = true) + { + parent::__construct($level, $bubble); + $this->client = $client; + $this->options = array_merge( + [ + 'index' => 'monolog', // Elastic index name + 'type' => '_doc', // Elastic document type + 'ignore_error' => false, // Suppress Elasticsearch exceptions + ], + $options + ); + } + + /** + * {@inheritDoc} + */ + protected function write(array $record): void + { + $this->bulkSend([$record['formatted']]); + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter): HandlerInterface + { + if ($formatter instanceof ElasticsearchFormatter) { + return parent::setFormatter($formatter); + } + throw new InvalidArgumentException('ElasticsearchHandler is only compatible with ElasticsearchFormatter'); + } + + /** + * Getter options + * + * @return array + */ + public function getOptions(): array + { + return $this->options; + } + + /** + * {@inheritDoc} + */ + protected function getDefaultFormatter(): FormatterInterface + { + return new ElasticsearchFormatter($this->options['index'], $this->options['type']); + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records): void + { + $documents = $this->getFormatter()->formatBatch($records); + $this->bulkSend($documents); + } + + /** + * Use Elasticsearch bulk API to send list of documents + * + * @param array $records + * @throws \RuntimeException + */ + protected function bulkSend(array $records): void + { + try { + $params = [ + 'body' => [], + ]; + + foreach ($records as $record) { + $params['body'][] = [ + 'index' => [ + '_index' => $record['_index'], + '_type' => $record['_type'], + ], + ]; + unset($record['_index'], $record['_type']); + + $params['body'][] = $record; + } + + $responses = $this->client->bulk($params); + + if ($responses['errors'] === true) { + throw new ElasticsearchRuntimeException('Elasticsearch returned error for one of the records'); + } + } catch (Throwable $e) { + if (! $this->options['ignore_error']) { + throw new RuntimeException('Error sending messages to Elasticsearch', 0, $e); + } + } + } +} diff --git a/tests/Monolog/Formatter/ElasticsearchFormatterTest.php b/tests/Monolog/Formatter/ElasticsearchFormatterTest.php new file mode 100644 index 00000000..d68c8f11 --- /dev/null +++ b/tests/Monolog/Formatter/ElasticsearchFormatterTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Formatter; + +use Monolog\Logger; + +class ElasticsearchFormatterTest extends \PHPUnit\Framework\TestCase +{ + /** + * @covers Monolog\Formatter\ElasticsearchFormatter::__construct + * @covers Monolog\Formatter\ElasticsearchFormatter::format + * @covers Monolog\Formatter\ElasticsearchFormatter::getDocument + */ + public function testFormat() + { + // Test log message + $msg = [ + 'level' => Logger::ERROR, + 'level_name' => 'ERROR', + 'channel' => 'meh', + 'context' => ['foo' => 7, 'bar', 'class' => new \stdClass], + 'datetime' => new \DateTimeImmutable("@0"), + 'extra' => [], + 'message' => 'log', + ]; + + // Expected values + $expected = $msg; + $expected['datetime'] = '1970-01-01T00:00:00+0000'; + $expected['context'] = [ + 'class' => ['stdClass' => []], + 'foo' => 7, + 0 => 'bar', + ]; + + // Format log message + $formatter = new ElasticsearchFormatter('my_index', 'doc_type'); + $doc = $formatter->format($msg); + $this->assertInternalType('array', $doc); + + // Record parameters + $this->assertEquals('my_index', $doc['_index']); + $this->assertEquals('doc_type', $doc['_type']); + + // Record data values + foreach (array_keys($expected) as $key) { + $this->assertEquals($expected[$key], $doc[$key]); + } + } + + /** + * @covers Monolog\Formatter\ElasticsearchFormatter::getIndex + * @covers Monolog\Formatter\ElasticsearchFormatter::getType + */ + public function testGetters() + { + $formatter = new ElasticsearchFormatter('my_index', 'doc_type'); + $this->assertEquals('my_index', $formatter->getIndex()); + $this->assertEquals('doc_type', $formatter->getType()); + } +} diff --git a/tests/Monolog/Handler/ElasticSearchHandlerTest.php b/tests/Monolog/Handler/ElasticaHandlerTest.php similarity index 81% rename from tests/Monolog/Handler/ElasticSearchHandlerTest.php rename to tests/Monolog/Handler/ElasticaHandlerTest.php index 7c92555a..fb64e657 100644 --- a/tests/Monolog/Handler/ElasticSearchHandlerTest.php +++ b/tests/Monolog/Handler/ElasticaHandlerTest.php @@ -19,7 +19,7 @@ use Elastica\Client; use Elastica\Request; use Elastica\Response; -class ElasticSearchHandlerTest extends TestCase +class ElasticaHandlerTest extends TestCase { /** * @var Client mock @@ -49,10 +49,10 @@ class ElasticSearchHandlerTest extends TestCase } /** - * @covers Monolog\Handler\ElasticSearchHandler::write - * @covers Monolog\Handler\ElasticSearchHandler::handleBatch - * @covers Monolog\Handler\ElasticSearchHandler::bulkSend - * @covers Monolog\Handler\ElasticSearchHandler::getDefaultFormatter + * @covers Monolog\Handler\ElasticaHandler::write + * @covers Monolog\Handler\ElasticaHandler::handleBatch + * @covers Monolog\Handler\ElasticaHandler::bulkSend + * @covers Monolog\Handler\ElasticaHandler::getDefaultFormatter */ public function testHandle() { @@ -77,17 +77,17 @@ class ElasticSearchHandlerTest extends TestCase ->with($expected); // perform tests - $handler = new ElasticSearchHandler($this->client, $this->options); + $handler = new ElasticaHandler($this->client, $this->options); $handler->handle($msg); $handler->handleBatch([$msg]); } /** - * @covers Monolog\Handler\ElasticSearchHandler::setFormatter + * @covers Monolog\Handler\ElasticaHandler::setFormatter */ public function testSetFormatter() { - $handler = new ElasticSearchHandler($this->client); + $handler = new ElasticaHandler($this->client); $formatter = new ElasticaFormatter('index_new', 'type_new'); $handler->setFormatter($formatter); $this->assertInstanceOf('Monolog\Formatter\ElasticaFormatter', $handler->getFormatter()); @@ -96,20 +96,20 @@ class ElasticSearchHandlerTest extends TestCase } /** - * @covers Monolog\Handler\ElasticSearchHandler::setFormatter + * @covers Monolog\Handler\ElasticaHandler::setFormatter * @expectedException InvalidArgumentException - * @expectedExceptionMessage ElasticSearchHandler is only compatible with ElasticaFormatter + * @expectedExceptionMessage ElasticaHandler is only compatible with ElasticaFormatter */ public function testSetFormatterInvalid() { - $handler = new ElasticSearchHandler($this->client); + $handler = new ElasticaHandler($this->client); $formatter = new NormalizerFormatter(); $handler->setFormatter($formatter); } /** - * @covers Monolog\Handler\ElasticSearchHandler::__construct - * @covers Monolog\Handler\ElasticSearchHandler::getOptions + * @covers Monolog\Handler\ElasticaHandler::__construct + * @covers Monolog\Handler\ElasticaHandler::getOptions */ public function testOptions() { @@ -118,12 +118,12 @@ class ElasticSearchHandlerTest extends TestCase 'type' => $this->options['type'], 'ignore_error' => false, ]; - $handler = new ElasticSearchHandler($this->client, $this->options); + $handler = new ElasticaHandler($this->client, $this->options); $this->assertEquals($expected, $handler->getOptions()); } /** - * @covers Monolog\Handler\ElasticSearchHandler::bulkSend + * @covers Monolog\Handler\ElasticaHandler::bulkSend * @dataProvider providerTestConnectionErrors */ public function testConnectionErrors($ignore, $expectedError) @@ -131,7 +131,7 @@ class ElasticSearchHandlerTest extends TestCase $clientOpts = ['host' => '127.0.0.1', 'port' => 1]; $client = new Client($clientOpts); $handlerOpts = ['ignore_error' => $ignore]; - $handler = new ElasticSearchHandler($client, $handlerOpts); + $handler = new ElasticaHandler($client, $handlerOpts); if ($expectedError) { $this->expectException($expectedError[0]); @@ -156,10 +156,10 @@ class ElasticSearchHandlerTest extends TestCase /** * Integration test using localhost Elastic Search server * - * @covers Monolog\Handler\ElasticSearchHandler::__construct - * @covers Monolog\Handler\ElasticSearchHandler::handleBatch - * @covers Monolog\Handler\ElasticSearchHandler::bulkSend - * @covers Monolog\Handler\ElasticSearchHandler::getDefaultFormatter + * @covers Monolog\Handler\ElasticaHandler::__construct + * @covers Monolog\Handler\ElasticaHandler::handleBatch + * @covers Monolog\Handler\ElasticaHandler::bulkSend + * @covers Monolog\Handler\ElasticaHandler::getDefaultFormatter */ public function testHandleIntegration() { @@ -182,8 +182,7 @@ class ElasticSearchHandlerTest extends TestCase ]; $client = new Client(); - $handler = new ElasticSearchHandler($client, $this->options); - + $handler = new ElasticaHandler($client, $this->options); try { $handler->handleBatch([$msg]); } catch (\RuntimeException $e) { diff --git a/tests/Monolog/Handler/ElasticsearchHandlerTest.php b/tests/Monolog/Handler/ElasticsearchHandlerTest.php new file mode 100644 index 00000000..943a67ee --- /dev/null +++ b/tests/Monolog/Handler/ElasticsearchHandlerTest.php @@ -0,0 +1,269 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Elasticsearch\ClientBuilder; +use Monolog\Formatter\ElasticsearchFormatter; +use Monolog\Formatter\NormalizerFormatter; +use Monolog\Test\TestCase; +use Monolog\Logger; +use Elasticsearch\Client; + +class ElasticsearchHandlerTest extends TestCase +{ + /** + * @var Client mock + */ + protected $client; + + /** + * @var array Default handler options + */ + protected $options = [ + 'index' => 'my_index', + 'type' => 'doc_type', + ]; + + public function setUp() + { + // Elasticsearch lib required + if (!class_exists('Elasticsearch\Client')) { + $this->markTestSkipped('elasticsearch/elasticsearch not installed'); + } + + // base mock Elasticsearch Client object + $this->client = $this->getMockBuilder('Elasticsearch\Client') + ->setMethods(['bulk']) + ->disableOriginalConstructor() + ->getMock(); + } + + /** + * @covers Monolog\Handler\ElasticsearchHandler::write + * @covers Monolog\Handler\ElasticsearchHandler::handleBatch + * @covers Monolog\Handler\ElasticsearchHandler::bulkSend + * @covers Monolog\Handler\ElasticsearchHandler::getDefaultFormatter + */ + public function testHandle() + { + // log message + $msg = [ + 'level' => Logger::ERROR, + 'level_name' => 'ERROR', + 'channel' => 'meh', + 'context' => ['foo' => 7, 'bar', 'class' => new \stdClass], + 'datetime' => new \DateTimeImmutable("@0"), + 'extra' => [], + 'message' => 'log', + ]; + + // format expected result + $formatter = new ElasticsearchFormatter($this->options['index'], $this->options['type']); + $data = $formatter->format($msg); + unset($data['_index'], $data['_type']); + + $expected = [ + 'body' => [ + [ + 'index' => [ + '_index' => $this->options['index'], + '_type' => $this->options['type'], + ], + ], + $data, + ] + ]; + + // setup ES client mock + $this->client->expects($this->any()) + ->method('bulk') + ->with($expected); + + // perform tests + $handler = new ElasticsearchHandler($this->client, $this->options); + $handler->handle($msg); + $handler->handleBatch([$msg]); + } + + /** + * @covers Monolog\Handler\ElasticsearchHandler::setFormatter + */ + public function testSetFormatter() + { + $handler = new ElasticsearchHandler($this->client); + $formatter = new ElasticsearchFormatter('index_new', 'type_new'); + $handler->setFormatter($formatter); + $this->assertInstanceOf('Monolog\Formatter\ElasticsearchFormatter', $handler->getFormatter()); + $this->assertEquals('index_new', $handler->getFormatter()->getIndex()); + $this->assertEquals('type_new', $handler->getFormatter()->getType()); + } + + /** + * @covers Monolog\Handler\ElasticsearchHandler::setFormatter + * @expectedException InvalidArgumentException + * @expectedExceptionMessage ElasticsearchHandler is only compatible with ElasticsearchFormatter + */ + public function testSetFormatterInvalid() + { + $handler = new ElasticsearchHandler($this->client); + $formatter = new NormalizerFormatter(); + $handler->setFormatter($formatter); + } + + /** + * @covers Monolog\Handler\ElasticsearchHandler::__construct + * @covers Monolog\Handler\ElasticsearchHandler::getOptions + */ + public function testOptions() + { + $expected = [ + 'index' => $this->options['index'], + 'type' => $this->options['type'], + 'ignore_error' => false, + ]; + $handler = new ElasticsearchHandler($this->client, $this->options); + $this->assertEquals($expected, $handler->getOptions()); + } + + /** + * @covers Monolog\Handler\ElasticsearchHandler::bulkSend + * @dataProvider providerTestConnectionErrors + */ + public function testConnectionErrors($ignore, $expectedError) + { + $hosts = [['host' => '127.0.0.1', 'port' => 1]]; + $client = ClientBuilder::create() + ->setHosts($hosts) + ->build(); + + $handlerOpts = ['ignore_error' => $ignore]; + $handler = new ElasticsearchHandler($client, $handlerOpts); + + if ($expectedError) { + $this->expectException($expectedError[0]); + $this->expectExceptionMessage($expectedError[1]); + $handler->handle($this->getRecord()); + } else { + $this->assertFalse($handler->handle($this->getRecord())); + } + } + + /** + * @return array + */ + public function providerTestConnectionErrors() + { + return [ + [false, ['RuntimeException', 'Error sending messages to Elasticsearch']], + [true, false], + ]; + } + + /** + * Integration test using localhost Elasticsearch server + * + * @covers Monolog\Handler\ElasticsearchHandler::__construct + * @covers Monolog\Handler\ElasticsearchHandler::handleBatch + * @covers Monolog\Handler\ElasticsearchHandler::bulkSend + * @covers Monolog\Handler\ElasticsearchHandler::getDefaultFormatter + */ + public function testHandleIntegration() + { + $msg = [ + 'level' => Logger::ERROR, + 'level_name' => 'ERROR', + 'channel' => 'meh', + 'context' => ['foo' => 7, 'bar', 'class' => new \stdClass], + 'datetime' => new \DateTimeImmutable("@0"), + 'extra' => [], + 'message' => 'log', + ]; + + $expected = $msg; + $expected['datetime'] = $msg['datetime']->format(\DateTime::ISO8601); + $expected['context'] = [ + 'class' => ["stdClass" => []], + 'foo' => 7, + 0 => 'bar', + ]; + + $hosts = [['host' => '127.0.0.1', 'port' => 9200]]; + $client = ClientBuilder::create() + ->setHosts($hosts) + ->build(); + $handler = new ElasticsearchHandler($client, $this->options); + + try { + $handler->handleBatch([$msg]); + } catch (\RuntimeException $e) { + $this->markTestSkipped('Cannot connect to Elasticsearch server on localhost'); + } + + // check document id from ES server response + $documentId = $this->getCreatedDocId($client->transport->getLastConnection()->getLastRequestInfo()); + $this->assertNotEmpty($documentId, 'No elastic document id received'); + + // retrieve document source from ES and validate + $document = $this->getDocSourceFromElastic( + $client, + $this->options['index'], + $this->options['type'], + $documentId + ); + + $this->assertEquals($expected, $document); + + // remove test index from ES + $client->indices()->delete(['index' => $this->options['index']]); + } + + /** + * Return last created document id from ES response + * + * @param array $info Elasticsearch last request info + * @return string|null + */ + protected function getCreatedDocId(array $info) + { + $data = json_decode($info['response']['body'], true); + + if (!empty($data['items'][0]['index']['_id'])) { + return $data['items'][0]['index']['_id']; + } + } + + /** + * Retrieve document by id from Elasticsearch + * + * @param Client $client Elasticsearch client + * @param string $index + * @param string $type + * @param string $documentId + * @return array + */ + protected function getDocSourceFromElastic(Client $client, $index, $type, $documentId) + { + $params = [ + 'index' => $index, + 'type' => $type, + 'id' => $documentId + ]; + + $data = $client->get($params); + + if (!empty($data['_source'])) { + return $data['_source']; + } + + return []; + } +}