]> git.mxchange.org Git - friendica.git/commitdiff
Add extended ErrorHandling
authorPhilipp <admin@philipp.info>
Sat, 23 Oct 2021 18:46:17 +0000 (20:46 +0200)
committerPhilipp <admin@philipp.info>
Sat, 23 Oct 2021 18:58:38 +0000 (20:58 +0200)
bin/auth_ejabberd.php
bin/console.php
bin/daemon.php
bin/worker.php
index.php
src/Core/Logger/Handler/ErrorHandler.php [new file with mode: 0644]

index faf302985a4cf7c737d65a4bf290493f11973060..88e5d034cb34d86101bf0ebe3bd1fbe0c011c831 100755 (executable)
@@ -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()) {
index 0684f240e2113b8fe782f69f4ff536faeaf049e7..35f0b5feefcf3820bfaca4739cca5eaee7e64e49 100755 (executable)
@@ -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();
index 4fa9f8bd3fb5e844f3810a6a9657d5ca88954333..7d4945fe0378771ce1e04406f5d2f6e78d1e3bfb 100755 (executable)
@@ -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()) {
index 46638a9ef3ad3b3a3bbe30d40ab9221e0a51c982..2fe03cb4b215320abab1a78f00ea748ba9c57b75 100755 (executable)
@@ -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);
index 2c18cb878c5504a82b2446f68e062b850428221b..011b9d7f90079533ae5ffa1a1fe7b54a1e842ade 100644 (file)
--- 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 (file)
index 0000000..1f2d6e1
--- /dev/null
@@ -0,0 +1,315 @@
+<?php
+/**
+ * @copyright Copyright (C) 2010-2021, the Friendica project
+ *
+ * @license GNU AGPL version 3 or any later version
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program.  If not, see <https://www.gnu.org/licenses/>.
+ *
+ */
+
+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: <code>ErrorHandler::register($logger);</code>
+ */
+class ErrorHandler
+{
+       /** @var LoggerInterface */
+       private $logger;
+
+       /** @var ?callable */
+       private $previousExceptionHandler = null;
+       /** @var array<class-string, LogLevel::*> an array of class name to LogLevel::* constant mapping */
+       private $uncaughtExceptionLevelMap = [];
+
+       /** @var callable|true|null */
+       private $previousErrorHandler = null;
+       /** @var array<int, LogLevel::*> 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<int, LogLevel::*>|false          $errorLevelMap     an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling
+        * @param  array<class-string, LogLevel::*>|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<class-string, LogLevel::*> $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<int, LogLevel::*> $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<class-string, LogLevel::*>
+        */
+       protected function defaultExceptionLevelMap(): array
+       {
+               return [
+                       'ParseError' => LogLevel::CRITICAL,
+                       'Throwable'  => LogLevel::ERROR,
+               ];
+       }
+
+       /**
+        * @return array<int, LogLevel::*>
+        */
+       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';
+       }
+}