Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add cron monitoring feature #774

Closed
wants to merge 4 commits into from
Closed
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
16 changes: 16 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,22 @@ The parameter `options` allows to fine-tune exceptions. To discover more options
[the Unified APIs](https://docs.sentry.io/development/sdk-dev/unified-api/#options) options and
the [PHP specific](https://docs.sentry.io/platforms/php/#php-specific-options) ones.

### Monitor scheduled jobs (crontabs)
Add cron monitoring slug and schedule to your command parameters. ref.
> --cron-monitor-slug=CRON-MONITOR-SLUG if command should be monitored then pass cron monitor slug
> --cron-monitor-schedule=CRON-MONITOR-SCHEDULE if command should be monitored then pass cron monitor schedule
> --cron-monitor-max-time=CRON-MONITOR-MAX-TIME if command should be monitored then pass cron monitor max execution time
> --cron-monitor-check-margin=CRON-MONITOR-CHECK-MARGIN if command should be monitored then pass cron monitor check margin
example usage in crontab
```
0 0 * * * user /app/bin/console app:statistics:update --cron-monitor-slug=statistics_update_midnight --cron-monitor-schedule "0 0 * * *"
```
Optionally you can also set max run time and check margin (see https://docs.sentry.io/platforms/php/crons/for ref.)

```
0 0 * * * user /app/bin/console app:statistics:update --cron-monitor-slug=statistics_update_midnight --cron-monitor-schedule "0 0 * * *" --cron-monitor-max-time=5 --cron-monitor-check-margin=2
```

#### Optional: use custom HTTP factory/transport

Since the SDK 2.0 uses HTTPlug to remain transport-agnostic, you need to install two packages that provide
Expand Down
71 changes: 71 additions & 0 deletions src/CronMonitoring/CronMonitor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\CronMonitoring;

use Sentry\CheckInStatus;
use Sentry\MonitorConfig;
use Sentry\State\HubInterface;

class CronMonitor
{
/**
* @var MonitorConfig
*/
private $monitorConfig;

/**
* @var HubInterface
*/
private $hub;

/**
* @var string
*/
private $slug;

/**
* @var string
*/
private $checkInId;

public function __construct(HubInterface $hub, MonitorConfig $monitorConfig, string $slug)
{
$this->hub = $hub;
$this->monitorConfig = $monitorConfig;
$this->slug = $slug;
}

public function start(): void
{
$this->checkInId = $this->hub->captureCheckIn(
$this->slug,
CheckInStatus::inProgress(),
null,
$this->monitorConfig
);
}

public function finishSuccess(): ?string
{
return $this->hub->captureCheckIn(
$this->slug,
CheckInStatus::OK(),
null,
$this->monitorConfig,
$this->checkInId
);
}

public function finishError(): ?string
{
return $this->hub->captureCheckIn(
$this->slug,
CheckInStatus::error(),
null,
$this->monitorConfig,
$this->checkInId
);
}
}
35 changes: 35 additions & 0 deletions src/CronMonitoring/CronMonitorFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\CronMonitoring;

use Sentry\MonitorConfig;
use Sentry\MonitorSchedule;
use Sentry\State\HubInterface;

class CronMonitorFactory
{
/**
* @var HubInterface
*/
private $hub;

public function __construct(HubInterface $hub)
{
$this->hub = $hub;
}

public function create(string $slug, string $schedule, ?int $checkMarginMinutes = null, ?int $maxRuntimeMinutes = null): CronMonitor
{
$monitorSchedule = MonitorSchedule::crontab($schedule);
$monitorConfig = new MonitorConfig(
$monitorSchedule,
$checkMarginMinutes,
$maxRuntimeMinutes,
date_default_timezone_get()
);

return new CronMonitor($this->hub, $monitorConfig, $slug);
}
}
66 changes: 66 additions & 0 deletions src/CronMonitoring/EventSubscriber/CronMonitorSubscriber.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\CronMonitoring\EventSubscriber;

use Sentry\SentryBundle\CronMonitoring\CronMonitor;
use Sentry\SentryBundle\CronMonitoring\CronMonitorFactory;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\ConsoleEvents;
use Symfony\Component\Console\Event\ConsoleCommandEvent;
use Symfony\Component\Console\Event\ConsoleTerminateEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class CronMonitorSubscriber implements EventSubscriberInterface
{
/**
* @var CronMonitorFactory
*/
private $cronMonitorFactory;

/**
* @var ?CronMonitor
*/
private $cronMonitor = null;

public function __construct(CronMonitorFactory $cronMonitorFactory)
{
$this->cronMonitorFactory = $cronMonitorFactory;
}

public function onConsoleCommandStart(ConsoleCommandEvent $event)
{
if (!$event->getInput()->hasOption('cron-monitor-slug')) {
return; // Cron monitor not enabled in application
}
$slug = $event->getInput()->getOption('cron-monitor-slug');
$schedule = $event->getInput()->getOption('cron-monitor-schedule');
$maxTime = $event->getInput()->getOption('cron-monitor-max-time');
$checkMargin = $event->getInput()->getOption('cron-monitor-check-margin');

if ($slug && $schedule) {
$this->cronMonitor = $this->cronMonitorFactory->create($slug, $schedule, $checkMargin ? (int) $checkMargin : null, $maxTime ? (int) $maxTime : null);
$this->cronMonitor->start();
}
}

public function onConsoleCommandTerminate(ConsoleTerminateEvent $event)
{
if ($this->cronMonitor) {
if (Command::SUCCESS === $event->getExitCode()) {
$this->cronMonitor->finishSuccess();
} else {
$this->cronMonitor->finishError();
}
}
}

public static function getSubscribedEvents(): array
{
return [
ConsoleEvents::COMMAND => 'onConsoleCommandStart',
ConsoleEvents::TERMINATE => 'onConsoleCommandTerminate',
];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\DependencyInjection\Compiler;

use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class AddCronMonitorOptionsCompilerPass implements CompilerPassInterface
{
public function process(ContainerBuilder $container)
{
$optionsArguments = [
['--cron-monitor-slug', null, InputOption::VALUE_REQUIRED, 'if command should be monitored then pass cron monitor slug'],
['--cron-monitor-schedule', null, InputOption::VALUE_REQUIRED, 'if command should be monitored then pass cron monitor schedule'],
['--cron-monitor-max-time', null, InputOption::VALUE_REQUIRED, 'if command should be monitored then pass cron monitor max execution time'],
['--cron-monitor-check-margin', null, InputOption::VALUE_REQUIRED, 'if command should be monitored then pass cron monitor check margin'],
];

$consoleCommands = $container->findTaggedServiceIds('console.command');
foreach ($consoleCommands as $name => $consoleCommand) {
$definition = $container->getDefinition($name);
foreach ($optionsArguments as $optionArguments) {
$definition->addMethodCall('addOption', $optionArguments);
}
}
}
}
3 changes: 3 additions & 0 deletions src/Resources/config/services.xml
Original file line number Diff line number Diff line change
Expand Up @@ -143,5 +143,8 @@
<service id="Sentry\SentryBundle\Twig\SentryExtension" class="Sentry\SentryBundle\Twig\SentryExtension">
<tag name="twig.extension" />
</service>

<service id="Sentry\SentryBundle\CronMonitoring\CronMonitorFactory" class="Sentry\SentryBundle\CronMonitoring\CronMonitorFactory" autowire="true" />
<service id="Sentry\SentryBundle\CronMonitoring\EventSubscriber\CronMonitorSubscriber" class="Sentry\SentryBundle\CronMonitoring\EventSubscriber\CronMonitorSubscriber" autowire="true" autoconfigure="true" />
</services>
</container>
2 changes: 2 additions & 0 deletions src/SentryBundle.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Sentry\SentryBundle;

use Sentry\SentryBundle\DependencyInjection\Compiler\AddCronMonitorOptionsCompilerPass;
use Sentry\SentryBundle\DependencyInjection\Compiler\AddLoginListenerTagPass;
use Sentry\SentryBundle\DependencyInjection\Compiler\CacheTracingPass;
use Sentry\SentryBundle\DependencyInjection\Compiler\DbalTracingPass;
Expand All @@ -25,5 +26,6 @@ public function build(ContainerBuilder $container): void
$container->addCompilerPass(new CacheTracingPass());
$container->addCompilerPass(new HttpClientTracingPass());
$container->addCompilerPass(new AddLoginListenerTagPass());
$container->addCompilerPass(new AddCronMonitorOptionsCompilerPass());
}
}
36 changes: 36 additions & 0 deletions tests/CronMonitoring/CronMonitorFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\Tests\CronMonitoring;

use PHPUnit\Framework\TestCase;
use Sentry\SentryBundle\CronMonitoring\CronMonitor;
use Sentry\SentryBundle\CronMonitoring\CronMonitorFactory;
use Sentry\State\HubInterface;

final class CronMonitorFactoryTest extends TestCase
{
/**
* @dataProvider createDataProvider
*/
public function testCreate(string $slug, string $schedule, ?int $checkMarginMinutes, ?int $maxRuntimeMinutes)
{
$hub = $this->createMock(HubInterface::class);

$cronMonitorFactory = new CronMonitorFactory($hub);
$cronMonitor = $cronMonitorFactory->create($slug, $schedule, $checkMarginMinutes, $maxRuntimeMinutes);

$this->assertInstanceOf(CronMonitor::class, $cronMonitor);
}

public function createDataProvider(): array
{
return [
['slug', '* * * * *', 1, 1],
['slug2', '* * * * *', null, 2],
['example_slug', '2 * * * *', 3, null],
['example_slug2', '2/5 * * * *', null, null],
];
}
}
76 changes: 76 additions & 0 deletions tests/CronMonitoring/CronMonitorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Sentry\SentryBundle\Tests\CronMonitoring;

use PHPUnit\Framework\TestCase;
use Sentry\CheckInStatus;
use Sentry\MonitorConfig;
use Sentry\MonitorSchedule;
use Sentry\SentryBundle\CronMonitoring\CronMonitor;
use Sentry\SentryBundle\Tests\Stubs\TestableHubInterface;

final class CronMonitorTest extends TestCase
{
/**
* @dataProvider monitorDataProvider
*/
public function testSuccess(string $slug, string $schedule, ?int $checkMarginMinutes, ?int $maxRuntimeMinutes, CheckInStatus $testedStatus)
{
// Arrange
$hub = $this->createMock(TestableHubInterface::class);
$monitorSchedule = MonitorSchedule::crontab($schedule);
$monitorConfig = new MonitorConfig(
$monitorSchedule,
$checkMarginMinutes,
$maxRuntimeMinutes,
date_default_timezone_get()
);

$checkInId = uniqid('checkInId');
$inProgressCalled = $finishCalled = false;
$hub
->expects($this->exactly(2))
->method('captureCheckIn')
->willReturnCallback(
function (string $callSlug, CheckInStatus $callStatus, ?int $callDuration, MonitorConfig $callMonitorConfig, $callCheckInId) use ($slug, $monitorConfig, $testedStatus, &$checkInId, &$inProgressCalled, &$finishCalled) {
if ($callSlug === $slug && $callStatus === CheckInStatus::inProgress() && null === $callDuration && $callMonitorConfig === $monitorConfig) {
$inProgressCalled = true;

return $checkInId;
}
if ($callSlug === $slug && $callStatus === $testedStatus && null === $callDuration && $callMonitorConfig === $monitorConfig && $callCheckInId === $checkInId) {
$finishCalled = true;

return null;
}
$this->fail('Unexpected call to Hub::captureCheckIn');
});

$cronMonitor = new CronMonitor($hub, $monitorConfig, $slug);
$cronMonitor->start();

// Act
if ($testedStatus === CheckInStatus::ok()) {
$cronMonitor->finishSuccess();
} else {
$cronMonitor->finishError();
}

// Assert
$this->assertTrue($inProgressCalled);
$this->assertTrue($finishCalled);
}

public function monitorDataProvider(): array
{
return [
['slug', '* * * * *', 1, 1, CheckInStatus::ok()],
['slug2', '* * * * *', null, 2, CheckInStatus::ok()],
['example_slug', '2 * * * *', 3, null, CheckInStatus::ok()],
['example_slug2', '2/5 * * * *', null, null, CheckInStatus::ok()],
['slug', '* * * * *', 1, 1, CheckInStatus::error()],
];
}
}
Loading