From e8e1d9efa3df4ae5456bdb417d721a5a3e613c6b Mon Sep 17 00:00:00 2001 From: Javier Spagnoletti Date: Fri, 25 Dec 2015 17:31:57 -0300 Subject: [PATCH 01/14] 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 | --- src/Monolog/Formatter/JsonFormatter.php | 87 ++++++++++++++++++- tests/Monolog/Formatter/JsonFormatterTest.php | 44 ++++++++++ 2 files changed, 128 insertions(+), 3 deletions(-) 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); + } } From e2a77060da9d0ef949a23d876783939c20937669 Mon Sep 17 00:00:00 2001 From: Dmitriy Garanzha Date: Wed, 20 Jan 2016 11:39:44 +0200 Subject: [PATCH 02/14] Raven client fingerprint support --- src/Monolog/Handler/RavenHandler.php | 4 ++++ tests/Monolog/Handler/RavenHandlerTest.php | 12 ++++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/Monolog/Handler/RavenHandler.php b/src/Monolog/Handler/RavenHandler.php index df2f557d..cba15f74 100644 --- a/src/Monolog/Handler/RavenHandler.php +++ b/src/Monolog/Handler/RavenHandler.php @@ -139,6 +139,10 @@ class RavenHandler extends AbstractProcessingHandler $options['tags'] = array_merge($options['tags'], $record['context']['tags']); unset($record['context']['tags']); } + if (!empty($record['context']['fingerprint'])) { + $options['fingerprint'] = $record['context']['fingerprint']; + unset($record['context']['fingerprint']); + } if (!empty($record['context']['logger'])) { $options['logger'] = $record['context']['logger']; unset($record['context']['logger']); diff --git a/tests/Monolog/Handler/RavenHandlerTest.php b/tests/Monolog/Handler/RavenHandlerTest.php index 8af486f4..9f55af64 100644 --- a/tests/Monolog/Handler/RavenHandlerTest.php +++ b/tests/Monolog/Handler/RavenHandlerTest.php @@ -99,6 +99,18 @@ class RavenHandlerTest extends TestCase $this->assertEquals($release, $ravenClient->lastData['release']); } + public function testFingerprint() + { + $ravenClient = $this->getRavenClient(); + $handler = $this->getHandler($ravenClient); + + $fingerprint = array('{{ default }}', 'other value'); + $record = $this->getRecord(Logger::INFO, 'test', array('fingerprint' => $fingerprint)); + $handler->handle($record); + + $this->assertEquals($fingerprint, $ravenClient->lastData['fingerprint']); + } + public function testUserContext() { $ravenClient = $this->getRavenClient(); From bf82600f0c46a426a876a03e8f0845c6b9ffb4ad Mon Sep 17 00:00:00 2001 From: Joris Berthelot Date: Thu, 21 Jan 2016 12:50:15 +0100 Subject: [PATCH 03/14] Allow GroupHandler to set a formatter to all nested handlers --- src/Monolog/Handler/GroupHandler.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/Monolog/Handler/GroupHandler.php b/src/Monolog/Handler/GroupHandler.php index 99384d35..48e7b8e5 100644 --- a/src/Monolog/Handler/GroupHandler.php +++ b/src/Monolog/Handler/GroupHandler.php @@ -11,6 +11,8 @@ namespace Monolog\Handler; +use Monolog\Formatter\FormatterInterface; + /** * Forwards records to multiple handlers * @@ -77,4 +79,16 @@ class GroupHandler extends AbstractHandler $handler->handleBatch($records); } } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter) + { + foreach ($this->handlers as $handler) { + $handler->setFormatter($formatter); + } + + return $this; + } } From f1a59c5e0f0e29364caa57fa8734c5f98f4e0eb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Paris?= Date: Sat, 23 Jan 2016 21:31:52 +0100 Subject: [PATCH 04/14] add release number to every log This adds an internal release number to the raven handler. The release number is added to what is sent to sentry unless something already is present because a release number was sent via "context" or "extra". --- src/Monolog/Handler/RavenHandler.php | 20 ++++++++++++++++++++ tests/Monolog/Handler/RavenHandlerTest.php | 16 ++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/Monolog/Handler/RavenHandler.php b/src/Monolog/Handler/RavenHandler.php index cba15f74..90758274 100644 --- a/src/Monolog/Handler/RavenHandler.php +++ b/src/Monolog/Handler/RavenHandler.php @@ -38,6 +38,12 @@ class RavenHandler extends AbstractProcessingHandler Logger::EMERGENCY => Raven_Client::FATAL, ); + /** + * @var string should represent the current version of the calling + * software. Can be any string (git commit, version number) + */ + private $release; + /** * @var Raven_Client the client object that sends the message to the server */ @@ -169,6 +175,10 @@ class RavenHandler extends AbstractProcessingHandler $options['extra']['extra'] = $record['extra']; } + if (!empty($this->release) && !isset($options['release'])) { + $options['release'] = $this->release; + } + if (isset($record['context']['exception']) && $record['context']['exception'] instanceof \Exception) { $options['extra']['message'] = $record['formatted']; $this->ravenClient->captureException($record['context']['exception'], $options); @@ -208,4 +218,14 @@ class RavenHandler extends AbstractProcessingHandler { return array('checksum', 'release'); } + + /** + * @param string $value + */ + public function setRelease($value) + { + $this->release = $value; + + return $this; + } } diff --git a/tests/Monolog/Handler/RavenHandlerTest.php b/tests/Monolog/Handler/RavenHandlerTest.php index 9f55af64..a7c4845f 100644 --- a/tests/Monolog/Handler/RavenHandlerTest.php +++ b/tests/Monolog/Handler/RavenHandlerTest.php @@ -204,6 +204,22 @@ class RavenHandlerTest extends TestCase $this->assertSame($formatter, $handler->getBatchFormatter()); } + public function testRelease() + { + $ravenClient = $this->getRavenClient(); + $handler = $this->getHandler($ravenClient); + $release = 'v42.42.42'; + $handler->setRelease($release); + $record = $this->getRecord(Logger::INFO, 'test'); + $handler->handle($record); + $this->assertEquals($release, $ravenClient->lastData['release']); + + $localRelease = 'v41.41.41'; + $record = $this->getRecord(Logger::INFO, 'test', array('release' => $localRelease)); + $handler->handle($record); + $this->assertEquals($localRelease, $ravenClient->lastData['release']); + } + private function methodThatThrowsAnException() { throw new \Exception('This is an exception'); From cb2778ae77fa64003afb1967e30a4aea07f4503b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sat, 13 Feb 2016 16:46:17 +0000 Subject: [PATCH 05/14] Add ->withName to clone a handler and get a new name, fixes #730 --- doc/01-usage.md | 3 +++ src/Monolog/Logger.php | 13 +++++++++++++ tests/Monolog/LoggerTest.php | 13 +++++++++++++ 3 files changed, 29 insertions(+) diff --git a/doc/01-usage.md b/doc/01-usage.md index 75bc4023..8e2551f3 100644 --- a/doc/01-usage.md +++ b/doc/01-usage.md @@ -189,6 +189,9 @@ $logger->pushHandler($firephp); $securityLogger = new Logger('security'); $securityLogger->pushHandler($stream); $securityLogger->pushHandler($firephp); + +// Or clone the first one to only change the channel +$securityLogger = $logger->withName('security'); ``` ## Customizing the log format diff --git a/src/Monolog/Logger.php b/src/Monolog/Logger.php index 5ac72104..2fbc02c7 100644 --- a/src/Monolog/Logger.php +++ b/src/Monolog/Logger.php @@ -153,6 +153,19 @@ class Logger implements LoggerInterface return $this->name; } + /** + * Return a new cloned instance with the name changed + * + * @return static + */ + public function withName($name) + { + $new = clone $this; + $new->name = $name; + + return $new; + } + /** * Pushes a handler on to the stack. * diff --git a/tests/Monolog/LoggerTest.php b/tests/Monolog/LoggerTest.php index fee98040..c0bd1da9 100644 --- a/tests/Monolog/LoggerTest.php +++ b/tests/Monolog/LoggerTest.php @@ -33,6 +33,19 @@ class LoggerTest extends \PHPUnit_Framework_TestCase $this->assertEquals('ERROR', Logger::getLevelName(Logger::ERROR)); } + /** + * @covers Monolog\Logger::withName + */ + public function testWithName() + { + $first = new Logger('first', array($handler = new TestHandler())); + $second = $first->withName('second'); + + $this->assertSame('first', $first->getName()); + $this->assertSame('second', $second->getName()); + $this->assertSame($handler, $second->popHandler()); + } + /** * @covers Monolog\Logger::toMonologLevel */ From 973d3ca7ae534d8f69ed8c51354c30120b38ea66 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 1 Mar 2016 15:44:46 +0000 Subject: [PATCH 06/14] Fix race condition in rotating file handler, fixes #709 --- src/Monolog/Handler/RotatingFileHandler.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Monolog/Handler/RotatingFileHandler.php b/src/Monolog/Handler/RotatingFileHandler.php index c6ca2531..22e6c3dc 100644 --- a/src/Monolog/Handler/RotatingFileHandler.php +++ b/src/Monolog/Handler/RotatingFileHandler.php @@ -115,7 +115,11 @@ class RotatingFileHandler extends StreamHandler foreach (array_slice($logFiles, $this->maxFiles) as $file) { if (is_writable($file)) { + // suppress errors here as unlink() might fail if two processes + // are cleaning up/rotating at the same time + set_error_handler(function ($errno, $errstr, $errfile, $errline) {}); unlink($file); + restore_error_handler(); } } From 0d2bef057998dd3294808d906aa6b3f71dc59439 Mon Sep 17 00:00:00 2001 From: Ronald Drenth Date: Sun, 6 Dec 2015 12:05:57 +0100 Subject: [PATCH 07/14] Add handleBatch implementation for PhpAmqpLib --- src/Monolog/Handler/AmqpHandler.php | 81 +++++++++++++++++++++++------ 1 file changed, 65 insertions(+), 16 deletions(-) diff --git a/src/Monolog/Handler/AmqpHandler.php b/src/Monolog/Handler/AmqpHandler.php index 7c20cfb7..bf0552c7 100644 --- a/src/Monolog/Handler/AmqpHandler.php +++ b/src/Monolog/Handler/AmqpHandler.php @@ -55,18 +55,12 @@ class AmqpHandler extends AbstractProcessingHandler protected function write(array $record) { $data = $record["formatted"]; - - $routingKey = sprintf( - '%s.%s', - // TODO 2.0 remove substr call - substr($record['level_name'], 0, 4), - $record['channel'] - ); + $routingKey = $this->getRoutingKey($record); if ($this->exchange instanceof AMQPExchange) { $this->exchange->publish( $data, - strtolower($routingKey), + $routingKey, 0, array( 'delivery_mode' => 2, @@ -75,19 +69,74 @@ class AmqpHandler extends AbstractProcessingHandler ); } else { $this->exchange->basic_publish( - new AMQPMessage( - (string) $data, - array( - 'delivery_mode' => 2, - 'content_type' => 'application/json', - ) - ), + $this->createAmqpMessage($data), $this->exchangeName, - strtolower($routingKey) + $routingKey ); } } + /** + * {@inheritDoc} + */ + public function handleBatch(array $records) + { + if ($this->exchange instanceof AMQPExchange) { + parent::handleBatch($records); + return; + } + + foreach ($records as $record) { + if (!$this->isHandling($record)) { + continue; + } + + $record = $this->processRecord($record); + $data = $this->getFormatter()->format($record); + + $this->exchange->batch_basic_publish( + $this->createAmqpMessage($data), + $this->exchangeName, + $this->getRoutingKey($record) + ); + } + + $this->exchange->publish_batch(); + } + + /** + * Gets the routing key for the AMQP exchange + * + * @param array $record + * @return string + */ + private function getRoutingKey(array $record) + { + $routingKey = sprintf( + '%s.%s', + // TODO 2.0 remove substr call + substr($record['level_name'], 0, 4), + $record['channel'] + ); + + return strtolower($routingKey); + } + + /** + * @param string $data + * @return AMQPMessage + */ + private function createAmqpMessage($data) + { + return new AMQPMessage( + (string) $data, + array( + 'delivery_mode' => 2, + 'content_type' => 'application/json', + ) + ); + } + /** * {@inheritDoc} */ From c688ff17ebc86876489deeccfe54d117c1e59132 Mon Sep 17 00:00:00 2001 From: Alexey Karapetov Date: Thu, 17 Dec 2015 18:02:13 -0800 Subject: [PATCH 08/14] HandlerWrapper --- doc/02-handlers-formatters-processors.md | 2 + src/Monolog/Handler/HandlerWrapper.php | 104 +++++++++++++++ tests/Monolog/Handler/HandlerWrapperTest.php | 130 +++++++++++++++++++ 3 files changed, 236 insertions(+) create mode 100644 src/Monolog/Handler/HandlerWrapper.php create mode 100644 tests/Monolog/Handler/HandlerWrapperTest.php diff --git a/doc/02-handlers-formatters-processors.md b/doc/02-handlers-formatters-processors.md index 71b1ca9f..95ba8e53 100644 --- a/doc/02-handlers-formatters-processors.md +++ b/doc/02-handlers-formatters-processors.md @@ -105,6 +105,8 @@ - _PsrHandler_: Can be used to forward log records to an existing PSR-3 logger - _TestHandler_: Used for testing, it records everything that is sent to it and has accessors to read out the information. +- _HandlerWrapper_: A simple handler wrapper you can inherit from to create + your own wrappers easily. ## Formatters diff --git a/src/Monolog/Handler/HandlerWrapper.php b/src/Monolog/Handler/HandlerWrapper.php new file mode 100644 index 00000000..8be8e5ea --- /dev/null +++ b/src/Monolog/Handler/HandlerWrapper.php @@ -0,0 +1,104 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\Formatter\FormatterInterface; + +/** + * This simple wrapper class can be used to extend handlers functionality. + * + * Example: A filtering handle. Inherit from this class, override isHandling() like this + * + * public function isHandling(array $record) + * { + * if ($record meets certain conditions) { + * return false; + * } + * return $this->handler->isHandling($record); + * } + * + * @author Alexey Karapetov + */ +class HandlerWrapper implements HandlerInterface +{ + /** + * @var HandlerInterface + */ + private $handler; + + /** + * HandlerWrapper constructor. + * @param HandlerInterface $handler + */ + public function __construct(HandlerInterface $handler) + { + $this->handler = $handler; + } + + /** + * {@inheritdoc} + */ + public function isHandling(array $record) + { + return $this->handler->isHandling($record); + } + + /** + * {@inheritdoc} + */ + public function handle(array $record) + { + return $this->handler->handle($record); + } + + /** + * {@inheritdoc} + */ + public function handleBatch(array $records) + { + return $this->handler->handleBatch($records); + } + + /** + * {@inheritdoc} + */ + public function pushProcessor($callback) + { + $this->handler->pushProcessor($callback); + return $this; + } + + /** + * {@inheritdoc} + */ + public function popProcessor() + { + return $this->handler->popProcessor(); + } + + /** + * {@inheritdoc} + */ + public function setFormatter(FormatterInterface $formatter) + { + $this->handler->setFormatter($formatter); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getFormatter() + { + return $this->handler->getFormatter(); + } +} \ No newline at end of file diff --git a/tests/Monolog/Handler/HandlerWrapperTest.php b/tests/Monolog/Handler/HandlerWrapperTest.php new file mode 100644 index 00000000..d8d0452c --- /dev/null +++ b/tests/Monolog/Handler/HandlerWrapperTest.php @@ -0,0 +1,130 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog\Handler; + +use Monolog\TestCase; + +/** + * @author Alexey Karapetov + */ +class HandlerWrapperTest extends TestCase +{ + /** + * @var HandlerWrapper + */ + private $wrapper; + + private $handler; + + public function setUp() + { + parent::setUp(); + $this->handler = $this->getMock('Monolog\\Handler\\HandlerInterface'); + $this->wrapper = new HandlerWrapper($this->handler); + } + + /** + * @return array + */ + public function trueFalseDataProvider() + { + return array( + array(true), + array(false), + ); + } + + /** + * @param $result + * @dataProvider trueFalseDataProvider + */ + public function testIsHandling($result) + { + $record = $this->getRecord(); + $this->handler->expects($this->once()) + ->method('isHandling') + ->with($record) + ->willReturn($result); + + $this->assertEquals($result, $this->wrapper->isHandling($record)); + } + + /** + * @param $result + * @dataProvider trueFalseDataProvider + */ + public function testHandle($result) + { + $record = $this->getRecord(); + $this->handler->expects($this->once()) + ->method('handle') + ->with($record) + ->willReturn($result); + + $this->assertEquals($result, $this->wrapper->handle($record)); + } + + /** + * @param $result + * @dataProvider trueFalseDataProvider + */ + public function testHandleBatch($result) + { + $records = $this->getMultipleRecords(); + $this->handler->expects($this->once()) + ->method('handleBatch') + ->with($records) + ->willReturn($result); + + $this->assertEquals($result, $this->wrapper->handleBatch($records)); + } + + public function testPushProcessor() + { + $processor = function () {}; + $this->handler->expects($this->once()) + ->method('pushProcessor') + ->with($processor); + + $this->assertEquals($this->wrapper, $this->wrapper->pushProcessor($processor)); + } + + public function testPopProcessor() + { + $processor = function () {}; + $this->handler->expects($this->once()) + ->method('popProcessor') + ->willReturn($processor); + + $this->assertEquals($processor, $this->wrapper->popProcessor()); + } + + public function testSetFormatter() + { + $formatter = $this->getMock('Monolog\\Formatter\\FormatterInterface'); + $this->handler->expects($this->once()) + ->method('setFormatter') + ->with($formatter); + + $this->assertEquals($this->wrapper, $this->wrapper->setFormatter($formatter)); + } + + public function testGetFormatter() + { + $formatter = $this->getMock('Monolog\\Formatter\\FormatterInterface'); + $this->handler->expects($this->once()) + ->method('getFormatter') + ->willReturn($formatter); + + $this->assertEquals($formatter, $this->wrapper->getFormatter()); + } +} From 481fc4cfa17808581d113673e7dd002b06dcc100 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 1 Mar 2016 16:20:06 +0000 Subject: [PATCH 09/14] PHP7 compat for exception classes, fixes #718 --- src/Monolog/Formatter/JsonFormatter.php | 9 +++++++-- src/Monolog/Formatter/LineFormatter.php | 9 ++++++--- src/Monolog/Formatter/NormalizerFormatter.php | 10 ++++++++-- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/Monolog/Formatter/JsonFormatter.php b/src/Monolog/Formatter/JsonFormatter.php index bd82f1df..06e9d13d 100644 --- a/src/Monolog/Formatter/JsonFormatter.php +++ b/src/Monolog/Formatter/JsonFormatter.php @@ -163,12 +163,17 @@ class JsonFormatter extends NormalizerFormatter * Normalizes given exception with or without its own stack trace based on * `includeStacktraces` property. * - * @param Exception $e + * @param Exception|Throwable $e * * @return array */ - protected function normalizeException(Exception $e) + protected function normalizeException($e) { + // TODO 2.0 only check for Throwable + if (!$e instanceof Exception && !$e instanceof \Throwable) { + throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.get_class($e)); + } + $data = array( 'class' => get_class($e), 'message' => $e->getMessage(), diff --git a/src/Monolog/Formatter/LineFormatter.php b/src/Monolog/Formatter/LineFormatter.php index 388e2266..f74dacd1 100644 --- a/src/Monolog/Formatter/LineFormatter.php +++ b/src/Monolog/Formatter/LineFormatter.php @@ -11,8 +11,6 @@ namespace Monolog\Formatter; -use Exception; - /** * Formats incoming records into a one-line string * @@ -114,8 +112,13 @@ class LineFormatter extends NormalizerFormatter return $this->replaceNewlines($this->convertToString($value)); } - protected function normalizeException(Exception $e) + protected function normalizeException($e) { + // TODO 2.0 only check for Throwable + if (!$e instanceof \Exception && !$e instanceof \Throwable) { + throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.get_class($e)); + } + $previousText = ''; if ($previous = $e->getPrevious()) { do { diff --git a/src/Monolog/Formatter/NormalizerFormatter.php b/src/Monolog/Formatter/NormalizerFormatter.php index 67f4b720..a76e2aed 100644 --- a/src/Monolog/Formatter/NormalizerFormatter.php +++ b/src/Monolog/Formatter/NormalizerFormatter.php @@ -90,7 +90,8 @@ class NormalizerFormatter implements FormatterInterface } if (is_object($data)) { - if ($data instanceof Exception) { + // TODO 2.0 only check for Throwable + if ($data instanceof Exception || (PHP_VERSION_ID > 70000 && $data instanceof \Throwable)) { return $this->normalizeException($data); } @@ -112,8 +113,13 @@ class NormalizerFormatter implements FormatterInterface return '[unknown('.gettype($data).')]'; } - protected function normalizeException(Exception $e) + protected function normalizeException($e) { + // TODO 2.0 only check for Throwable + if (!$e instanceof Exception && !$e instanceof \Throwable) { + throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.get_class($e)); + } + $data = array( 'class' => get_class($e), 'message' => $e->getMessage(), From 1048aa4a5932db4c6471fecbb22eb19e56e47e74 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 1 Mar 2016 16:39:55 +0000 Subject: [PATCH 10/14] Allow BrowserConsoleHandler to be overridden for better integration in frameworks --- src/Monolog/Handler/BrowserConsoleHandler.php | 72 ++++++++++++++----- 1 file changed, 53 insertions(+), 19 deletions(-) diff --git a/src/Monolog/Handler/BrowserConsoleHandler.php b/src/Monolog/Handler/BrowserConsoleHandler.php index 45b54697..86a59a79 100644 --- a/src/Monolog/Handler/BrowserConsoleHandler.php +++ b/src/Monolog/Handler/BrowserConsoleHandler.php @@ -46,9 +46,9 @@ class BrowserConsoleHandler extends AbstractProcessingHandler self::$records[] = $record; // Register shutdown handler if not already done - if (PHP_SAPI !== 'cli' && !self::$initialized) { + if (!self::$initialized) { self::$initialized = true; - register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send')); + $this->registerShutdownFunction(); } } @@ -58,26 +58,16 @@ class BrowserConsoleHandler extends AbstractProcessingHandler */ public static function send() { - $htmlTags = true; - // Check content type - foreach (headers_list() as $header) { - if (stripos($header, 'content-type:') === 0) { - // This handler only works with HTML and javascript outputs - // text/javascript is obsolete in favour of application/javascript, but still used - if (stripos($header, 'application/javascript') !== false || stripos($header, 'text/javascript') !== false) { - $htmlTags = false; - } elseif (stripos($header, 'text/html') === false) { - return; - } - break; - } + $format = self::getResponseFormat(); + if ($format === 'unknown') { + return; } if (count(self::$records)) { - if ($htmlTags) { - echo ''; - } else { - echo self::generateScript(); + if ($format === 'html') { + self::writeOutput(''); + } elseif ($format === 'js') { + self::writeOutput(self::generateScript()); } self::reset(); } @@ -91,6 +81,50 @@ class BrowserConsoleHandler extends AbstractProcessingHandler self::$records = array(); } + /** + * Wrapper for register_shutdown_function to allow overriding + */ + protected function registerShutdownFunction() + { + if (PHP_SAPI !== 'cli') { + register_shutdown_function(array('Monolog\Handler\BrowserConsoleHandler', 'send')); + } + } + + /** + * Wrapper for echo to allow overriding + * + * @param string $str + */ + protected static function writeOutput($str) + { + echo $str; + } + + /** + * Checks the format of the response + * + * @return string One of 'js', 'html' or 'unknown' + */ + protected static function getResponseFormat() + { + // Check content type + foreach (headers_list() as $header) { + if (stripos($header, 'content-type:') === 0) { + // This handler only works with HTML and javascript outputs + // text/javascript is obsolete in favour of application/javascript, but still used + if (stripos($header, 'application/javascript') !== false || stripos($header, 'text/javascript') !== false) { + return 'js'; + } elseif (stripos($header, 'text/html') !== false) { + return 'html'; + } + break; + } + } + + return 'unknown'; + } + private static function generateScript() { $script = array(); From bf61d9ff3fba3bbed2abce486b175f31a807f384 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 1 Mar 2016 16:46:08 +0000 Subject: [PATCH 11/14] Allow using LOG_LOCAL* on windows, fixes #713 --- src/Monolog/Handler/AbstractSyslogHandler.php | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/Monolog/Handler/AbstractSyslogHandler.php b/src/Monolog/Handler/AbstractSyslogHandler.php index 309cb89c..e2b2832d 100644 --- a/src/Monolog/Handler/AbstractSyslogHandler.php +++ b/src/Monolog/Handler/AbstractSyslogHandler.php @@ -70,6 +70,15 @@ abstract class AbstractSyslogHandler extends AbstractProcessingHandler $this->facilities['local5'] = LOG_LOCAL5; $this->facilities['local6'] = LOG_LOCAL6; $this->facilities['local7'] = LOG_LOCAL7; + } else { + $this->facilities['local0'] = 128; // LOG_LOCAL0 + $this->facilities['local1'] = 136; // LOG_LOCAL1 + $this->facilities['local2'] = 144; // LOG_LOCAL2 + $this->facilities['local3'] = 152; // LOG_LOCAL3 + $this->facilities['local4'] = 160; // LOG_LOCAL4 + $this->facilities['local5'] = 168; // LOG_LOCAL5 + $this->facilities['local6'] = 176; // LOG_LOCAL6 + $this->facilities['local7'] = 184; // LOG_LOCAL7 } // convert textual description of facility to syslog constant From 25131bb5f655f0c71214209c23dd30061e8a10c3 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 1 Mar 2016 16:58:06 +0000 Subject: [PATCH 12/14] Add context.* replacement in line formatter, fixes #717 --- src/Monolog/Formatter/LineFormatter.php | 7 +++++++ tests/Monolog/Formatter/LineFormatterTest.php | 14 ++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/Monolog/Formatter/LineFormatter.php b/src/Monolog/Formatter/LineFormatter.php index f74dacd1..0e62aa35 100644 --- a/src/Monolog/Formatter/LineFormatter.php +++ b/src/Monolog/Formatter/LineFormatter.php @@ -76,6 +76,13 @@ class LineFormatter extends NormalizerFormatter } } + foreach ($vars['context'] as $var => $val) { + if (false !== strpos($output, '%context.'.$var.'%')) { + $output = str_replace('%context.'.$var.'%', $this->stringify($val), $output); + unset($vars['context'][$var]); + } + } + if ($this->ignoreEmptyContextAndExtra) { if (empty($vars['context'])) { unset($vars['context']); diff --git a/tests/Monolog/Formatter/LineFormatterTest.php b/tests/Monolog/Formatter/LineFormatterTest.php index 7116ecac..310d93ca 100644 --- a/tests/Monolog/Formatter/LineFormatterTest.php +++ b/tests/Monolog/Formatter/LineFormatterTest.php @@ -91,6 +91,20 @@ class LineFormatterTest extends \PHPUnit_Framework_TestCase $this->assertEquals('['.date('Y-m-d').'] meh.ERROR: log '."\n", $message); } + public function testContextAndExtraReplacement() + { + $formatter = new LineFormatter('%context.foo% => %extra.foo%'); + $message = $formatter->format(array( + 'level_name' => 'ERROR', + 'channel' => 'meh', + 'context' => array('foo' => 'bar'), + 'datetime' => new \DateTime, + 'extra' => array('foo' => 'xbar'), + 'message' => 'log', + )); + $this->assertEquals('bar => xbar', $message); + } + public function testDefFormatWithObject() { $formatter = new LineFormatter(null, 'Y-m-d'); From 3521455e513ebad1a7a0dd0451a55aac33e4cc7e Mon Sep 17 00:00:00 2001 From: Daniel Gimenes Date: Wed, 3 Feb 2016 21:23:46 -0200 Subject: [PATCH 13/14] LineFormatter if configured to use short attachment --- src/Monolog/Handler/SlackHandler.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Monolog/Handler/SlackHandler.php b/src/Monolog/Handler/SlackHandler.php index 59d4836f..c5428759 100644 --- a/src/Monolog/Handler/SlackHandler.php +++ b/src/Monolog/Handler/SlackHandler.php @@ -96,7 +96,8 @@ class SlackHandler extends SocketHandler $this->useAttachment = $useAttachment; $this->useShortAttachment = $useShortAttachment; $this->includeContextAndExtra = $includeContextAndExtra; - if ($this->includeContextAndExtra) { + + if ($this->includeContextAndExtra && $this->useShortAttachment) { $this->lineFormatter = new LineFormatter; } } From fa96f6aa8f49706df6bfa0929609ed8b3345d94b Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 1 Mar 2016 17:13:07 +0000 Subject: [PATCH 14/14] Add support for firefox in ChromePHPHandler --- src/Monolog/Handler/ChromePHPHandler.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Monolog/Handler/ChromePHPHandler.php b/src/Monolog/Handler/ChromePHPHandler.php index c0e789f7..a6f30858 100644 --- a/src/Monolog/Handler/ChromePHPHandler.php +++ b/src/Monolog/Handler/ChromePHPHandler.php @@ -17,6 +17,8 @@ use Monolog\Logger; /** * Handler sending logs to the ChromePHP extension (http://www.chromephp.com/) * + * This also works out of the box with Firefox 43+ + * * @author Christophe Coevoet */ class ChromePHPHandler extends AbstractProcessingHandler @@ -175,7 +177,8 @@ class ChromePHPHandler extends AbstractProcessingHandler return false; } - return preg_match('{\bChrome/\d+[\.\d+]*\b}', $_SERVER['HTTP_USER_AGENT']); + // matches any Chrome, or Firefox 43+ + return preg_match('{\b(?:Chrome/\d+(?:\.\d+)*|Firefox/(?:4[3-9]|[5-9]\d|\d{3,})(?:\.\d)*)\b}', $_SERVER['HTTP_USER_AGENT']); } /**