From acb06af28df717ac5e58f7f42ce31dfe10d7dfef Mon Sep 17 00:00:00 2001 From: Philipp Date: Sat, 23 Oct 2021 20:46:17 +0200 Subject: [PATCH 1/2] Add extended ErrorHandling --- bin/auth_ejabberd.php | 2 +- bin/console.php | 2 + bin/daemon.php | 1 + bin/worker.php | 1 + index.php | 2 + src/Core/Logger/Handler/ErrorHandler.php | 315 +++++++++++++++++++++++ 6 files changed, 322 insertions(+), 1 deletion(-) create mode 100644 src/Core/Logger/Handler/ErrorHandler.php diff --git a/bin/auth_ejabberd.php b/bin/auth_ejabberd.php index faf302985a..88e5d034cb 100755 --- a/bin/auth_ejabberd.php +++ b/bin/auth_ejabberd.php @@ -81,7 +81,7 @@ $dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config $dice = $dice->addRule(LoggerInterface::class,['constructParams' => ['auth_ejabberd']]); \Friendica\DI::init($dice); - +\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); $appMode = $dice->create(Mode::class); if ($appMode->isNormal()) { diff --git a/bin/console.php b/bin/console.php index 0684f240e2..35f0b5feef 100755 --- a/bin/console.php +++ b/bin/console.php @@ -33,4 +33,6 @@ require dirname(__DIR__) . '/vendor/autoload.php'; $dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config.php'); $dice = $dice->addRule(LoggerInterface::class,['constructParams' => ['console']]); +\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); + (new Friendica\Core\Console($dice, $argv))->execute(); diff --git a/bin/daemon.php b/bin/daemon.php index 4fa9f8bd3f..7d4945fe03 100755 --- a/bin/daemon.php +++ b/bin/daemon.php @@ -60,6 +60,7 @@ $dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config $dice = $dice->addRule(LoggerInterface::class,['constructParams' => ['daemon']]); DI::init($dice); +\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); $a = DI::app(); if (DI::mode()->isInstall()) { diff --git a/bin/worker.php b/bin/worker.php index 46638a9ef3..2fe03cb4b2 100755 --- a/bin/worker.php +++ b/bin/worker.php @@ -57,6 +57,7 @@ $dice = (new Dice())->addRules(include __DIR__ . '/../static/dependencies.config $dice = $dice->addRule(LoggerInterface::class,['constructParams' => ['worker']]); DI::init($dice); +\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); $a = DI::app(); DI::mode()->setExecutor(Mode::WORKER); diff --git a/index.php b/index.php index 2c18cb878c..011b9d7f90 100644 --- a/index.php +++ b/index.php @@ -34,6 +34,8 @@ $dice = $dice->addRule(Friendica\App\Mode::class, ['call' => [['determineRunMode \Friendica\DI::init($dice); +\Friendica\Core\Logger\Handler\ErrorHandler::register($dice->create(\Psr\Log\LoggerInterface::class)); + $a = \Friendica\DI::app(); \Friendica\DI::mode()->setExecutor(\Friendica\App\Mode::INDEX); diff --git a/src/Core/Logger/Handler/ErrorHandler.php b/src/Core/Logger/Handler/ErrorHandler.php new file mode 100644 index 0000000000..1f2d6e1644 --- /dev/null +++ b/src/Core/Logger/Handler/ErrorHandler.php @@ -0,0 +1,315 @@ +. + * + */ + +declare(strict_types=1); + +namespace Friendica\Core\Logger\Handler; + +use Psr\Log\LoggerInterface; +use Psr\Log\LogLevel; + +/** + * A facility to enable logging of runtime errors, exceptions and fatal errors. + * + * Quick setup: ErrorHandler::register($logger); + */ +class ErrorHandler +{ + /** @var LoggerInterface */ + private $logger; + + /** @var ?callable */ + private $previousExceptionHandler = null; + /** @var array an array of class name to LogLevel::* constant mapping */ + private $uncaughtExceptionLevelMap = []; + + /** @var callable|true|null */ + private $previousErrorHandler = null; + /** @var array an array of E_* constant to LogLevel::* constant mapping */ + private $errorLevelMap = []; + /** @var bool */ + private $handleOnlyReportedErrors = true; + + /** @var bool */ + private $hasFatalErrorHandler = false; + /** @var LogLevel::* */ + private $fatalLevel = LogLevel::ALERT; + /** @var ?string */ + private $reservedMemory = null; + /** @var ?mixed */ + private $lastFatalTrace; + /** @var int[] */ + private static $fatalErrors = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR]; + + public function __construct(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * Registers a new ErrorHandler for a given Logger + * + * By default it will handle errors, exceptions and fatal errors + * + * @param LoggerInterface $logger + * @param array|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling + * @param array|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling + * @param LogLevel::*|null|false $fatalLevel a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling + * @return ErrorHandler + */ + public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self + { + /** @phpstan-ignore-next-line */ + $handler = new static($logger); + if ($errorLevelMap !== false) { + $handler->registerErrorHandler($errorLevelMap); + } + if ($exceptionLevelMap !== false) { + $handler->registerExceptionHandler($exceptionLevelMap); + } + if ($fatalLevel !== false) { + $handler->registerFatalHandler($fatalLevel); + } + + return $handler; + } + + public static function getClass(object $object): string + { + $class = \get_class($object); + + if (false === ($pos = \strpos($class, "@anonymous\0"))) { + return $class; + } + + if (false === ($parent = \get_parent_class($class))) { + return \substr($class, 0, $pos + 10); + } + + return $parent . '@anonymous'; + } + + /** + * @param array $levelMap an array of class name to LogLevel::* constant mapping + * @return $this + */ + public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self + { + $prev = set_exception_handler(function (\Throwable $e): void { + $this->handleException($e); + }); + $this->uncaughtExceptionLevelMap = $levelMap; + foreach ($this->defaultExceptionLevelMap() as $class => $level) { + if (!isset($this->uncaughtExceptionLevelMap[$class])) { + $this->uncaughtExceptionLevelMap[$class] = $level; + } + } + if ($callPrevious && $prev) { + $this->previousExceptionHandler = $prev; + } + + return $this; + } + + /** + * @param array $levelMap an array of E_* constant to LogLevel::* constant mapping + * @return $this + */ + public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self + { + $prev = set_error_handler([$this, 'handleError'], $errorTypes); + $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap); + if ($callPrevious) { + $this->previousErrorHandler = $prev ?: true; + } else { + $this->previousErrorHandler = null; + } + + $this->handleOnlyReportedErrors = $handleOnlyReportedErrors; + + return $this; + } + + /** + * @param LogLevel::*|null $level a LogLevel::* constant, null to use the default LogLevel::ALERT + * @param int $reservedMemorySize Amount of KBs to reserve in memory so that it can be freed when handling fatal errors giving Monolog some room in memory to get its job done + */ + public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self + { + register_shutdown_function([$this, 'handleFatalError']); + + $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize); + $this->fatalLevel = null === $level ? LogLevel::ALERT : $level; + $this->hasFatalErrorHandler = true; + + return $this; + } + + /** + * @return array + */ + protected function defaultExceptionLevelMap(): array + { + return [ + 'ParseError' => LogLevel::CRITICAL, + 'Throwable' => LogLevel::ERROR, + ]; + } + + /** + * @return array + */ + protected function defaultErrorLevelMap(): array + { + return [ + E_ERROR => LogLevel::CRITICAL, + E_WARNING => LogLevel::WARNING, + E_PARSE => LogLevel::ALERT, + E_NOTICE => LogLevel::NOTICE, + E_CORE_ERROR => LogLevel::CRITICAL, + E_CORE_WARNING => LogLevel::WARNING, + E_COMPILE_ERROR => LogLevel::ALERT, + E_COMPILE_WARNING => LogLevel::WARNING, + E_USER_ERROR => LogLevel::ERROR, + E_USER_WARNING => LogLevel::WARNING, + E_USER_NOTICE => LogLevel::NOTICE, + E_STRICT => LogLevel::NOTICE, + E_RECOVERABLE_ERROR => LogLevel::ERROR, + E_DEPRECATED => LogLevel::NOTICE, + E_USER_DEPRECATED => LogLevel::NOTICE, + ]; + } + + private function handleException(\Throwable $e): void + { + $level = LogLevel::ERROR; + foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) { + if ($e instanceof $class) { + $level = $candidate; + break; + } + } + + $this->logger->log( + $level, + sprintf('Uncaught Exception %s: "%s" at %s line %s', self::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()), + ['exception' => $e] + ); + + if ($this->previousExceptionHandler) { + ($this->previousExceptionHandler)($e); + } + + if (!headers_sent() && !ini_get('display_errors')) { + http_response_code(500); + } + + exit(255); + } + + /** + * @private + * + * @param mixed[] $context + */ + public function handleError(int $code, string $message, string $file = '', int $line = 0, array $context = []): bool + { + if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) { + return false; + } + + // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries + if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) { + $level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL; + $this->logger->log($level, self::codeToString($code).': '.$message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]); + } else { + $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); + array_shift($trace); // Exclude handleError from trace + $this->lastFatalTrace = $trace; + } + + if ($this->previousErrorHandler === true) { + return false; + } elseif ($this->previousErrorHandler) { + return (bool) ($this->previousErrorHandler)($code, $message, $file, $line, $context); + } + + return true; + } + + /** + * @private + */ + public function handleFatalError(): void + { + $this->reservedMemory = ''; + + $lastError = error_get_last(); + if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) { + $this->logger->log( + $this->fatalLevel, + 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'], + ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace] + ); + } + } + + /** + * @param int $code + */ + private static function codeToString($code): string + { + switch ($code) { + case E_ERROR: + return 'E_ERROR'; + case E_WARNING: + return 'E_WARNING'; + case E_PARSE: + return 'E_PARSE'; + case E_NOTICE: + return 'E_NOTICE'; + case E_CORE_ERROR: + return 'E_CORE_ERROR'; + case E_CORE_WARNING: + return 'E_CORE_WARNING'; + case E_COMPILE_ERROR: + return 'E_COMPILE_ERROR'; + case E_COMPILE_WARNING: + return 'E_COMPILE_WARNING'; + case E_USER_ERROR: + return 'E_USER_ERROR'; + case E_USER_WARNING: + return 'E_USER_WARNING'; + case E_USER_NOTICE: + return 'E_USER_NOTICE'; + case E_STRICT: + return 'E_STRICT'; + case E_RECOVERABLE_ERROR: + return 'E_RECOVERABLE_ERROR'; + case E_DEPRECATED: + return 'E_DEPRECATED'; + case E_USER_DEPRECATED: + return 'E_USER_DEPRECATED'; + } + + return 'Unknown PHP error'; + } +} From 8f688b2a8961ffbb6fbd5a5c317a733fa18883f3 Mon Sep 17 00:00:00 2001 From: Philipp Date: Sun, 24 Oct 2021 19:44:38 +0200 Subject: [PATCH 2/2] Update ErrorHandler --- src/Core/Logger/Handler/ErrorHandler.php | 46 ++++++++++++++++++++---- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/Core/Logger/Handler/ErrorHandler.php b/src/Core/Logger/Handler/ErrorHandler.php index 1f2d6e1644..c4e85fe6cc 100644 --- a/src/Core/Logger/Handler/ErrorHandler.php +++ b/src/Core/Logger/Handler/ErrorHandler.php @@ -25,6 +25,7 @@ namespace Friendica\Core\Logger\Handler; use Psr\Log\LoggerInterface; use Psr\Log\LogLevel; +use Throwable; /** * A facility to enable logging of runtime errors, exceptions and fatal errors. @@ -73,6 +74,7 @@ class ErrorHandler * @param array|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling * @param array|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling * @param LogLevel::*|null|false $fatalLevel a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling + * * @return ErrorHandler */ public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self @@ -92,6 +94,13 @@ class ErrorHandler return $handler; } + /** + * Stringify the class of the given object for logging purpose + * + * @param object $object An object to retrieve the class + * + * @return string the classname of the object + */ public static function getClass(object $object): string { $class = \get_class($object); @@ -108,12 +117,14 @@ class ErrorHandler } /** - * @param array $levelMap an array of class name to LogLevel::* constant mapping + * @param array $levelMap an array of class name to LogLevel::* constant mapping + * @param bool $callPrevious Set to true, if a previously defined exception handler should be called after handling this exception + * * @return $this */ public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self { - $prev = set_exception_handler(function (\Throwable $e): void { + $prev = set_exception_handler(function (Throwable $e): void { $this->handleException($e); }); $this->uncaughtExceptionLevelMap = $levelMap; @@ -130,7 +141,11 @@ class ErrorHandler } /** - * @param array $levelMap an array of E_* constant to LogLevel::* constant mapping + * @param array $levelMap an array of E_* constant to LogLevel::* constant mapping + * @param bool $callPrevious Set to true, if a previously defined exception handler should be called after handling this exception + * @param int $errorTypes a Mask for masking the errortypes, which should be handled by this error handler + * @param bool $handleOnlyReportedErrors Set to true, only errors set per error_reporting() will be logged + * * @return $this */ public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self @@ -151,6 +166,8 @@ class ErrorHandler /** * @param LogLevel::*|null $level a LogLevel::* constant, null to use the default LogLevel::ALERT * @param int $reservedMemorySize Amount of KBs to reserve in memory so that it can be freed when handling fatal errors giving Monolog some room in memory to get its job done + * + * @return $this */ public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self { @@ -198,7 +215,12 @@ class ErrorHandler ]; } - private function handleException(\Throwable $e): void + /** + * The Exception handler + * + * @param Throwable $e The Exception to handle + */ + private function handleException(Throwable $e): void { $level = LogLevel::ERROR; foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) { @@ -226,11 +248,19 @@ class ErrorHandler } /** + * The Error handler + * * @private * - * @param mixed[] $context + * @param int $code The PHP error code + * @param string $message The error message + * @param string $file If possible, set the file at which the failure occurred + * @param int $line + * @param array|null $context If possible, add a context to the error for better analysis + * + * @return bool */ - public function handleError(int $code, string $message, string $file = '', int $line = 0, array $context = []): bool + public function handleError(int $code, string $message, string $file = '', int $line = 0, ?array $context = []): bool { if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) { return false; @@ -273,7 +303,9 @@ class ErrorHandler } /** - * @param int $code + * @param mixed $code + * + * @return string */ private static function codeToString($code): string {