3 * @copyright Copyright (C) 2010-2023, 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;
31 * A facility to enable logging of runtime errors, exceptions and fatal errors.
33 * Quick setup: <code>ErrorHandler::register($logger);</code>
37 /** @var LoggerInterface */
41 private $previousExceptionHandler = null;
42 /** @var array<class-string, LogLevel::*> an array of class name to LogLevel::* constant mapping */
43 private $uncaughtExceptionLevelMap = [];
45 /** @var callable|true|null */
46 private $previousErrorHandler = null;
47 /** @var array<int, LogLevel::*> an array of E_* constant to LogLevel::* constant mapping */
48 private $errorLevelMap = [];
50 private $handleOnlyReportedErrors = true;
53 private $hasFatalErrorHandler = false;
54 /** @var LogLevel::* */
55 private $fatalLevel = LogLevel::ALERT;
57 private $reservedMemory = null;
59 private $lastFatalTrace;
61 private static $fatalErrors = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
63 public function __construct(LoggerInterface $logger)
65 $this->logger = $logger;
69 * Registers a new ErrorHandler for a given Logger
71 * By default it will handle errors, exceptions and fatal errors
73 * @param LoggerInterface $logger
74 * @param array<int, LogLevel::*>|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling
75 * @param array<class-string, LogLevel::*>|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling
76 * @param LogLevel::*|null|false $fatalLevel a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling
78 * @return ErrorHandler
80 public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self
82 /** @phpstan-ignore-next-line */
83 $handler = new static($logger);
84 if ($errorLevelMap !== false) {
85 $handler->registerErrorHandler($errorLevelMap);
87 if ($exceptionLevelMap !== false) {
88 $handler->registerExceptionHandler($exceptionLevelMap);
90 if ($fatalLevel !== false) {
91 $handler->registerFatalHandler($fatalLevel);
98 * Stringify the class of the given object for logging purpose
100 * @param object $object An object to retrieve the class
102 * @return string the classname of the object
104 public static function getClass(object $object): string
106 $class = \get_class($object);
108 if (false === ($pos = \strpos($class, "@anonymous\0"))) {
112 if (false === ($parent = \get_parent_class($class))) {
113 return \substr($class, 0, $pos + 10);
116 return $parent . '@anonymous';
120 * @param array<class-string, LogLevel::*> $levelMap an array of class name to LogLevel::* constant mapping
121 * @param bool $callPrevious Set to true, if a previously defined exception handler should be called after handling this exception
125 public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self
127 $prev = set_exception_handler(function (Throwable $e): void {
128 $this->handleException($e);
130 $this->uncaughtExceptionLevelMap = $levelMap;
131 foreach ($this->defaultExceptionLevelMap() as $class => $level) {
132 if (!isset($this->uncaughtExceptionLevelMap[$class])) {
133 $this->uncaughtExceptionLevelMap[$class] = $level;
136 if ($callPrevious && $prev) {
137 $this->previousExceptionHandler = $prev;
144 * @param array<int, LogLevel::*> $levelMap an array of E_* constant to LogLevel::* constant mapping
145 * @param bool $callPrevious Set to true, if a previously defined exception handler should be called after handling this exception
146 * @param int $errorTypes a Mask for masking the errortypes, which should be handled by this error handler
147 * @param bool $handleOnlyReportedErrors Set to true, only errors set per error_reporting() will be logged
151 public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self
153 $prev = set_error_handler([$this, 'handleError'], $errorTypes);
154 $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
156 $this->previousErrorHandler = $prev ?: true;
158 $this->previousErrorHandler = null;
161 $this->handleOnlyReportedErrors = $handleOnlyReportedErrors;
167 * @param LogLevel::*|null $level a LogLevel::* constant, null to use the default LogLevel::ALERT
168 * @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
172 public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self
174 register_shutdown_function([$this, 'handleFatalError']);
176 $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize);
177 $this->fatalLevel = null === $level ? LogLevel::ALERT : $level;
178 $this->hasFatalErrorHandler = true;
184 * @return array<class-string, LogLevel::*>
186 protected function defaultExceptionLevelMap(): array
189 'ParseError' => LogLevel::CRITICAL,
190 'Throwable' => LogLevel::ERROR,
195 * @return array<int, LogLevel::*>
197 protected function defaultErrorLevelMap(): array
200 E_ERROR => LogLevel::CRITICAL,
201 E_WARNING => LogLevel::WARNING,
202 E_PARSE => LogLevel::ALERT,
203 E_NOTICE => LogLevel::NOTICE,
204 E_CORE_ERROR => LogLevel::CRITICAL,
205 E_CORE_WARNING => LogLevel::WARNING,
206 E_COMPILE_ERROR => LogLevel::ALERT,
207 E_COMPILE_WARNING => LogLevel::WARNING,
208 E_USER_ERROR => LogLevel::ERROR,
209 E_USER_WARNING => LogLevel::WARNING,
210 E_USER_NOTICE => LogLevel::NOTICE,
211 E_STRICT => LogLevel::NOTICE,
212 E_RECOVERABLE_ERROR => LogLevel::ERROR,
213 E_DEPRECATED => LogLevel::NOTICE,
214 E_USER_DEPRECATED => LogLevel::NOTICE,
219 * The Exception handler
221 * @param Throwable $e The Exception to handle
223 private function handleException(Throwable $e): void
225 $level = LogLevel::ERROR;
226 foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) {
227 if ($e instanceof $class) {
235 sprintf('Uncaught Exception %s: "%s" at %s line %s', self::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
239 if ($this->previousExceptionHandler) {
240 ($this->previousExceptionHandler)($e);
243 if (!headers_sent() && !ini_get('display_errors')) {
244 http_response_code(500);
255 * @param int $code The PHP error code
256 * @param string $message The error message
257 * @param string $file If possible, set the file at which the failure occurred
259 * @param array|null $context If possible, add a context to the error for better analysis
263 public function handleError(int $code, string $message, string $file = '', int $line = 0, ?array $context = []): bool
265 if ($this->handleOnlyReportedErrors && !(error_reporting() & $code)) {
269 // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries
270 if (!$this->hasFatalErrorHandler || !in_array($code, self::$fatalErrors, true)) {
271 $level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL;
272 $this->logger->log($level, self::codeToString($code).': '.$message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]);
274 $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
275 array_shift($trace); // Exclude handleError from trace
276 $this->lastFatalTrace = $trace;
279 if ($this->previousErrorHandler === true) {
281 } elseif ($this->previousErrorHandler) {
282 return (bool) ($this->previousErrorHandler)($code, $message, $file, $line, $context);
291 public function handleFatalError(): void
293 $this->reservedMemory = '';
295 $lastError = error_get_last();
296 if ($lastError && in_array($lastError['type'], self::$fatalErrors, true)) {
299 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'],
300 ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $this->lastFatalTrace]
310 private static function codeToString($code): string
322 return 'E_CORE_ERROR';
324 return 'E_CORE_WARNING';
325 case E_COMPILE_ERROR:
326 return 'E_COMPILE_ERROR';
327 case E_COMPILE_WARNING:
328 return 'E_COMPILE_WARNING';
330 return 'E_USER_ERROR';
332 return 'E_USER_WARNING';
334 return 'E_USER_NOTICE';
337 case E_RECOVERABLE_ERROR:
338 return 'E_RECOVERABLE_ERROR';
340 return 'E_DEPRECATED';
341 case E_USER_DEPRECATED:
342 return 'E_USER_DEPRECATED';
345 return 'Unknown PHP error';