diff --git a/src/Cli/LogRenderer.php b/src/Cli/LogRenderer.php index 27483ec..87e31f5 100644 --- a/src/Cli/LogRenderer.php +++ b/src/Cli/LogRenderer.php @@ -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, 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,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) { diff --git a/tests/Cli/LogRendererTest.php b/tests/Cli/LogRendererTest.php index 79c7574..51a134f 100644 --- a/tests/Cli/LogRendererTest.php +++ b/tests/Cli/LogRendererTest.php @@ -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;