Skip to content

Commit ca4507b

Browse files
Naorayclaude
andauthored
fix(tracing): truncate serialized job payload data (#44)
* fix(tracing): truncate serialized job payload data Closes #34 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * refactor: use Str::limit for payload truncation Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 85efdc4 commit ca4507b

File tree

2 files changed

+147
-1
lines changed

2 files changed

+147
-1
lines changed

src/Tracing/JobContextCollector.php

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Illuminate\Queue\Events\JobExceptionOccurred;
66
use Illuminate\Support\Facades\Context;
7+
use Illuminate\Support\Str;
78
use Naoray\LaravelGithubMonolog\Tracing\Concerns\RedactsData;
89
use Naoray\LaravelGithubMonolog\Tracing\Concerns\ResolvesTracingConfig;
910
use Naoray\LaravelGithubMonolog\Tracing\Contracts\EventDrivenCollectorInterface;
@@ -13,6 +14,11 @@ class JobContextCollector implements EventDrivenCollectorInterface
1314
use RedactsData;
1415
use ResolvesTracingConfig;
1516

17+
/**
18+
* Maximum length for serialized string values in the payload before truncation.
19+
*/
20+
protected const MAX_SERIALIZED_LENGTH = 500;
21+
1622
public function isEnabled(): bool
1723
{
1824
return $this->isTracingFeatureEnabled('jobs');
@@ -28,7 +34,47 @@ public function __invoke(JobExceptionOccurred $event): void
2834
'queue' => $job->getQueue(),
2935
'connection' => $job->getConnectionName(),
3036
'attempts' => $job->attempts(),
31-
'payload' => $this->redactPayload($job->payload()),
37+
'payload' => $this->cleanPayload($job->payload()),
3238
]);
3339
}
40+
41+
/**
42+
* Clean the job payload by redacting sensitive data and truncating serialized values.
43+
*
44+
* @param array<string, mixed> $payload
45+
* @return array<string, mixed>
46+
*/
47+
protected function cleanPayload(array $payload): array
48+
{
49+
$payload = $this->redactPayload($payload);
50+
51+
return $this->truncateSerializedValues($payload);
52+
}
53+
54+
/**
55+
* Recursively truncate long serialized string values in the payload.
56+
*
57+
* Serialized PHP objects (e.g. payload.data.command) can contain hundreds
58+
* of lines of unreadable data. This method truncates them to keep GitHub
59+
* issues clean and readable.
60+
*
61+
* @param array<string, mixed> $data
62+
* @return array<string, mixed>
63+
*/
64+
protected function truncateSerializedValues(array $data): array
65+
{
66+
$result = [];
67+
68+
foreach ($data as $key => $value) {
69+
if (is_array($value)) {
70+
$result[$key] = $this->truncateSerializedValues($value);
71+
} elseif (is_string($value) && strlen($value) > self::MAX_SERIALIZED_LENGTH) {
72+
$result[$key] = Str::limit($value, self::MAX_SERIALIZED_LENGTH, '... [truncated]');
73+
} else {
74+
$result[$key] = $value;
75+
}
76+
}
77+
78+
return $result;
79+
}
3480
}

