Skip to content

Commit dfd0f03

Browse files
committed
feat: create messenger logging bundle
1 parent 4bd135e commit dfd0f03

23 files changed

Lines changed: 1375 additions & 0 deletions

README.md

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Messenger Logging Bundle
2+
3+
Symfony bundle for Messenger-Lifecycle-Logging, suitable for monitoring.
4+
5+
## Installation
6+
7+
```bash
8+
composer require ckrack/messenger-logging-bundle
9+
```
10+
11+
If you are not using Symfony Flex, register the bundle manually in
12+
`config/bundles.php`.
13+
14+
The bundle targets Symfony `6.4`, `7.4`, and `8.0`.
15+
16+
## Configuration
17+
18+
```yaml
19+
c10k_messenger_logging:
20+
enabled: true
21+
log_channel: messenger
22+
log_levels:
23+
queued: info
24+
received: info
25+
handled: info
26+
failed: error
27+
retried: warning
28+
skipped: warning
29+
```
30+
31+
Alle PSR-3-Levels sind erlaubt. Wenn Failure-Logs in einer retry-lastigen
32+
Umgebung zu laut sind, kannst du `failed: info` setzen.
33+
34+
Wenn `log_channel` gesetzt ist, werden nur die von diesem Bundle erzeugten
35+
Logs auf diesen Monolog-Channel gelegt. Andere Projekt-Logs bleiben davon
36+
unberührt, solange sie nicht separat auf denselben Channel konfiguriert werden.
37+
Ohne `log_channel` bleibt das bisherige Default-Logger-Verhalten unverändert.
38+
39+
Das Bundle hängt sich an Messenger-Events und sorgt dafür, dass eine Message
40+
beim Queueing eine UUIDv7 erhält. Dieselbe UUID taucht dann in den Logs für
41+
Queueing, Consume, Success, Failure und Retry wieder auf, zusammen mit der
42+
Message-Klasse und dem vollständigen Stamp-Kontext.
43+
44+
Wenn die installierte Messenger-Version `WorkerMessageSkipEvent` unterstützt,
45+
werden auch übersprungene Messages geloggt.
46+
47+
## Local development
48+
49+
- Docker with Compose V2
50+
- pre-commit
51+
- GNU Make
52+
53+
```bash
54+
make setup
55+
make check
56+
make fix
57+
```
58+
59+
`make setup` installs Composer dependencies and both the `pre-commit` and
60+
`pre-push` hooks.

