ErrorHandler.php 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of the Monolog package.
  4. *
  5. * (c) Jordi Boggiano <j.boggiano@seld.be>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace Monolog;
  11. use Closure;
  12. use Psr\Log\LoggerInterface;
  13. use Psr\Log\LogLevel;
  14. /**
  15. * Monolog error handler
  16. *
  17. * A facility to enable logging of runtime errors, exceptions and fatal errors.
  18. *
  19. * Quick setup: <code>ErrorHandler::register($logger);</code>
  20. *
  21. * @author Jordi Boggiano <j.boggiano@seld.be>
  22. */
  23. class ErrorHandler
  24. {
  25. private Closure|null $previousExceptionHandler = null;
  26. /** @var array<class-string, LogLevel::*> an array of class name to LogLevel::* constant mapping */
  27. private array $uncaughtExceptionLevelMap = [];
  28. /** @var Closure|true|null */
  29. private Closure|bool|null $previousErrorHandler = null;
  30. /** @var array<int, LogLevel::*> an array of E_* constant to LogLevel::* constant mapping */
  31. private array $errorLevelMap = [];
  32. private bool $handleOnlyReportedErrors = true;
  33. private bool $hasFatalErrorHandler = false;
  34. private string $fatalLevel = LogLevel::ALERT;
  35. private string|null $reservedMemory = null;
  36. /** @var ?array{type: int, message: string, file: string, line: int, trace: mixed} */
  37. private array|null $lastFatalData = null;
  38. private const FATAL_ERRORS = [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR];
  39. public function __construct(
  40. private LoggerInterface $logger
  41. ) {
  42. }
  43. /**
  44. * Registers a new ErrorHandler for a given Logger
  45. *
  46. * By default it will handle errors, exceptions and fatal errors
  47. *
  48. * @param array<int, LogLevel::*>|false $errorLevelMap an array of E_* constant to LogLevel::* constant mapping, or false to disable error handling
  49. * @param array<class-string, LogLevel::*>|false $exceptionLevelMap an array of class name to LogLevel::* constant mapping, or false to disable exception handling
  50. * @param LogLevel::*|null|false $fatalLevel a LogLevel::* constant, null to use the default LogLevel::ALERT or false to disable fatal error handling
  51. * @return static
  52. */
  53. public static function register(LoggerInterface $logger, $errorLevelMap = [], $exceptionLevelMap = [], $fatalLevel = null): self
  54. {
  55. /** @phpstan-ignore-next-line */
  56. $handler = new static($logger);
  57. if ($errorLevelMap !== false) {
  58. $handler->registerErrorHandler($errorLevelMap);
  59. }
  60. if ($exceptionLevelMap !== false) {
  61. $handler->registerExceptionHandler($exceptionLevelMap);
  62. }
  63. if ($fatalLevel !== false) {
  64. $handler->registerFatalHandler($fatalLevel);
  65. }
  66. return $handler;
  67. }
  68. /**
  69. * @param array<class-string, LogLevel::*> $levelMap an array of class name to LogLevel::* constant mapping
  70. * @return $this
  71. */
  72. public function registerExceptionHandler(array $levelMap = [], bool $callPrevious = true): self
  73. {
  74. $prev = set_exception_handler(function (\Throwable $e): void {
  75. $this->handleException($e);
  76. });
  77. $this->uncaughtExceptionLevelMap = $levelMap;
  78. foreach ($this->defaultExceptionLevelMap() as $class => $level) {
  79. if (!isset($this->uncaughtExceptionLevelMap[$class])) {
  80. $this->uncaughtExceptionLevelMap[$class] = $level;
  81. }
  82. }
  83. if ($callPrevious && null !== $prev) {
  84. $this->previousExceptionHandler = $prev(...);
  85. }
  86. return $this;
  87. }
  88. /**
  89. * @param array<int, LogLevel::*> $levelMap an array of E_* constant to LogLevel::* constant mapping
  90. * @return $this
  91. */
  92. public function registerErrorHandler(array $levelMap = [], bool $callPrevious = true, int $errorTypes = -1, bool $handleOnlyReportedErrors = true): self
  93. {
  94. $prev = set_error_handler($this->handleError(...), $errorTypes);
  95. $this->errorLevelMap = array_replace($this->defaultErrorLevelMap(), $levelMap);
  96. if ($callPrevious) {
  97. $this->previousErrorHandler = $prev !== null ? $prev(...) : true;
  98. } else {
  99. $this->previousErrorHandler = null;
  100. }
  101. $this->handleOnlyReportedErrors = $handleOnlyReportedErrors;
  102. return $this;
  103. }
  104. /**
  105. * @param LogLevel::*|null $level a LogLevel::* constant, null to use the default LogLevel::ALERT
  106. * @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
  107. * @return $this
  108. */
  109. public function registerFatalHandler($level = null, int $reservedMemorySize = 20): self
  110. {
  111. register_shutdown_function($this->handleFatalError(...));
  112. $this->reservedMemory = str_repeat(' ', 1024 * $reservedMemorySize);
  113. $this->fatalLevel = null === $level ? LogLevel::ALERT : $level;
  114. $this->hasFatalErrorHandler = true;
  115. return $this;
  116. }
  117. /**
  118. * @return array<class-string, LogLevel::*>
  119. */
  120. protected function defaultExceptionLevelMap(): array
  121. {
  122. return [
  123. 'ParseError' => LogLevel::CRITICAL,
  124. 'Throwable' => LogLevel::ERROR,
  125. ];
  126. }
  127. /**
  128. * @return array<int, LogLevel::*>
  129. */
  130. protected function defaultErrorLevelMap(): array
  131. {
  132. return [
  133. E_ERROR => LogLevel::CRITICAL,
  134. E_WARNING => LogLevel::WARNING,
  135. E_PARSE => LogLevel::ALERT,
  136. E_NOTICE => LogLevel::NOTICE,
  137. E_CORE_ERROR => LogLevel::CRITICAL,
  138. E_CORE_WARNING => LogLevel::WARNING,
  139. E_COMPILE_ERROR => LogLevel::ALERT,
  140. E_COMPILE_WARNING => LogLevel::WARNING,
  141. E_USER_ERROR => LogLevel::ERROR,
  142. E_USER_WARNING => LogLevel::WARNING,
  143. E_USER_NOTICE => LogLevel::NOTICE,
  144. E_STRICT => LogLevel::NOTICE,
  145. E_RECOVERABLE_ERROR => LogLevel::ERROR,
  146. E_DEPRECATED => LogLevel::NOTICE,
  147. E_USER_DEPRECATED => LogLevel::NOTICE,
  148. ];
  149. }
  150. private function handleException(\Throwable $e): never
  151. {
  152. $level = LogLevel::ERROR;
  153. foreach ($this->uncaughtExceptionLevelMap as $class => $candidate) {
  154. if ($e instanceof $class) {
  155. $level = $candidate;
  156. break;
  157. }
  158. }
  159. $this->logger->log(
  160. $level,
  161. sprintf('Uncaught Exception %s: "%s" at %s line %s', Utils::getClass($e), $e->getMessage(), $e->getFile(), $e->getLine()),
  162. ['exception' => $e]
  163. );
  164. if (null !== $this->previousExceptionHandler) {
  165. ($this->previousExceptionHandler)($e);
  166. }
  167. if (!headers_sent() && in_array(strtolower((string) ini_get('display_errors')), ['0', '', 'false', 'off', 'none', 'no'], true)) {
  168. http_response_code(500);
  169. }
  170. exit(255);
  171. }
  172. private function handleError(int $code, string $message, string $file = '', int $line = 0): bool
  173. {
  174. if ($this->handleOnlyReportedErrors && 0 === (error_reporting() & $code)) {
  175. return false;
  176. }
  177. // fatal error codes are ignored if a fatal error handler is present as well to avoid duplicate log entries
  178. if (!$this->hasFatalErrorHandler || !in_array($code, self::FATAL_ERRORS, true)) {
  179. $level = $this->errorLevelMap[$code] ?? LogLevel::CRITICAL;
  180. $this->logger->log($level, self::codeToString($code).': '.$message, ['code' => $code, 'message' => $message, 'file' => $file, 'line' => $line]);
  181. } else {
  182. $trace = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS);
  183. array_shift($trace); // Exclude handleError from trace
  184. $this->lastFatalData = ['type' => $code, 'message' => $message, 'file' => $file, 'line' => $line, 'trace' => $trace];
  185. }
  186. if ($this->previousErrorHandler === true) {
  187. return false;
  188. }
  189. if ($this->previousErrorHandler instanceof Closure) {
  190. return (bool) ($this->previousErrorHandler)($code, $message, $file, $line);
  191. }
  192. return true;
  193. }
  194. /**
  195. * @private
  196. */
  197. public function handleFatalError(): void
  198. {
  199. $this->reservedMemory = '';
  200. if (is_array($this->lastFatalData)) {
  201. $lastError = $this->lastFatalData;
  202. } else {
  203. $lastError = error_get_last();
  204. }
  205. if (is_array($lastError) && in_array($lastError['type'], self::FATAL_ERRORS, true)) {
  206. $trace = $lastError['trace'] ?? null;
  207. $this->logger->log(
  208. $this->fatalLevel,
  209. 'Fatal Error ('.self::codeToString($lastError['type']).'): '.$lastError['message'],
  210. ['code' => $lastError['type'], 'message' => $lastError['message'], 'file' => $lastError['file'], 'line' => $lastError['line'], 'trace' => $trace]
  211. );
  212. if ($this->logger instanceof Logger) {
  213. foreach ($this->logger->getHandlers() as $handler) {
  214. $handler->close();
  215. }
  216. }
  217. }
  218. }
  219. private static function codeToString(int $code): string
  220. {
  221. return match ($code) {
  222. E_ERROR => 'E_ERROR',
  223. E_WARNING => 'E_WARNING',
  224. E_PARSE => 'E_PARSE',
  225. E_NOTICE => 'E_NOTICE',
  226. E_CORE_ERROR => 'E_CORE_ERROR',
  227. E_CORE_WARNING => 'E_CORE_WARNING',
  228. E_COMPILE_ERROR => 'E_COMPILE_ERROR',
  229. E_COMPILE_WARNING => 'E_COMPILE_WARNING',
  230. E_USER_ERROR => 'E_USER_ERROR',
  231. E_USER_WARNING => 'E_USER_WARNING',
  232. E_USER_NOTICE => 'E_USER_NOTICE',
  233. E_STRICT => 'E_STRICT',
  234. E_RECOVERABLE_ERROR => 'E_RECOVERABLE_ERROR',
  235. E_DEPRECATED => 'E_DEPRECATED',
  236. E_USER_DEPRECATED => 'E_USER_DEPRECATED',
  237. default => 'Unknown PHP error',
  238. };
  239. }
  240. }