diff --git a/CHANGELOG.md b/CHANGELOG.md index 1d6770e9..e086a2c6 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/doc/03-utilities.md b/doc/03-utilities.md index c62aa416..fd3fd0e7 100644 --- a/doc/03-utilities.md +++ b/doc/03-utilities.md @@ -5,6 +5,8 @@ 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. +- _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 d32c25d5..317494a8 100644 --- a/src/Monolog/ErrorHandler.php +++ b/src/Monolog/ErrorHandler.php @@ -13,6 +13,7 @@ namespace Monolog; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; +use Monolog\Registry; /** * Monolog error handler @@ -163,7 +164,7 @@ class ErrorHandler $this->logger->log( $level, - 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', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), ['exception' => $e] ); diff --git a/src/Monolog/Formatter/JsonFormatter.php b/src/Monolog/Formatter/JsonFormatter.php index 3281be12..c61c90b4 100644 --- a/src/Monolog/Formatter/JsonFormatter.php +++ b/src/Monolog/Formatter/JsonFormatter.php @@ -11,6 +11,7 @@ namespace Monolog\Formatter; +use Monolog\Utils; use Throwable; /** @@ -166,7 +167,7 @@ class JsonFormatter extends NormalizerFormatter protected function normalizeException(Throwable $e, int $depth = 0): array { $data = [ - 'class' => get_class($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 48bb2c05..07f92f15 100644 --- a/src/Monolog/Formatter/LineFormatter.php +++ b/src/Monolog/Formatter/LineFormatter.php @@ -11,6 +11,8 @@ namespace Monolog\Formatter; +use Monolog\Utils; + /** * Formats incoming records into a one-line string * @@ -168,7 +170,7 @@ class LineFormatter extends NormalizerFormatter private function formatException(\Throwable $e): string { - $str = '[object] (' . get_class($e) . '(code: ' . $e->getCode() . '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . ')'; + $str = '[object] (' . Utils::getClass($e) . '(code: ' . $e->getCode() . '): ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine() . ')'; 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 8c40e3ec..d38c3ef1 100644 --- a/src/Monolog/Formatter/MongoDBFormatter.php +++ b/src/Monolog/Formatter/MongoDBFormatter.php @@ -12,6 +12,7 @@ namespace Monolog\Formatter; use MongoDB\BSON\UTCDateTime; +use Monolog\Utils; /** * Formats a record for use with the MongoDBHandler. @@ -83,7 +84,7 @@ class MongoDBFormatter implements FormatterInterface protected function formatObject($value, int $nestingLevel) { $objectVars = get_object_vars($value); - $objectVars['class'] = get_class($value); + $objectVars['class'] = Utils::getClass($value); return $this->formatArray($objectVars, $nestingLevel); } @@ -91,7 +92,7 @@ class MongoDBFormatter implements FormatterInterface protected function formatException(\Throwable $exception, int $nestingLevel) { $formattedException = [ - 'class' => get_class($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 c325865b..907efa96 100644 --- a/src/Monolog/Formatter/NormalizerFormatter.php +++ b/src/Monolog/Formatter/NormalizerFormatter.php @@ -13,6 +13,7 @@ namespace Monolog\Formatter; use Throwable; use Monolog\DateTimeImmutable; +use Monolog\Utils; /** * Normalizes incoming records to remove objects/resources so it's easier to dump to various targets @@ -146,7 +147,7 @@ class NormalizerFormatter implements FormatterInterface } } - return [get_class($data) => $value]; + return [Utils::getClass($data) => $value]; } if (is_resource($data)) { @@ -162,7 +163,7 @@ class NormalizerFormatter implements FormatterInterface protected function normalizeException(Throwable $e, int $depth = 0) { $data = [ - 'class' => get_class($e), + 'class' => Utils::getClass($e), 'message' => $e->getMessage(), 'code' => $e->getCode(), 'file' => $e->getFile().':'.$e->getLine(), @@ -195,7 +196,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 \DateTimeInterface) { - return sprintf("[object] (%s)", get_class($arg)); + return sprintf("[object] (%s)", Utils::getClass($arg)); } return $arg; diff --git a/src/Monolog/Handler/AbstractProcessingHandler.php b/src/Monolog/Handler/AbstractProcessingHandler.php index 474c4860..5e9e5837 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 * @@ -19,7 +21,7 @@ namespace Monolog\Handler; * @author Jordi Boggiano * @author Christophe Coevoet */ -abstract class AbstractProcessingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface +abstract class AbstractProcessingHandler extends AbstractHandler implements ProcessableHandlerInterface, FormattableHandlerInterface, ResettableInterface { use ProcessableHandlerTrait; use FormattableHandlerTrait; @@ -48,4 +50,15 @@ abstract class AbstractProcessingHandler extends AbstractHandler implements Proc * Writes the record down to the log of the implementing handler */ abstract protected function write(array $record): void; + + public function reset() + { + $this->close(); + + foreach ($this->processors as $processor) { + if ($processor instanceof ResettableInterface) { + $processor->reset(); + } + } + } } diff --git a/src/Monolog/Handler/BrowserConsoleHandler.php b/src/Monolog/Handler/BrowserConsoleHandler.php index de6f5b71..a39b0582 100755 --- a/src/Monolog/Handler/BrowserConsoleHandler.php +++ b/src/Monolog/Handler/BrowserConsoleHandler.php @@ -70,14 +70,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(): void + public static function resetStatic(): void { static::$records = []; } diff --git a/src/Monolog/Handler/BufferHandler.php b/src/Monolog/Handler/BufferHandler.php index a8a64394..3ac7f551 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 implements ProcessableHandlerInterfa $this->bufferSize = 0; $this->buffer = []; } + + 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 8e33f253..305396ff 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 Handler implements ProcessableHandlerInterfa */ 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 6201ad00..17090311 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 Handler implements ProcessableHandlerInterface } } + 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 8dc8a846..c44b7036 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, ProcessableHandlerInterface, FormattableHandlerInterface +class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, FormattableHandlerInterface, ResettableInterface { /** * @var HandlerInterface @@ -127,4 +128,11 @@ class HandlerWrapper implements HandlerInterface, ProcessableHandlerInterface, F throw new \LogicException('The wrapped handler does not implement ' . FormattableHandlerInterface::class); } + + public function reset() + { + if ($this->handler instanceof ResettableInterface) { + return $this->handler->reset(); + } + } } diff --git a/src/Monolog/Handler/RavenHandler.php b/src/Monolog/Handler/RavenHandler.php index 36f9bb82..204a540e 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 */ diff --git a/src/Monolog/Logger.php b/src/Monolog/Logger.php index c44959f6..eb98c08f 100644 --- a/src/Monolog/Logger.php +++ b/src/Monolog/Logger.php @@ -25,7 +25,7 @@ use Throwable; * * @author Jordi Boggiano */ -class Logger implements LoggerInterface +class Logger implements LoggerInterface, ResettableInterface { /** * Detailed debug information @@ -333,6 +333,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(); + } + } + } + /** * Gets all supported logging levels. * diff --git a/src/Monolog/Processor/PsrLogMessageProcessor.php b/src/Monolog/Processor/PsrLogMessageProcessor.php index e923ea36..9efff4e2 100644 --- a/src/Monolog/Processor/PsrLogMessageProcessor.php +++ b/src/Monolog/Processor/PsrLogMessageProcessor.php @@ -11,6 +11,8 @@ namespace Monolog\Processor; +use Monolog\Utils; + /** * Processes a record's message according to PSR-3 rules * @@ -66,7 +68,7 @@ class PsrLogMessageProcessor implements ProcessorInterface $replacements[$placeholder] = $val->format($this->dateFormat ?: static::SIMPLE_DATE); } } elseif (is_object($val)) { - $replacements[$placeholder] = '[object '.get_class($val).']'; + $replacements[$placeholder] = '[object '.Utils::getClass($val).']'; } elseif (is_array($val)) { $replacements[$placeholder] = 'array'.@json_encode($val); } else { diff --git a/src/Monolog/Processor/UidProcessor.php b/src/Monolog/Processor/UidProcessor.php index 19e11898..1089e690 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 implements ProcessorInterface +class UidProcessor implements ProcessorInterface, ResettableInterface { private $uid; @@ -26,7 +28,7 @@ class UidProcessor implements ProcessorInterface throw new \InvalidArgumentException('The uid length must be an integer between 1 and 32'); } - $this->uid = substr(bin2hex(random_bytes((int) ceil($length / 2))), 0, $length); + $this->uid = $this->generateUid($length); } public function __invoke(array $record): array @@ -43,4 +45,14 @@ class UidProcessor implements ProcessorInterface { return $this->uid; } + + public function reset() + { + $this->uid = $this->generateUid(strlen($this->uid)); + } + + private function generateUid($length) + { + return substr(bin2hex(random_bytes((int) ceil($length / 2))), 0, $length); + } } diff --git a/src/Monolog/Registry.php b/src/Monolog/Registry.php index 99a43440..2a662cee 100644 --- a/src/Monolog/Registry.php +++ b/src/Monolog/Registry.php @@ -130,4 +130,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; + } } diff --git a/src/Monolog/ResettableInterface.php b/src/Monolog/ResettableInterface.php new file mode 100644 index 00000000..375b0544 --- /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. This should also generally close() the handler. + * + * @author Grégoire Pineau + */ +interface ResettableInterface +{ + public function reset(); +} 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/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; + } +} diff --git a/tests/Monolog/Handler/BrowserConsoleHandlerTest.php b/tests/Monolog/Handler/BrowserConsoleHandlerTest.php index 048ee1c9..cbb216a1 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 7168ce49..a876b7dd 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 = []; - 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 5b25a365..ab22d345 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 07df2fe8..aceac54b 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 = []; - 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 69ea4010..7d66df3d 100644 --- a/tests/Monolog/LoggerTest.php +++ b/tests/Monolog/LoggerTest.php @@ -635,4 +635,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()); + } } 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), + ); + } + +}