Skip to content

Commit dfed772

Browse files
Naorayclaude
andcommitted
fix: prevent large context data from being serialized into job payloads
Use Context::addHidden() for large tracing data (queries, outgoing_requests, session, request) to prevent serialization into Laravel job payloads. Hidden context is available during the request for error logging but is NOT serialized when jobs are queued, preventing ~6.8MB job payloads and Redis OOM errors. - QueryCollector: use hidden context for queries - OutgoingRequestSendingCollector: use hidden context for request tracking - OutgoingRequestResponseCollector: use hidden context for outgoing requests - SessionCollector: use hidden context for session data - RequestDataCollector: use hidden context for request data - ContextProcessor: merge both regular and hidden context for logging - GithubMonologServiceProvider: add dehydration callback as safety net Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b686091 commit dfed772

15 files changed

+72
-39
lines changed

src/GithubMonologServiceProvider.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,18 @@
22

33
namespace Naoray\LaravelGithubMonolog;
44

5+
use Illuminate\Support\Facades\Context;
56
use Illuminate\Support\Facades\Event;
67
use Illuminate\Support\ServiceProvider;
78
use Naoray\LaravelGithubMonolog\Tracing\EventHandler;
89

910
class GithubMonologServiceProvider extends ServiceProvider
1011
{
12+
public function register(): void
13+
{
14+
$this->registerContextDehydration();
15+
}
16+
1117
public function boot(): void
1218
{
1319
$config = config('logging.channels.github.tracing');
@@ -22,4 +28,29 @@ public function boot(): void
2228
], 'github-monolog-views');
2329
}
2430
}
31+
32+
/**
33+
* Register context dehydration callback to prevent large tracing data
34+
* from being serialized into job payloads.
35+
*/
36+
protected function registerContextDehydration(): void
37+
{
38+
if (! method_exists(Context::class, 'dehydrating')) {
39+
return;
40+
}
41+
42+
Context::dehydrating(function ($context) {
43+
foreach (['queries', 'outgoing_requests', 'session', 'request'] as $key) {
44+
if ($context->has($key)) {
45+
$context->forget($key);
46+
}
47+
}
48+
49+
foreach (array_keys($context->all()) as $key) {
50+
if (str_starts_with($key, 'outgoing_request.')) {
51+
$context->forget($key);
52+
}
53+
}
54+
});
55+
}
2556
}

src/Tracing/ContextProcessor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function __invoke(LogRecord $record): LogRecord
2828
$sessionCollector->collect();
2929
}
3030

31-
$contextData = Context::all();
31+
$contextData = array_merge(Context::all(), Context::allHidden());
3232

3333
if (empty($contextData)) {
3434
return $record;

src/Tracing/OutgoingRequestResponseCollector.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -30,11 +30,11 @@ public function __invoke(ResponseReceived $event): void
3030
$config = config('logging.channels.github.tracing.outgoing_requests', []);
3131
$limit = $config['limit'] ?? self::DEFAULT_LIMIT;
3232

33-
$requestData = Context::get("outgoing_request.{$requestId}", []);
33+
$requestData = Context::getHidden("outgoing_request.{$requestId}") ?? [];
3434
$startedAt = $requestData['started_at'] ?? microtime(true);
3535
$duration = (microtime(true) - $startedAt) * 1000; // Convert to milliseconds
3636

37-
$outgoingRequests = Context::get('outgoing_requests', []);
37+
$outgoingRequests = Context::getHidden('outgoing_requests') ?? [];
3838

3939
$outgoingRequests[] = [
4040
'url' => $request->url(),
@@ -49,7 +49,7 @@ public function __invoke(ResponseReceived $event): void
4949
$outgoingRequests = array_slice($outgoingRequests, -$limit);
5050
}
5151

52-
Context::add('outgoing_requests', $outgoingRequests);
53-
Context::forget("outgoing_request.{$requestId}");
52+
Context::addHidden('outgoing_requests', $outgoingRequests);
53+
Context::forgetHidden("outgoing_request.{$requestId}");
5454
}
5555
}

src/Tracing/OutgoingRequestSendingCollector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ public function __invoke(RequestSending $event): void
3131
$headers = $request->headers();
3232
$headerBag = new \Symfony\Component\HttpFoundation\HeaderBag($headers);
3333

