Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/php.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ jobs:
ports:
- 6379:6379
mongo:
image: mongo:8.0
image: mongo:8.2
ports:
- 27017:27017
env:
Expand Down
15 changes: 15 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,18 @@
## [11.3.0](https://github.com/anzusystems/common-bundle/compare/11.2.0...11.3.0) (2026-07-13)

### Features
* New `LogContextContentProcessor` — expands the JSON-encoded `LogContext::content` string back into an array for handlers that report structured context. Without it, structured log context (e.g. `['taskId' => 1, 'detail' => 'reason']`) reaches Sentry as one opaque JSON string inside `monolog.context.content` — not searchable and easy to miss. Registered as a plain service (no `monolog.processor` tag) on purpose, so the journal/audit mongo pipeline keeps persisting `content` as a string; apps push it onto their outbound handler:
```php
$services
->set('sentry.monolog.handler', SentryHandler::class)
->arg('$hub', service(HubInterface::class))
->arg('$level', Level::Warning)
->arg('$bubble', true)
->arg('$fillExtraContext', true)
->call('pushProcessor', [service(LogContextContentProcessor::class)])
;
```

## [11.2.0](https://github.com/anzusystems/common-bundle/compare/11.1.1...11.2.0) (2026-07-03)

### Features
Expand Down
2 changes: 1 addition & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ services:
hostname: redis

mongo:
image: mongo:8.0
image: mongo:8.2
command: --logappend ${MONGO_NOTABLESCAN:-}
env_file:
- .env.docker.dist
Expand Down
41 changes: 41 additions & 0 deletions src/Monolog/LogContextContentProcessor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

declare(strict_types=1);

namespace AnzuSystems\CommonBundle\Monolog;

use JsonException;
use Monolog\LogRecord;

/**
* Expands the JSON-encoded LogContext `content` string back into an array so handlers reporting
* structured context (e.g. the Sentry monolog handler with fillExtraContext) show the individual
* fields instead of one opaque JSON string.
*
* Not registered globally on purpose — the journal/audit mongo pipeline persists `content` as
* a string. Push it onto the specific outbound handler instead:
*
* $services->set('sentry.monolog.handler', SentryHandler::class)
* ->call('pushProcessor', [service(LogContextContentProcessor::class)]);
*/
final class LogContextContentProcessor
{
public function __invoke(LogRecord $record): LogRecord
{
$content = $record->context['content'] ?? null;
if (false === is_string($content) || '' === $content) {
return $record;
}

try {
$decodedContent = json_decode($content, associative: true, flags: JSON_THROW_ON_ERROR);
} catch (JsonException) {
return $record;
}
if (false === is_array($decodedContent)) {
return $record;
}

return $record->with(context: array_merge($record->context, ['content' => $decodedContent]));
}
}
3 changes: 3 additions & 0 deletions src/Resources/config/logs.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use AnzuSystems\CommonBundle\Messenger\Middleware\ContextIdentityMiddleware;
use AnzuSystems\CommonBundle\Messenger\MonologHandler\MessengerHandler;
use AnzuSystems\CommonBundle\Monolog\ContextProcessor;
use AnzuSystems\CommonBundle\Monolog\LogContextContentProcessor;
use AnzuSystems\CommonBundle\Repository\Mongo\AbstractAnzuMongoRepository;
use AnzuSystems\CommonBundle\Serializer\Service\BsonConverter;
use AnzuSystems\SerializerBundle\Metadata\MetadataRegistry;
Expand All @@ -35,6 +36,8 @@
$services->set(ContextProcessor::class)
->tag('monolog.processor');

$services->set(LogContextContentProcessor::class);

$services->set(LogContextFactory::class)
->arg('$userProvider', service(CurrentAnzuUserProvider::class))
->arg('$serializer', service(Serializer::class))
Expand Down
69 changes: 69 additions & 0 deletions tests/Monolog/LogContextContentProcessorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

declare(strict_types=1);

namespace AnzuSystems\CommonBundle\Tests\Monolog;

use AnzuSystems\CommonBundle\Monolog\LogContextContentProcessor;
use DateTimeImmutable;
use Monolog\Level;
use Monolog\LogRecord;
use PHPUnit\Framework\TestCase;

final class LogContextContentProcessorTest extends TestCase
{
private LogContextContentProcessor $processor;

protected function setUp(): void
{
$this->processor = new LogContextContentProcessor();
}

public function testDecodesJsonContentIntoArray(): void
{
$record = $this->createRecord([
'appSystem' => 'cms',
'content' => '{"taskId":123,"executionId":45,"detail":"missing_block_ids"}',
]);

$processed = ($this->processor)($record);

self::assertSame(
[
'taskId' => 123,
'executionId' => 45,
'detail' => 'missing_block_ids',
],
$processed->context['content'],
);
self::assertSame('cms', $processed->context['appSystem']);
// the original record stays untouched — only the handler pipeline sees the decoded copy
self::assertSame('{"taskId":123,"executionId":45,"detail":"missing_block_ids"}', $record->context['content']);
}

public function testKeepsRecordWhenContentIsNotDecodableToArray(): void
{
$emptyContent = $this->createRecord(['content' => '']);
$invalidJson = $this->createRecord(['content' => 'not a json']);
$scalarJson = $this->createRecord(['content' => '"just a string"']);
$noContent = $this->createRecord(['foo' => 'bar']);
$arrayContent = $this->createRecord(['content' => ['already' => 'decoded']]);

self::assertSame($emptyContent, ($this->processor)($emptyContent));
self::assertSame($invalidJson, ($this->processor)($invalidJson));
self::assertSame($scalarJson, ($this->processor)($scalarJson));
self::assertSame($noContent, ($this->processor)($noContent));
self::assertSame($arrayContent, ($this->processor)($arrayContent));
}

private function createRecord(array $context): LogRecord
{
return new LogRecord(
datetime: new DateTimeImmutable(),
channel: 'journal',
level: Level::Error,
message: '[RavenAi] Translate result cannot be applied',
context: $context,
);
}
}
Loading