From fc9db8ba1b3cbc3a79c7dab1d4d8cafc7e406160 Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Sat, 26 Sep 2026 21:47:09 +0200 Subject: [PATCH 1/2] Show the short request ID of log lines Bref's log formatters start lines with the Lambda request ID, and Bref Cloud returns it as `request_id`. Show its first 8 characters after the instance, like the dashboard: enough to tell requests apart, and to find all the logs of one with `--search= --all`. Claude-Session: https://claude.ai/code/session_0168N55uBK9UjfpL6r8oGjDm --- src/Cli/LogRenderer.php | 15 +++++++++++---- tests/Cli/LogRendererTest.php | 16 ++++++++++++++++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/Cli/LogRenderer.php b/src/Cli/LogRenderer.php index 27483ec..572ca10 100644 --- a/src/Cli/LogRenderer.php +++ b/src/Cli/LogRenderer.php @@ -5,15 +5,17 @@ /** * 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 45f01a 8f507cfc ERROR message {"context"}`, * 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, previous?: array} - * @phpstan-type LogRecord array{timestamp: string, function: string, instance: string, level: string|null, message: string, context?: array, extra?: array, exception?: LogException} + * @phpstan-type LogRecord array{timestamp: string, function: string, instance: string, request_id?: string, level: string|null, message: string, context?: array, extra?: array, 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( @@ -29,20 +31,25 @@ 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 { $columns = [ $this->gray(str_replace('T', ' ', rtrim($record['timestamp'], 'Z'))), str_pad($record['function'], $functionWidth), $this->gray($record['instance']), ]; + // Logs written before Bref's log formatters started lines with the request ID get no column + if ($hasRequestIds) { + $columns[] = $this->gray(str_pad(substr($record['request_id'] ?? '', 0, self::SHORT_REQUEST_ID_LENGTH), self::SHORT_REQUEST_ID_LENGTH)); + } // 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) { $columns[] = $this->level(str_pad($record['level'] ?? '', $levelWidth)); diff --git a/tests/Cli/LogRendererTest.php b/tests/Cli/LogRendererTest.php index 79c7574..188ea1a 100644 --- a/tests/Cli/LogRendererTest.php +++ b/tests/Cli/LogRendererTest.php @@ -63,6 +63,22 @@ 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: a line without one, e.g. on a cold start, keeps the columns aligned. + */ + public function test_the_short_request_id_follows_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 0f9e8d Creating storage directories', + '2026-09-23 10:14:00.120 web 0f9e8d 8f507cfc INFO User signed in', + ], $lines); + } + public function test_the_causes_of_an_exception_are_shown_with_their_message(): void { $record = self::ERROR; From 790fc8dfcfff60927db1d816ace576532913645a Mon Sep 17 00:00:00 2001 From: Matthieu Napoli Date: Sat, 26 Sep 2026 21:52:12 +0200 Subject: [PATCH 2/2] Show the request ID instead of the instance, not next to it Two unlabeled IDs side by side were confusing. Logs without request IDs (older versions of Bref's log formatters) keep the instance. Claude-Session: https://claude.ai/code/session_0168N55uBK9UjfpL6r8oGjDm --- src/Cli/LogRenderer.php | 15 +++++++++------ tests/Cli/LogRendererTest.php | 9 +++++---- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/Cli/LogRenderer.php b/src/Cli/LogRenderer.php index 572ca10..87e31f5 100644 --- a/src/Cli/LogRenderer.php +++ b/src/Cli/LogRenderer.php @@ -5,9 +5,12 @@ /** * 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 8f507cfc 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, previous?: array} * @phpstan-type LogRecord array{timestamp: string, function: string, instance: string, request_id?: string, level: string|null, message: string, context?: array, extra?: array, exception?: LogException} */ @@ -41,15 +44,15 @@ public function render(array $records): array */ 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), ]; - // Logs written before Bref's log formatters started lines with the request ID get no column - if ($hasRequestIds) { - $columns[] = $this->gray(str_pad(substr($record['request_id'] ?? '', 0, self::SHORT_REQUEST_ID_LENGTH), self::SHORT_REQUEST_ID_LENGTH)); - } // 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) { $columns[] = $this->level(str_pad($record['level'] ?? '', $levelWidth)); diff --git a/tests/Cli/LogRendererTest.php b/tests/Cli/LogRendererTest.php index 188ea1a..51a134f 100644 --- a/tests/Cli/LogRendererTest.php +++ b/tests/Cli/LogRendererTest.php @@ -64,9 +64,10 @@ public function test_no_level_column_when_no_line_has_a_level(): void } /** - * Bref's log formatters start lines with the request ID: a line without one, e.g. on a cold start, keeps the columns aligned. + * 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_follows_the_instance(): void + 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'], @@ -74,8 +75,8 @@ public function test_the_short_request_id_follows_the_instance(): void ]); $this->assertSame([ - '2026-09-23 10:14:00.000 web 0f9e8d Creating storage directories', - '2026-09-23 10:14:00.120 web 0f9e8d 8f507cfc INFO User signed in', + '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); }