diff --git a/src/Monolog/Formatter/JsonFormatter.php b/src/Monolog/Formatter/JsonFormatter.php index e5a1d2c4..bd82f1df 100644 --- a/src/Monolog/Formatter/JsonFormatter.php +++ b/src/Monolog/Formatter/JsonFormatter.php @@ -11,6 +11,8 @@ namespace Monolog\Formatter; +use Exception; + /** * Encodes whatever record data is passed to it as json * @@ -18,13 +20,17 @@ namespace Monolog\Formatter; * * @author Jordi Boggiano */ -class JsonFormatter implements FormatterInterface +class JsonFormatter extends NormalizerFormatter { const BATCH_MODE_JSON = 1; const BATCH_MODE_NEWLINES = 2; protected $batchMode; protected $appendNewline; + /** + * @var bool + */ + protected $includeStacktraces = false; /** * @param int $batchMode @@ -64,7 +70,7 @@ class JsonFormatter implements FormatterInterface */ public function format(array $record) { - return json_encode($record) . ($this->appendNewline ? "\n" : ''); + return $this->toJson($this->normalize($record), true) . ($this->appendNewline ? "\n" : ''); } /** @@ -82,6 +88,14 @@ class JsonFormatter implements FormatterInterface } } + /** + * @param bool $include + */ + public function includeStacktraces($include = true) + { + $this->includeStacktraces = $include; + } + /** * Return a JSON-encoded array of records. * @@ -90,7 +104,7 @@ class JsonFormatter implements FormatterInterface */ protected function formatBatchJson(array $records) { - return json_encode($records); + return $this->toJson($this->normalize($records), true); } /** @@ -113,4 +127,71 @@ class JsonFormatter implements FormatterInterface return implode("\n", $records); } + + /** + * Normalizes given $data. + * + * @param mixed $data + * + * @return mixed + */ + protected function normalize($data) + { + if (is_array($data) || $data instanceof \Traversable) { + $normalized = array(); + + $count = 1; + foreach ($data as $key => $value) { + if ($count++ >= 1000) { + $normalized['...'] = 'Over 1000 items, aborting normalization'; + break; + } + $normalized[$key] = $this->normalize($value); + } + + return $normalized; + } + + if ($data instanceof Exception) { + return $this->normalizeException($data); + } + + return $data; + } + + /** + * Normalizes given exception with or without its own stack trace based on + * `includeStacktraces` property. + * + * @param Exception $e + * + * @return array + */ + protected function normalizeException(Exception $e) + { + $data = array( + 'class' => get_class($e), + 'message' => $e->getMessage(), + 'code' => $e->getCode(), + 'file' => $e->getFile().':'.$e->getLine(), + ); + + if ($this->includeStacktraces) { + $trace = $e->getTrace(); + foreach ($trace as $frame) { + if (isset($frame['file'])) { + $data['trace'][] = $frame['file'].':'.$frame['line']; + } else { + // We should again normalize the frames, because it might contain invalid items + $data['trace'][] = $this->normalize($frame); + } + } + } + + if ($previous = $e->getPrevious()) { + $data['previous'] = $this->normalizeException($previous); + } + + return $data; + } } diff --git a/tests/Monolog/Formatter/JsonFormatterTest.php b/tests/Monolog/Formatter/JsonFormatterTest.php index 69e20077..df7e35de 100644 --- a/tests/Monolog/Formatter/JsonFormatterTest.php +++ b/tests/Monolog/Formatter/JsonFormatterTest.php @@ -75,4 +75,48 @@ class JsonFormatterTest extends TestCase }); $this->assertEquals(implode("\n", $expected), $formatter->formatBatch($records)); } + + public function testDefFormatWithException() + { + $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', + )); + + 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); + } + + public function testDefFormatWithPreviousException() + { + $formatter = new JsonFormatter(); + $exception = new \RuntimeException('Foo', 0, new \LogicException('Wut?')); + $message = $formatter->format(array( + 'level_name' => 'CRITICAL', + 'channel' => 'core', + 'context' => array('exception' => $exception), + 'datetime' => new \DateTime(), + 'extra' => array(), + 'message' => 'foobar', + )); + + 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); + } + $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); + } }