diff --git a/README.md b/README.md index a5d064fd..3eea275d 100644 --- a/README.md +++ b/README.md @@ -577,6 +577,14 @@ liip_monitor: default_ttl: null logging: enabled: false + mailer: + enabled: false + recipient: [] # Required + sender: null + subject: 'Health Check Failed' + send_on_warning: false + send_on_skip: false + send_on_unknown: false checks: # fails/warns if system memory usage % is above thresholds diff --git a/composer.json b/composer.json index 8e3d4fcc..3f9b1a4f 100644 --- a/composer.json +++ b/composer.json @@ -32,11 +32,13 @@ "phpstan/phpstan": "^1.4", "phpunit/phpunit": "^9.5.0", "symfony/http-client": "^6.3", + "symfony/mailer": "^6.3|^7.0", "symfony/messenger": "^6.3|^7.0", "symfony/phpunit-bridge": "^6.3|^7.0", "symfony/process": "^6.3|^7.0", "symfony/var-dumper": "^6.0|^7.0", - "zenstruck/console-test": "^1.4" + "zenstruck/console-test": "^1.4", + "zenstruck/mailer-test": "^1.4" }, "config": { "preferred-install": "dist", diff --git a/config/mailer.php b/config/mailer.php new file mode 100644 index 00000000..aaf67f98 --- /dev/null +++ b/config/mailer.php @@ -0,0 +1,14 @@ +services() + ->set('.liip_monitor.mailer_subscriber', MailerSubscriber::class) + ->args([service('mailer')]) + ->tag('kernel.event_listener', ['event' => PostRunCheckSuiteEvent::class, 'method' => 'afterSuite']) + ; +}; diff --git a/src/DependencyInjection/Configuration.php b/src/DependencyInjection/Configuration.php index c5ed1fee..545a1cec 100644 --- a/src/DependencyInjection/Configuration.php +++ b/src/DependencyInjection/Configuration.php @@ -66,6 +66,30 @@ public function getConfigTreeBuilder(): TreeBuilder ->arrayNode('logging') ->canBeEnabled() ->end() + ->arrayNode('mailer') + ->canBeEnabled() + ->children() + ->arrayNode('recipient') + ->isRequired() + ->requiresAtLeastOneElement() + ->scalarPrototype()->end() + ->beforeNormalization() + ->ifString() + ->then(fn($v) => [$v]) + ->end() + ->end() + ->scalarNode('sender') + ->defaultNull() + ->end() + ->scalarNode('subject') + ->defaultValue('Health Check Failed') + ->cannotBeEmpty() + ->end() + ->booleanNode('send_on_warning')->defaultFalse()->end() + ->booleanNode('send_on_skip')->defaultFalse()->end() + ->booleanNode('send_on_unknown')->defaultFalse()->end() + ->end() + ->end() ->arrayNode('checks') ->children() ; diff --git a/src/DependencyInjection/LiipMonitorExtension.php b/src/DependencyInjection/LiipMonitorExtension.php index 41468eea..f0355c0e 100644 --- a/src/DependencyInjection/LiipMonitorExtension.php +++ b/src/DependencyInjection/LiipMonitorExtension.php @@ -87,6 +87,13 @@ protected function loadInternal(array $mergedConfig, ContainerBuilder $container $loader->load('logging.php'); } + if ($mergedConfig['mailer']['enabled']) { + $loader->load('mailer.php'); + $container->getDefinition('.liip_monitor.mailer_subscriber') + ->setArgument('$config', $mergedConfig['mailer']) + ; + } + $container->getDefinition('liip_monitor.check_registry') ->setArgument('$defaultTtl', $mergedConfig['default_ttl']) ; diff --git a/src/EventListener/MailerSubscriber.php b/src/EventListener/MailerSubscriber.php new file mode 100644 index 00000000..72a8022a --- /dev/null +++ b/src/EventListener/MailerSubscriber.php @@ -0,0 +1,88 @@ + + * + * @internal + */ +final class MailerSubscriber +{ + /** + * @param array{ + * recipient: string[], + * sender: ?string, + * subject: string, + * send_on_warning: bool, + * send_on_skip: bool, + * send_on_unknown: bool, + * } $config + */ + public function __construct(private MailerInterface $mailer, private array $config) + { + } + + public function afterSuite(PostRunCheckSuiteEvent $event): void + { + $results = $event->results(); + $failureStatuses = []; + + if ($this->config['send_on_warning']) { + $failureStatuses[] = Status::WARNING; + } + + if ($this->config['send_on_skip']) { + $failureStatuses[] = Status::SKIP; + } + + if ($this->config['send_on_unknown']) { + $failureStatuses[] = Status::UNKNOWN; + } + + $defects = $results->defects(...$failureStatuses); + + if (0 === $defects->count()) { + return; + } + + $body = \implode( + "\n", + \array_map( + static fn(ResultContext $result) => \sprintf( + '[%s] %s', + $result->check(), + $result, + ), + $defects->all(), + ), + ); + + $message = (new Email()) + ->to(...$this->config['recipient']) + ->subject($this->config['subject']) + ->text($body) + ; + + if ($this->config['sender']) { + $message->from($this->config['sender']); + } + + $this->mailer->send($message); + } +} diff --git a/tests/Fixture/CheckService2.php b/tests/Fixture/CheckService2.php index b345a688..2999f2be 100644 --- a/tests/Fixture/CheckService2.php +++ b/tests/Fixture/CheckService2.php @@ -28,6 +28,6 @@ public function __toString(): string public function run(): Result { - return Result::success(); + return Result::failure('failed'); } } diff --git a/tests/Fixture/TestKernel.php b/tests/Fixture/TestKernel.php index fb9621e2..d7a9838a 100644 --- a/tests/Fixture/TestKernel.php +++ b/tests/Fixture/TestKernel.php @@ -20,6 +20,7 @@ use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Kernel; use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator; +use Zenstruck\Mailer\Test\ZenstruckMailerTestBundle; /** * @author Kevin Bond @@ -32,6 +33,7 @@ public function registerBundles(): iterable { yield new FrameworkBundle(); yield new DoctrineBundle(); + yield new ZenstruckMailerTestBundle(); yield new LiipMonitorBundle(); } @@ -43,6 +45,9 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load 'router' => ['utf8' => true], 'test' => true, 'messenger' => true, + 'mailer' => [ + 'dsn' => 'null://null', + ], ]); $c->loadFromExtension('doctrine', [ @@ -56,6 +61,10 @@ protected function configureContainer(ContainerBuilder $c, LoaderInterface $load $c->loadFromExtension('liip_monitor', [ 'logging' => true, + 'mailer' => [ + 'sender' => 'admin@example.com', + 'recipient' => 'alerts@example.com', + ], 'checks' => [ 'system_memory_usage' => true, 'system_disk_usage' => true, diff --git a/tests/LiipMonitorBundleTest.php b/tests/LiipMonitorBundleTest.php index 5fa6e4e2..28668660 100644 --- a/tests/LiipMonitorBundleTest.php +++ b/tests/LiipMonitorBundleTest.php @@ -23,13 +23,15 @@ use Symfony\Component\Messenger\HandleTrait; use Symfony\Component\Messenger\MessageBusInterface; use Zenstruck\Console\Test\InteractsWithConsole; +use Zenstruck\Mailer\Test\InteractsWithMailer; +use Zenstruck\Mailer\Test\TestEmail; /** * @author Kevin Bond */ final class LiipMonitorBundleTest extends KernelTestCase { - use HandleTrait, InteractsWithConsole; + use HandleTrait, InteractsWithConsole, InteractsWithMailer; /** * @test @@ -73,6 +75,18 @@ public function execute_health_command(): void ->assertOutputContains('OK Check Service 1: Success') ; + $this->mailer() + ->assertEmailSentTo('alerts@example.com', function(TestEmail $email) { + $email + ->assertFrom('admin@example.com') + ->assertSubjectContains('Health Check Failed') + ->assertTextContains('[Custom Check Service 2] failed') + ; + }) + ->sentEmails() + ->assertCount(1) + ; + $this->executeConsoleCommand('monitor:health -v') // verbose ->assertOutputContains('19 check executed') ;