34-
Context::add("outgoing_request.{$requestId}", [
34+
Context::addHidden("outgoing_request.{$requestId}", [
3535
'url' => $request->url(),
3636
'method' => $request->method(),
3737
'headers' => $this->redactHeaders($headerBag),

src/Tracing/QueryCollector.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public function __invoke(QueryExecuted $event): void
3030

3131
$limit = $config['limit'] ?? self::DEFAULT_LIMIT;
3232

33-
$queries = Context::get('queries', []);
33+
$queries = Context::getHidden('queries') ?? [];
3434
$queries[] = [
3535
'sql' => $event->sql,
3636
'bindings' => $this->redactBindings($event->bindings),
@@ -43,6 +43,6 @@ public function __invoke(QueryExecuted $event): void
4343
$queries = array_slice($queries, -$limit);
4444
}
4545

46-
Context::add('queries', $queries);
46+
Context::addHidden('queries', $queries);
4747
}
4848
}

src/Tracing/RequestDataCollector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ public function __invoke(RequestHandled $event): void
2828
$files = null;
2929
}
3030

31-
Context::add('request', array_filter([
31+
Context::addHidden('request', array_filter([
3232
'url' => $request->url(),
3333
'full_url' => $request->fullUrl(),
3434
'method' => $request->method(),

src/Tracing/SessionCollector.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ public function collect(): void
2727
return;
2828
}
2929

30-
Context::add('session', [
30+
Context::addHidden('session', [
3131
'data' => $this->redactPayload(Session::all()),
3232
'flash' => [
3333
'old' => Session::get('_flash.old', []),

tests/Issues/TemplateRendererRouteTest.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
// Verify route and request data are in Context
4545
expect(Context::get('route'))->toHaveKey('name');
4646
expect(Context::get('route')['name'])->toBe('api.users.index');
47-
expect(Context::get('request'))->toHaveKey('url');
47+
expect(Context::getHidden('request'))->toHaveKey('url');
4848

4949
$record = createLogRecord('Test error');
5050
$record = ($this->processor)($record);
@@ -87,7 +87,7 @@
8787
expect(Context::get('user'))->toBe(['id' => 123, 'email' => 'user@example.com']);
8888
expect(Context::get('route'))->toHaveKey('name');
8989
expect(Context::get('route')['name'])->toBe('api.posts.store');
90-
expect(Context::get('request'))->toHaveKey('url');
90+
expect(Context::getHidden('request'))->toHaveKey('url');
9191

9292
$record = createLogRecord('Test error');
9393
$record = ($this->processor)($record);

tests/Tracing/ContextProcessorTest.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
it('merges context data into log record context', function () {
1919
// Arrange
2020
Context::add('user', ['id' => 123, 'name' => 'John']);
21-
Context::add('request', ['url' => 'https://example.com', 'method' => 'GET']);
21+
Context::addHidden('request', ['url' => 'https://example.com', 'method' => 'GET']);
2222

2323
$record = createLogRecord(
2424
'Test message',

tests/Tracing/EventHandlerTest.php

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@
4242

4343
Event::dispatch($event);
4444

45-
expect(Context::get('request'))->not->toBeNull();
46-
expect(Context::get('request')['url'])->toBe('https://example.com/test');
45+
expect(Context::getHidden('request'))->not->toBeNull();
46+
expect(Context::getHidden('request')['url'])->toBe('https://example.com/test');
4747
});
4848

4949
it('registers route collector when enabled', function () {
@@ -97,8 +97,8 @@
9797

9898
Event::dispatch($event);
9999

100-
expect(Context::get('queries'))->not->toBeNull();
101-
expect(Context::get('queries'))->toHaveCount(1);
100+
expect(Context::getHidden('queries'))->not->toBeNull();
101+
expect(Context::getHidden('queries'))->toHaveCount(1);
102102
});
103103

104104
it('registers job collector when enabled', function () {
@@ -151,7 +151,7 @@
151151

152152
// Check that outgoing request data was stored (using object hash as key)
153153
$requestId = spl_object_hash($request);
154-
expect(Context::get("outgoing_request.{$requestId}"))->not->toBeNull();
154+
expect(Context::getHidden("outgoing_request.{$requestId}"))->not->toBeNull();
155155

156156
// Mock the same request object for the response event
157157
$request->shouldReceive('url')->andReturn('https://example.com/api');
@@ -163,7 +163,7 @@
163163
$responseEvent = new ResponseReceived($request, $response);
164164
Event::dispatch($responseEvent);
165165

166-
$outgoingRequests = Context::get('outgoing_requests');
166+
$outgoingRequests = Context::getHidden('outgoing_requests');
167167
expect($outgoingRequests)->toHaveCount(1);
168168
expect($outgoingRequests[0]['url'])->toBe('https://example.com/api');
169169
expect($outgoingRequests[0]['status'])->toBe(200);
@@ -216,7 +216,7 @@
216216

217217
// Queries collector is disabled, so it should not have collected data
218218
// (Note: Context might have data from previous tests, but queries should be empty or unchanged)
219-
$queries = Context::get('queries', []);
219+
$queries = Context::getHidden('queries') ?? [];
220220
expect($queries)->toBeEmpty();
221221
});
222222

0 commit comments

Comments
 (0)