From cf979844dc722552283487f1086d651d4f6b31c0 Mon Sep 17 00:00:00 2001 From: Robert Gust-Bardon Date: Thu, 22 Feb 2018 11:16:36 -0600 Subject: [PATCH 1/9] Register signal handlers --- doc/03-utilities.md | 3 +- src/Monolog/ErrorHandler.php | 80 ++++++++- tests/Monolog/ErrorHandlerTest.php | 267 ++++++++++++++++++++++++++++- 3 files changed, 347 insertions(+), 3 deletions(-) diff --git a/doc/03-utilities.md b/doc/03-utilities.md index c62aa416..513d08aa 100644 --- a/doc/03-utilities.md +++ b/doc/03-utilities.md @@ -4,7 +4,8 @@ can then statically access from anywhere. It is not really a best practice but can help in some older codebases or for ease of use. - _ErrorHandler_: The `Monolog\ErrorHandler` class allows you to easily register - a Logger instance as an exception handler, error handler or fatal error handler. + a Logger instance as an exception handler, error handler, fatal error handler or + signal handler. - _ErrorLevelActivationStrategy_: Activates a FingersCrossedHandler when a certain log level is reached. - _ChannelLevelActivationStrategy_: Activates a FingersCrossedHandler when a certain diff --git a/src/Monolog/ErrorHandler.php b/src/Monolog/ErrorHandler.php index 7bfcd833..e6041055 100644 --- a/src/Monolog/ErrorHandler.php +++ b/src/Monolog/ErrorHandler.php @@ -14,11 +14,12 @@ namespace Monolog; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Monolog\Handler\AbstractHandler; +use ReflectionExtension; /** * Monolog error handler * - * A facility to enable logging of runtime errors, exceptions and fatal errors. + * A facility to enable logging of runtime errors, exceptions, fatal errors and signals. * * Quick setup: ErrorHandler::register($logger); * @@ -40,6 +41,10 @@ class ErrorHandler private $reservedMemory; private static $fatalErrors = array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR); + private $previousSignalHandler = array(); + private $signalLevelMap = array(); + private $signalRestartSyscalls = array(); + public function __construct(LoggerInterface $logger) { $this->logger = $logger; @@ -104,6 +109,37 @@ class ErrorHandler $this->hasFatalErrorHandler = true; } + public function registerSignalHandler($signo, $level = LogLevel::CRITICAL, $callPrevious = true, $restartSyscalls = true, $async = true) + { + if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) { + return $this; + } + + if ($callPrevious) { + if (function_exists('pcntl_signal_get_handler')) { + $handler = pcntl_signal_get_handler($signo); + if ($handler === false) { + return $this; + } + $this->previousSignalHandler[$signo] = $handler; + } else { + $this->previousSignalHandler[$signo] = true; + } + } else { + unset($this->previousSignalHandler[$signo]); + } + $this->signalLevelMap[$signo] = $level; + $this->signalRestartSyscalls[$signo] = $restartSyscalls; + + if (function_exists('pcntl_async_signals') && $async !== null) { + pcntl_async_signals($async); + } + + pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls); + + return $this; + } + protected function defaultErrorLevelMap() { return array( @@ -190,6 +226,48 @@ class ErrorHandler } } + public function handleSignal($signo, array $siginfo = null) + { + static $signals = array(); + + if (!$signals && extension_loaded('pcntl')) { + $pcntl = new ReflectionExtension('pcntl'); + foreach ($pcntl->getConstants() as $name => $value) { + if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_') { + $signals[$value] = $name; + } + } + } + + $level = isset($this->signalLevelMap[$signo]) ? $this->signalLevelMap[$signo] : LogLevel::CRITICAL; + $signal = isset($signals[$signo]) ? $signals[$signo] : $signo; + $context = isset($siginfo) ? $siginfo : array(); + $this->logger->log($level, sprintf('Program received signal %s', $signal), $context); + + if (!isset($this->previousSignalHandler[$signo])) { + return; + } + + if ($this->previousSignalHandler[$signo] === true || $this->previousSignalHandler[$signo] === SIG_DFL) { + if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch') + && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill')) { + $restartSyscalls = isset($this->restartSyscalls[$signo]) ? $this->restartSyscalls[$signo] : true; + pcntl_signal($signo, SIG_DFL, $restartSyscalls); + pcntl_sigprocmask(SIG_UNBLOCK, array($signo), $oldset); + posix_kill(posix_getpid(), $signo); + pcntl_signal_dispatch(); + pcntl_sigprocmask(SIG_SETMASK, $oldset); + pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls); + } + } elseif (is_callable($this->previousSignalHandler[$signo])) { + if (PHP_VERSION >= 71000) { + $this->previousSignalHandler[$signo]($signo, $siginfo); + } else { + $this->previousSignalHandler[$signo]($signo); + } + } + } + private static function codeToString($code) { switch ($code) { diff --git a/tests/Monolog/ErrorHandlerTest.php b/tests/Monolog/ErrorHandlerTest.php index a9a3f301..d444dcde 100644 --- a/tests/Monolog/ErrorHandlerTest.php +++ b/tests/Monolog/ErrorHandlerTest.php @@ -11,10 +11,55 @@ namespace Monolog; +use Monolog\Handler\StreamHandler; use Monolog\Handler\TestHandler; +use Psr\Log\LogLevel; -class ErrorHandlerTest extends \PHPUnit_Framework_TestCase +class ErrorHandlerTest extends TestCase { + + private $asyncSignalHandling; + private $blockedSignals; + private $signalHandlers; + + protected function setUp() + { + $this->signalHandlers = array(); + if (extension_loaded('pcntl')) { + if (function_exists('pcntl_async_signals')) { + $this->asyncSignalHandling = pcntl_async_signals(); + } + if (function_exists('pcntl_sigprocmask')) { + pcntl_sigprocmask(SIG_BLOCK, array(), $this->blockedSignals); + } + } + } + + protected function tearDown() + { + if ($this->asyncSignalHandling !== null) { + pcntl_async_signals($this->asyncSignalHandling); + } + if ($this->blockedSignals !== null) { + pcntl_sigprocmask(SIG_SETMASK, $this->blockedSignals); + } + if ($this->signalHandlers) { + pcntl_signal_dispatch(); + foreach ($this->signalHandlers as $signo => $handler) { + pcntl_signal($signo, $handler); + } + } + } + + private function setSignalHandler($signo, $handler = SIG_DFL) { + if (function_exists('pcntl_signal_get_handler')) { + $this->signalHandlers[$signo] = pcntl_signal_get_handler($signo); + } else { + $this->signalHandlers[$signo] = SIG_DFL; + } + $this->assertTrue(pcntl_signal($signo, $handler)); + } + public function testHandleError() { $logger = new Logger('test', array($handler = new TestHandler)); @@ -28,4 +73,224 @@ class ErrorHandlerTest extends \PHPUnit_Framework_TestCase $this->assertCount(2, $handler->getRecords()); $this->assertTrue($handler->hasEmergencyRecords()); } + + public function testHandleSignal() + { + $logger = new Logger('test', array($handler = new TestHandler)); + $errHandler = new ErrorHandler($logger); + $signo = 2; // SIGINT. + $siginfo = array('signo' => $signo, 'errno' => 0, 'code' => 0); + $errHandler->handleSignal($signo, $siginfo); + $this->assertCount(1, $handler->getRecords()); + $this->assertTrue($handler->hasCriticalRecords()); + $records = $handler->getRecords(); + $this->assertSame($siginfo, $records[0]['context']); + } + + /** + * @depends testHandleSignal + * @requires extension pcntl + * @requires extension posix + * @requires function pcntl_signal + * @requires function pcntl_signal_dispatch + * @requires function posix_getpid + * @requires function posix_kill + */ + public function testRegisterSignalHandler() + { + // SIGCONT and SIGURG should be ignored by default. + if (!defined('SIGCONT') || !defined('SIGURG')) { + $this->markTestSkipped('This test requires the SIGCONT and SIGURG pcntl constants.'); + } + + $this->setSignalHandler(SIGCONT, SIG_IGN); + $this->setSignalHandler(SIGURG, SIG_IGN); + + $logger = new Logger('test', array($handler = new TestHandler)); + $errHandler = new ErrorHandler($logger); + $pid = posix_getpid(); + + $this->assertTrue(posix_kill($pid, SIGURG)); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount(0, $handler->getRecords()); + + $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, false, false); + + $this->assertTrue(posix_kill($pid, SIGCONT)); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount(0, $handler->getRecords()); + + $this->assertTrue(posix_kill($pid, SIGURG)); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount(1, $handler->getRecords()); + $this->assertTrue($handler->hasInfoThatContains('SIGURG')); + } + + /** + * @dataProvider defaultPreviousProvider + * @depends testRegisterSignalHandler + * @requires function pcntl_fork + * @requires function pcntl_sigprocmask + * @requires function pcntl_wait + */ + public function testRegisterDefaultPreviousSignalHandler($signo, $callPrevious, $expected) + { + $this->setSignalHandler($signo, SIG_DFL); + + $path = tempnam(sys_get_temp_dir(), 'monolog-'); + $this->assertNotFalse($path); + + $pid = pcntl_fork(); + if ($pid === 0) { // Child. + $streamHandler = new StreamHandler($path); + $streamHandler->setFormatter($this->getIdentityFormatter()); + $logger = new Logger('test', array($streamHandler)); + $errHandler = new ErrorHandler($logger); + $errHandler->registerSignalHandler($signo, LogLevel::INFO, $callPrevious, false, false); + pcntl_sigprocmask(SIG_SETMASK, array(SIGCONT)); + posix_kill(posix_getpid(), $signo); + pcntl_signal_dispatch(); + // If $callPrevious is true, SIGINT should terminate by this line. + pcntl_sigprocmask(SIG_BLOCK, array(), $oldset); + file_put_contents($path, implode(' ', $oldset), FILE_APPEND); + posix_kill(posix_getpid(), $signo); + pcntl_signal_dispatch(); + exit(); + } + + $this->assertNotSame(-1, $pid); + $this->assertNotSame(-1, pcntl_wait($status)); + $this->assertNotSame(-1, $status); + $this->assertSame($expected, file_get_contents($path)); + } + + public function defaultPreviousProvider() + { + if (!defined('SIGCONT') || !defined('SIGINT') || !defined('SIGURG')) { + return array(); + } + + return array( + array(SIGINT, false, 'Program received signal SIGINT'.SIGCONT.'Program received signal SIGINT'), + array(SIGINT, true, 'Program received signal SIGINT'), + array(SIGURG, false, 'Program received signal SIGURG'.SIGCONT.'Program received signal SIGURG'), + array(SIGURG, true, 'Program received signal SIGURG'.SIGCONT.'Program received signal SIGURG'), + ); + } + + /** + * @depends testRegisterSignalHandler + * @requires function pcntl_signal_get_handler + */ + public function testRegisterCallablePreviousSignalHandler($callPrevious) + { + $this->setSignalHandler(SIGURG, SIG_IGN); + + $logger = new Logger('test', array($handler = new TestHandler)); + $errHandler = new ErrorHandler($logger); + $previousCalled = 0; + pcntl_signal(SIGURG, function ($signo, array $siginfo = null) use (&$previousCalled) { + ++$previousCalled; + }); + $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, $callPrevious, false, false); + $this->assertTrue(posix_kill(posix_getpid(), SIGURG)); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount(1, $handler->getRecords()); + $this->assertTrue($handler->hasInfoThatContains('SIGURG')); + $this->assertSame($callPrevious ? 1 : 0, $previousCalled); + } + + public function callablePreviousProvider() + { + return array( + array(false), + array(true), + ); + } + + /** + * @dataProvider restartSyscallsProvider + * @depends testRegisterDefaultPreviousSignalHandler + * @requires function pcntl_fork + * @requires function pcntl_wait + */ + public function testRegisterSyscallRestartingSignalHandler($restartSyscalls) + { + $this->setSignalHandler(SIGURG, SIG_IGN); + + $parentPid = posix_getpid(); + $microtime = microtime(true); + + $pid = pcntl_fork(); + if ($pid === 0) { // Child. + usleep(100000); + posix_kill($parentPid, SIGURG); + usleep(100000); + exit(); + } + + $this->assertNotSame(-1, $pid); + $logger = new Logger('test', array($handler = new TestHandler)); + $errHandler = new ErrorHandler($logger); + $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, $restartSyscalls, false); + if ($restartSyscalls) { + // pcntl_wait is expected to be restarted after the signal handler. + $this->assertNotSame(-1, pcntl_wait($status)); + } else { + // pcntl_wait is expected to be interrupted when the signal handler is invoked. + $this->assertSame(-1, pcntl_wait($status)); + } + $this->assertSame($restartSyscalls, microtime(true) - $microtime > 0.15); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount(1, $handler->getRecords()); + if ($restartSyscalls) { + // The child has already exited. + $this->assertSame(-1, pcntl_wait($status)); + } else { + // The child has not exited yet. + $this->assertNotSame(-1, pcntl_wait($status)); + } + } + + public function restartSyscallsProvider() + { + return array( + array(false), + array(true), + array(false), + array(true), + ); + } + + /** + * @dataProvider asyncProvider + * @depends testRegisterDefaultPreviousSignalHandler + * @requires function pcntl_async_signals + */ + public function testRegisterAsyncSignalHandler($initialAsync, $desiredAsync, $expectedBefore, $expectedAfter) + { + $this->setSignalHandler(SIGURG, SIG_IGN); + pcntl_async_signals($initialAsync); + + $logger = new Logger('test', array($handler = new TestHandler)); + $errHandler = new ErrorHandler($logger); + $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, false, $desiredAsync); + $this->assertTrue(posix_kill(posix_getpid(), SIGURG)); + $this->assertCount($expectedBefore, $handler->getRecords()); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount($expectedAfter, $handler->getRecords()); + } + + public function asyncProvider() + { + return array( + array(false, false, 0, 1), + array(false, null, 0, 1), + array(false, true, 1, 1), + array(true, false, 0, 1), + array(true, null, 1, 1), + array(true, true, 1, 1), + ); + } + } From 4fea44bf962b857d308fff371212949fa7df8dde Mon Sep 17 00:00:00 2001 From: Robert Gust-Bardon Date: Thu, 22 Feb 2018 13:48:30 -0600 Subject: [PATCH 2/9] Fix the reflection of constants in HHVM --- src/Monolog/ErrorHandler.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Monolog/ErrorHandler.php b/src/Monolog/ErrorHandler.php index e6041055..f0ff3d8c 100644 --- a/src/Monolog/ErrorHandler.php +++ b/src/Monolog/ErrorHandler.php @@ -232,11 +232,18 @@ class ErrorHandler if (!$signals && extension_loaded('pcntl')) { $pcntl = new ReflectionExtension('pcntl'); - foreach ($pcntl->getConstants() as $name => $value) { - if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_') { + $constants = $pcntl->getConstants(); + if (!$constants) { + // HHVM 3.24.2 returns an empty array. + $constants = get_defined_constants(true); + $constants = $constants['Core']; + } + foreach ($constants as $name => $value) { + if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) { $signals[$value] = $name; } } + unset($constants); } $level = isset($this->signalLevelMap[$signo]) ? $this->signalLevelMap[$signo] : LogLevel::CRITICAL; From 25c427a0e4def624048ce03205e610bdfa2e2921 Mon Sep 17 00:00:00 2001 From: Robert Gust-Bardon Date: Mon, 18 Jun 2018 11:17:10 -0500 Subject: [PATCH 3/9] Add a missing @dataProvider --- tests/Monolog/ErrorHandlerTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Monolog/ErrorHandlerTest.php b/tests/Monolog/ErrorHandlerTest.php index d444dcde..7026bb61 100644 --- a/tests/Monolog/ErrorHandlerTest.php +++ b/tests/Monolog/ErrorHandlerTest.php @@ -179,6 +179,7 @@ class ErrorHandlerTest extends TestCase } /** + * @dataProvider callablePreviousProvider * @depends testRegisterSignalHandler * @requires function pcntl_signal_get_handler */ From 23fd84fec03d7412bbfa31d28c98e70d623b4483 Mon Sep 17 00:00:00 2001 From: Robert Gust-Bardon Date: Mon, 18 Jun 2018 11:25:41 -0500 Subject: [PATCH 4/9] Wait for children that are being tested --- tests/Monolog/ErrorHandlerTest.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/Monolog/ErrorHandlerTest.php b/tests/Monolog/ErrorHandlerTest.php index 7026bb61..00300806 100644 --- a/tests/Monolog/ErrorHandlerTest.php +++ b/tests/Monolog/ErrorHandlerTest.php @@ -131,7 +131,7 @@ class ErrorHandlerTest extends TestCase * @depends testRegisterSignalHandler * @requires function pcntl_fork * @requires function pcntl_sigprocmask - * @requires function pcntl_wait + * @requires function pcntl_waitpid */ public function testRegisterDefaultPreviousSignalHandler($signo, $callPrevious, $expected) { @@ -159,7 +159,7 @@ class ErrorHandlerTest extends TestCase } $this->assertNotSame(-1, $pid); - $this->assertNotSame(-1, pcntl_wait($status)); + $this->assertNotSame(-1, pcntl_waitpid($pid, $status)); $this->assertNotSame(-1, $status); $this->assertSame($expected, file_get_contents($path)); } @@ -213,7 +213,7 @@ class ErrorHandlerTest extends TestCase * @dataProvider restartSyscallsProvider * @depends testRegisterDefaultPreviousSignalHandler * @requires function pcntl_fork - * @requires function pcntl_wait + * @requires function pcntl_waitpid */ public function testRegisterSyscallRestartingSignalHandler($restartSyscalls) { @@ -236,20 +236,20 @@ class ErrorHandlerTest extends TestCase $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, $restartSyscalls, false); if ($restartSyscalls) { // pcntl_wait is expected to be restarted after the signal handler. - $this->assertNotSame(-1, pcntl_wait($status)); + $this->assertNotSame(-1, pcntl_waitpid($pid, $status)); } else { // pcntl_wait is expected to be interrupted when the signal handler is invoked. - $this->assertSame(-1, pcntl_wait($status)); + $this->assertSame(-1, pcntl_waitpid($pid, $status)); } $this->assertSame($restartSyscalls, microtime(true) - $microtime > 0.15); $this->assertTrue(pcntl_signal_dispatch()); $this->assertCount(1, $handler->getRecords()); if ($restartSyscalls) { // The child has already exited. - $this->assertSame(-1, pcntl_wait($status)); + $this->assertSame(-1, pcntl_waitpid($pid, $status)); } else { // The child has not exited yet. - $this->assertNotSame(-1, pcntl_wait($status)); + $this->assertNotSame(-1, pcntl_waitpid($pid, $status)); } } From 37b587687d9475323afdd0ffec6e96b04c1d6163 Mon Sep 17 00:00:00 2001 From: Robert Gust-Bardon Date: Sun, 1 Jul 2018 23:42:41 -0500 Subject: [PATCH 5/9] Move POSIX signal handling to SignalHandler* Suggested by Helmut Hummel (@helhum). --- doc/03-utilities.md | 5 +- src/Monolog/ErrorHandler.php | 87 +-------- src/Monolog/SignalHandler.php | 115 +++++++++++ tests/Monolog/ErrorHandlerTest.php | 268 +------------------------- tests/Monolog/SignalHandlerTest.php | 287 ++++++++++++++++++++++++++++ 5 files changed, 407 insertions(+), 355 deletions(-) create mode 100644 src/Monolog/SignalHandler.php create mode 100644 tests/Monolog/SignalHandlerTest.php diff --git a/doc/03-utilities.md b/doc/03-utilities.md index 513d08aa..fd3fd0e7 100644 --- a/doc/03-utilities.md +++ b/doc/03-utilities.md @@ -4,8 +4,9 @@ can then statically access from anywhere. It is not really a best practice but can help in some older codebases or for ease of use. - _ErrorHandler_: The `Monolog\ErrorHandler` class allows you to easily register - a Logger instance as an exception handler, error handler, fatal error handler or - signal handler. + a Logger instance as an exception handler, error handler or fatal error handler. +- _SignalHandler_: The `Monolog\SignalHandler` class allows you to easily register + a Logger instance as a POSIX signal handler. - _ErrorLevelActivationStrategy_: Activates a FingersCrossedHandler when a certain log level is reached. - _ChannelLevelActivationStrategy_: Activates a FingersCrossedHandler when a certain diff --git a/src/Monolog/ErrorHandler.php b/src/Monolog/ErrorHandler.php index b96c58ae..b3025738 100644 --- a/src/Monolog/ErrorHandler.php +++ b/src/Monolog/ErrorHandler.php @@ -14,12 +14,11 @@ namespace Monolog; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Monolog\Handler\AbstractHandler; -use ReflectionExtension; /** * Monolog error handler * - * A facility to enable logging of runtime errors, exceptions, fatal errors and signals. + * A facility to enable logging of runtime errors, exceptions and fatal errors. * * Quick setup: ErrorHandler::register($logger); * @@ -42,10 +41,6 @@ class ErrorHandler private $lastFatalTrace; private static $fatalErrors = array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR); - private $previousSignalHandler = array(); - private $signalLevelMap = array(); - private $signalRestartSyscalls = array(); - public function __construct(LoggerInterface $logger) { $this->logger = $logger; @@ -110,37 +105,6 @@ class ErrorHandler $this->hasFatalErrorHandler = true; } - public function registerSignalHandler($signo, $level = LogLevel::CRITICAL, $callPrevious = true, $restartSyscalls = true, $async = true) - { - if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) { - return $this; - } - - if ($callPrevious) { - if (function_exists('pcntl_signal_get_handler')) { - $handler = pcntl_signal_get_handler($signo); - if ($handler === false) { - return $this; - } - $this->previousSignalHandler[$signo] = $handler; - } else { - $this->previousSignalHandler[$signo] = true; - } - } else { - unset($this->previousSignalHandler[$signo]); - } - $this->signalLevelMap[$signo] = $level; - $this->signalRestartSyscalls[$signo] = $restartSyscalls; - - if (function_exists('pcntl_async_signals') && $async !== null) { - pcntl_async_signals($async); - } - - pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls); - - return $this; - } - protected function defaultErrorLevelMap() { return array( @@ -234,55 +198,6 @@ class ErrorHandler } } - public function handleSignal($signo, array $siginfo = null) - { - static $signals = array(); - - if (!$signals && extension_loaded('pcntl')) { - $pcntl = new ReflectionExtension('pcntl'); - $constants = $pcntl->getConstants(); - if (!$constants) { - // HHVM 3.24.2 returns an empty array. - $constants = get_defined_constants(true); - $constants = $constants['Core']; - } - foreach ($constants as $name => $value) { - if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) { - $signals[$value] = $name; - } - } - unset($constants); - } - - $level = isset($this->signalLevelMap[$signo]) ? $this->signalLevelMap[$signo] : LogLevel::CRITICAL; - $signal = isset($signals[$signo]) ? $signals[$signo] : $signo; - $context = isset($siginfo) ? $siginfo : array(); - $this->logger->log($level, sprintf('Program received signal %s', $signal), $context); - - if (!isset($this->previousSignalHandler[$signo])) { - return; - } - - if ($this->previousSignalHandler[$signo] === true || $this->previousSignalHandler[$signo] === SIG_DFL) { - if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch') - && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill')) { - $restartSyscalls = isset($this->restartSyscalls[$signo]) ? $this->restartSyscalls[$signo] : true; - pcntl_signal($signo, SIG_DFL, $restartSyscalls); - pcntl_sigprocmask(SIG_UNBLOCK, array($signo), $oldset); - posix_kill(posix_getpid(), $signo); - pcntl_signal_dispatch(); - pcntl_sigprocmask(SIG_SETMASK, $oldset); - pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls); - } - } elseif (is_callable($this->previousSignalHandler[$signo])) { - if (PHP_VERSION >= 71000) { - $this->previousSignalHandler[$signo]($signo, $siginfo); - } else { - $this->previousSignalHandler[$signo]($signo); - } - } - } - private static function codeToString($code) { switch ($code) { diff --git a/src/Monolog/SignalHandler.php b/src/Monolog/SignalHandler.php new file mode 100644 index 00000000..d5907805 --- /dev/null +++ b/src/Monolog/SignalHandler.php @@ -0,0 +1,115 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; +use ReflectionExtension; + +/** + * Monolog POSIX signal handler + * + * @author Robert Gust-Bardon + */ +class SignalHandler +{ + private $logger; + + private $previousSignalHandler = array(); + private $signalLevelMap = array(); + private $signalRestartSyscalls = array(); + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + public function registerSignalHandler($signo, $level = LogLevel::CRITICAL, $callPrevious = true, $restartSyscalls = true, $async = true) + { + if (!extension_loaded('pcntl') || !function_exists('pcntl_signal')) { + return $this; + } + + if ($callPrevious) { + if (function_exists('pcntl_signal_get_handler')) { + $handler = pcntl_signal_get_handler($signo); + if ($handler === false) { + return $this; + } + $this->previousSignalHandler[$signo] = $handler; + } else { + $this->previousSignalHandler[$signo] = true; + } + } else { + unset($this->previousSignalHandler[$signo]); + } + $this->signalLevelMap[$signo] = $level; + $this->signalRestartSyscalls[$signo] = $restartSyscalls; + + if (function_exists('pcntl_async_signals') && $async !== null) { + pcntl_async_signals($async); + } + + pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls); + + return $this; + } + + public function handleSignal($signo, array $siginfo = null) + { + static $signals = array(); + + if (!$signals && extension_loaded('pcntl')) { + $pcntl = new ReflectionExtension('pcntl'); + $constants = $pcntl->getConstants(); + if (!$constants) { + // HHVM 3.24.2 returns an empty array. + $constants = get_defined_constants(true); + $constants = $constants['Core']; + } + foreach ($constants as $name => $value) { + if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) { + $signals[$value] = $name; + } + } + unset($constants); + } + + $level = isset($this->signalLevelMap[$signo]) ? $this->signalLevelMap[$signo] : LogLevel::CRITICAL; + $signal = isset($signals[$signo]) ? $signals[$signo] : $signo; + $context = isset($siginfo) ? $siginfo : array(); + $this->logger->log($level, sprintf('Program received signal %s', $signal), $context); + + if (!isset($this->previousSignalHandler[$signo])) { + return; + } + + if ($this->previousSignalHandler[$signo] === true || $this->previousSignalHandler[$signo] === SIG_DFL) { + if (extension_loaded('pcntl') && function_exists('pcntl_signal') && function_exists('pcntl_sigprocmask') && function_exists('pcntl_signal_dispatch') + && extension_loaded('posix') && function_exists('posix_getpid') && function_exists('posix_kill')) { + $restartSyscalls = isset($this->restartSyscalls[$signo]) ? $this->restartSyscalls[$signo] : true; + pcntl_signal($signo, SIG_DFL, $restartSyscalls); + pcntl_sigprocmask(SIG_UNBLOCK, array($signo), $oldset); + posix_kill(posix_getpid(), $signo); + pcntl_signal_dispatch(); + pcntl_sigprocmask(SIG_SETMASK, $oldset); + pcntl_signal($signo, array($this, 'handleSignal'), $restartSyscalls); + } + } elseif (is_callable($this->previousSignalHandler[$signo])) { + if (PHP_VERSION_ID >= 70100) { + $this->previousSignalHandler[$signo]($signo, $siginfo); + } else { + $this->previousSignalHandler[$signo]($signo); + } + } + } +} diff --git a/tests/Monolog/ErrorHandlerTest.php b/tests/Monolog/ErrorHandlerTest.php index 00300806..a9a3f301 100644 --- a/tests/Monolog/ErrorHandlerTest.php +++ b/tests/Monolog/ErrorHandlerTest.php @@ -11,55 +11,10 @@ namespace Monolog; -use Monolog\Handler\StreamHandler; use Monolog\Handler\TestHandler; -use Psr\Log\LogLevel; -class ErrorHandlerTest extends TestCase +class ErrorHandlerTest extends \PHPUnit_Framework_TestCase { - - private $asyncSignalHandling; - private $blockedSignals; - private $signalHandlers; - - protected function setUp() - { - $this->signalHandlers = array(); - if (extension_loaded('pcntl')) { - if (function_exists('pcntl_async_signals')) { - $this->asyncSignalHandling = pcntl_async_signals(); - } - if (function_exists('pcntl_sigprocmask')) { - pcntl_sigprocmask(SIG_BLOCK, array(), $this->blockedSignals); - } - } - } - - protected function tearDown() - { - if ($this->asyncSignalHandling !== null) { - pcntl_async_signals($this->asyncSignalHandling); - } - if ($this->blockedSignals !== null) { - pcntl_sigprocmask(SIG_SETMASK, $this->blockedSignals); - } - if ($this->signalHandlers) { - pcntl_signal_dispatch(); - foreach ($this->signalHandlers as $signo => $handler) { - pcntl_signal($signo, $handler); - } - } - } - - private function setSignalHandler($signo, $handler = SIG_DFL) { - if (function_exists('pcntl_signal_get_handler')) { - $this->signalHandlers[$signo] = pcntl_signal_get_handler($signo); - } else { - $this->signalHandlers[$signo] = SIG_DFL; - } - $this->assertTrue(pcntl_signal($signo, $handler)); - } - public function testHandleError() { $logger = new Logger('test', array($handler = new TestHandler)); @@ -73,225 +28,4 @@ class ErrorHandlerTest extends TestCase $this->assertCount(2, $handler->getRecords()); $this->assertTrue($handler->hasEmergencyRecords()); } - - public function testHandleSignal() - { - $logger = new Logger('test', array($handler = new TestHandler)); - $errHandler = new ErrorHandler($logger); - $signo = 2; // SIGINT. - $siginfo = array('signo' => $signo, 'errno' => 0, 'code' => 0); - $errHandler->handleSignal($signo, $siginfo); - $this->assertCount(1, $handler->getRecords()); - $this->assertTrue($handler->hasCriticalRecords()); - $records = $handler->getRecords(); - $this->assertSame($siginfo, $records[0]['context']); - } - - /** - * @depends testHandleSignal - * @requires extension pcntl - * @requires extension posix - * @requires function pcntl_signal - * @requires function pcntl_signal_dispatch - * @requires function posix_getpid - * @requires function posix_kill - */ - public function testRegisterSignalHandler() - { - // SIGCONT and SIGURG should be ignored by default. - if (!defined('SIGCONT') || !defined('SIGURG')) { - $this->markTestSkipped('This test requires the SIGCONT and SIGURG pcntl constants.'); - } - - $this->setSignalHandler(SIGCONT, SIG_IGN); - $this->setSignalHandler(SIGURG, SIG_IGN); - - $logger = new Logger('test', array($handler = new TestHandler)); - $errHandler = new ErrorHandler($logger); - $pid = posix_getpid(); - - $this->assertTrue(posix_kill($pid, SIGURG)); - $this->assertTrue(pcntl_signal_dispatch()); - $this->assertCount(0, $handler->getRecords()); - - $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, false, false); - - $this->assertTrue(posix_kill($pid, SIGCONT)); - $this->assertTrue(pcntl_signal_dispatch()); - $this->assertCount(0, $handler->getRecords()); - - $this->assertTrue(posix_kill($pid, SIGURG)); - $this->assertTrue(pcntl_signal_dispatch()); - $this->assertCount(1, $handler->getRecords()); - $this->assertTrue($handler->hasInfoThatContains('SIGURG')); - } - - /** - * @dataProvider defaultPreviousProvider - * @depends testRegisterSignalHandler - * @requires function pcntl_fork - * @requires function pcntl_sigprocmask - * @requires function pcntl_waitpid - */ - public function testRegisterDefaultPreviousSignalHandler($signo, $callPrevious, $expected) - { - $this->setSignalHandler($signo, SIG_DFL); - - $path = tempnam(sys_get_temp_dir(), 'monolog-'); - $this->assertNotFalse($path); - - $pid = pcntl_fork(); - if ($pid === 0) { // Child. - $streamHandler = new StreamHandler($path); - $streamHandler->setFormatter($this->getIdentityFormatter()); - $logger = new Logger('test', array($streamHandler)); - $errHandler = new ErrorHandler($logger); - $errHandler->registerSignalHandler($signo, LogLevel::INFO, $callPrevious, false, false); - pcntl_sigprocmask(SIG_SETMASK, array(SIGCONT)); - posix_kill(posix_getpid(), $signo); - pcntl_signal_dispatch(); - // If $callPrevious is true, SIGINT should terminate by this line. - pcntl_sigprocmask(SIG_BLOCK, array(), $oldset); - file_put_contents($path, implode(' ', $oldset), FILE_APPEND); - posix_kill(posix_getpid(), $signo); - pcntl_signal_dispatch(); - exit(); - } - - $this->assertNotSame(-1, $pid); - $this->assertNotSame(-1, pcntl_waitpid($pid, $status)); - $this->assertNotSame(-1, $status); - $this->assertSame($expected, file_get_contents($path)); - } - - public function defaultPreviousProvider() - { - if (!defined('SIGCONT') || !defined('SIGINT') || !defined('SIGURG')) { - return array(); - } - - return array( - array(SIGINT, false, 'Program received signal SIGINT'.SIGCONT.'Program received signal SIGINT'), - array(SIGINT, true, 'Program received signal SIGINT'), - array(SIGURG, false, 'Program received signal SIGURG'.SIGCONT.'Program received signal SIGURG'), - array(SIGURG, true, 'Program received signal SIGURG'.SIGCONT.'Program received signal SIGURG'), - ); - } - - /** - * @dataProvider callablePreviousProvider - * @depends testRegisterSignalHandler - * @requires function pcntl_signal_get_handler - */ - public function testRegisterCallablePreviousSignalHandler($callPrevious) - { - $this->setSignalHandler(SIGURG, SIG_IGN); - - $logger = new Logger('test', array($handler = new TestHandler)); - $errHandler = new ErrorHandler($logger); - $previousCalled = 0; - pcntl_signal(SIGURG, function ($signo, array $siginfo = null) use (&$previousCalled) { - ++$previousCalled; - }); - $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, $callPrevious, false, false); - $this->assertTrue(posix_kill(posix_getpid(), SIGURG)); - $this->assertTrue(pcntl_signal_dispatch()); - $this->assertCount(1, $handler->getRecords()); - $this->assertTrue($handler->hasInfoThatContains('SIGURG')); - $this->assertSame($callPrevious ? 1 : 0, $previousCalled); - } - - public function callablePreviousProvider() - { - return array( - array(false), - array(true), - ); - } - - /** - * @dataProvider restartSyscallsProvider - * @depends testRegisterDefaultPreviousSignalHandler - * @requires function pcntl_fork - * @requires function pcntl_waitpid - */ - public function testRegisterSyscallRestartingSignalHandler($restartSyscalls) - { - $this->setSignalHandler(SIGURG, SIG_IGN); - - $parentPid = posix_getpid(); - $microtime = microtime(true); - - $pid = pcntl_fork(); - if ($pid === 0) { // Child. - usleep(100000); - posix_kill($parentPid, SIGURG); - usleep(100000); - exit(); - } - - $this->assertNotSame(-1, $pid); - $logger = new Logger('test', array($handler = new TestHandler)); - $errHandler = new ErrorHandler($logger); - $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, $restartSyscalls, false); - if ($restartSyscalls) { - // pcntl_wait is expected to be restarted after the signal handler. - $this->assertNotSame(-1, pcntl_waitpid($pid, $status)); - } else { - // pcntl_wait is expected to be interrupted when the signal handler is invoked. - $this->assertSame(-1, pcntl_waitpid($pid, $status)); - } - $this->assertSame($restartSyscalls, microtime(true) - $microtime > 0.15); - $this->assertTrue(pcntl_signal_dispatch()); - $this->assertCount(1, $handler->getRecords()); - if ($restartSyscalls) { - // The child has already exited. - $this->assertSame(-1, pcntl_waitpid($pid, $status)); - } else { - // The child has not exited yet. - $this->assertNotSame(-1, pcntl_waitpid($pid, $status)); - } - } - - public function restartSyscallsProvider() - { - return array( - array(false), - array(true), - array(false), - array(true), - ); - } - - /** - * @dataProvider asyncProvider - * @depends testRegisterDefaultPreviousSignalHandler - * @requires function pcntl_async_signals - */ - public function testRegisterAsyncSignalHandler($initialAsync, $desiredAsync, $expectedBefore, $expectedAfter) - { - $this->setSignalHandler(SIGURG, SIG_IGN); - pcntl_async_signals($initialAsync); - - $logger = new Logger('test', array($handler = new TestHandler)); - $errHandler = new ErrorHandler($logger); - $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, false, $desiredAsync); - $this->assertTrue(posix_kill(posix_getpid(), SIGURG)); - $this->assertCount($expectedBefore, $handler->getRecords()); - $this->assertTrue(pcntl_signal_dispatch()); - $this->assertCount($expectedAfter, $handler->getRecords()); - } - - public function asyncProvider() - { - return array( - array(false, false, 0, 1), - array(false, null, 0, 1), - array(false, true, 1, 1), - array(true, false, 0, 1), - array(true, null, 1, 1), - array(true, true, 1, 1), - ); - } - } diff --git a/tests/Monolog/SignalHandlerTest.php b/tests/Monolog/SignalHandlerTest.php new file mode 100644 index 00000000..9fa07929 --- /dev/null +++ b/tests/Monolog/SignalHandlerTest.php @@ -0,0 +1,287 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +use Monolog\Handler\StreamHandler; +use Monolog\Handler\TestHandler; +use Psr\Log\LogLevel; + +/** + * @author Robert Gust-Bardon + * @covers Monolog\SignalHandler + */ +class SignalHandlerTest extends TestCase +{ + + private $asyncSignalHandling; + private $blockedSignals; + private $signalHandlers; + + protected function setUp() + { + $this->signalHandlers = array(); + if (extension_loaded('pcntl')) { + if (function_exists('pcntl_async_signals')) { + $this->asyncSignalHandling = pcntl_async_signals(); + } + if (function_exists('pcntl_sigprocmask')) { + pcntl_sigprocmask(SIG_BLOCK, array(), $this->blockedSignals); + } + } + } + + protected function tearDown() + { + if ($this->asyncSignalHandling !== null) { + pcntl_async_signals($this->asyncSignalHandling); + } + if ($this->blockedSignals !== null) { + pcntl_sigprocmask(SIG_SETMASK, $this->blockedSignals); + } + if ($this->signalHandlers) { + pcntl_signal_dispatch(); + foreach ($this->signalHandlers as $signo => $handler) { + pcntl_signal($signo, $handler); + } + } + } + + private function setSignalHandler($signo, $handler = SIG_DFL) { + if (function_exists('pcntl_signal_get_handler')) { + $this->signalHandlers[$signo] = pcntl_signal_get_handler($signo); + } else { + $this->signalHandlers[$signo] = SIG_DFL; + } + $this->assertTrue(pcntl_signal($signo, $handler)); + } + + public function testHandleSignal() + { + $logger = new Logger('test', array($handler = new TestHandler)); + $errHandler = new SignalHandler($logger); + $signo = 2; // SIGINT. + $siginfo = array('signo' => $signo, 'errno' => 0, 'code' => 0); + $errHandler->handleSignal($signo, $siginfo); + $this->assertCount(1, $handler->getRecords()); + $this->assertTrue($handler->hasCriticalRecords()); + $records = $handler->getRecords(); + $this->assertSame($siginfo, $records[0]['context']); + } + + /** + * @depends testHandleSignal + * @requires extension pcntl + * @requires extension posix + * @requires function pcntl_signal + * @requires function pcntl_signal_dispatch + * @requires function posix_getpid + * @requires function posix_kill + */ + public function testRegisterSignalHandler() + { + // SIGCONT and SIGURG should be ignored by default. + if (!defined('SIGCONT') || !defined('SIGURG')) { + $this->markTestSkipped('This test requires the SIGCONT and SIGURG pcntl constants.'); + } + + $this->setSignalHandler(SIGCONT, SIG_IGN); + $this->setSignalHandler(SIGURG, SIG_IGN); + + $logger = new Logger('test', array($handler = new TestHandler)); + $errHandler = new SignalHandler($logger); + $pid = posix_getpid(); + + $this->assertTrue(posix_kill($pid, SIGURG)); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount(0, $handler->getRecords()); + + $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, false, false); + + $this->assertTrue(posix_kill($pid, SIGCONT)); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount(0, $handler->getRecords()); + + $this->assertTrue(posix_kill($pid, SIGURG)); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount(1, $handler->getRecords()); + $this->assertTrue($handler->hasInfoThatContains('SIGURG')); + } + + /** + * @dataProvider defaultPreviousProvider + * @depends testRegisterSignalHandler + * @requires function pcntl_fork + * @requires function pcntl_sigprocmask + * @requires function pcntl_waitpid + */ + public function testRegisterDefaultPreviousSignalHandler($signo, $callPrevious, $expected) + { + $this->setSignalHandler($signo, SIG_DFL); + + $path = tempnam(sys_get_temp_dir(), 'monolog-'); + $this->assertNotFalse($path); + + $pid = pcntl_fork(); + if ($pid === 0) { // Child. + $streamHandler = new StreamHandler($path); + $streamHandler->setFormatter($this->getIdentityFormatter()); + $logger = new Logger('test', array($streamHandler)); + $errHandler = new SignalHandler($logger); + $errHandler->registerSignalHandler($signo, LogLevel::INFO, $callPrevious, false, false); + pcntl_sigprocmask(SIG_SETMASK, array(SIGCONT)); + posix_kill(posix_getpid(), $signo); + pcntl_signal_dispatch(); + // If $callPrevious is true, SIGINT should terminate by this line. + pcntl_sigprocmask(SIG_BLOCK, array(), $oldset); + file_put_contents($path, implode(' ', $oldset), FILE_APPEND); + posix_kill(posix_getpid(), $signo); + pcntl_signal_dispatch(); + exit(); + } + + $this->assertNotSame(-1, $pid); + $this->assertNotSame(-1, pcntl_waitpid($pid, $status)); + $this->assertNotSame(-1, $status); + $this->assertSame($expected, file_get_contents($path)); + } + + public function defaultPreviousProvider() + { + if (!defined('SIGCONT') || !defined('SIGINT') || !defined('SIGURG')) { + return array(); + } + + return array( + array(SIGINT, false, 'Program received signal SIGINT'.SIGCONT.'Program received signal SIGINT'), + array(SIGINT, true, 'Program received signal SIGINT'), + array(SIGURG, false, 'Program received signal SIGURG'.SIGCONT.'Program received signal SIGURG'), + array(SIGURG, true, 'Program received signal SIGURG'.SIGCONT.'Program received signal SIGURG'), + ); + } + + /** + * @dataProvider callablePreviousProvider + * @depends testRegisterSignalHandler + * @requires function pcntl_signal_get_handler + */ + public function testRegisterCallablePreviousSignalHandler($callPrevious) + { + $this->setSignalHandler(SIGURG, SIG_IGN); + + $logger = new Logger('test', array($handler = new TestHandler)); + $errHandler = new SignalHandler($logger); + $previousCalled = 0; + pcntl_signal(SIGURG, function ($signo, array $siginfo = null) use (&$previousCalled) { + ++$previousCalled; + }); + $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, $callPrevious, false, false); + $this->assertTrue(posix_kill(posix_getpid(), SIGURG)); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount(1, $handler->getRecords()); + $this->assertTrue($handler->hasInfoThatContains('SIGURG')); + $this->assertSame($callPrevious ? 1 : 0, $previousCalled); + } + + public function callablePreviousProvider() + { + return array( + array(false), + array(true), + ); + } + + /** + * @dataProvider restartSyscallsProvider + * @depends testRegisterDefaultPreviousSignalHandler + * @requires function pcntl_fork + * @requires function pcntl_waitpid + */ + public function testRegisterSyscallRestartingSignalHandler($restartSyscalls) + { + $this->setSignalHandler(SIGURG, SIG_IGN); + + $parentPid = posix_getpid(); + $microtime = microtime(true); + + $pid = pcntl_fork(); + if ($pid === 0) { // Child. + usleep(100000); + posix_kill($parentPid, SIGURG); + usleep(100000); + exit(); + } + + $this->assertNotSame(-1, $pid); + $logger = new Logger('test', array($handler = new TestHandler)); + $errHandler = new SignalHandler($logger); + $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, $restartSyscalls, false); + if ($restartSyscalls) { + // pcntl_wait is expected to be restarted after the signal handler. + $this->assertNotSame(-1, pcntl_waitpid($pid, $status)); + } else { + // pcntl_wait is expected to be interrupted when the signal handler is invoked. + $this->assertSame(-1, pcntl_waitpid($pid, $status)); + } + $this->assertSame($restartSyscalls, microtime(true) - $microtime > 0.15); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount(1, $handler->getRecords()); + if ($restartSyscalls) { + // The child has already exited. + $this->assertSame(-1, pcntl_waitpid($pid, $status)); + } else { + // The child has not exited yet. + $this->assertNotSame(-1, pcntl_waitpid($pid, $status)); + } + } + + public function restartSyscallsProvider() + { + return array( + array(false), + array(true), + array(false), + array(true), + ); + } + + /** + * @dataProvider asyncProvider + * @depends testRegisterDefaultPreviousSignalHandler + * @requires function pcntl_async_signals + */ + public function testRegisterAsyncSignalHandler($initialAsync, $desiredAsync, $expectedBefore, $expectedAfter) + { + $this->setSignalHandler(SIGURG, SIG_IGN); + pcntl_async_signals($initialAsync); + + $logger = new Logger('test', array($handler = new TestHandler)); + $errHandler = new SignalHandler($logger); + $errHandler->registerSignalHandler(SIGURG, LogLevel::INFO, false, false, $desiredAsync); + $this->assertTrue(posix_kill(posix_getpid(), SIGURG)); + $this->assertCount($expectedBefore, $handler->getRecords()); + $this->assertTrue(pcntl_signal_dispatch()); + $this->assertCount($expectedAfter, $handler->getRecords()); + } + + public function asyncProvider() + { + return array( + array(false, false, 0, 1), + array(false, null, 0, 1), + array(false, true, 1, 1), + array(true, false, 0, 1), + array(true, null, 1, 1), + array(true, true, 1, 1), + ); + } + +} From 0625068bf0a6667d18cca284eda1ba40907d2538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9goire=20Pineau?= Date: Wed, 31 May 2017 17:19:21 +0200 Subject: [PATCH 6/9] Added a new ResettableInterface and implemented it where possible. When one use Monolog in a long process like an AMQP worker with a `FingersCrossedHandler` or `BufferHandler` there is a drawback: as soon as there is an AMQP message that generate a log >= error (for example), all next AMQP messages will output logs, even if theses messages don't generate log where level >= error. In the same context there is a drawback for processor that add an UUID to the logs. The UUID should change for each AMQP messages. --- This patch address this issue with a new interface: `ResettableInterface` interface. Side note: `reset()`, `flush()`, `clear()`, are already used in Monolog. So basically, one can use the `reset()` on the `Logger` and on some `Handler`s / `Processor`s. It's especially useful for * the `FingersCrossedHandler`: it `close()` the buffer, then it `clear()` the buffer. * the `BufferHandler`: it `flush()` the buffer, then it `clear()` the buffer. * the `UidProcessor`: it renew the `uid`. --- CHANGELOG.md | 2 + src/Monolog/Handler/AbstractHandler.php | 16 +++- .../Handler/AbstractProcessingHandler.php | 2 + src/Monolog/Handler/BrowserConsoleHandler.php | 9 ++- src/Monolog/Handler/BufferHandler.php | 10 +++ src/Monolog/Handler/FingersCrossedHandler.php | 8 ++ src/Monolog/Handler/GroupHandler.php | 12 +++ src/Monolog/Handler/HandlerWrapper.php | 10 ++- src/Monolog/Logger.php | 17 ++++- src/Monolog/Processor/UidProcessor.php | 17 ++++- src/Monolog/ResettableInterface.php | 25 +++++++ .../Handler/BrowserConsoleHandlerTest.php | 2 +- .../Monolog/Handler/ChromePHPHandlerTest.php | 4 +- .../Handler/FingersCrossedHandlerTest.php | 4 +- tests/Monolog/Handler/FirePHPHandlerTest.php | 4 +- tests/Monolog/LoggerTest.php | 73 +++++++++++++++++++ 16 files changed, 200 insertions(+), 15 deletions(-) create mode 100644 src/Monolog/ResettableInterface.php diff --git a/CHANGELOG.md b/CHANGELOG.md index a69362b6..75ac442b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,8 @@ * Fixed table row styling issues in HtmlFormatter * Fixed RavenHandler dropping the message when logging exception * Fixed WhatFailureGroupHandler skipping processors when using handleBatch + * Added a `ResettableInterface` in order to reset/reset/clear/flush handlers and processors + and implement it where possible ### 1.23.0 (2017-06-19) diff --git a/src/Monolog/Handler/AbstractHandler.php b/src/Monolog/Handler/AbstractHandler.php index bf56549d..b286a01c 100644 --- a/src/Monolog/Handler/AbstractHandler.php +++ b/src/Monolog/Handler/AbstractHandler.php @@ -11,16 +11,17 @@ namespace Monolog\Handler; -use Monolog\Logger; use Monolog\Formatter\FormatterInterface; use Monolog\Formatter\LineFormatter; +use Monolog\Logger; +use Monolog\ResettableInterface; /** * Base Handler class providing the Handler structure * * @author Jordi Boggiano */ -abstract class AbstractHandler implements HandlerInterface +abstract class AbstractHandler implements HandlerInterface, ResettableInterface { protected $level = Logger::DEBUG; protected $bubble = true; @@ -174,6 +175,17 @@ abstract class AbstractHandler implements HandlerInterface } } + public function reset() + { + $this->close(); + + foreach ($this->processors as $processor) { + if ($processor instanceof ResettableInterface) { + $processor->reset(); + } + } + } + /** * Gets the default formatter. * diff --git a/src/Monolog/Handler/AbstractProcessingHandler.php b/src/Monolog/Handler/AbstractProcessingHandler.php index 6f18f72e..e1e89530 100644 --- a/src/Monolog/Handler/AbstractProcessingHandler.php +++ b/src/Monolog/Handler/AbstractProcessingHandler.php @@ -11,6 +11,8 @@ namespace Monolog\Handler; +use Monolog\ResettableInterface; + /** * Base Handler class providing the Handler structure * diff --git a/src/Monolog/Handler/BrowserConsoleHandler.php b/src/Monolog/Handler/BrowserConsoleHandler.php index 0225ee71..14fa0dc7 100644 --- a/src/Monolog/Handler/BrowserConsoleHandler.php +++ b/src/Monolog/Handler/BrowserConsoleHandler.php @@ -69,14 +69,19 @@ class BrowserConsoleHandler extends AbstractProcessingHandler } elseif ($format === 'js') { static::writeOutput(static::generateScript()); } - static::reset(); + static::resetStatic(); } } + public function reset() + { + self::resetStatic(); + } + /** * Forget all logged records */ - public static function reset() + public static function resetStatic() { static::$records = array(); } diff --git a/src/Monolog/Handler/BufferHandler.php b/src/Monolog/Handler/BufferHandler.php index c15e28a2..ca2c7a79 100644 --- a/src/Monolog/Handler/BufferHandler.php +++ b/src/Monolog/Handler/BufferHandler.php @@ -12,6 +12,7 @@ namespace Monolog\Handler; use Monolog\Logger; +use Monolog\ResettableInterface; /** * Buffers all records until closing the handler and then pass them as batch. @@ -114,4 +115,13 @@ class BufferHandler extends AbstractHandler $this->bufferSize = 0; $this->buffer = array(); } + + public function reset() + { + parent::reset(); + + if ($this->handler instanceof ResettableInterface) { + $this->handler->reset(); + } + } } diff --git a/src/Monolog/Handler/FingersCrossedHandler.php b/src/Monolog/Handler/FingersCrossedHandler.php index d8026be6..a2d85dde 100644 --- a/src/Monolog/Handler/FingersCrossedHandler.php +++ b/src/Monolog/Handler/FingersCrossedHandler.php @@ -14,6 +14,7 @@ namespace Monolog\Handler; use Monolog\Handler\FingersCrossed\ErrorLevelActivationStrategy; use Monolog\Handler\FingersCrossed\ActivationStrategyInterface; use Monolog\Logger; +use Monolog\ResettableInterface; /** * Buffers all records until a certain level is reached @@ -147,7 +148,14 @@ class FingersCrossedHandler extends AbstractHandler */ public function reset() { + parent::reset(); + + $this->buffer = array(); $this->buffering = true; + + if ($this->handler instanceof ResettableInterface) { + $this->handler->reset(); + } } /** diff --git a/src/Monolog/Handler/GroupHandler.php b/src/Monolog/Handler/GroupHandler.php index c38508c2..28e5c564 100644 --- a/src/Monolog/Handler/GroupHandler.php +++ b/src/Monolog/Handler/GroupHandler.php @@ -12,6 +12,7 @@ namespace Monolog\Handler; use Monolog\Formatter\FormatterInterface; +use Monolog\ResettableInterface; /** * Forwards records to multiple handlers @@ -90,6 +91,17 @@ class GroupHandler extends AbstractHandler } } + public function reset() + { + parent::reset(); + + foreach ($this->handlers as $handler) { + if ($handler instanceof ResettableInterface) { + $handler->reset(); + } + } + } + /** * {@inheritdoc} */ diff --git a/src/Monolog/Handler/HandlerWrapper.php b/src/Monolog/Handler/HandlerWrapper.php index e540d80f..55e64986 100644 --- a/src/Monolog/Handler/HandlerWrapper.php +++ b/src/Monolog/Handler/HandlerWrapper.php @@ -11,6 +11,7 @@ namespace Monolog\Handler; +use Monolog\ResettableInterface; use Monolog\Formatter\FormatterInterface; /** @@ -30,7 +31,7 @@ use Monolog\Formatter\FormatterInterface; * * @author Alexey Karapetov */ -class HandlerWrapper implements HandlerInterface +class HandlerWrapper implements HandlerInterface, ResettableInterface { /** * @var HandlerInterface @@ -105,4 +106,11 @@ class HandlerWrapper implements HandlerInterface { return $this->handler->getFormatter(); } + + public function reset() + { + if ($this->handler instanceof ResettableInterface) { + return $this->handler->reset(); + } + } } diff --git a/src/Monolog/Logger.php b/src/Monolog/Logger.php index 5034ead1..d998bb65 100644 --- a/src/Monolog/Logger.php +++ b/src/Monolog/Logger.php @@ -25,7 +25,7 @@ use Exception; * * @author Jordi Boggiano */ -class Logger implements LoggerInterface +class Logger implements LoggerInterface, ResettableInterface { /** * Detailed debug information @@ -354,6 +354,21 @@ class Logger implements LoggerInterface return true; } + public function reset() + { + foreach ($this->handlers as $handler) { + if ($handler instanceof ResettableInterface) { + $handler->reset(); + } + } + + foreach ($this->processors as $processor) { + if ($processor instanceof ResettableInterface) { + $processor->reset(); + } + } + } + /** * Adds a log record at the DEBUG level. * diff --git a/src/Monolog/Processor/UidProcessor.php b/src/Monolog/Processor/UidProcessor.php index 812707cd..47f13f00 100644 --- a/src/Monolog/Processor/UidProcessor.php +++ b/src/Monolog/Processor/UidProcessor.php @@ -11,12 +11,14 @@ namespace Monolog\Processor; +use Monolog\ResettableInterface; + /** * Adds a unique identifier into records * * @author Simon Mönch */ -class UidProcessor +class UidProcessor implements ResettableInterface { private $uid; @@ -26,7 +28,8 @@ class UidProcessor throw new \InvalidArgumentException('The uid length must be an integer between 1 and 32'); } - $this->uid = substr(hash('md5', uniqid('', true)), 0, $length); + + $this->uid = $this->generateUid($length); } public function __invoke(array $record) @@ -43,4 +46,14 @@ class UidProcessor { return $this->uid; } + + public function reset() + { + $this->uid = $this->generateUid(strlen($this->uid)); + } + + private function generateUid($length) + { + return substr(hash('md5', uniqid('', true)), 0, $length); + } } diff --git a/src/Monolog/ResettableInterface.php b/src/Monolog/ResettableInterface.php new file mode 100644 index 00000000..5e7bd6f3 --- /dev/null +++ b/src/Monolog/ResettableInterface.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +/** + * Handler or Processor implementing this interface will be reset when Logger::reset() is called. + * + * Resetting an Handler or a Processor usually means cleaning all buffers or + * resetting in its internal state. + * + * @author Grégoire Pineau + */ +interface ResettableInterface +{ + public function reset(); +} diff --git a/tests/Monolog/Handler/BrowserConsoleHandlerTest.php b/tests/Monolog/Handler/BrowserConsoleHandlerTest.php index ffb1d746..ffe45da2 100644 --- a/tests/Monolog/Handler/BrowserConsoleHandlerTest.php +++ b/tests/Monolog/Handler/BrowserConsoleHandlerTest.php @@ -21,7 +21,7 @@ class BrowserConsoleHandlerTest extends TestCase { protected function setUp() { - BrowserConsoleHandler::reset(); + BrowserConsoleHandler::resetStatic(); } protected function generateScript() diff --git a/tests/Monolog/Handler/ChromePHPHandlerTest.php b/tests/Monolog/Handler/ChromePHPHandlerTest.php index 0449f8b1..421cc491 100644 --- a/tests/Monolog/Handler/ChromePHPHandlerTest.php +++ b/tests/Monolog/Handler/ChromePHPHandlerTest.php @@ -21,7 +21,7 @@ class ChromePHPHandlerTest extends TestCase { protected function setUp() { - TestChromePHPHandler::reset(); + TestChromePHPHandler::resetStatic(); $_SERVER['HTTP_USER_AGENT'] = 'Monolog Test; Chrome/1.0'; } @@ -136,7 +136,7 @@ class TestChromePHPHandler extends ChromePHPHandler { protected $headers = array(); - public static function reset() + public static function resetStatic() { self::$initialized = false; self::$overflowed = false; diff --git a/tests/Monolog/Handler/FingersCrossedHandlerTest.php b/tests/Monolog/Handler/FingersCrossedHandlerTest.php index b92bf437..0ec36531 100644 --- a/tests/Monolog/Handler/FingersCrossedHandlerTest.php +++ b/tests/Monolog/Handler/FingersCrossedHandlerTest.php @@ -58,7 +58,7 @@ class FingersCrossedHandlerTest extends TestCase * @covers Monolog\Handler\FingersCrossedHandler::activate * @covers Monolog\Handler\FingersCrossedHandler::reset */ - public function testHandleRestartBufferingAfterReset() + public function testHandleResetBufferingAfterReset() { $test = new TestHandler(); $handler = new FingersCrossedHandler($test); @@ -76,7 +76,7 @@ class FingersCrossedHandlerTest extends TestCase * @covers Monolog\Handler\FingersCrossedHandler::handle * @covers Monolog\Handler\FingersCrossedHandler::activate */ - public function testHandleRestartBufferingAfterBeingTriggeredWhenStopBufferingIsDisabled() + public function testHandleResetBufferingAfterBeingTriggeredWhenStopBufferingIsDisabled() { $test = new TestHandler(); $handler = new FingersCrossedHandler($test, Logger::WARNING, 0, false, false); diff --git a/tests/Monolog/Handler/FirePHPHandlerTest.php b/tests/Monolog/Handler/FirePHPHandlerTest.php index 0eb10a63..7a404e66 100644 --- a/tests/Monolog/Handler/FirePHPHandlerTest.php +++ b/tests/Monolog/Handler/FirePHPHandlerTest.php @@ -21,7 +21,7 @@ class FirePHPHandlerTest extends TestCase { public function setUp() { - TestFirePHPHandler::reset(); + TestFirePHPHandler::resetStatic(); $_SERVER['HTTP_USER_AGENT'] = 'Monolog Test; FirePHP/1.0'; } @@ -77,7 +77,7 @@ class TestFirePHPHandler extends FirePHPHandler { protected $headers = array(); - public static function reset() + public static function resetStatic() { self::$initialized = false; self::$sendHeaders = true; diff --git a/tests/Monolog/LoggerTest.php b/tests/Monolog/LoggerTest.php index f938ea02..442e87de 100644 --- a/tests/Monolog/LoggerTest.php +++ b/tests/Monolog/LoggerTest.php @@ -614,4 +614,77 @@ class LoggerTest extends \PHPUnit_Framework_TestCase $logger->pushHandler($handler); $logger->info('test'); } + + public function testReset() + { + $logger = new Logger('app'); + + $testHandler = new Handler\TestHandler(); + $bufferHandler = new Handler\BufferHandler($testHandler); + $groupHandler = new Handler\GroupHandler(array($bufferHandler)); + $fingersCrossedHandler = new Handler\FingersCrossedHandler($groupHandler); + + $logger->pushHandler($fingersCrossedHandler); + + $processorUid1 = new Processor\UidProcessor(10); + $uid1 = $processorUid1->getUid(); + $groupHandler->pushProcessor($processorUid1); + + $processorUid2 = new Processor\UidProcessor(5); + $uid2 = $processorUid2->getUid(); + $logger->pushProcessor($processorUid2); + + $getProperty = function ($object, $property) { + $reflectionProperty = new \ReflectionProperty(get_class($object), $property); + $reflectionProperty->setAccessible(true); + + return $reflectionProperty->getValue($object); + }; + $that = $this; + $assertBufferOfBufferHandlerEmpty = function () use ($getProperty, $bufferHandler, $that) { + $that->assertEmpty($getProperty($bufferHandler, 'buffer')); + }; + $assertBuffersEmpty = function() use ($assertBufferOfBufferHandlerEmpty, $getProperty, $fingersCrossedHandler, $that) { + $assertBufferOfBufferHandlerEmpty(); + $that->assertEmpty($getProperty($fingersCrossedHandler, 'buffer')); + }; + + $logger->debug('debug'); + $logger->reset(); + $assertBuffersEmpty(); + $this->assertFalse($testHandler->hasDebugRecords()); + $this->assertFalse($testHandler->hasErrorRecords()); + $this->assertNotSame($uid1, $uid1 = $processorUid1->getUid()); + $this->assertNotSame($uid2, $uid2 = $processorUid2->getUid()); + + $logger->debug('debug'); + $logger->error('error'); + $logger->reset(); + $assertBuffersEmpty(); + $this->assertTrue($testHandler->hasDebugRecords()); + $this->assertTrue($testHandler->hasErrorRecords()); + $this->assertNotSame($uid1, $uid1 = $processorUid1->getUid()); + $this->assertNotSame($uid2, $uid2 = $processorUid2->getUid()); + + $logger->info('info'); + $this->assertNotEmpty($getProperty($fingersCrossedHandler, 'buffer')); + $assertBufferOfBufferHandlerEmpty(); + $this->assertFalse($testHandler->hasInfoRecords()); + + $logger->reset(); + $assertBuffersEmpty(); + $this->assertFalse($testHandler->hasInfoRecords()); + $this->assertNotSame($uid1, $uid1 = $processorUid1->getUid()); + $this->assertNotSame($uid2, $uid2 = $processorUid2->getUid()); + + $logger->notice('notice'); + $logger->emergency('emergency'); + $logger->reset(); + $assertBuffersEmpty(); + $this->assertFalse($testHandler->hasInfoRecords()); + $this->assertTrue($testHandler->hasNoticeRecords()); + $this->assertTrue($testHandler->hasEmergencyRecords()); + $this->assertNotSame($uid1, $processorUid1->getUid()); + $this->assertNotSame($uid2, $processorUid2->getUid()); + } } From bff2c8488e69dc7f2a6fa8f50ec02a5f2c07009e Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Thu, 9 Aug 2018 08:21:24 +0200 Subject: [PATCH 7/9] Update Raven client package URL --- src/Monolog/Handler/RavenHandler.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Monolog/Handler/RavenHandler.php b/src/Monolog/Handler/RavenHandler.php index 34ff0091..e52bd413 100644 --- a/src/Monolog/Handler/RavenHandler.php +++ b/src/Monolog/Handler/RavenHandler.php @@ -18,7 +18,7 @@ use Raven_Client; /** * Handler to send messages to a Sentry (https://github.com/getsentry/sentry) server - * using raven-php (https://github.com/getsentry/raven-php) + * using sentry-php (https://github.com/getsentry/sentry-php) * * @author Marc Abramowitz */ From 9117a6c747524c78bf0fff7eb5fb5e33a6a4f021 Mon Sep 17 00:00:00 2001 From: Nicolas Grekas Date: Fri, 17 Aug 2018 09:54:55 +0200 Subject: [PATCH 8/9] Fix displaying anonymous classes --- src/Monolog/ErrorHandler.php | 3 ++- src/Monolog/Formatter/JsonFormatter.php | 5 +++-- src/Monolog/Formatter/LineFormatter.php | 8 +++++--- src/Monolog/Formatter/MongoDBFormatter.php | 6 ++++-- src/Monolog/Formatter/NormalizerFormatter.php | 9 +++++---- src/Monolog/Processor/PsrLogMessageProcessor.php | 4 +++- src/Monolog/Registry.php | 10 ++++++++++ 7 files changed, 32 insertions(+), 13 deletions(-) diff --git a/src/Monolog/ErrorHandler.php b/src/Monolog/ErrorHandler.php index b3025738..7c7d48c2 100644 --- a/src/Monolog/ErrorHandler.php +++ b/src/Monolog/ErrorHandler.php @@ -14,6 +14,7 @@ namespace Monolog; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; use Monolog\Handler\AbstractHandler; +use Monolog\Registry; /** * Monolog error handler @@ -133,7 +134,7 @@ class ErrorHandler { $this->logger->log( $this->uncaughtExceptionLevel === null ? LogLevel::ERROR : $this->uncaughtExceptionLevel, - sprintf('Uncaught Exception %s: "%s" at %s line %s', get_class($e), $e->getMessage(), $e->getFile(), $e->getLine()), + sprintf('Uncaught Exception %s: "%s" at %s line %s', Registry::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), array('exception' => $e) ); diff --git a/src/Monolog/Formatter/JsonFormatter.php b/src/Monolog/Formatter/JsonFormatter.php index b8309b10..fcb08474 100644 --- a/src/Monolog/Formatter/JsonFormatter.php +++ b/src/Monolog/Formatter/JsonFormatter.php @@ -12,6 +12,7 @@ namespace Monolog\Formatter; use Exception; +use Monolog\Registry; use Throwable; /** @@ -179,11 +180,11 @@ class JsonFormatter extends NormalizerFormatter { // 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)); + throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Registry::getClass($e)); } $data = array( - 'class' => get_class($e), + 'class' => Registry::getClass($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile().':'.$e->getLine(), diff --git a/src/Monolog/Formatter/LineFormatter.php b/src/Monolog/Formatter/LineFormatter.php index d3e209e6..939cd043 100644 --- a/src/Monolog/Formatter/LineFormatter.php +++ b/src/Monolog/Formatter/LineFormatter.php @@ -11,6 +11,8 @@ namespace Monolog\Formatter; +use Monolog\Registry; + /** * Formats incoming records into a one-line string * @@ -129,17 +131,17 @@ class LineFormatter extends NormalizerFormatter { // 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)); + throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Registry::getClass($e)); } $previousText = ''; if ($previous = $e->getPrevious()) { do { - $previousText .= ', '.get_class($previous).'(code: '.$previous->getCode().'): '.$previous->getMessage().' at '.$previous->getFile().':'.$previous->getLine(); + $previousText .= ', '.Registry::getClass($previous).'(code: '.$previous->getCode().'): '.$previous->getMessage().' at '.$previous->getFile().':'.$previous->getLine(); } while ($previous = $previous->getPrevious()); } - $str = '[object] ('.get_class($e).'(code: '.$e->getCode().'): '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().$previousText.')'; + $str = '[object] ('.Registry::getClass($e).'(code: '.$e->getCode().'): '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().$previousText.')'; if ($this->includeStacktraces) { $str .= "\n[stacktrace]\n".$e->getTraceAsString()."\n"; } diff --git a/src/Monolog/Formatter/MongoDBFormatter.php b/src/Monolog/Formatter/MongoDBFormatter.php index eb067bb7..46944c53 100644 --- a/src/Monolog/Formatter/MongoDBFormatter.php +++ b/src/Monolog/Formatter/MongoDBFormatter.php @@ -11,6 +11,8 @@ namespace Monolog\Formatter; +use Monolog\Registry; + /** * Formats a record for use with the MongoDBHandler. * @@ -75,7 +77,7 @@ class MongoDBFormatter implements FormatterInterface protected function formatObject($value, $nestingLevel) { $objectVars = get_object_vars($value); - $objectVars['class'] = get_class($value); + $objectVars['class'] = Registry::getClass($value); return $this->formatArray($objectVars, $nestingLevel); } @@ -83,7 +85,7 @@ class MongoDBFormatter implements FormatterInterface protected function formatException(\Exception $exception, $nestingLevel) { $formattedException = array( - 'class' => get_class($exception), + 'class' => Registry::getClass($exception), 'message' => $exception->getMessage(), 'code' => $exception->getCode(), 'file' => $exception->getFile() . ':' . $exception->getLine(), diff --git a/src/Monolog/Formatter/NormalizerFormatter.php b/src/Monolog/Formatter/NormalizerFormatter.php index 0d96ed09..50b4f50e 100644 --- a/src/Monolog/Formatter/NormalizerFormatter.php +++ b/src/Monolog/Formatter/NormalizerFormatter.php @@ -12,6 +12,7 @@ namespace Monolog\Formatter; use Exception; +use Monolog\Registry; /** * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets @@ -108,7 +109,7 @@ class NormalizerFormatter implements FormatterInterface $value = $this->toJson($data, true); } - return sprintf("[object] (%s: %s)", get_class($data), $value); + return sprintf("[object] (%s: %s)", Registry::getClass($data), $value); } if (is_resource($data)) { @@ -122,11 +123,11 @@ class NormalizerFormatter implements FormatterInterface { // 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)); + throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Registry::getClass($e)); } $data = array( - 'class' => get_class($e), + 'class' => Registry::getClass($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile().':'.$e->getLine(), @@ -159,7 +160,7 @@ class NormalizerFormatter implements FormatterInterface // as a class name to avoid any unexpected leak of sensitive information $frame['args'] = array_map(function ($arg) { if (is_object($arg) && !($arg instanceof \DateTime || $arg instanceof \DateTimeInterface)) { - return sprintf("[object] (%s)", get_class($arg)); + return sprintf("[object] (%s)", Registry::getClass($arg)); } return $arg; diff --git a/src/Monolog/Processor/PsrLogMessageProcessor.php b/src/Monolog/Processor/PsrLogMessageProcessor.php index c2686ce5..f7df0074 100644 --- a/src/Monolog/Processor/PsrLogMessageProcessor.php +++ b/src/Monolog/Processor/PsrLogMessageProcessor.php @@ -11,6 +11,8 @@ namespace Monolog\Processor; +use Monolog\Registry; + /** * Processes a record's message according to PSR-3 rules * @@ -35,7 +37,7 @@ class PsrLogMessageProcessor if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { $replacements['{'.$key.'}'] = $val; } elseif (is_object($val)) { - $replacements['{'.$key.'}'] = '[object '.get_class($val).']'; + $replacements['{'.$key.'}'] = '[object '.Registry::getClass($val).']'; } else { $replacements['{'.$key.'}'] = '['.gettype($val).']'; } diff --git a/src/Monolog/Registry.php b/src/Monolog/Registry.php index 159b751c..b165ccd4 100644 --- a/src/Monolog/Registry.php +++ b/src/Monolog/Registry.php @@ -131,4 +131,14 @@ class Registry { return self::getInstance($name); } + + /** + * @internal + */ + public function getClass($object) + { + $class = \get_class($object); + + return 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class; + } } From 42d84e6a8d94e68ba52e15bbdd58dde1023609ad Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 4 Nov 2018 18:23:20 +0100 Subject: [PATCH 9/9] Move getClass method to a Utils class, refs #1190 --- src/Monolog/ErrorHandler.php | 2 +- src/Monolog/Formatter/JsonFormatter.php | 6 ++--- src/Monolog/Formatter/LineFormatter.php | 8 +++--- src/Monolog/Formatter/MongoDBFormatter.php | 6 ++--- src/Monolog/Formatter/NormalizerFormatter.php | 10 ++++---- .../Processor/PsrLogMessageProcessor.php | 4 +-- src/Monolog/Utils.php | 25 +++++++++++++++++++ 7 files changed, 43 insertions(+), 18 deletions(-) create mode 100644 src/Monolog/Utils.php diff --git a/src/Monolog/ErrorHandler.php b/src/Monolog/ErrorHandler.php index 7c7d48c2..adc55bdf 100644 --- a/src/Monolog/ErrorHandler.php +++ b/src/Monolog/ErrorHandler.php @@ -134,7 +134,7 @@ class ErrorHandler { $this->logger->log( $this->uncaughtExceptionLevel === null ? LogLevel::ERROR : $this->uncaughtExceptionLevel, - sprintf('Uncaught Exception %s: "%s" at %s line %s', Registry::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), + sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), array('exception' => $e) ); diff --git a/src/Monolog/Formatter/JsonFormatter.php b/src/Monolog/Formatter/JsonFormatter.php index fcb08474..9bd305f2 100644 --- a/src/Monolog/Formatter/JsonFormatter.php +++ b/src/Monolog/Formatter/JsonFormatter.php @@ -12,7 +12,7 @@ namespace Monolog\Formatter; use Exception; -use Monolog\Registry; +use Monolog\Utils; use Throwable; /** @@ -180,11 +180,11 @@ class JsonFormatter extends NormalizerFormatter { // TODO 2.0 only check for Throwable if (!$e instanceof Exception && !$e instanceof Throwable) { - throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Registry::getClass($e)); + throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e)); } $data = array( - 'class' => Registry::getClass($e), + 'class' => Utils::getClass($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile().':'.$e->getLine(), diff --git a/src/Monolog/Formatter/LineFormatter.php b/src/Monolog/Formatter/LineFormatter.php index 939cd043..f98e1a6f 100644 --- a/src/Monolog/Formatter/LineFormatter.php +++ b/src/Monolog/Formatter/LineFormatter.php @@ -11,7 +11,7 @@ namespace Monolog\Formatter; -use Monolog\Registry; +use Monolog\Utils; /** * Formats incoming records into a one-line string @@ -131,17 +131,17 @@ class LineFormatter extends NormalizerFormatter { // TODO 2.0 only check for Throwable if (!$e instanceof \Exception && !$e instanceof \Throwable) { - throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Registry::getClass($e)); + throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e)); } $previousText = ''; if ($previous = $e->getPrevious()) { do { - $previousText .= ', '.Registry::getClass($previous).'(code: '.$previous->getCode().'): '.$previous->getMessage().' at '.$previous->getFile().':'.$previous->getLine(); + $previousText .= ', '.Utils::getClass($previous).'(code: '.$previous->getCode().'): '.$previous->getMessage().' at '.$previous->getFile().':'.$previous->getLine(); } while ($previous = $previous->getPrevious()); } - $str = '[object] ('.Registry::getClass($e).'(code: '.$e->getCode().'): '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().$previousText.')'; + $str = '[object] ('.Utils::getClass($e).'(code: '.$e->getCode().'): '.$e->getMessage().' at '.$e->getFile().':'.$e->getLine().$previousText.')'; if ($this->includeStacktraces) { $str .= "\n[stacktrace]\n".$e->getTraceAsString()."\n"; } diff --git a/src/Monolog/Formatter/MongoDBFormatter.php b/src/Monolog/Formatter/MongoDBFormatter.php index 46944c53..eb7be849 100644 --- a/src/Monolog/Formatter/MongoDBFormatter.php +++ b/src/Monolog/Formatter/MongoDBFormatter.php @@ -11,7 +11,7 @@ namespace Monolog\Formatter; -use Monolog\Registry; +use Monolog\Utils; /** * Formats a record for use with the MongoDBHandler. @@ -77,7 +77,7 @@ class MongoDBFormatter implements FormatterInterface protected function formatObject($value, $nestingLevel) { $objectVars = get_object_vars($value); - $objectVars['class'] = Registry::getClass($value); + $objectVars['class'] = Utils::getClass($value); return $this->formatArray($objectVars, $nestingLevel); } @@ -85,7 +85,7 @@ class MongoDBFormatter implements FormatterInterface protected function formatException(\Exception $exception, $nestingLevel) { $formattedException = array( - 'class' => Registry::getClass($exception), + 'class' => Utils::getClass($exception), 'message' => $exception->getMessage(), 'code' => $exception->getCode(), 'file' => $exception->getFile() . ':' . $exception->getLine(), diff --git a/src/Monolog/Formatter/NormalizerFormatter.php b/src/Monolog/Formatter/NormalizerFormatter.php index 50b4f50e..66866578 100644 --- a/src/Monolog/Formatter/NormalizerFormatter.php +++ b/src/Monolog/Formatter/NormalizerFormatter.php @@ -12,7 +12,7 @@ namespace Monolog\Formatter; use Exception; -use Monolog\Registry; +use Monolog\Utils; /** * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets @@ -109,7 +109,7 @@ class NormalizerFormatter implements FormatterInterface $value = $this->toJson($data, true); } - return sprintf("[object] (%s: %s)", Registry::getClass($data), $value); + return sprintf("[object] (%s: %s)", Utils::getClass($data), $value); } if (is_resource($data)) { @@ -123,11 +123,11 @@ class NormalizerFormatter implements FormatterInterface { // TODO 2.0 only check for Throwable if (!$e instanceof Exception && !$e instanceof \Throwable) { - throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Registry::getClass($e)); + throw new \InvalidArgumentException('Exception/Throwable expected, got '.gettype($e).' / '.Utils::getClass($e)); } $data = array( - 'class' => Registry::getClass($e), + 'class' => Utils::getClass($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile().':'.$e->getLine(), @@ -160,7 +160,7 @@ class NormalizerFormatter implements FormatterInterface // as a class name to avoid any unexpected leak of sensitive information $frame['args'] = array_map(function ($arg) { if (is_object($arg) && !($arg instanceof \DateTime || $arg instanceof \DateTimeInterface)) { - return sprintf("[object] (%s)", Registry::getClass($arg)); + return sprintf("[object] (%s)", Utils::getClass($arg)); } return $arg; diff --git a/src/Monolog/Processor/PsrLogMessageProcessor.php b/src/Monolog/Processor/PsrLogMessageProcessor.php index 543c4584..00885054 100644 --- a/src/Monolog/Processor/PsrLogMessageProcessor.php +++ b/src/Monolog/Processor/PsrLogMessageProcessor.php @@ -11,7 +11,7 @@ namespace Monolog\Processor; -use Monolog\Registry; +use Monolog\Utils; /** * Processes a record's message according to PSR-3 rules @@ -37,7 +37,7 @@ class PsrLogMessageProcessor implements ProcessorInterface if (is_null($val) || is_scalar($val) || (is_object($val) && method_exists($val, "__toString"))) { $replacements['{'.$key.'}'] = $val; } elseif (is_object($val)) { - $replacements['{'.$key.'}'] = '[object '.Registry::getClass($val).']'; + $replacements['{'.$key.'}'] = '[object '.Utils::getClass($val).']'; } else { $replacements['{'.$key.'}'] = '['.gettype($val).']'; } diff --git a/src/Monolog/Utils.php b/src/Monolog/Utils.php new file mode 100644 index 00000000..df611a0d --- /dev/null +++ b/src/Monolog/Utils.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Monolog; + +class Utils +{ + /** + * @internal + */ + public function getClass($object) + { + $class = \get_class($object); + + return 'c' === $class[0] && 0 === strpos($class, "class@anonymous\0") ? get_parent_class($class).'@anonymous' : $class; + } +}