diff --git a/app/Livewire/Project/Shared/GetLogs.php b/app/Livewire/Project/Shared/GetLogs.php index 4777345546..e2c0b6fe3c 100644 --- a/app/Livewire/Project/Shared/GetLogs.php +++ b/app/Livewire/Project/Shared/GetLogs.php @@ -172,7 +172,7 @@ class GetLogs extends Component $since = is_string($since) ? $since : null; if (is_string($since) && preg_match(self::DOCKER_TIMESTAMP_PATTERN, $since)) { - $options = "--since {$since} -t"; + $options = $this->sinceOptions($since); } else { $options = "-n {$this->resolveLogTail()} -t"; } @@ -281,7 +281,7 @@ class GetLogs extends Component if ($all) { $options = '-t'; } elseif ($sinceValue !== null) { - $options = "--since {$sinceValue} -t"; + $options = $this->sinceOptions($sinceValue); } else { $options = "-n {$tail} -t"; } @@ -441,6 +441,16 @@ class GetLogs extends Component return collect(); } + /** + * `--since` without `-n` makes dockerd decode the complete log file of the container on each + * stream tick, which keeps it at full CPU for large logs. With `-n`, dockerd reads from the end. + * The viewer keeps at most MAX_LOG_LINES lines, so a stream tick never needs more. + */ + private function sinceOptions(string $since): string + { + return "--since {$since} -n ".self::MAX_LOG_LINES.' -t'; + } + private function resolveLogTail(): int|string { if ($this->numberOfLines === -1) { diff --git a/tests/Feature/GetLogsCommandInjectionTest.php b/tests/Feature/GetLogsCommandInjectionTest.php index ca04c917a7..2757f9685b 100644 --- a/tests/Feature/GetLogsCommandInjectionTest.php +++ b/tests/Feature/GetLogsCommandInjectionTest.php @@ -221,7 +221,7 @@ describe('GetLogs browser-rendered output', function () { ->call('getLogs', '2026-09-28T10:00:02.123456789Z') ->assertReturned('2026-09-28T10:00:03Z new'); - Process::assertRan(fn ($process) => str_contains(remoteProcessScript($process), 'docker logs --since 2026-09-28T10:00:02.123456789Z -t test-container')); + Process::assertRan(fn ($process) => str_contains(remoteProcessScript($process), 'docker logs --since 2026-09-28T10:00:02.123456789Z -n 50000 -t test-container')); }); test('getLogs prefixes the streaming command with sudo for non-root servers', function () { @@ -234,7 +234,7 @@ describe('GetLogs browser-rendered output', function () { 'container' => 'test-container', ])->call('getLogs', '2026-09-28T10:00:02Z'); - Process::assertRan(fn ($process) => str_contains(remoteProcessScript($process), '(sudo docker logs --since 2026-09-28T10:00:02Z -t test-container) 2>&1 | head -c')); + Process::assertRan(fn ($process) => str_contains(remoteProcessScript($process), '(sudo docker logs --since 2026-09-28T10:00:02Z -n 50000 -t test-container) 2>&1 | head -c')); }); test('getLogs ignores a since value that is not a docker timestamp', function (string $since) { diff --git a/tests/Feature/GetLogsMergedSourcesTest.php b/tests/Feature/GetLogsMergedSourcesTest.php index 55c16a7fe4..aee9c352e9 100644 --- a/tests/Feature/GetLogsMergedSourcesTest.php +++ b/tests/Feature/GetLogsMergedSourcesTest.php @@ -180,7 +180,7 @@ it('asks each source only for lines after its own timestamp', function () { $frankfurt = collect($commands)->first(fn (string $command): bool => str_contains($command, 'web-frankfurt')); $helsinki = collect($commands)->first(fn (string $command): bool => str_contains($command, 'web-helsinki')); - expect($frankfurt)->toContain('--since 2026-09-28T10:00:01.123456789Z') + expect($frankfurt)->toContain('--since 2026-09-28T10:00:01.123456789Z -n 50000 -t') ->and($helsinki)->toContain('docker logs -n ') ->and($helsinki)->not->toContain('--since'); });