diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 00000000..9007f6ba --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.{yml,yaml}] +indent_size = 2 diff --git a/.gitattributes b/.gitattributes index a1e258de..f6044225 100644 --- a/.gitattributes +++ b/.gitattributes @@ -5,3 +5,4 @@ /phpunit.xml.dist export-ignore /_config.yml export-ignore /UPGRADE.md export-ignore +/.editorconfig export-ignore diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 9661ca20..5aeecbba 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -10,11 +10,6 @@ parameters: count: 1 path: src/Monolog/Formatter/JsonFormatter.php - - - message: "#^Cannot access offset 'table' on array\\\\|bool\\|float\\|int\\|object\\|string\\.$#" - count: 1 - path: src/Monolog/Formatter/WildfireFormatter.php - - message: "#^Return type \\(array\\\\|bool\\|float\\|int\\|object\\|string\\|null\\) of method Monolog\\\\Formatter\\\\WildfireFormatter\\:\\:normalize\\(\\) should be covariant with return type \\(array\\\\|bool\\|float\\|int\\|string\\|null\\) of method Monolog\\\\Formatter\\\\NormalizerFormatter\\:\\:normalize\\(\\)$#" count: 1 @@ -80,16 +75,21 @@ parameters: count: 1 path: src/Monolog/Handler/SamplingHandler.php - - - message: "#^Match expression does not handle remaining value\\: 'EMERGENCY'$#" - count: 1 - path: src/Monolog/Level.php - - message: "#^Variable property access on \\$this\\(Monolog\\\\LogRecord\\)\\.$#" count: 4 path: src/Monolog/LogRecord.php + - + message: "#^Cannot assign offset Fiber to WeakMap\\, int\\>\\.$#" + count: 1 + path: src/Monolog/Logger.php + + - + message: "#^Only numeric types are allowed in post\\-decrement, int\\|null given\\.$#" + count: 1 + path: src/Monolog/Logger.php + - message: "#^Parameter \\#1 \\$level \\('alert'\\|'critical'\\|'debug'\\|'emergency'\\|'error'\\|'info'\\|'notice'\\|'warning'\\|Monolog\\\\Level\\) of method Monolog\\\\Logger\\:\\:log\\(\\) should be contravariant with parameter \\$level \\(mixed\\) of method Psr\\\\Log\\\\LoggerInterface\\:\\:log\\(\\)$#" count: 1 @@ -105,11 +105,6 @@ parameters: count: 1 path: src/Monolog/Processor/UidProcessor.php - - - message: "#^Method Monolog\\\\Processor\\\\UidProcessor\\:\\:generateUid\\(\\) should return non\\-empty\\-string but returns string\\.$#" - count: 1 - path: src/Monolog/Processor/UidProcessor.php - - message: "#^Parameter \\#1 \\$length of function random_bytes expects int\\<1, max\\>, int given\\.$#" count: 1 @@ -119,4 +114,3 @@ parameters: message: "#^Parameter \\#2 \\$callback of function preg_replace_callback expects callable\\(array\\\\)\\: string, Closure\\(mixed\\)\\: array\\\\|string\\|false given\\.$#" count: 1 path: src/Monolog/Utils.php - diff --git a/src/Monolog/Handler/AmqpHandler.php b/src/Monolog/Handler/AmqpHandler.php index b25e4f13..72265d4b 100644 --- a/src/Monolog/Handler/AmqpHandler.php +++ b/src/Monolog/Handler/AmqpHandler.php @@ -139,13 +139,14 @@ class AmqpHandler extends AbstractProcessingHandler private function createAmqpMessage(string $data): AMQPMessage { - return new AMQPMessage( - $data, - [ - 'delivery_mode' => 2, - 'content_type' => 'application/json', - ] - ); + $attributes = [ + 'delivery_mode' => 2, + 'content_type' => 'application/json', + ]; + if (\count($this->extraAttributes) > 0) { + $attributes = array_merge($attributes, $this->extraAttributes); + } + return new AMQPMessage($data, $attributes); } /** diff --git a/src/Monolog/Handler/RotatingFileHandler.php b/src/Monolog/Handler/RotatingFileHandler.php index 12ce6923..9d57af99 100644 --- a/src/Monolog/Handler/RotatingFileHandler.php +++ b/src/Monolog/Handler/RotatingFileHandler.php @@ -170,7 +170,7 @@ class RotatingFileHandler extends StreamHandler $timedFilename = str_replace( ['{filename}', '{date}'], [$fileInfo['filename'], date($this->dateFormat)], - $fileInfo['dirname'] . '/' . $this->filenameFormat + ($fileInfo['dirname'] ?? '') . '/' . $this->filenameFormat ); if (isset($fileInfo['extension'])) { @@ -190,7 +190,7 @@ class RotatingFileHandler extends StreamHandler ['[0-9][0-9][0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]', '[0-9][0-9]'], $this->dateFormat) ], - $fileInfo['dirname'] . '/' . $this->filenameFormat + ($fileInfo['dirname'] ?? '') . '/' . $this->filenameFormat ); if (isset($fileInfo['extension'])) { $glob .= '.'.$fileInfo['extension']; diff --git a/src/Monolog/Logger.php b/src/Monolog/Logger.php index 5aacc6f8..303b11fc 100644 --- a/src/Monolog/Logger.php +++ b/src/Monolog/Logger.php @@ -13,6 +13,7 @@ namespace Monolog; use Closure; use DateTimeZone; +use Fiber; use Monolog\Handler\HandlerInterface; use Monolog\Processor\ProcessorInterface; use Psr\Log\LoggerInterface; @@ -20,6 +21,7 @@ use Psr\Log\InvalidArgumentException; use Psr\Log\LogLevel; use Throwable; use Stringable; +use WeakMap; /** * Monolog log channel @@ -151,9 +153,13 @@ class Logger implements LoggerInterface, ResettableInterface */ private int $logDepth = 0; + /** + * @var WeakMap, int> Keeps track of depth inside fibers to prevent infinite logging loops + */ + private WeakMap $fiberLogDepth; + /** * Whether to detect infinite logging loops - * * This can be disabled via {@see useLoggingLoopDetection} if you have async handlers that do not play well with this */ private bool $detectCycles = true; @@ -172,6 +178,7 @@ class Logger implements LoggerInterface, ResettableInterface $this->setHandlers($handlers); $this->processors = $processors; $this->timezone = $timezone ?? new DateTimeZone(date_default_timezone_get()); + $this->fiberLogDepth = new \WeakMap(); } public function getName(): string @@ -318,12 +325,19 @@ class Logger implements LoggerInterface, ResettableInterface } if ($this->detectCycles) { - $this->logDepth += 1; + if (null !== ($fiber = Fiber::getCurrent())) { + $logDepth = $this->fiberLogDepth[$fiber] = ($this->fiberLogDepth[$fiber] ?? 0) + 1; + } else { + $logDepth = ++$this->logDepth; + } + } else { + $logDepth = 0; } - if ($this->logDepth === 3) { + + if ($logDepth === 3) { $this->warning('A possible infinite logging loop was detected and aborted. It appears some of your handler code is triggering logging, see the previous log record for a hint as to what may be the cause.'); return false; - } elseif ($this->logDepth >= 5) { // log depth 4 is let through so we can log the warning above + } elseif ($logDepth >= 5) { // log depth 4 is let through, so we can log the warning above return false; } @@ -375,7 +389,11 @@ class Logger implements LoggerInterface, ResettableInterface return $handled; } finally { if ($this->detectCycles) { - $this->logDepth--; + if (isset($fiber)) { + $this->fiberLogDepth[$fiber]--; + } else { + $this->logDepth--; + } } } } diff --git a/src/Monolog/Processor/PsrLogMessageProcessor.php b/src/Monolog/Processor/PsrLogMessageProcessor.php index f2407d56..aad2aad2 100644 --- a/src/Monolog/Processor/PsrLogMessageProcessor.php +++ b/src/Monolog/Processor/PsrLogMessageProcessor.php @@ -67,6 +67,8 @@ class PsrLogMessageProcessor implements ProcessorInterface } else { $replacements[$placeholder] = $val->format($this->dateFormat ?? static::SIMPLE_DATE); } + } elseif ($val instanceof \UnitEnum) { + $replacements[$placeholder] = $val instanceof \BackedEnum ? $val->value : $val->name; } elseif (is_object($val)) { $replacements[$placeholder] = '[object '.Utils::getClass($val).']'; } elseif (is_array($val)) { diff --git a/src/Monolog/SignalHandler.php b/src/Monolog/SignalHandler.php index e6b02b08..b930ca43 100644 --- a/src/Monolog/SignalHandler.php +++ b/src/Monolog/SignalHandler.php @@ -73,9 +73,10 @@ class SignalHandler */ public function handleSignal(int $signo, $siginfo = null): void { + /** @var array $signals */ static $signals = []; - if (!$signals && extension_loaded('pcntl')) { + if (\count($signals) === 0 && extension_loaded('pcntl')) { $pcntl = new ReflectionExtension('pcntl'); foreach ($pcntl->getConstants() as $name => $value) { if (substr($name, 0, 3) === 'SIG' && $name[3] !== '_' && is_int($value)) { diff --git a/tests/Monolog/LoggerTest.php b/tests/Monolog/LoggerTest.php index 697518e2..e5e90f64 100644 --- a/tests/Monolog/LoggerTest.php +++ b/tests/Monolog/LoggerTest.php @@ -793,6 +793,58 @@ class LoggerTest extends TestCase $this->assertEquals($datetime->format('Y-m-d H:i:s'), $record->datetime->format('Y-m-d H:i:s')); } } + + /** + * @requires PHP 8.1 + */ + public function testLogCycleDetectionWithFibersWithoutCycle() + { + $logger = new Logger(__METHOD__); + + $fiberSuspendHandler = new FiberSuspendHandler(); + $testHandler = new TestHandler(); + + $logger->pushHandler($fiberSuspendHandler); + $logger->pushHandler($testHandler); + + $fibers = []; + for ($i = 0; $i < 10; $i++) { + $fiber = new \Fiber(static function () use ($logger) { + $logger->info('test'); + }); + + $fiber->start(); + + // We need to keep a reference here, because otherwise the fiber gets automatically cleaned up + $fibers[] = $fiber; + } + + self::assertCount(10, $testHandler->getRecords()); + } + + /** + * @requires PHP 8.1 + */ + public function testLogCycleDetectionWithFibersWithCycle() + { + $logger = new Logger(__METHOD__); + + $fiberSuspendHandler = new FiberSuspendHandler(); + $loggingHandler = new LoggingHandler($logger); + $testHandler = new TestHandler(); + + $logger->pushHandler($fiberSuspendHandler); + $logger->pushHandler($loggingHandler); + $logger->pushHandler($testHandler); + + $fiber = new \Fiber(static function () use ($logger) { + $logger->info('test'); + }); + + $fiber->start(); + + self::assertCount(3, $testHandler->getRecords()); + } } class LoggingHandler implements HandlerInterface @@ -827,3 +879,27 @@ class LoggingHandler implements HandlerInterface { } } + + +class FiberSuspendHandler implements HandlerInterface +{ + public function isHandling(LogRecord $record): bool + { + return true; + } + + public function handle(LogRecord $record): bool + { + \Fiber::suspend(); + + return true; + } + + public function handleBatch(array $records): void + { + } + + public function close(): void + { + } +} diff --git a/tests/Monolog/Processor/PsrLogMessageProcessorTest.php b/tests/Monolog/Processor/PsrLogMessageProcessorTest.php index e44d13c6..ab1ce52f 100644 --- a/tests/Monolog/Processor/PsrLogMessageProcessorTest.php +++ b/tests/Monolog/Processor/PsrLogMessageProcessorTest.php @@ -11,6 +11,7 @@ namespace Monolog\Processor; +use Monolog\Level; use Monolog\Test\TestCase; class PsrLogMessageProcessorTest extends TestCase @@ -66,6 +67,7 @@ class PsrLogMessageProcessorTest extends TestCase [[1, 2, 3], 'array[1,2,3]'], [['foo' => 'bar'], 'array{"foo":"bar"}'], [stream_context_create(), '[resource]'], + [Level::Info, Level::Info->value], ]; } }