Sfoglia il codice sorgente

Add ability to include exception's stack traces in `Monolog\Formatter\JsonFormatter`

| Q             | A
| ------------- | ---
| Bug fix?      | no
| New feature?  | yes
| BC breaks?    | no
| Deprecations? | no
| Tests pass?   | yes
| Fixed tickets |
| License       | MIT
| Doc PR        |
Javier Spagnoletti 10 anni fa
parent
commit
e8e1d9efa3

+ 84 - 3
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 <j.boggiano@seld.be>
  */
-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;
+    }
 }

+ 44 - 0
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);
+    }
 }