Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 15 additions & 11 deletions src/Cli/LogRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
* Renders the log records returned by Bref Cloud, which already parsed and truncated them.
*
* One line per record: `2026-09-23 10:12:51.863 web 45f01a ERROR message {"context"}`,
* then the exception on an indented line, if any.
* then the exception and its causes on indented lines, if any.
*
* @phpstan-type LogException array{class: string, message: string, file: string, frames: int, trace?: list<string>, previous?: array<string, mixed>}
* @phpstan-type LogRecord array{timestamp: string, function: string, instance: string, level: string|null, message: string, context?: array<mixed>, extra?: array<mixed>, exception?: LogException}
Expand Down Expand Up @@ -71,19 +71,23 @@ private function renderException(array $exception, bool $isPrevious = false): st
// Bref's runtime errors have no file
$location = $exception['file'] !== '' ? " at {$exception['file']}" : '';

if (! $this->full) {
if ($this->full) {
$lines = [$prefix . $this->red($exception['class']) . ': ' . $this->indent($exception['message'], 2)];
if ($location !== '') {
$lines[] = self::INDENT . ' ' . $this->gray(ltrim($location));
}
foreach ($exception['trace'] ?? [] as $i => $frame) {
$lines[] = self::INDENT . ' ' . $this->gray("#$i $frame");
}
} elseif ($isPrevious) {
// Its message is usually the actual cause, e.g. the HTTP error behind a RuntimeException
$lines = [$prefix . $this->red($exception['class']) . ': ' . $this->indent($exception['message'], 2)];
} else {
// Its message is usually the log message already
$frames = $exception['frames'] > 0 ? " ({$exception['frames']} frames)" : '';

return $prefix . $this->red($exception['class']) . $this->gray($location . $frames);
$lines = [$prefix . $this->red($exception['class']) . $this->gray($location . $frames)];
}

$lines = [$prefix . $this->red($exception['class']) . ': ' . $this->indent($exception['message'], 2)];
if ($location !== '') {
$lines[] = self::INDENT . ' ' . $this->gray(ltrim($location));
}
foreach ($exception['trace'] ?? [] as $i => $frame) {
$lines[] = self::INDENT . ' ' . $this->gray("#$i $frame");
}
if (isset($exception['previous'])) {
/** @var LogException $previous */
$previous = $exception['previous'];
Expand Down
2 changes: 1 addition & 1 deletion src/Commands/Logs.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ protected function configure(): void
->setHelp(<<<'HELP'
Shows the logs of all the functions of an environment, oldest first. Times are in UTC.

Laravel logs show their level and context, and exceptions show their class and location
Laravel logs show their level and context, and exceptions show their class, location and cause
(their stack trace with --full). Other logs are shown as they are, one line per entry.
The lines that Lambda and PHP-FPM write on every invocation are hidden, --all shows them.

Expand Down
19 changes: 19 additions & 0 deletions tests/Cli/LogRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,25 @@ public function test_no_level_column_when_no_line_has_a_level(): void
$this->assertSame(['2026-09-23 10:14:00.000 web 0f9e8d START processing batch 12 of 40'], $lines);
}

public function test_the_causes_of_an_exception_are_shown_with_their_message(): void
{
$record = self::ERROR;
$record['exception']['previous'] = [
'class' => 'GuzzleHttp\Exception\ServerException',
'message' => '502 Bad Gateway',
'file' => 'vendor/guzzlehttp/guzzle/src/Middleware.php:69',
'frames' => 0,
];

$lines = (new LogRenderer(colors: false, full: false))->render([$record]);

$this->assertSame([implode("\n", [
'2026-09-23 10:12:51.863 web 45f01a ERROR Payment gateway returned 502',
' ↳ RuntimeException at app/Billing.php:37 (2 frames)',
' ↳ Caused by GuzzleHttp\Exception\ServerException: 502 Bad Gateway',
])], $lines);
}

public function test_full_records_show_the_stack_trace_and_the_previous_exceptions(): void
{
$record = self::ERROR;
Expand Down
Loading