From 00bfec630a1115b1a65f887aea47e2ceca25ea08 Mon Sep 17 00:00:00 2001 From: Thomas Ploch Date: Tue, 16 Dec 2014 10:13:12 +0100 Subject: [PATCH] Recursively check for resource arguments if trace is coming from internal function. With `React` or `Guzzle`, that register stream wrappers with PHP, the traces are treated as coming from internal functions with no line and file inside the frame. But they almost always contain resources as arguments, on which the `json_encode()` call will choke (probably this should be addressed in json_encode internally, since it is very easy to cast a resource to a string). I added a test case proving the situation and a pretty basic recursive checker for resources which just casts them as a string into the frame again. --- src/Monolog/Formatter/NormalizerFormatter.php | 23 +++++++++++ .../Formatter/GelfMessageFormatterTest.php | 40 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/Monolog/Formatter/NormalizerFormatter.php b/src/Monolog/Formatter/NormalizerFormatter.php index 95b3de36..080e1ab0 100644 --- a/src/Monolog/Formatter/NormalizerFormatter.php +++ b/src/Monolog/Formatter/NormalizerFormatter.php @@ -108,6 +108,7 @@ class NormalizerFormatter implements FormatterInterface if (isset($frame['file'])) { $data['trace'][] = $frame['file'].':'.$frame['line']; } else { + $this->convertResourceArgs($frame); $data['trace'][] = json_encode($frame); } } @@ -136,4 +137,26 @@ class NormalizerFormatter implements FormatterInterface return json_encode($data); } + + /** + * This method checks recursively for resource args inside the frame, since json_encode is choking on them. + * + * @param array &$frame Reference to current frame + */ + private function convertResourceArgs(array &$frame) + { + foreach ($frame as $key => &$item) { + if (is_scalar($item)) { + continue; + } + + if (is_resource($item)) { + $frame[$key] = (string) $item; + } + + if (is_array($item)) { + $this->convertResourceArgs($item); + } + } + } } diff --git a/tests/Monolog/Formatter/GelfMessageFormatterTest.php b/tests/Monolog/Formatter/GelfMessageFormatterTest.php index 3f47a09a..d92a2104 100644 --- a/tests/Monolog/Formatter/GelfMessageFormatterTest.php +++ b/tests/Monolog/Formatter/GelfMessageFormatterTest.php @@ -182,6 +182,46 @@ class GelfMessageFormatterTest extends \PHPUnit_Framework_TestCase $this->assertEquals('pair', $message_array['_EXTkey']); } + /** + * @covers Monolog\Formatter\GelfMessageFormatter::format + */ + public function testExceptionObjectWithResourceTrace() + { + // This happens i.e. in React promises or Guzzle streams where stream wrappers are registered + // and no file or line are included in the trace because it's treated as internal function + set_error_handler(function ($errno, $errstr, $errfile, $errline ) { + throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); + }); + + try { + $resource = fopen('php://memory', '+w'); + // Just do something stupid with a resource as argument + strpos($resource); + } catch (\Exception $e) { + } + + // restore the error handler + restore_error_handler(); + + $formatter = new GelfMessageFormatter(); + $record = array( + 'level' => Logger::ERROR, + 'level_name' => 'ERROR', + 'channel' => 'meh', + 'context' => array('from' => 'logger', 'exception' => $e), + 'datetime' => new \DateTime("@0"), + 'extra' => array(), + 'message' => 'log' + ); + + $message = $formatter->format($record); + + $this->assertRegExp( + '%\\\\"resource\\\\":\\\\"Resource id #\d+\\\\"%', + $message->getAdditional('ctxt_exception') + ); + } + private function isLegacy() { return interface_exists('\Gelf\IMessagePublisher');