Skip to content

Commit c910b9a

Browse files
Naorayclaude
andcommitted
fix(tracing): prevent large context data from serializing into job payloads
Use Context::addHidden() for large tracing data (queries, outgoing_requests, session, request) to prevent them from being serialized into queued job payloads. This fixes Redis OOM errors caused by ~6.8 MB job payloads when query tracing is enabled. Changes: - QueryCollector: use getHidden/addHidden for queries - OutgoingRequestSendingCollector: use addHidden for request data - OutgoingRequestResponseCollector: use getHidden/addHidden/forgetHidden - SessionCollector: use addHidden for session data - RequestDataCollector: use addHidden for request data - ContextProcessor: merge both regular and hidden context for logging - GithubMonologServiceProvider: add dehydrating callback as safety net Hidden context is available during the request for error logging but is NOT serialized into job payloads, keeping job sizes at ~10-50 KB instead of MB. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent b686091 commit c910b9a

16 files changed

+385
-79
lines changed

src/GithubMonologServiceProvider.php

Lines changed: 196 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,24 +2,218 @@
22

33
namespace Naoray\LaravelGithubMonolog;
44

5+
use Illuminate\Contracts\Config\Repository;
6+
use Illuminate\Support\Facades\Context;
57
use Illuminate\Support\Facades\Event;
68
use Illuminate\Support\ServiceProvider;
79
use Naoray\LaravelGithubMonolog\Tracing\EventHandler;
10+
use Naoray\LaravelGithubMonolog\Tracing\LivewireDataCollector;
11+
use Naoray\LaravelGithubMonolog\Tracing\UserDataCollector;
812

