fix(logs): show container logs that contain invalid UTF-8

Closes #10628
This commit is contained in:
Andras Bacsai
2026-10-27 14:43:21 +01:00
parent 80df5431dc
commit f658d6df76
2 changed files with 79 additions and 2 deletions
+3 -2
View File
@@ -297,7 +297,7 @@ class GetLogs extends Component
$stripTimestamps ??= $export && ! $this->showTimeStamps;
$merged = $this->joinMergedLines($groups, $stripTimestamps, $export);
if (strlen($merged) > $maxBytes) {
$merged = rtrim(substr($merged, 0, $maxBytes), "\n");
$merged = rtrim(mb_strcut($merged, 0, $maxBytes, 'UTF-8'), "\n");
$truncated = true;
}
if ($truncated && $merged !== '') {
@@ -501,7 +501,8 @@ class GetLogs extends Component
// boundedLogCommand() caps the output at $maxBytes + 1, so one extra byte means truncation.
$output = Process::timeout(config('constants.ssh.command_timeout'))->run($sshCommand)->output();
$truncated = strlen($output) > $maxBytes;
$logs = rtrim(removeAnsiColors($truncated ? substr($output, 0, $maxBytes) : $output), "\n");
// Invalid UTF-8 from a container would make the JSON response fail, so the viewer would stay empty.
$logs = rtrim(sanitize_utf8_text(removeAnsiColors($truncated ? substr($output, 0, $maxBytes) : $output)), "\n");
return ['logs' => $logs, 'truncated' => $truncated];
}
+76
View File
@@ -0,0 +1,76 @@
<?php
use App\Livewire\Project\Shared\GetLogs;
use App\Models\Application;
use App\Models\Environment;
use App\Models\PrivateKey;
use App\Models\Project;
use App\Models\Server;
use App\Models\StandaloneDocker;
use App\Models\Team;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Process;
use Livewire\Livewire;
uses(RefreshDatabase::class);
beforeEach(function () {
$this->user = User::factory()->create();
$this->team = Team::factory()->create();
$this->user->teams()->attach($this->team, ['role' => 'owner']);
$privateKey = PrivateKey::factory()->create(['team_id' => $this->team->id]);
$server = Server::factory()->create([
'team_id' => $this->team->id,
'private_key_id' => $privateKey->id,
]);
$server->settings->fill(['is_reachable' => true, 'is_usable' => true, 'force_disabled' => false])->save();
$this->server = Server::with('settings')->findOrFail($server->id);
$destination = StandaloneDocker::where('server_id', $this->server->id)->firstOrFail();
$project = Project::factory()->create(['team_id' => $this->team->id]);
$environment = Environment::factory()->create(['project_id' => $project->id]);
$this->application = Application::factory()->create([
'environment_id' => $environment->id,
'destination_id' => $destination->id,
'destination_type' => $destination->getMorphClass(),
'build_pack' => 'dockercompose',
]);
$this->actingAs($this->user);
session(['currentTeam' => $this->team]);
});
it('returns container logs that contain invalid UTF-8 bytes', function () {
Process::fake(['*' => Process::result(output: "2026-06-17T21:00:00Z job \xC3\x28 processed\n2026-06-17T21:00:01Z done\n")]);
$logs = Livewire::test(GetLogs::class, [
'server' => $this->server,
'resource' => $this->application,
'container' => 'worker-abc123',
])->call('getLogs')->effects['returns'][0];
expect(mb_check_encoding($logs, 'UTF-8'))->toBeTrue()
->and($logs)->toContain('2026-06-17T21:00:00Z job ')
->and($logs)->toContain('processed')
->and($logs)->toContain('2026-06-17T21:00:01Z done');
});
it('keeps merged logs valid UTF-8 when the size limit cuts them', function (int $padding) {
$line = '2026-06-17T21:00:00Z '.str_repeat('a', $padding).str_repeat('€', 1000);
Process::fake(['*' => Process::result(output: implode("\n", array_fill(0, 1000, $line)))]);
$logs = Livewire::test(GetLogs::class, [
'server' => $this->server,
'resource' => $this->application,
'container' => 'worker-1',
'sources' => [
['server' => $this->server->uuid, 'container' => 'worker-1', 'label' => 'worker-1'],
['server' => $this->server->uuid, 'container' => 'worker-2', 'label' => 'worker-2'],
],
])->call('getLogs')->effects['returns'][0];
expect(mb_check_encoding($logs, 'UTF-8'))->toBeTrue()
->and($logs)->toEndWith('[... Output truncated at 5MB limit ...]');
})->with([0, 1, 2]);