ElasticsearchHandlerTest.php 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of the Monolog package.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Monolog\Handler;
  11. use Monolog\Formatter\ElasticsearchFormatter;
  12. use Monolog\Formatter\NormalizerFormatter;
  13. use Monolog\Test\TestCase;
  14. use Monolog\Logger;
  15. use Elasticsearch\Client;
  16. use Elastic\Elasticsearch\Client as Client8;
  17. use Elasticsearch\ClientBuilder;
  18. use Elastic\Elasticsearch\ClientBuilder as ClientBuilder8;
  19. /**
  20. * @group Elasticsearch
  21. */
  22. class ElasticsearchHandlerTest extends TestCase
  23. {
  24. /**
  25. * @var Client|Client8 mock
  26. */
  27. protected $client;
  28. /**
  29. * @var array Default handler options
  30. */
  31. protected $options = [
  32. 'index' => 'my_index',
  33. 'type' => 'doc_type',
  34. ];
  35. public function setUp(): void
  36. {
  37. $hosts = ['http://elastic:changeme@127.0.0.1:9200'];
  38. $this->client = $this->getClientBuilder()
  39. ->setHosts($hosts)
  40. ->build();
  41. try {
  42. $this->client->info();
  43. } catch (\Throwable $e) {
  44. $this->markTestSkipped('Could not connect to Elasticsearch on 127.0.0.1:9200');
  45. }
  46. }
  47. public function tearDown(): void
  48. {
  49. parent::tearDown();
  50. unset($this->client);
  51. }
  52. /**
  53. * @covers Monolog\Handler\ElasticsearchHandler::setFormatter
  54. */
  55. public function testSetFormatter()
  56. {
  57. $handler = new ElasticsearchHandler($this->client);
  58. $formatter = new ElasticsearchFormatter('index_new', 'type_new');
  59. $handler->setFormatter($formatter);
  60. $this->assertInstanceOf('Monolog\Formatter\ElasticsearchFormatter', $handler->getFormatter());
  61. $this->assertEquals('index_new', $handler->getFormatter()->getIndex());
  62. $this->assertEquals('type_new', $handler->getFormatter()->getType());
  63. }
  64. /**
  65. * @covers Monolog\Handler\ElasticsearchHandler::setFormatter
  66. */
  67. public function testSetFormatterInvalid()
  68. {
  69. $handler = new ElasticsearchHandler($this->client);
  70. $formatter = new NormalizerFormatter();
  71. $this->expectException(\InvalidArgumentException::class);
  72. $this->expectExceptionMessage('ElasticsearchHandler is only compatible with ElasticsearchFormatter');
  73. $handler->setFormatter($formatter);
  74. }
  75. /**
  76. * @covers Monolog\Handler\ElasticsearchHandler::__construct
  77. * @covers Monolog\Handler\ElasticsearchHandler::getOptions
  78. */
  79. public function testOptions()
  80. {
  81. $expected = [
  82. 'index' => $this->options['index'],
  83. 'type' => $this->options['type'],
  84. 'ignore_error' => false,
  85. ];
  86. if ($this->client instanceof Client8 || $this->client::VERSION[0] === '7') {
  87. $expected['type'] = '_doc';
  88. }
  89. $handler = new ElasticsearchHandler($this->client, $this->options);
  90. $this->assertEquals($expected, $handler->getOptions());
  91. }
  92. /**
  93. * @covers Monolog\Handler\ElasticsearchHandler::bulkSend
  94. * @dataProvider providerTestConnectionErrors
  95. */
  96. public function testConnectionErrors($ignore, $expectedError)
  97. {
  98. $hosts = ['http://127.0.0.1:1'];
  99. $client = $this->getClientBuilder()
  100. ->setHosts($hosts)
  101. ->build();
  102. $handlerOpts = ['ignore_error' => $ignore];
  103. $handler = new ElasticsearchHandler($client, $handlerOpts);
  104. if ($expectedError) {
  105. $this->expectException($expectedError[0]);
  106. $this->expectExceptionMessage($expectedError[1]);
  107. $handler->handle($this->getRecord());
  108. } else {
  109. $this->assertFalse($handler->handle($this->getRecord()));
  110. }
  111. }
  112. /**
  113. * @return array
  114. */
  115. public function providerTestConnectionErrors()
  116. {
  117. return [
  118. [false, ['RuntimeException', 'Error sending messages to Elasticsearch']],
  119. [true, false],
  120. ];
  121. }
  122. /**
  123. * Integration test using localhost Elasticsearch server
  124. *
  125. * @covers Monolog\Handler\ElasticsearchHandler::__construct
  126. * @covers Monolog\Handler\ElasticsearchHandler::handleBatch
  127. * @covers Monolog\Handler\ElasticsearchHandler::bulkSend
  128. * @covers Monolog\Handler\ElasticsearchHandler::getDefaultFormatter
  129. */
  130. public function testHandleBatchIntegration()
  131. {
  132. $msg = [
  133. 'level' => Logger::ERROR,
  134. 'level_name' => 'ERROR',
  135. 'channel' => 'meh',
  136. 'context' => ['foo' => 7, 'bar', 'class' => new \stdClass],
  137. 'datetime' => new \DateTimeImmutable("@0"),
  138. 'extra' => [],
  139. 'message' => 'log',
  140. ];
  141. $expected = $msg;
  142. $expected['datetime'] = $msg['datetime']->format(\DateTime::ISO8601);
  143. $expected['context'] = [
  144. 'class' => ["stdClass" => []],
  145. 'foo' => 7,
  146. 0 => 'bar',
  147. ];
  148. $hosts = ['http://elastic:changeme@127.0.0.1:9200'];
  149. $client = $this->getClientBuilder()
  150. ->setHosts($hosts)
  151. ->build();
  152. $handler = new ElasticsearchHandler($client, $this->options);
  153. $handler->handleBatch([$msg]);
  154. // check document id from ES server response
  155. if ($client instanceof Client8) {
  156. $messageBody = $client->getTransport()->getLastResponse()->getBody();
  157. $info = json_decode((string) $messageBody, true);
  158. $this->assertNotNull($info, 'Decoding failed');
  159. $documentId = $this->getCreatedDocIdV8($info);
  160. $this->assertNotEmpty($documentId, 'No elastic document id received');
  161. } else {
  162. $documentId = $this->getCreatedDocId($client->transport->getLastConnection()->getLastRequestInfo());
  163. $this->assertNotEmpty($documentId, 'No elastic document id received');
  164. }
  165. // retrieve document source from ES and validate
  166. $document = $this->getDocSourceFromElastic(
  167. $client,
  168. $this->options['index'],
  169. $this->options['type'],
  170. $documentId
  171. );
  172. $this->assertEquals($expected, $document);
  173. // remove test index from ES
  174. $client->indices()->delete(['index' => $this->options['index']]);
  175. }
  176. /**
  177. * Return last created document id from ES response
  178. *
  179. * @param array $info Elasticsearch last request info
  180. * @return string|null
  181. */
  182. protected function getCreatedDocId(array $info)
  183. {
  184. $data = json_decode($info['response']['body'], true);
  185. if (!empty($data['items'][0]['index']['_id'])) {
  186. return $data['items'][0]['index']['_id'];
  187. }
  188. return null;
  189. }
  190. /**
  191. * Return last created document id from ES response
  192. *
  193. * @param array $data Elasticsearch last request info
  194. * @return string|null
  195. */
  196. protected function getCreatedDocIdV8(array $data)
  197. {
  198. if (!empty($data['items'][0]['index']['_id'])) {
  199. return $data['items'][0]['index']['_id'];
  200. }
  201. return null;
  202. }
  203. /**
  204. * Retrieve document by id from Elasticsearch
  205. *
  206. * @param Client|Client8 $client Elasticsearch client
  207. * @param string $index
  208. * @param string $type
  209. * @param string $documentId
  210. * @return array
  211. */
  212. protected function getDocSourceFromElastic($client, $index, $type, $documentId)
  213. {
  214. $params = [
  215. 'index' => $index,
  216. 'id' => $documentId,
  217. ];
  218. if (!$client instanceof Client8 && $client::VERSION[0] !== '7') {
  219. $params['type'] = $type;
  220. }
  221. $data = $client->get($params);
  222. if (!empty($data['_source'])) {
  223. return $data['_source'];
  224. }
  225. return [];
  226. }
  227. /**
  228. * @return ClientBuilder|ClientBuilder8
  229. */
  230. private function getClientBuilder()
  231. {
  232. if (class_exists(ClientBuilder8::class)) {
  233. return ClientBuilder8::create();
  234. }
  235. return ClientBuilder::create();
  236. }
  237. }