tests/Tracing/JobContextCollectorTest.php

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,3 +35,103 @@
3535
expect($jobContext['connection'])->toBe('redis');
3636
expect($jobContext['attempts'])->toBe(2);
3737
});
38+
39+
it('truncates long serialized command strings in payload', function () {
40+
$longSerializedCommand = str_repeat('O:32:"App\\Jobs\\TestJob":3:{s:6:"tries";', 50);
41+
42+
$job = Mockery::mock('Illuminate\Contracts\Queue\Job');
43+
$job->shouldReceive('getName')->andReturn('App\Jobs\TestJob');
44+
$job->shouldReceive('getQueue')->andReturn('default');
45+
$job->shouldReceive('getConnectionName')->andReturn('redis');
46+
$job->shouldReceive('attempts')->andReturn(1);
47+
$job->shouldReceive('payload')->andReturn([
48+
'displayName' => 'App\Jobs\TestJob',
49+
'uuid' => 'test-uuid-1234',
50+
'job' => 'Illuminate\Queue\CallQueuedHandler@call',
51+
'maxTries' => 3,
52+
'timeout' => 120,
53+
'data' => [
54+
'commandName' => 'App\Jobs\TestJob',
55+
'command' => $longSerializedCommand,
56+
],
57+
]);
58+
59+
$event = new JobExceptionOccurred('redis', $job, new \RuntimeException('Test exception'));
60+
61+
($this->collector)($event);
62+
63+
$jobContext = Context::get('job');
64+
$payload = $jobContext['payload'];
65+
66+
// The command field should be truncated
67+
expect(strlen($payload['data']['command']))->toBeLessThan(strlen($longSerializedCommand));
68+
expect($payload['data']['command'])->toEndWith('... [truncated]');
69+
// Truncated to 500 chars + the "... [truncated]" marker
70+
expect(strlen($payload['data']['command']))->toBe(500 + strlen('... [truncated]'));
71+
72+
// Other payload fields should be preserved as-is
73+
expect($payload['displayName'])->toBe('App\Jobs\TestJob');
74+
expect($payload['uuid'])->toBe('test-uuid-1234');
75+
expect($payload['job'])->toBe('Illuminate\Queue\CallQueuedHandler@call');
76+
expect($payload['maxTries'])->toBe(3);
77+
expect($payload['timeout'])->toBe(120);
78+
expect($payload['data']['commandName'])->toBe('App\Jobs\TestJob');
79+
});
80+
81+
it('does not truncate short string values in payload', function () {
82+
$job = Mockery::mock('Illuminate\Contracts\Queue\Job');
83+
$job->shouldReceive('getName')->andReturn('App\Jobs\TestJob');
84+
$job->shouldReceive('getQueue')->andReturn('default');
85+
$job->shouldReceive('getConnectionName')->andReturn('redis');
86+
$job->shouldReceive('attempts')->andReturn(1);
87+
$job->shouldReceive('payload')->andReturn([
88+
'displayName' => 'App\Jobs\TestJob',
89+
'uuid' => 'test-uuid-1234',
90+
'data' => [
91+
'commandName' => 'App\Jobs\TestJob',
92+
'command' => 'short-command-value',
93+
],
94+
]);
95+
96+
$event = new JobExceptionOccurred('redis', $job, new \RuntimeException('Test exception'));
97+
98+
($this->collector)($event);
99+
100+
$jobContext = Context::get('job');
101+
$payload = $jobContext['payload'];
102+
103+
// Short values should not be truncated
104+
expect($payload['data']['command'])->toBe('short-command-value');
105+
expect($payload['displayName'])->toBe('App\Jobs\TestJob');
106+
});
107+
108+
it('preserves non-string values in payload during truncation', function () {
109+
$job = Mockery::mock('Illuminate\Contracts\Queue\Job');
110+
$job->shouldReceive('getName')->andReturn('App\Jobs\TestJob');
111+
$job->shouldReceive('getQueue')->andReturn('default');
112+
$job->shouldReceive('getConnectionName')->andReturn('redis');
113+
$job->shouldReceive('attempts')->andReturn(1);
114+
$job->shouldReceive('payload')->andReturn([
115+
'displayName' => 'App\Jobs\TestJob',
116+
'maxTries' => 3,
117+
'timeout' => null,
118+
'attempts' => 1,
119+
'tags' => ['tag1', 'tag2'],
120+
'data' => [
121+
'commandName' => 'App\Jobs\TestJob',
122+
],
123+
]);
124+
125+
$event = new JobExceptionOccurred('redis', $job, new \RuntimeException('Test exception'));
126+
127+
($this->collector)($event);
128+
129+
$jobContext = Context::get('job');
130+
$payload = $jobContext['payload'];
131+
132+
// Integer, null, and array values should be preserved
133+
expect($payload['maxTries'])->toBe(3);
134+
expect($payload['timeout'])->toBeNull();
135+
expect($payload['attempts'])->toBe(1);
136+
expect($payload['tags'])->toBe(['tag1', 'tag2']);
137+
});

0 commit comments

Comments
 (0)