913
class GithubMonologServiceProvider extends ServiceProvider
1014
{
15+
public function register(): void
16+
{
17+
$this->mergeConfigFrom(__DIR__.'/../config/github-monolog.php', 'github-monolog');
18+
$this->registerLogger();
19+
$this->registerTracingDefaults();
20+
$this->registerContextDehydration();
21+
}
22+
1123
public function boot(): void
1224
{
13-
$config = config('logging.channels.github.tracing');
25+
$this->registerTracingSubscribers();
26+
$this->registerLivewireHooks();
27+
$this->registerPublishables();
28+
$this->registerOctaneListeners();
29+
}
30+
31+
/**
32+
* Register the github logging channel if enabled.
33+
*
34+
* This follows Nightwatch's pattern of auto-registering the logging channel
35+
* without requiring explicit configuration.
36+
*/
37+
protected function registerLogger(): void
38+
{
39+
/** @var Repository $config */
40+
$config = $this->app->make(Repository::class);
41+
42+
// Check if github-monolog is enabled (defaults to true for zero-config)
43+
if (! $config->get('github-monolog.enabled', true)) {
44+
return;
45+
}
46+
47+
// Auto-register the github logging channel if not already configured
48+
if (! $config->has('logging.channels.github')) {
49+
$config->set('logging.channels.github', [
50+
'driver' => 'custom',
51+
'via' => GithubIssueHandlerFactory::class,
52+
'repo' => $config->get('github-monolog.repo'),
53+
'token' => $config->get('github-monolog.token'),
54+
'labels' => $config->get('github-monolog.labels', []),
55+
'level' => $config->get('github-monolog.level', 'error'),
56+
'signature_generator' => $config->get('github-monolog.signature_generator'),
57+
'deduplication' => $config->get('github-monolog.deduplication', []),
58+
'buffer' => $config->get('github-monolog.buffer', []),
59+
'tracing' => $config->get('github-monolog.tracing', []),
60+
]);
61+
}
62+
}
63+
64+
/**
65+
* Register zero-config defaults for tracing.
66+
*
67+
* This follows Nightwatch's pattern of setting sensible defaults
68+
* without requiring explicit configuration.
69+
*/
70+
protected function registerTracingDefaults(): void
71+
{
72+
/** @var Repository $config */
73+
$config = $this->app->make(Repository::class);
1474

15-
if (isset($config['enabled']) && $config['enabled']) {
75+
// Skip if github-monolog is disabled
76+
if (! $config->get('github-monolog.enabled', true)) {
77+
return;
78+
}
79+
80+
// Skip if github channel doesn't exist (user may have disabled auto-registration)
81+
if (! $config->has('logging.channels.github')) {
82+
return;
83+
}
84+
85+
// Merge tracing config from github-monolog.tracing into logging.channels.github.tracing
86+
// This ensures users can configure tracing in either location
87+
$packageTracingConfig = $config->get('github-monolog.tracing', []);
88+
$channelTracingConfig = $config->get('logging.channels.github.tracing', []);
89+
90+
// Package config takes precedence over channel config
91+
$mergedConfig = array_merge($channelTracingConfig, $packageTracingConfig);
92+
93+
$config->set('logging.channels.github.tracing', $mergedConfig);
94+
}
95+
96+
/**
97+
* Register tracing event subscribers.
98+
*/
99+
protected function registerTracingSubscribers(): void
100+
{
101+
// Check if package is enabled
102+
if (! config('github-monolog.enabled', true)) {
103+
return;
104+
}
105+
106+
// Check tracing config from either location (package config takes precedence)
107+
$tracingEnabled = config('github-monolog.tracing.enabled')
108+
?? config('logging.channels.github.tracing.enabled', true);
109+
110+
if ($tracingEnabled) {
16111
Event::subscribe(EventHandler::class);
17112
}
113+
}
18114

115+
/**
116+
* Register Livewire hooks if Livewire is available.
117+
*
118+
* This follows Nightwatch's pattern of registering Livewire hooks
119+
* directly in the service provider.
120+
*/
121+
protected function registerLivewireHooks(): void
122+
{
123+
// Check if package is enabled
124+
if (! config('github-monolog.enabled', true)) {
125+
return;
126+
}
127+
128+
// Check if Livewire tracing is enabled (package config takes precedence)
129+
$livewireEnabled = config('github-monolog.tracing.livewire')
130+
?? config('logging.channels.github.tracing.livewire', true);
131+
132+
if (! $livewireEnabled) {
133+
return;
134+
}
135+
136+
$this->app->booted(static function ($app) {
137+
// Check if Livewire is available
138+
if (! class_exists('Livewire\Livewire')) {
139+
return;
140+
}
141+
142+
if (! $app->bound('Livewire\LivewireManager')) {
143+
return;
144+
}
145+
146+
$collector = new LivewireDataCollector;
147+
148+
if (! $collector->isEnabled()) {
149+
return;
150+
}
151+
152+
// Use call_user_func to avoid static analysis issues with optional dependencies
153+
// Livewire 2: component.hydrate.subsequent
154+
call_user_func(['Livewire\Livewire', 'listen'], 'component.hydrate.subsequent', fn ($component) => rescue(
155+
fn () => $collector->componentHydrateSubsequent($component)
156+
));
157+
158+
// Livewire 3: hydrate
159+
call_user_func(['Livewire\Livewire', 'listen'], 'hydrate', fn ($component) => rescue(
160+
fn () => $collector->hydrate($component)
161+
));
162+
});
163+
}
164+
165+
/**
166+
* Register publishable assets.
167+
*/
168+
protected function registerPublishables(): void
169+
{
19170
if ($this->app->runningInConsole()) {
171+
$this->publishes([
172+
__DIR__.'/../config/github-monolog.php' => config_path('github-monolog.php'),
173+
], 'github-monolog-config');
174+
20175
$this->publishes([
21176
__DIR__.'/../resources/views' => resource_path('views/vendor/github-monolog'),
22177
], 'github-monolog-views');
23178
}
24179
}
180+
181+
/**
182+
* Register context dehydration callback to prevent large tracing data from being serialized into job payloads.
183+
*/
184+
protected function registerContextDehydration(): void
185+
{
186+
if (! method_exists(Context::class, 'dehydrating')) {
187+
return;
188+
}
189+
190+
Context::dehydrating(function ($context) {
191+
foreach (['queries', 'outgoing_requests', 'session', 'request'] as $key) {
192+
if ($context->has($key)) {
193+
$context->forget($key);
194+
}
195+
}
196+
197+
foreach (array_keys($context->all()) as $key) {
198+
if (str_starts_with($key, 'outgoing_request.')) {
199+
$context->forget($key);
200+
}
201+
}
202+
});
203+
}
204+
205+
/**
206+
* Register Octane listeners for proper state management.
207+
*/
208+
protected function registerOctaneListeners(): void
209+
{
210+
if (! class_exists(\Laravel\Octane\Events\RequestReceived::class)) {
211+
return;
212+
}
213+
214+
// Flush user data between requests in Octane
215+
Event::listen(\Laravel\Octane\Events\RequestReceived::class, function () {
216+
UserDataCollector::flush();
217+
});
218+
}
25219
}

src/Tracing/ContextProcessor.php

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

31-
$contextData = Context::all();
31+
// Note: Livewire data is captured via event listeners (EventHandler)
32+
// and will already be in Context when this processor runs
33+
34+
$contextData = array_merge(Context::all(), Context::allHidden());
3235

3336
if (empty($contextData)) {
3437
return $record;
3538
}
3639

40+
// Clean up internal context keys that shouldn't be in the final output
41+
unset($contextData['livewire_originating_page']);
42+
3743
return $record->with(
3844
context: array_merge($record->context, $contextData)
3945
);

src/Tracing/OutgoingRequestResponseCollector.php

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,17 +4,18 @@
44

55
use Illuminate\Http\Client\Events\ResponseReceived;
66
use Illuminate\Support\Facades\Context;
7+
use Naoray\LaravelGithubMonolog\Tracing\Concerns\ResolvesTracingConfig;
78
use Naoray\LaravelGithubMonolog\Tracing\Contracts\EventDrivenCollectorInterface;
89

910
class OutgoingRequestResponseCollector implements EventDrivenCollectorInterface
1011
{
11-
private const DEFAULT_LIMIT = 5;
12+
use ResolvesTracingConfig;
13+
14+
private const DEFAULT_LIMIT = 20;
1215

1316
public function isEnabled(): bool
1417
{
15-
$config = config('logging.channels.github.tracing.outgoing_requests', []);
16-
17-
return isset($config['enabled']) && $config['enabled'];
18+
return $this->isTracingFeatureEnabled('outgoing_requests');
1819
}
1920

2021
public function __invoke(ResponseReceived $event): void
@@ -27,14 +28,13 @@ public function __invoke(ResponseReceived $event): void
2728
$response = $event->response;
2829
$requestId = spl_object_hash($request);
2930

30-
$config = config('logging.channels.github.tracing.outgoing_requests', []);
31-
$limit = $config['limit'] ?? self::DEFAULT_LIMIT;
31+
$limit = (int) $this->getTracingConfig('outgoing_request_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: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
use Illuminate\Http\Client\Events\RequestSending;
66
use Illuminate\Support\Facades\Context;
77
use Naoray\LaravelGithubMonolog\Tracing\Concerns\RedactsData;
8+
use Naoray\LaravelGithubMonolog\Tracing\Concerns\ResolvesTracingConfig;
89
use Naoray\LaravelGithubMonolog\Tracing\Contracts\EventDrivenCollectorInterface;
910

1011
class OutgoingRequestSendingCollector implements EventDrivenCollectorInterface
1112
{
1213
use RedactsData;
14+
use ResolvesTracingConfig;
1315

1416
public function isEnabled(): bool
1517
{
16-
$config = config('logging.channels.github.tracing.outgoing_requests', []);
17-
18-
return isset($config['enabled']) && $config['enabled'];
18+
return $this->isTracingFeatureEnabled('outgoing_requests');
1919
}
2020

2121
public function __invoke(RequestSending $event): void
@@ -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: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,19 @@
55
use Illuminate\Database\Events\QueryExecuted;
66
use Illuminate\Support\Facades\Context;
77
use Naoray\LaravelGithubMonolog\Tracing\Concerns\RedactsData;
8+
use Naoray\LaravelGithubMonolog\Tracing\Concerns\ResolvesTracingConfig;
89
use Naoray\LaravelGithubMonolog\Tracing\Contracts\EventDrivenCollectorInterface;
910

1011
class QueryCollector implements EventDrivenCollectorInterface
1112
{
1213
use RedactsData;
14+
use ResolvesTracingConfig;
1315

14-
private const DEFAULT_LIMIT = 10;
16+
private const DEFAULT_LIMIT = 50;
1517

1618
public function isEnabled(): bool
1719
{
18-
$config = config('logging.channels.github.tracing.queries', []);
19-
20-
return isset($config['enabled']) && $config['enabled'];
20+
return $this->isTracingFeatureEnabled('queries');
2121
}
2222

2323
public function __invoke(QueryExecuted $event): void
@@ -26,11 +26,9 @@ public function __invoke(QueryExecuted $event): void
2626
return;
2727
}
2828

29-
$config = config('logging.channels.github.tracing.queries', []);
30-
31-
$limit = $config['limit'] ?? self::DEFAULT_LIMIT;
29+
$limit = (int) $this->getTracingConfig('query_limit', self::DEFAULT_LIMIT);
3230

33-
$queries = Context::get('queries', []);
31+
$queries = Context::getHidden('queries') ?? [];
3432
$queries[] = [
3533
'sql' => $event->sql,
3634
'bindings' => $this->redactBindings($event->bindings),
@@ -43,6 +41,6 @@ public function __invoke(QueryExecuted $event): void
4341
$queries = array_slice($queries, -$limit);
4442
}
4543

46-
Context::add('queries', $queries);
44+
Context::addHidden('queries', $queries);
4745
}
4846
}

src/Tracing/RequestDataCollector.php

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,17 +5,17 @@
55
use Illuminate\Foundation\Http\Events\RequestHandled;
66
use Illuminate\Support\Facades\Context;
77
use Naoray\LaravelGithubMonolog\Tracing\Concerns\RedactsData;
8+
use Naoray\LaravelGithubMonolog\Tracing\Concerns\ResolvesTracingConfig;
89
use Naoray\LaravelGithubMonolog\Tracing\Contracts\EventDrivenCollectorInterface;
910

1011
class RequestDataCollector implements EventDrivenCollectorInterface
1112
{
1213
use RedactsData;
14+
use ResolvesTracingConfig;
1315

1416
public function isEnabled(): bool
1517
{
16-
$config = config('logging.channels.github.tracing', []);
17-
18-
return isset($config['requests']) && $config['requests'];
18+
return $this->isTracingFeatureEnabled('requests');
1919
}
2020

2121
public function __invoke(RequestHandled $event): void
@@ -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(),

0 commit comments

Comments
 (0)