src/C10kMessengerLoggingBundle.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace C10k\MessengerLoggingBundle;
6+
7+
use Symfony\Component\HttpKernel\Bundle\Bundle;
8+
9+
final class C10kMessengerLoggingBundle extends Bundle
10+
{
11+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace C10k\MessengerLoggingBundle\DependencyInjection;
6+
7+
use C10k\MessengerLoggingBundle\EventSubscriber\SendMessageToTransportsEventSubscriber;
8+
use C10k\MessengerLoggingBundle\EventSubscriber\WorkerMessageFailedEventSubscriber;
9+
use C10k\MessengerLoggingBundle\EventSubscriber\WorkerMessageHandledEventSubscriber;
10+
use C10k\MessengerLoggingBundle\EventSubscriber\WorkerMessageReceivedEventSubscriber;
11+
use C10k\MessengerLoggingBundle\EventSubscriber\WorkerMessageRetriedEventSubscriber;
12+
use C10k\MessengerLoggingBundle\EventSubscriber\WorkerMessageSkipEventSubscriber;
13+
use Symfony\Component\Config\FileLocator;
14+
use Symfony\Component\DependencyInjection\ContainerBuilder;
15+
use Symfony\Component\DependencyInjection\Extension\Extension;
16+
use Symfony\Component\DependencyInjection\Loader\PhpFileLoader;
17+
use Symfony\Component\Messenger\Event\WorkerMessageSkipEvent;
18+
19+
final class C10kMessengerLoggingExtension extends Extension
20+
{
21+
/** @param array<int, array<string, mixed>> $configs */
22+
public function load(array $configs, ContainerBuilder $container): void
23+
{
24+
$configuration = new Configuration();
25+
$config = $this->processConfiguration($configuration, $configs);
26+
$enabled = (bool) $config['enabled'];
27+
/** @var string|null $logChannel */
28+
$logChannel = is_string($config['log_channel']) ? $config['log_channel'] : null;
29+
/** @var array<string, string> $logLevels */
30+
$logLevels = $config['log_levels'];
31+
32+
$container->setParameter('c10k_messenger_logging.enabled', $enabled);
33+
$container->setParameter('c10k_messenger_logging.log_channel', $logChannel);
34+
35+
foreach ($logLevels as $event => $logLevel) {
36+
$container->setParameter(
37+
'c10k_messenger_logging.log_levels.'.$event,
38+
$logLevel,
39+
);
40+
}
41+
42+
if ($enabled !== true) {
43+
return;
44+
}
45+
46+
$loader = new PhpFileLoader(
47+
$container,
48+
new FileLocator(__DIR__ . '/../Resources/config'),
49+
);
50+
51+
$loader->load('services.php');
52+
53+
if (!is_string($logChannel)) {
54+
return;
55+
}
56+
57+
foreach (self::subscriberServiceIds() as $subscriberServiceId) {
58+
$container
59+
->getDefinition($subscriberServiceId)
60+
->addTag('monolog.logger', ['channel' => $logChannel]);
61+
}
62+
}
63+
64+
/**
65+
* @return list<class-string>
66+
*/
67+
private static function subscriberServiceIds(): array
68+
{
69+
$subscriberServiceIds = [
70+
SendMessageToTransportsEventSubscriber::class,
71+
WorkerMessageReceivedEventSubscriber::class,
72+
WorkerMessageHandledEventSubscriber::class,
73+
WorkerMessageFailedEventSubscriber::class,
74+
WorkerMessageRetriedEventSubscriber::class,
75+
];
76+
77+
if (class_exists(WorkerMessageSkipEvent::class)) {
78+
$subscriberServiceIds[] = WorkerMessageSkipEventSubscriber::class;
79+
}
80+
81+
return $subscriberServiceIds;
82+
}
83+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace C10k\MessengerLoggingBundle\DependencyInjection;
6+
7+
use Psr\Log\LogLevel;
8+
use Symfony\Component\Config\Definition\Builder\TreeBuilder;
9+
use Symfony\Component\Config\Definition\ConfigurationInterface;
10+
11+
final class Configuration implements ConfigurationInterface
12+
{
13+
private const LOG_LEVELS = [
14+
LogLevel::EMERGENCY,
15+
LogLevel::ALERT,
16+
LogLevel::CRITICAL,
17+
LogLevel::ERROR,
18+
LogLevel::WARNING,
19+
LogLevel::NOTICE,
20+
LogLevel::INFO,
21+
LogLevel::DEBUG,
22+
];
23+
24+
/** @return TreeBuilder<'array'> */
25+
public function getConfigTreeBuilder(): TreeBuilder
26+
{
27+
$treeBuilder = new TreeBuilder('c10k_messenger_logging');
28+
29+
$treeBuilder
30+
->getRootNode()
31+
->children()
32+
->booleanNode('enabled')
33+
->defaultTrue()
34+
->end()
35+
->scalarNode('log_channel')
36+
->defaultNull()
37+
->cannotBeEmpty()
38+
->validate()
39+
->ifTrue(static fn (mixed $value): bool => $value !== null && !is_string($value))
40+
->thenInvalid('The "log_channel" option must be a string or null.')
41+
->end()
42+
->end()
43+
->arrayNode('log_levels')
44+
->addDefaultsIfNotSet()
45+
->children()
46+
->enumNode('queued')
47+
->values(self::LOG_LEVELS)
48+
->defaultValue(LogLevel::INFO)
49+
->end()
50+
->enumNode('received')
51+
->values(self::LOG_LEVELS)
52+
->defaultValue(LogLevel::INFO)
53+
->end()
54+
->enumNode('handled')
55+
->values(self::LOG_LEVELS)
56+
->defaultValue(LogLevel::INFO)
57+
->end()
58+
->enumNode('failed')
59+
->values(self::LOG_LEVELS)
60+
->defaultValue(LogLevel::ERROR)
61+
->end()
62+
->enumNode('retried')
63+
->values(self::LOG_LEVELS)
64+
->defaultValue(LogLevel::WARNING)
65+
->end()
66+
->enumNode('skipped')
67+
->values(self::LOG_LEVELS)
68+
->defaultValue(LogLevel::WARNING)
69+
->end()
70+
->end()
71+
->end()
72+
->end()
73+
;
74+
75+
return $treeBuilder;
76+
}
77+
}
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace C10k\MessengerLoggingBundle\EventSubscriber;
6+
7+
use C10k\MessengerLoggingBundle\Logging\MessengerLogContextBuilder;
8+
use Psr\Log\LoggerInterface;
9+
use Psr\Log\LogLevel;
10+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
11+
use Symfony\Component\Messenger\Event\SendMessageToTransportsEvent;
12+
13+
use function array_keys;
14+
15+
final class SendMessageToTransportsEventSubscriber implements EventSubscriberInterface
16+
{
17+
public function __construct(
18+
private readonly MessengerLogContextBuilder $contextBuilder,
19+
private readonly LoggerInterface|null $logger = null,
20+
private readonly string $logLevel = LogLevel::INFO,
21+
) {
22+
}
23+
24+
public function onQueued(SendMessageToTransportsEvent $event): void
25+
{
26+
$envelope = $this->contextBuilder->withUuid($event->getEnvelope());
27+
$event->setEnvelope($envelope);
28+
29+
$this->logger?->log(
30+
$this->logLevel,
31+
'Messenger message queued.',
32+
$this->contextBuilder->build(
33+
$envelope,
34+
[
35+
'sender_names' => array_keys($event->getSenders()),
36+
],
37+
),
38+
);
39+
}
40+
41+
public static function getSubscribedEvents(): array
42+
{
43+
return [
44+
SendMessageToTransportsEvent::class => 'onQueued',
45+
];
46+
}
47+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace C10k\MessengerLoggingBundle\EventSubscriber;
6+
7+
use C10k\MessengerLoggingBundle\Logging\MessengerLogContextBuilder;
8+
use Psr\Log\LoggerInterface;
9+
use Psr\Log\LogLevel;
10+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
11+
use Symfony\Component\Messenger\Event\WorkerMessageFailedEvent;
12+
13+
final class WorkerMessageFailedEventSubscriber implements EventSubscriberInterface
14+
{
15+
public function __construct(
16+
private readonly MessengerLogContextBuilder $contextBuilder,
17+
private readonly LoggerInterface|null $logger = null,
18+
private readonly string $logLevel = LogLevel::ERROR,
19+
) {
20+
}
21+
22+
public function onFailed(WorkerMessageFailedEvent $event): void
23+
{
24+
$this->contextBuilder->ensureUuidOnWorkerEvent($event);
25+
26+
$throwable = $event->getThrowable();
27+
28+
$this->logger?->log(
29+
$this->logLevel,
30+
'Messenger message failed.',
31+
$this->contextBuilder->build(
32+
$event->getEnvelope(),
33+
[
34+
'receiver_name' => $event->getReceiverName(),
35+
'will_retry' => $event->willRetry(),
36+
'exception_class' => $throwable::class,
37+
'exception_message' => $throwable->getMessage(),
38+
'exception_code' => $throwable->getCode(),
39+
],
40+
),
41+
);
42+
}
43+
44+
public static function getSubscribedEvents(): array
45+
{
46+
return [
47+
WorkerMessageFailedEvent::class => ['onFailed', 0],
48+
];
49+
}
50+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace C10k\MessengerLoggingBundle\EventSubscriber;
6+
7+
use C10k\MessengerLoggingBundle\Logging\MessengerLogContextBuilder;
8+
use Psr\Log\LoggerInterface;
9+
use Psr\Log\LogLevel;
10+
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
11+
use Symfony\Component\Messenger\Event\WorkerMessageHandledEvent;
12+
13+
final class WorkerMessageHandledEventSubscriber implements EventSubscriberInterface
14+
{
15+
public function __construct(
16+
private readonly MessengerLogContextBuilder $contextBuilder,
17+
private readonly LoggerInterface|null $logger = null,
18+
private readonly string $logLevel = LogLevel::INFO,
19+
) {
20+
}
21+
22+
public function onHandled(WorkerMessageHandledEvent $event): void
23+
{
24+
$this->contextBuilder->ensureUuidOnWorkerEvent($event);
25+
26+
$this->logger?->log(
27+
$this->logLevel,
28+
'Messenger message handled.',
29+
$this->contextBuilder->build(
30+
$event->getEnvelope(),
31+
[
32+
'receiver_name' => $event->getReceiverName(),
33+
],
34+
),
35+
);
36+
}
37+
38+
public static function getSubscribedEvents(): array
39+
{
40+
return [
41+
WorkerMessageHandledEvent::class => 'onHandled',
42+
];
43+
}
44+
}

0 commit comments

Comments
 (0)