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