diff --git a/src/Diagnostics/PanelLogger.php b/src/Diagnostics/PanelLogger.php index 06135480..f51b2f21 100644 --- a/src/Diagnostics/PanelLogger.php +++ b/src/Diagnostics/PanelLogger.php @@ -201,7 +201,7 @@ private function logQuery( 'uri' => $request->getUri()->getPath(), 'requestBody' => \Tracy\Dumper::toHtml( - \Nette\Utils\Json::decode($contents, \JSON_OBJECT_AS_ARRAY), + $this->decodeBody($contents), [ \Tracy\Dumper::DEPTH => 30, ], @@ -218,10 +218,9 @@ private function logQuery( if ($contents === '') { $contents = '{}'; } - $decoded = \Nette\Utils\Json::decode($contents, \JSON_OBJECT_AS_ARRAY); $query['responseBody'] = \Tracy\Dumper::toHtml( - $decoded, + $this->decodeBody($contents), [ \Tracy\Dumper::DEPTH => 30, ], @@ -236,4 +235,36 @@ private function logQuery( } + /** + * Bulk API bodies are NDJSON (one JSON document per line), not a single JSON document. + * Diagnostics must never break the request itself, so anything undecodable is dumped + * as the raw string instead of throwing. + */ + private function decodeBody( + string $contents, + ): mixed + { + try { + return \Nette\Utils\Json::decode($contents, \JSON_OBJECT_AS_ARRAY); + } catch (\Nette\Utils\JsonException) { + // Not a single JSON document, try NDJSON below. + } + + $documents = []; + foreach (\explode("\n", $contents) as $line) { + if (\trim($line) === '') { + continue; + } + + try { + $documents[] = \Nette\Utils\Json::decode($line, \JSON_OBJECT_AS_ARRAY); + } catch (\Nette\Utils\JsonException) { + return $contents; + } + } + + return $documents; + } + + } diff --git a/tests/SpameriTests/Elastic/Diagnostics/PanelLoggerTest.phpt b/tests/SpameriTests/Elastic/Diagnostics/PanelLoggerTest.phpt new file mode 100644 index 00000000..e2b256c3 --- /dev/null +++ b/tests/SpameriTests/Elastic/Diagnostics/PanelLoggerTest.phpt @@ -0,0 +1,113 @@ +panelLogger = new \Spameri\Elastic\Diagnostics\PanelLogger( + new \Psr\Log\NullLogger(), + ); + } + + + public function testLogsSingleJsonDocumentRequest(): void + { + $this->panelLogger->info('Request: POST /index/_search', [ + 'request' => new \GuzzleHttp\Psr7\Request( + 'POST', + 'http://localhost:9200/index/_search', + [], + '{"query": {"match_all": {}}}', + ), + ]); + + $queries = $this->panelLogger->getQueries(); + + \Tester\Assert::count(1, $queries); + \Tester\Assert::same('/index/_search', $queries[0]['uri']); + \Tester\Assert::contains('match_all', $queries[0]['requestBody']); + } + + + public function testLogsNdjsonBulkRequestWithoutThrowing(): void + { + $ndjsonBody = '{"index": {"_index": "index", "_id": "1"}}' . "\n" + . '{"name": "First document"}' . "\n" + . '{"index": {"_index": "index", "_id": "2"}}' . "\n" + . '{"name": "Second document"}' . "\n"; + + $this->panelLogger->info('Request: POST /_bulk', [ + 'request' => new \GuzzleHttp\Psr7\Request( + 'POST', + 'http://localhost:9200/_bulk', + [], + $ndjsonBody, + ), + ]); + + $queries = $this->panelLogger->getQueries(); + + \Tester\Assert::count(1, $queries); + \Tester\Assert::same('/_bulk', $queries[0]['uri']); + \Tester\Assert::contains('First document', $queries[0]['requestBody']); + \Tester\Assert::contains('Second document', $queries[0]['requestBody']); + } + + + public function testLogsUndecodableBodyAsRawString(): void + { + $this->panelLogger->info('Request: POST /index/_doc', [ + 'request' => new \GuzzleHttp\Psr7\Request( + 'POST', + 'http://localhost:9200/index/_doc', + [], + 'not json at all', + ), + ]); + + $queries = $this->panelLogger->getQueries(); + + \Tester\Assert::count(1, $queries); + \Tester\Assert::contains('not json at all', $queries[0]['requestBody']); + } + + + public function testPairsResponseWithRequest(): void + { + $this->panelLogger->info('Request: POST /index/_search', [ + 'request' => new \GuzzleHttp\Psr7\Request( + 'POST', + 'http://localhost:9200/index/_search', + [], + '{"query": {"match_all": {}}}', + ), + ]); + $this->panelLogger->info('Response (retry 0): 200', [ + 'response' => new \GuzzleHttp\Psr7\Response( + 200, + [], + '{"took": 1, "hits": {"total": {"value": 0}}}', + ), + ]); + + $queries = $this->panelLogger->getQueries(); + + \Tester\Assert::count(1, $queries); + \Tester\Assert::contains('hits', $queries[0]['responseBody']); + \Tester\Assert::true(isset($queries[0]['duration'])); + } + +} + +(new PanelLoggerTest())->run();