3 * @copyright Copyright (C) 2010-2021, the Friendica project
5 * @license GNU AGPL version 3 or any later version
7 * This program is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU Affero General Public License as
9 * published by the Free Software Foundation, either version 3 of the
10 * License, or (at your option) any later version.
12 * This program is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 * GNU Affero General Public License for more details.
17 * You should have received a copy of the GNU Affero General Public License
18 * along with this program. If not, see <https://www.gnu.org/licenses/>.
22 declare(strict_types=1);
24 namespace Friendica\Core\Logger\Handler;
26 use Psr\Log\LoggerInterface;
30 * A facility to enable logging of runtime errors, exceptions and fatal errors.
32 * Quick setup: <code>ErrorHandler::register($logger);</code>
36 /** @var LoggerInterface */
40 private $previousExceptionHandler = null;
41 /** @var array<class-string, LogLevel::*> an array of class name to LogLevel::* constant mapping */
42 private $uncaughtExceptionLevelMap = [];
44 /** @var callable|true|null */
45 private $previousErrorHandler = null;
46 /** @var array<int, LogLevel::*> an array of E_* constant to LogLevel::* constant mapping */
47 private $errorLevelMap = [];
49 private $handleOnlyReportedErrors = true;
52 private $hasFatalErrorHandler = false;
53 /** @var LogLevel::* */
54 private $fatalLevel = LogLevel::ALERT;
56 private $reservedMemory = null;
58 private $lastFatalTrace;
60 private static $fatalErrors = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
62 public function __construct(LoggerInterface $logger)
64 $this->logger = $logger;
68 * Registers a new ErrorHandler for a given Logger
70 * By default it will handle errors, exceptions and fatal errors
72 * @param LoggerInterface $logger
73 * @param array<int, LogLevel::*>|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling
74 * @param array<class-string, LogLevel::*>|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling
75 * @param LogLevel::*|null|false $fatalLevel a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling
76 * @return ErrorHandler
78 public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self
80 /** @phpstan-ignore-next-line */
81 $handler = new static($logger);
82 if ($errorLevelMap !== false) {
83 $handler->registerErrorHandler($errorLevelMap);
85 if ($exceptionLevelMap !== false) {
86 $handler->registerExceptionHandler($exceptionLevelMap);
88 if ($fatalLevel !== false) {
89 $handler->registerFatalHandler($fatalLevel);
95 public static function getClass(object $object): string
97 $class = \get_class($object);
99 if (false === ($pos = \strpos($class, "@anonymous\0"))) {
103 if (false === ($parent = \get_parent_class($class))) {
104 return \substr($class, 0, $pos + 10);
107 return $parent . '@anonymous';
111 * @param array<class-string, LogLevel::*> $levelMap an array of class name to LogLevel::* constant mapping
114 public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self
116 $prev = set_exception_handler(function (\Throwable $e): void {
117 $this->handleException($e);
119 $this->uncaughtExceptionLevelMap = $levelMap;
120 foreach ($this->defaultExceptionLevelMap() as $class => $level) {
121 if (!isset($this->uncaughtExceptionLevelMap[$class])) {
122 $this->uncaughtExceptionLevelMap[$class] = $level;
125 if ($callPrevious && $prev) {
126 $this->previousExceptionHandler = $prev;
133 * @param array<int, LogLevel::*> $levelMap an array of E_* constant to LogLevel::* constant mapping
136 public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self
138 $prev = set_error_handler([$this, 'handleError'], $errorTypes);
139 $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
141 $this->previousErrorHandler = $prev ?: true;
143 $this->previousErrorHandler = null;
146 $this->handleOnlyReportedErrors = $handleOnlyReportedErrors;
152 * @param LogLevel::*|null $level a LogLevel::* constant, null to use the default LogLevel::ALERT
153 * @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
155 public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self
157 register_shutdown_function([$this, 'handleFatalError']);
159 $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize);
160 $this->fatalLevel = null === $level ? LogLevel::ALERT : $level;
161 $this->hasFatalErrorHandler = true;
167 * @return array<class-string, LogLevel::*>
169 protected function defaultExceptionLevelMap(): array
172 'ParseError' => LogLevel::CRITICAL,
173 'Throwable' => LogLevel::ERROR,
178 * @return array<int, LogLevel::*>
180 protected function defaultErrorLevelMap(): array
183 E_ERROR => LogLevel::CRITICAL,
184 E_WARNING => LogLevel::WARNING,
185 E_PARSE => LogLevel::ALERT,
186 E_NOTICE => LogLevel::NOTICE,
187 E_CORE_ERROR => LogLevel::CRITICAL,
188 E_CORE_WARNING => LogLevel::WARNING,
189 E_COMPILE_ERROR => LogLevel::ALERT,
190 E_COMPILE_WARNING => LogLevel::WARNING,
191 E_USER_ERROR => LogLevel::ERROR,
192 E_USER_WARNING => LogLevel::WARNING,
193 E_USER_NOTICE => LogLevel::NOTICE,
194 E_STRICT => LogLevel::NOTICE,
195 E_RECOVERABLE_ERROR => LogLevel::ERROR,
196 E_DEPRECATED => LogLevel::NOTICE,
197 E_USER_DEPRECATED => LogLevel::NOTICE,
201 private function handleException(\Throwable $e): void
203 $level = LogLevel::ERROR;
204 foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) {
205 if ($e instanceof $class) {
213 sprintf('Uncaught Exception %s: "%s" at %s line %s', self::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
217 if ($this->previousExceptionHandler) {
218 ($this->previousExceptionHandler)($e);
221 if (!headers_sent() && !ini_get('display_errors')) {
222 http_response_code(500);
231 * @param mixed[] $context
233 public function handleError(int $code, string $message, string $file = '', int $line = 0, array $context = []): bool
235 if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) {
239 // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries
240 if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) {
241 $level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL;
242 $this->logger->log($level, self::codeToString($code).': '.$message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]);
244 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
245 array_shift($trace); // Exclude handleError from trace
246 $this->lastFatalTrace = $trace;
249 if ($this->previousErrorHandler === true) {
251 } elseif ($this->previousErrorHandler) {
252 return (bool) ($this->previousErrorHandler)($code, $message, $file, $line, $context);
261 public function handleFatalError(): void
263 $this->reservedMemory = '';
265 $lastError = error_get_last();
266 if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) {
269 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'],
270 ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace]
278 private static function codeToString($code): string
290 return 'E_CORE_ERROR';
292 return 'E_CORE_WARNING';
293 case E_COMPILE_ERROR:
294 return 'E_COMPILE_ERROR';
295 case E_COMPILE_WARNING:
296 return 'E_COMPILE_WARNING';
298 return 'E_USER_ERROR';
300 return 'E_USER_WARNING';
302 return 'E_USER_NOTICE';
305 case E_RECOVERABLE_ERROR:
306 return 'E_RECOVERABLE_ERROR';
308 return 'E_DEPRECATED';
309 case E_USER_DEPRECATED:
310 return 'E_USER_DEPRECATED';
313 return 'Unknown PHP error';