diff --git a/src/Monolog/Formatter/JsonFormatter.php b/src/Monolog/Formatter/JsonFormatter.php index a985e2ab..4b2be77d 100644 --- a/src/Monolog/Formatter/JsonFormatter.php +++ b/src/Monolog/Formatter/JsonFormatter.php @@ -12,6 +12,7 @@ namespace Monolog\Formatter; use Exception; +use Throwable; /** * Encodes whatever record data is passed to it as json @@ -152,7 +153,7 @@ class JsonFormatter extends NormalizerFormatter return $normalized; } - if ($data instanceof Exception) { + if ($data instanceof Exception || $data instanceof Throwable) { return $this->normalizeException($data); } @@ -170,7 +171,7 @@ class JsonFormatter extends NormalizerFormatter protected function normalizeException($e) { // TODO 2.0 only check for Throwable - if (!$e instanceof Exception && !$e instanceof \Throwable) { + if (!$e instanceof Exception && !$e instanceof Throwable) { throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.get_class($e)); } diff --git a/tests/Monolog/Formatter/JsonFormatterTest.php b/tests/Monolog/Formatter/JsonFormatterTest.php index df7e35de..c9445f36 100644 --- a/tests/Monolog/Formatter/JsonFormatterTest.php +++ b/tests/Monolog/Formatter/JsonFormatterTest.php @@ -80,43 +80,104 @@ class JsonFormatterTest extends TestCase { $formatter = new JsonFormatter(); $exception = new \RuntimeException('Foo'); - $message = $formatter->format(array( - 'level_name' => 'CRITICAL', - 'channel' => 'core', - 'context' => array('exception' => $exception), - 'datetime' => new \DateTime(), - 'extra' => array(), - 'message' => 'foobar', - )); + $formattedException = $this->formatException($exception); - if (version_compare(PHP_VERSION, '5.4.0', '>=')) { - $path = substr(json_encode($exception->getFile(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 1, -1); - } else { - $path = substr(json_encode($exception->getFile()), 1, -1); - } - $this->assertEquals('{"level_name":"CRITICAL","channel":"core","context":{"exception":{"class":"RuntimeException","message":"'.$exception->getMessage().'","code":'.$exception->getCode().',"file":"'.$path.':'.$exception->getLine().'"}},"datetime":'.json_encode(new \DateTime()).',"extra":[],"message":"foobar"}'."\n", $message); + $message = $this->formatRecordWithExceptionInContext($formatter, $exception); + + $this->assertContextContainsFormattedException($formattedException, $message); } public function testDefFormatWithPreviousException() { $formatter = new JsonFormatter(); $exception = new \RuntimeException('Foo', 0, new \LogicException('Wut?')); + $formattedPrevException = $this->formatException($exception->getPrevious()); + $formattedException = $this->formatException($exception, $formattedPrevException); + + $message = $this->formatRecordWithExceptionInContext($formatter, $exception); + + $this->assertContextContainsFormattedException($formattedException, $message); + } + + public function testDefFormatWithThrowable() + { + if (!class_exists('Error') || !is_subclass_of('Error', 'Throwable')) { + $this->markTestSkipped('Requires PHP >=7'); + } + + $formatter = new JsonFormatter(); + $throwable = new \Error('Foo'); + $formattedThrowable = $this->formatException($throwable); + + $message = $this->formatRecordWithExceptionInContext($formatter, $throwable); + + $this->assertContextContainsFormattedException($formattedThrowable, $message); + } + + /** + * @param string $expected + * @param string $actual + * + * @internal param string $exception + */ + private function assertContextContainsFormattedException($expected, $actual) + { + $this->assertEquals( + '{"level_name":"CRITICAL","channel":"core","context":{"exception":'.$expected.'},"datetime":null,"extra":[],"message":"foobar"}'."\n", + $actual + ); + } + + /** + * @param JsonFormatter $formatter + * @param \Exception|\Throwable $exception + * + * @return string + */ + private function formatRecordWithExceptionInContext(JsonFormatter $formatter, $exception) + { $message = $formatter->format(array( 'level_name' => 'CRITICAL', 'channel' => 'core', 'context' => array('exception' => $exception), - 'datetime' => new \DateTime(), + 'datetime' => null, 'extra' => array(), 'message' => 'foobar', )); + return $message; + } + /** + * @param \Exception|\Throwable $exception + * + * @return string + */ + private function formatExceptionFilePathWithLine($exception) + { + $options = 0; if (version_compare(PHP_VERSION, '5.4.0', '>=')) { - $pathPrevious = substr(json_encode($exception->getPrevious()->getFile(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 1, -1); - $pathException = substr(json_encode($exception->getFile(), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE), 1, -1); - } else { - $pathPrevious = substr(json_encode($exception->getPrevious()->getFile()), 1, -1); - $pathException = substr(json_encode($exception->getFile()), 1, -1); + $options = JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE; } - $this->assertEquals('{"level_name":"CRITICAL","channel":"core","context":{"exception":{"class":"RuntimeException","message":"'.$exception->getMessage().'","code":'.$exception->getCode().',"file":"'.$pathException.':'.$exception->getLine().'","previous":{"class":"LogicException","message":"'.$exception->getPrevious()->getMessage().'","code":'.$exception->getPrevious()->getCode().',"file":"'.$pathPrevious.':'.$exception->getPrevious()->getLine().'"}}},"datetime":'.json_encode(new \DateTime()).',"extra":[],"message":"foobar"}'."\n", $message); + $path = substr(json_encode($exception->getFile(), $options), 1, -1); + return $path . ':' . $exception->getLine(); + } + + /** + * @param \Exception|\Throwable $exception + * + * @param null|string $previous + * + * @return string + */ + private function formatException($exception, $previous = null) + { + $formattedException = + '{"class":"' . get_class($exception) . + '","message":"' . $exception->getMessage() . + '","code":' . $exception->getCode() . + ',"file":"' . $this->formatExceptionFilePathWithLine($exception) . + ($previous ? '","previous":' . $previous : '"') . + '}'; + return $formattedException; } }