Skip to content

Commit 7f69f4e

Browse files
Add attribute/tag to monitor cron commands
1 parent b4dc2ab commit 7f69f4e

18 files changed

+431
-0
lines changed
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
namespace Sentry\SentryBundle\Attribute;
4+
5+
#[\Attribute(\Attribute::TARGET_CLASS)]
6+
class SentryMonitorCommand
7+
{
8+
/**
9+
* @var string
10+
*/
11+
private $slug;
12+
13+
public function __construct(string $slug)
14+
{
15+
$this->slug = $slug;
16+
}
17+
18+
public function getSlug(): string
19+
{
20+
return $this->slug;
21+
}
22+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\DependencyInjection\Compiler;
6+
7+
use Sentry\SentryBundle\EventListener\CronMonitorListener;
8+
use Symfony\Component\DependencyInjection\Compiler\PriorityTaggedServiceTrait;
9+
use Symfony\Component\DependencyInjection\ContainerBuilder;
10+
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
11+
12+
class CronMonitorPass implements CompilerPassInterface
13+
{
14+
use PriorityTaggedServiceTrait;
15+
16+
public function process(ContainerBuilder $container): void
17+
{
18+
if (!$container->getParameter('sentry.cron.enabled')) {
19+
return;
20+
}
21+
22+
$commands = $this->findAndSortTaggedServices('sentry.monitor_command', $container);
23+
24+
$commandSlugMapping = [];
25+
foreach ($commands as $reference) {
26+
$id = $reference->__toString();
27+
foreach ($container->getDefinition($id)->getTag('sentry.monitor_command') as $attributes) {
28+
$commandSlugMapping[$id] = $attributes['slug'];
29+
}
30+
}
31+
32+
$container->getDefinition(CronMonitorListener::class)->setArgument(1, $commandSlugMapping);
33+
}
34+
}

src/DependencyInjection/Configuration.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@ public function getConfigTreeBuilder(): TreeBuilder
4646
->end()
4747
->booleanNode('register_error_listener')->defaultTrue()->end()
4848
->booleanNode('register_error_handler')->defaultTrue()->end()
49+
->booleanNode('register_cron_monitor')->defaultTrue()->end()
4950
->scalarNode('logger')
5051
->info('The service ID of the PSR-3 logger used to log messages coming from the SDK client. Be aware that setting the same logger of the application may create a circular loop when an event fails to be sent.')
5152
->defaultNull()

src/DependencyInjection/SentryExtension.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
use Sentry\Integration\RequestFetcherInterface;
1414
use Sentry\Integration\RequestIntegration;
1515
use Sentry\Options;
16+
use Sentry\SentryBundle\Attribute\SentryMonitorCommand;
1617
use Sentry\SentryBundle\EventListener\ConsoleListener;
18+
use Sentry\SentryBundle\EventListener\CronMonitorListener;
1719
use Sentry\SentryBundle\EventListener\ErrorListener;
1820
use Sentry\SentryBundle\EventListener\LoginListener;
1921
use Sentry\SentryBundle\EventListener\MessengerListener;
@@ -29,6 +31,7 @@
2931
use Symfony\Bundle\TwigBundle\TwigBundle;
3032
use Symfony\Component\Cache\CacheItem;
3133
use Symfony\Component\Config\FileLocator;
34+
use Symfony\Component\DependencyInjection\ChildDefinition;
3235
use Symfony\Component\DependencyInjection\ContainerBuilder;
3336
use Symfony\Component\DependencyInjection\Definition;
3437
use Symfony\Component\DependencyInjection\Loader;
@@ -77,6 +80,7 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container
7780
$this->registerTwigTracingConfiguration($container, $mergedConfig['tracing']);
7881
$this->registerCacheTracingConfiguration($container, $mergedConfig['tracing']);
7982
$this->registerHttpClientTracingConfiguration($container, $mergedConfig['tracing']);
83+
$this->registerCronMonitoringConfiguration($container, $mergedConfig);
8084

8185
if (!interface_exists(TokenStorageInterface::class)) {
8286
$container->removeDefinition(LoginListener::class);
@@ -285,6 +289,32 @@ private function registerHttpClientTracingConfiguration(ContainerBuilder $contai
285289
$container->setParameter('sentry.tracing.http_client.enabled', $isConfigEnabled);
286290
}
287291

292+
/**
293+
* @param array<string, mixed> $config
294+
*/
295+
private function registerCronMonitoringConfiguration(ContainerBuilder $container, array $config): void
296+
{
297+
$container->setParameter('sentry.cron.enabled', (bool) $config['register_cron_monitor']);
298+
299+
if (!$config['register_cron_monitor']) {
300+
$container->removeDefinition(CronMonitorListener::class);
301+
302+
return;
303+
}
304+
305+
if (\PHP_VERSION > 8.1) {
306+
$container->registerAttributeForAutoconfiguration(
307+
SentryMonitorCommand::class,
308+
static function (
309+
ChildDefinition $definition,
310+
SentryMonitorCommand $attribute
311+
) {
312+
$definition->addTag('sentry.monitor_command', ['slug' => $attribute->getSlug()]);
313+
}
314+
);
315+
}
316+
}
317+
288318
/**
289319
* @param string[] $integrations
290320
* @param array<string, mixed> $config
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Sentry\SentryBundle\EventListener;
6+
7+
use Sentry\CheckInStatus;
8+
use Sentry\State\HubInterface;
9+
use Symfony\Component\Console\Command\Command;
10+
use Symfony\Component\Console\Event\ConsoleCommandEvent;
11+
use Symfony\Component\Console\Event\ConsoleErrorEvent;
12+
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
13+
14+
class CronMonitorListener
15+
{
16+
/**
17+
* @var HubInterface
18+
*/
19+
private $hub;
20+
21+
/**
22+
* @var array<string>
23+
*/
24+
private $registeredCommands;
25+
26+
/**
27+
* @param HubInterface $hub
28+
* @param array<string> $registeredCommands
29+
*/
30+
public function __construct(HubInterface $hub, array $registeredCommands = [])
31+
{
32+
$this->hub = $hub;
33+
$this->registeredCommands = $registeredCommands;
34+
}
35+
36+
public function handleConsoleCommandEvent(ConsoleCommandEvent $event): void
37+
{
38+
$command = $event->getCommand();
39+
40+
if (false === $this->isValid($command)) {
41+
return;
42+
}
43+
44+
$checkinId = $this->hub->captureCheckIn(
45+
$this->registeredCommands[$this->getCommandIndex($command)],
46+
CheckInStatus::inProgress()
47+
);
48+
}
49+
50+
public function handleConsoleTerminateEvent(ConsoleTerminateEvent $event): void
51+
{
52+
$command = $event->getCommand();
53+
54+
if (false === $this->isValid($command)) {
55+
return;
56+
}
57+
58+
$this->hub->captureCheckIn(
59+
$this->registeredCommands[$this->getCommandIndex($command)],
60+
Command::SUCCESS === $event->getExitCode()
61+
? CheckInStatus::ok()
62+
: CheckInStatus::error()
63+
);
64+
}
65+
66+
public function handleConsoleErrorEvent(ConsoleErrorEvent $event): void
67+
{
68+
$command = $event->getCommand();
69+
70+
if (false === $this->isValid($command)) {
71+
return;
72+
}
73+
74+
$this->hub->captureCheckIn(
75+
$this->registeredCommands[$this->getCommandIndex($command)],
76+
CheckInStatus::error()
77+
);
78+
}
79+
80+
private function isValid(?Command $command): bool
81+
{
82+
return $command instanceof Command && isset($this->registeredCommands[$this->getCommandIndex($command)]);
83+
}
84+
85+
private function getCommandIndex(?Command $command): string
86+
{
87+
if (null === $command) {
88+
return '';
89+
}
90+
91+
if (\PHP_VERSION > 8.0) {
92+
return $command::class;
93+
}
94+
95+
return \get_class($command);
96+
}
97+
}

src/Resources/config/schema/sentry-1.0.xsd

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
<xsd:attribute name="register-error-listener" type="xsd:boolean" />
1818
<xsd:attribute name="register-error-handler" type="xsd:boolean" />
19+
<xsd:attribute name="register-cron-monitor" type="xsd:boolean" />
1920
<xsd:attribute name="transport-factory" type="xsd:string" />
2021
<xsd:attribute name="dsn" type="xsd:string" />
2122
<xsd:attribute name="logger" type="xsd:string" />

src/Resources/config/services.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,14 @@
2626
<tag name="kernel.event_listener" event="console.error" method="handleConsoleErrorEvent" priority="-64" />
2727
</service>
2828

29+
<service id="Sentry\SentryBundle\EventListener\CronMonitorListener" class="Sentry\SentryBundle\EventListener\CronMonitorListener">
30+
<argument type="service" id="Sentry\State\HubInterface" />
31+
32+
<tag name="kernel.event_listener" event="console.command" method="handleConsoleCommandEvent" priority="128" />
33+
<tag name="kernel.event_listener" event="console.terminate" method="handleConsoleTerminateEvent" priority="-64" />
34+
<tag name="kernel.event_listener" event="console.error" method="handleConsoleErrorEvent" priority="-64" />
35+
</service>
36+
2937
<service id="Sentry\SentryBundle\EventListener\ErrorListener" class="Sentry\SentryBundle\EventListener\ErrorListener">
3038
<argument type="service" id="Sentry\State\HubInterface" />
3139

src/SentryBundle.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
use Sentry\SentryBundle\DependencyInjection\Compiler\AddLoginListenerTagPass;
88
use Sentry\SentryBundle\DependencyInjection\Compiler\CacheTracingPass;
9+
use Sentry\SentryBundle\DependencyInjection\Compiler\CronMonitorPass;
910
use Sentry\SentryBundle\DependencyInjection\Compiler\DbalTracingPass;
1011
use Sentry\SentryBundle\DependencyInjection\Compiler\HttpClientTracingPass;
1112
use Symfony\Component\DependencyInjection\Compiler\PassConfig;
@@ -26,5 +27,6 @@ public function build(ContainerBuilder $container): void
2627
$container->addCompilerPass(new CacheTracingPass());
2728
$container->addCompilerPass(new HttpClientTracingPass());
2829
$container->addCompilerPass(new AddLoginListenerTagPass());
30+
$container->addCompilerPass(new CronMonitorPass());
2931
}
3032
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
<?php
2+
3+
namespace Sentry\SentryBundle\Tests\DependencyInjection\Compiler;
4+
5+
use PHPUnit\Framework\TestCase;
6+
use Sentry\SentryBundle\Command\SentryTestCommand;
7+
use Sentry\SentryBundle\DependencyInjection\Compiler\CronMonitorPass;
8+
use Sentry\SentryBundle\EventListener\CronMonitorListener;
9+
use Sentry\State\HubInterface;
10+
use Symfony\Component\DependencyInjection\ContainerBuilder;
11+
use Symfony\Component\DependencyInjection\Definition;
12+
13+
class CronMonitorPassTest extends TestCase
14+
{
15+
public function testProcess(): void
16+
{
17+
$container = $this->createContainerBuilder(true);
18+
19+
$container->setDefinition(
20+
SentryTestCommand::class,
21+
(new Definition(SentryTestCommand::class))
22+
->setPublic(false)
23+
->addTag('console.command')
24+
->addTag('sentry.monitor_command', ['slug' => 'test-command'])
25+
);
26+
27+
$container->compile();
28+
29+
$insertedCronMonitorListenerArgument = $container->getDefinition(CronMonitorListener::class)->getArgument(1);
30+
31+
$this->assertIsArray($insertedCronMonitorListenerArgument);
32+
$this->assertArrayHasKey(SentryTestCommand::class, $insertedCronMonitorListenerArgument);
33+
$this->assertEquals('test-command', $insertedCronMonitorListenerArgument[SentryTestCommand::class]);
34+
}
35+
36+
public function testProcessDoesNothingIfConditionsForEnablingCronIsFalse(): void
37+
{
38+
$container = $this->createContainerBuilder(false);
39+
$container->compile();
40+
41+
$this->assertFalse($container->getDefinition(CronMonitorListener::class)->getArgument(1));
42+
}
43+
44+
private function createContainerBuilder(bool $isCronActive): ContainerBuilder
45+
{
46+
$container = new ContainerBuilder();
47+
$container->addCompilerPass(new CronMonitorPass());
48+
$container->setParameter('sentry.cron.enabled', $isCronActive);
49+
50+
$cronMonitorListenerMock = $this->createMock(CronMonitorListener::class);
51+
52+
$container->setDefinition(CronMonitorListener::class, (new Definition(\get_class($cronMonitorListenerMock)))
53+
->setPublic(true))
54+
->setArgument(0, HubInterface::class)
55+
->setArgument(1, false);
56+
57+
return $container;
58+
}
59+
}

tests/DependencyInjection/ConfigurationTest.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ public function testProcessConfigurationWithDefaultConfiguration(): void
2222
$expectedBundleDefaultConfig = [
2323
'register_error_listener' => true,
2424
'register_error_handler' => true,
25+
'register_cron_monitor' => true,
2526
'logger' => null,
2627
'options' => [
2728
'integrations' => [],

0 commit comments

Comments
 (0)