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
20 changes: 15 additions & 5 deletions src/Cli/LogRenderer.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,20 @@
/**
* 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"}`,
* One line per record: `2026-09-23 10:12:51.863 web 8f507cfc ERROR message {"context"}`,
* then the exception and its causes on indented lines, if any.
*
* The ID after the function is the short request ID. For logs that don't have one (written by an older version of
* Bref's log formatters), it is the instance instead: the end of the log stream, one per Lambda execution environment.
*
* @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}
* @phpstan-type LogRecord array{timestamp: string, function: string, instance: string, request_id?: string, level: string|null, message: string, context?: array<mixed>, extra?: array<mixed>, exception?: LogException}
*/
class LogRenderer
{
private const MAX_CONTEXT_LENGTH = 500;
/** Like a short commit hash, and like the dashboard: enough to tell requests apart, and to search for one with `--search` */
private const SHORT_REQUEST_ID_LENGTH = 8;
private const INDENT = ' ';

public function __construct(
Expand All @@ -29,19 +34,24 @@ public function render(array $records): array
{
$functionWidth = max([0, ...array_map(fn(array $record) => strlen($record['function']), $records)]);
$levelWidth = max([0, ...array_map(fn(array $record) => strlen($record['level'] ?? ''), $records)]);
$hasRequestIds = array_filter($records, fn(array $record) => isset($record['request_id'])) !== [];

return array_map(fn(array $record) => $this->renderRecord($record, $functionWidth, $levelWidth), $records);
return array_map(fn(array $record) => $this->renderRecord($record, $functionWidth, $levelWidth, $hasRequestIds), $records);
}

/**
* @param LogRecord $record
*/
private function renderRecord(array $record, int $functionWidth, int $levelWidth): string
private function renderRecord(array $record, int $functionWidth, int $levelWidth, bool $hasRequestIds): string
{
// A single ID column: two unlabeled IDs side by side would be confusing
$id = $hasRequestIds
? str_pad(substr($record['request_id'] ?? '', 0, self::SHORT_REQUEST_ID_LENGTH), self::SHORT_REQUEST_ID_LENGTH)
: $record['instance'];
$columns = [
$this->gray(str_replace('T', ' ', rtrim($record['timestamp'], 'Z'))),
str_pad($record['function'], $functionWidth),
$this->gray($record['instance']),
$this->gray($id),
];
// Only logs written by Bref's Monolog formatter have a level: there is no column for apps that don't use it
if ($levelWidth > 0) {
Expand Down
17 changes: 17 additions & 0 deletions tests/Cli/LogRendererTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,23 @@ 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);
}

/**
* Bref's log formatters start lines with the request ID: it replaces the instance, and a line without one, e.g. on a
* cold start, keeps the columns aligned.
*/
public function test_the_short_request_id_replaces_the_instance(): void
{
$lines = (new LogRenderer(colors: false, full: false))->render([
['timestamp' => '2026-09-23T10:14:00.000Z', 'function' => 'web', 'instance' => '0f9e8d', 'level' => null, 'message' => 'Creating storage directories'],
['timestamp' => '2026-09-23T10:14:00.120Z', 'function' => 'web', 'instance' => '0f9e8d', 'request_id' => '8f507cfc-8b35-4e7e-9f26-f2a3a6e7e1a2', 'level' => 'INFO', 'message' => 'User signed in'],
]);

$this->assertSame([
'2026-09-23 10:14:00.000 web Creating storage directories',
'2026-09-23 10:14:00.120 web 8f507cfc INFO User signed in',
], $lines);
}

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