diff --git a/system/Debug/Exceptions.php b/system/Debug/Exceptions.php index 0d96d7c087ca..879dab718a16 100644 --- a/system/Debug/Exceptions.php +++ b/system/Debug/Exceptions.php @@ -102,8 +102,11 @@ public function exceptionHandler(Throwable $exception) [$statusCode, $exitCode] = $this->determineCodes($exception); if ($this->config->log === true && ! in_array($statusCode, $this->config->ignoreCodes, true)) { - log_message('critical', $exception->getMessage() . "\n{trace}", [ - 'trace' => $exception->getTraceAsString(), + log_message('critical', "{message}\nin {exFile} on line {exLine}.\n{trace}", [ + 'message' => $exception->getMessage(), + 'exFile' => clean_path($exception->getFile()), // {file} refers to THIS file + 'exLine' => $exception->getLine(), // {line} refers to THIS line + 'trace' => self::renderBacktrace($exception->getTrace()), ]); } @@ -434,4 +437,55 @@ public static function highlightFile(string $file, int $lineNumber, int $lines = return '
' . $out . '
'; } + + private static function renderBacktrace(array $backtrace): string + { + $backtraces = []; + + foreach ($backtrace as $index => $trace) { + $frame = $trace + ['file' => '[internal function]', 'line' => '', 'class' => '', 'type' => '', 'args' => []]; + + if ($frame['file'] !== '[internal function]') { + $frame['file'] = sprintf('%s(%s)', $frame['file'], $frame['line']); + } + + unset($frame['line']); + $idx = $index; + $idx = str_pad((string) ++$idx, 2, ' ', STR_PAD_LEFT); + + $args = implode(', ', array_map(static function ($value): string { + switch (true) { + case is_object($value): + return sprintf('Object(%s)', get_class($value)); + + case is_array($value): + return $value !== [] ? '[...]' : '[]'; + + case $value === null: + return 'null'; + + case is_resource($value): + return sprintf('resource (%s)', get_resource_type($value)); + + case is_string($value): + return var_export(clean_path($value), true); + + default: + return var_export($value, true); + } + }, $frame['args'])); + + $backtraces[] = sprintf( + '%s %s: %s%s%s(%s)', + $idx, + clean_path($frame['file']), + $frame['class'], + $frame['type'], + $frame['function'], + $args + ); + } + + return implode("\n", $backtraces); + } } diff --git a/tests/system/Debug/ExceptionsTest.php b/tests/system/Debug/ExceptionsTest.php index 247629933c69..8bd4f01e0925 100644 --- a/tests/system/Debug/ExceptionsTest.php +++ b/tests/system/Debug/ExceptionsTest.php @@ -61,4 +61,20 @@ public function testDetermineCodes(): void $this->assertSame([500, 1], $determineCodes(new RuntimeException('That.', 600))); $this->assertSame([404, 1], $determineCodes(new RuntimeException('There.', 404))); } + + public function testRenderBacktrace(): void + { + $renderer = self::getPrivateMethodInvoker(Exceptions::class, 'renderBacktrace'); + $exception = new RuntimeException('This.'); + + $renderedBacktrace = $renderer($exception->getTrace()); + $renderedBacktrace = explode("\n", $renderedBacktrace); + + foreach ($renderedBacktrace as $trace) { + $this->assertMatchesRegularExpression( + '/^\s*\d* .+(?:\(\d+\))?: \S+(?:(?:\->|::)\S+)?\(.*\)$/', + $trace + ); + } + } } diff --git a/user_guide_src/source/changelogs/v4.2.0.rst b/user_guide_src/source/changelogs/v4.2.0.rst index abb5e9dc3c01..cc1561a98949 100644 --- a/user_guide_src/source/changelogs/v4.2.0.rst +++ b/user_guide_src/source/changelogs/v4.2.0.rst @@ -29,6 +29,8 @@ Enhancements - Added new OCI8 driver for database. - It can access Oracle Database and supports SQL and PL/SQL statements. - The ``spark routes`` command now shows closure routes, auto routtes, and filters. See :ref:`URI Routing `. +- Exception information logged through ``log_message()`` has now improved. It now includes the file and line where the exception originated. It also does not truncate the message anymore. + - The log format has also changed. If users are depending on the log format in their apps, the new log format is "<1-based count> (): " Changes *******