Skip to content

Commit 9430b06

Browse files
committed
Redirect stdout loggers to stderr preventing json response polutions
1 parent 3ccfd9a commit 9430b06

File tree

3 files changed

+104
-0
lines changed

3 files changed

+104
-0
lines changed

src/Command/SubprocessJobRunnerCommand.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use Cake\Console\Arguments;
2121
use Cake\Console\ConsoleIo;
2222
use Cake\Core\ContainerInterface;
23+
use Cake\Log\Log;
2324
use Cake\Queue\Job\Message;
2425
use Cake\Queue\Queue\Processor;
2526
use Enqueue\Null\NullConnectionFactory;
@@ -84,6 +85,11 @@ public function execute(Arguments $args, ConsoleIo $io): int
8485
return self::CODE_ERROR;
8586
}
8687

88+
// Redirect the configured logger to STDERR to keep STDOUT clean for JSON output
89+
if (isset($data['logger'])) {
90+
$this->redirectLoggerToStderr($data['logger']);
91+
}
92+
8793
try {
8894
$result = $this->executeJob($data);
8995
$this->outputResult($io, [
@@ -184,4 +190,43 @@ protected function outputResult(ConsoleIo $io, array $result): void
184190
$io->out($json);
185191
}
186192
}
193+
194+
/**
195+
* Redirect a specific logger to STDERR to keep STDOUT clean for JSON.
196+
*
197+
* Only redirects the 'stdout' logger to prevent it from corrupting JSON output.
198+
* Other loggers are left unchanged as they don't interfere with STDOUT.
199+
*
200+
* @param string $loggerName Logger name to redirect
201+
* @return void
202+
*/
203+
protected function redirectLoggerToStderr(string $loggerName): void
204+
{
205+
// Only redirect the 'stdout' logger, leave all others untouched
206+
if ($loggerName !== 'stdout') {
207+
return;
208+
}
209+
210+
$config = Log::getConfig($loggerName);
211+
if ($config === null) {
212+
// Logger not configured, nothing to redirect
213+
return;
214+
}
215+
216+
$className = $config['className'] ?? null;
217+
if (!is_string($className)) {
218+
// Logger is an object instance, we can't reconfigure it
219+
return;
220+
}
221+
222+
// Only redirect console loggers that write to STDOUT
223+
if (str_contains($className, 'Console')) {
224+
$stream = $config['stream'] ?? 'php://stdout';
225+
if ($stream === 'php://stdout') {
226+
$config['stream'] = 'php://stderr';
227+
Log::drop($loggerName);
228+
Log::setConfig($loggerName, $config);
229+
}
230+
}
231+
}
187232
}

src/Queue/SubprocessProcessor.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ protected function prepareJobData(QueueMessage $message): array
185185
'messageClass' => get_class($message),
186186
'body' => $body,
187187
'properties' => $properties,
188+
'logger' => $this->config['logger'] ?? null,
188189
];
189190
}
190191

tests/TestCase/Command/SubprocessJobRunnerCommandTest.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
use Enqueue\Null\NullMessage;
2525
use Interop\Queue\Processor;
2626
use ReflectionClass;
27+
use TestApp\Job\LogToDebugJob;
2728
use TestApp\TestProcessor;
2829

2930
class SubprocessJobRunnerCommandTest extends TestCase
@@ -252,4 +253,61 @@ public function testOutputResult(): void
252253

253254
$method->invoke($command, $io, ['success' => true, 'result' => 'ack']);
254255
}
256+
257+
/**
258+
* Test that logs are redirected to STDERR and don't corrupt JSON output
259+
*/
260+
public function testLogsRedirectedToStderr(): void
261+
{
262+
$jobData = [
263+
'messageClass' => NullMessage::class,
264+
'body' => [
265+
'class' => [LogToDebugJob::class, 'execute'],
266+
'args' => [['foo' => 'bar']],
267+
],
268+
'properties' => [],
269+
];
270+
271+
$command = 'php ' . ROOT . 'bin/cake.php queue subprocess-runner';
272+
273+
$descriptors = [
274+
0 => ['pipe', 'r'],
275+
1 => ['pipe', 'w'],
276+
2 => ['pipe', 'w'],
277+
];
278+
279+
$process = proc_open($command, $descriptors, $pipes);
280+
$this->assertIsResource($process);
281+
282+
// Write job data to STDIN
283+
$jobDataJson = json_encode($jobData);
284+
if ($jobDataJson !== false) {
285+
fwrite($pipes[0], $jobDataJson);
286+
}
287+
288+
fclose($pipes[0]);
289+
290+
// Read STDOUT (should be clean JSON only)
291+
$stdout = stream_get_contents($pipes[1]);
292+
fclose($pipes[1]);
293+
294+
// Read STDERR to prevent pipe blocking
295+
stream_get_contents($pipes[2]);
296+
fclose($pipes[2]);
297+
298+
proc_close($process);
299+
300+
// STDOUT should be valid JSON without any log messages
301+
$result = json_decode($stdout, true);
302+
$this->assertIsArray($result, 'STDOUT should contain valid JSON: ' . $stdout);
303+
$this->assertArrayHasKey('success', $result);
304+
$this->assertTrue($result['success']);
305+
$this->assertSame(Processor::ACK, $result['result']);
306+
307+
// STDOUT should not contain log text
308+
$this->assertStringNotContainsString('Debug job was run', $stdout);
309+
310+
// STDERR may contain log messages (depends on log configuration)
311+
// We just verify it doesn't interfere with JSON parsing
312+
}
255313
}

0 commit comments

Comments
 (0)