ErrorHandler.php 11 KB

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