From 695b614df25d9bce219823b99b3471f60e336583 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 22 Oct 2024 14:18:32 +0200 Subject: [PATCH 1/6] Add health checks --- CHANGELOG.md | 6 +++ composer.json | 15 ++++--- config/routes.yaml | 3 ++ config/routes/api.yaml | 7 +++ src/Controller/HealthApiController.php | 61 ++++++++++++++++++++++++++ src/Event/HealthCheckLiveEvent.php | 46 +++++++++++++++++++ src/Event/HealthCheckReadyEvent.php | 38 ++++++++++++++++ 7 files changed, 169 insertions(+), 7 deletions(-) create mode 100644 config/routes.yaml create mode 100644 config/routes/api.yaml create mode 100644 src/Controller/HealthApiController.php create mode 100644 src/Event/HealthCheckLiveEvent.php create mode 100644 src/Event/HealthCheckReadyEvent.php diff --git a/CHANGELOG.md b/CHANGELOG.md index eb31203..ee805eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +3.2.0 +===== + +* (feature) Add health checks (live + ready). + + 3.1.0 ===== diff --git a/composer.json b/composer.json index 72fd53e..479b9ae 100644 --- a/composer.json +++ b/composer.json @@ -20,19 +20,20 @@ "21torr/cli": "^1.2.3", "psr/log": "^3.0", "symfony/cache-contracts": "^3.5", - "symfony/clock": "^7.0", - "symfony/config": "^7.0", - "symfony/console": "^7.0", - "symfony/dependency-injection": "^7.0", + "symfony/clock": "^7.1", + "symfony/config": "^7.1", + "symfony/console": "^7.1", + "symfony/dependency-injection": "^7.1", "symfony/event-dispatcher-contracts": "^3.5", - "symfony/filesystem": "^7.0", - "symfony/process": "^7.0" + "symfony/framework-bundle": "^7.1", + "symfony/filesystem": "^7.1", + "symfony/process": "^7.1" }, "require-dev": { "21torr/janus": "^1.3.4", "bamarni/composer-bin-plugin": "^1.8", "roave/security-advisories": "dev-latest", - "symfony/phpunit-bridge": "^7.0" + "symfony/phpunit-bridge": "^7.1" }, "replace": { "symfony/polyfill-ctype": "*", diff --git a/config/routes.yaml b/config/routes.yaml new file mode 100644 index 0000000..041b506 --- /dev/null +++ b/config/routes.yaml @@ -0,0 +1,3 @@ +_import.hosting.api: + prefix: /_h + resource: routes/api.yaml diff --git a/config/routes/api.yaml b/config/routes/api.yaml new file mode 100644 index 0000000..25660e8 --- /dev/null +++ b/config/routes/api.yaml @@ -0,0 +1,7 @@ +hosting.api.live: + path: /live + controller: Torr\Hosting\Controller\HealthApiController::liveEndpoint + +hosting.api.ready: + path: /ready + controller: Torr\Hosting\Controller\HealthApiController::readyEndpoint diff --git a/src/Controller/HealthApiController.php b/src/Controller/HealthApiController.php new file mode 100644 index 0000000..953dec5 --- /dev/null +++ b/src/Controller/HealthApiController.php @@ -0,0 +1,61 @@ +runHealthCheck( + $dispatcher->dispatch(new HealthCheckLiveEvent()), + ); + } + + + /** + * + */ + public function readyEndpoint ( + EventDispatcherInterface $dispatcher, + ) : Response + { + return $this->runHealthCheck( + $dispatcher->dispatch(new HealthCheckReadyEvent()), + ); + } + + /** + * + */ + private function runHealthCheck ( + HealthCheckLiveEvent|HealthCheckReadyEvent $event, + ) : JsonResponse + { + if (!$event->isHealthy()) + { + return $this->json([ + "ok" => false, + "failed" => $event->getFailedCheck(), + ], 503); + } + + return $this->json([ + "ok" => true, + ]); + } +} diff --git a/src/Event/HealthCheckLiveEvent.php b/src/Event/HealthCheckLiveEvent.php new file mode 100644 index 0000000..d9a7f6a --- /dev/null +++ b/src/Event/HealthCheckLiveEvent.php @@ -0,0 +1,46 @@ +failedCheck = $failedCheck; + $this->stopPropagation(); + } + + /** + * + */ + public function isHealthy () : bool + { + return null === $this->failedCheck; + } + + /** + * + */ + public function getFailedCheck () : ?string + { + return $this->failedCheck; + } +} diff --git a/src/Event/HealthCheckReadyEvent.php b/src/Event/HealthCheckReadyEvent.php new file mode 100644 index 0000000..d0a0556 --- /dev/null +++ b/src/Event/HealthCheckReadyEvent.php @@ -0,0 +1,38 @@ +failedCheck = $failedCheck; + $this->stopPropagation(); + } + + /** + * + */ + public function isHealthy () : bool + { + return null === $this->failedCheck; + } + + /** + * + */ + public function getFailedCheck () : ?string + { + return $this->failedCheck; + } +} From 12a2a825bf8a987d720565be1e089a9d2661defe Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 22 Oct 2024 16:30:18 +0200 Subject: [PATCH 2/6] Bump Janus and update CI --- CHANGELOG.md | 1 + composer.json | 6 +++--- phpstan.neon | 2 -- src/BuildInfo/BuildInfoStorage.php | 4 ++-- src/Command/BuildHooksCommand.php | 2 +- src/Command/DeployHooksCommand.php | 2 +- src/Command/ShowBuildInfoCommand.php | 2 +- src/Controller/HealthApiController.php | 1 - src/Deployment/TaskCli.php | 2 +- 9 files changed, 10 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ee805eb..15b8457 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ===== * (feature) Add health checks (live + ready). +* (improvement) Bump Janus and update CI. 3.1.0 diff --git a/composer.json b/composer.json index 479b9ae..800608f 100644 --- a/composer.json +++ b/composer.json @@ -25,12 +25,12 @@ "symfony/console": "^7.1", "symfony/dependency-injection": "^7.1", "symfony/event-dispatcher-contracts": "^3.5", - "symfony/framework-bundle": "^7.1", "symfony/filesystem": "^7.1", + "symfony/framework-bundle": "^7.1", "symfony/process": "^7.1" }, "require-dev": { - "21torr/janus": "^1.3.4", + "21torr/janus": "^1.4", "bamarni/composer-bin-plugin": "^1.8", "roave/security-advisories": "dev-latest", "symfony/phpunit-bridge": "^7.1" @@ -94,4 +94,4 @@ "vendor-bin/phpstan/vendor/bin/phpstan analyze -c phpstan.neon . --ansi -v" ] } -} +} \ No newline at end of file diff --git a/phpstan.neon b/phpstan.neon index c97c69f..7b95bf7 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -1,8 +1,6 @@ includes: - vendor/21torr/janus/phpstan/lib.neon -# If you use simple-phpunit, you need to uncomment the following line. -# Always make sure to first run simple-phpunit and then PHPStan. parameters: bootstrapFiles: - vendor/bin/.phpunit/phpunit/vendor/autoload.php diff --git a/src/BuildInfo/BuildInfoStorage.php b/src/BuildInfo/BuildInfoStorage.php index 984ff81..a8c5912 100644 --- a/src/BuildInfo/BuildInfoStorage.php +++ b/src/BuildInfo/BuildInfoStorage.php @@ -65,7 +65,7 @@ private function loadBuildInfo () : BuildInfo catch (\JsonException $exception) { throw new InvalidBuildInfoException( - sprintf("Invalid build info JSON: %s", $exception->getMessage()), + \sprintf("Invalid build info JSON: %s", $exception->getMessage()), previous: $exception, ); } @@ -98,7 +98,7 @@ public function refresh () : void catch (\JsonException $exception) { throw new InvalidBuildInfoException( - sprintf("Failed to write build info JSON: %s", $exception->getMessage()), + \sprintf("Failed to write build info JSON: %s", $exception->getMessage()), previous: $exception, ); } diff --git a/src/Command/BuildHooksCommand.php b/src/Command/BuildHooksCommand.php index 0ab2e2e..e21aca7 100644 --- a/src/Command/BuildHooksCommand.php +++ b/src/Command/BuildHooksCommand.php @@ -37,7 +37,7 @@ protected function execute (InputInterface $input, OutputInterface $output) : in // @todo remove code block + alias in v4 if ("hosting:hook:build" !== $input->getFirstArgument()) { - $message = sprintf( + $message = \sprintf( "The command `%s` was deprecated. Use `hosting:hook:build` instead.", $input->getFirstArgument(), ); diff --git a/src/Command/DeployHooksCommand.php b/src/Command/DeployHooksCommand.php index ebb3f53..4f857b5 100644 --- a/src/Command/DeployHooksCommand.php +++ b/src/Command/DeployHooksCommand.php @@ -36,7 +36,7 @@ protected function execute (InputInterface $input, OutputInterface $output) : in // @todo remove code block + alias in v4 if ("hosting:hook:build" !== $input->getFirstArgument()) { - $message = sprintf( + $message = \sprintf( "The command `%s` was deprecated. Use `hosting:hook:deploy` instead.", $input->getFirstArgument(), ); diff --git a/src/Command/ShowBuildInfoCommand.php b/src/Command/ShowBuildInfoCommand.php index e44c731..c85792a 100644 --- a/src/Command/ShowBuildInfoCommand.php +++ b/src/Command/ShowBuildInfoCommand.php @@ -43,7 +43,7 @@ protected function execute (InputInterface $input, OutputInterface $output) : in foreach ($info->getAll() as $key => $value) { $rows[] = [ - sprintf("%s", $key), + \sprintf("%s", $key), $this->formatValue($value), ]; } diff --git a/src/Controller/HealthApiController.php b/src/Controller/HealthApiController.php index 953dec5..a0bf033 100644 --- a/src/Controller/HealthApiController.php +++ b/src/Controller/HealthApiController.php @@ -26,7 +26,6 @@ public function liveEndpoint ( ); } - /** * */ diff --git a/src/Deployment/TaskCli.php b/src/Deployment/TaskCli.php index 5c7b3e8..33ffdc1 100644 --- a/src/Deployment/TaskCli.php +++ b/src/Deployment/TaskCli.php @@ -11,7 +11,7 @@ final class TaskCli extends TorrStyle */ public function done (string $message) : void { - $this->write(sprintf( + $this->write(\sprintf( "✓ %s", $message, )); From b156accf5827ef0f4be64fd97facdae0a5ced1eb Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 22 Oct 2024 16:30:25 +0200 Subject: [PATCH 3/6] Add default doctrine check integration --- CHANGELOG.md | 1 + composer.json | 4 ++ src/Listener/DoctrineHealthCheckListener.php | 44 ++++++++++++++++++++ 3 files changed, 49 insertions(+) create mode 100644 src/Listener/DoctrineHealthCheckListener.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 15b8457..604ca7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ * (feature) Add health checks (live + ready). * (improvement) Bump Janus and update CI. +* (feature) Add default doctrine check integration. 3.1.0 diff --git a/composer.json b/composer.json index 800608f..3775858 100644 --- a/composer.json +++ b/composer.json @@ -32,6 +32,7 @@ "require-dev": { "21torr/janus": "^1.4", "bamarni/composer-bin-plugin": "^1.8", + "doctrine/orm": "^3.0", "roave/security-advisories": "dev-latest", "symfony/phpunit-bridge": "^7.1" }, @@ -54,6 +55,9 @@ "symfony/polyfill-php82": "*", "symfony/polyfill-php83": "*" }, + "suggest": { + "doctrine/orm": "For automatic integration of health checks" + }, "autoload": { "psr-4": { "Torr\\Hosting\\": "src/" diff --git a/src/Listener/DoctrineHealthCheckListener.php b/src/Listener/DoctrineHealthCheckListener.php new file mode 100644 index 0000000..1a0465e --- /dev/null +++ b/src/Listener/DoctrineHealthCheckListener.php @@ -0,0 +1,44 @@ +entityManager || !class_exists(SchemaValidator::class)) + { + return; + } + + $validator = new SchemaValidator($this->entityManager); + $result = $validator->validateMapping(); + + if (!empty($result)) + { + $event->markAsFailed("doctrine: invalid mapping"); + + return; + } + + if (!$validator->schemaInSyncWithMetadata()) + { + $event->markAsFailed("doctrine: database out of sync"); + } + } +} From 6dd2c50ace800447aed41a3026fa6808b3e2e0fb Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 22 Oct 2024 16:45:58 +0200 Subject: [PATCH 4/6] Unify endpoint handling and cache health checks --- composer.json | 3 +- config/routes/api.yaml | 8 +++- src/Controller/HealthApiController.php | 54 ++++++++++++++------------ 3 files changed, 37 insertions(+), 28 deletions(-) diff --git a/composer.json b/composer.json index 3775858..a9f17ac 100644 --- a/composer.json +++ b/composer.json @@ -19,6 +19,7 @@ "21torr/bundle-helpers": "^2.2", "21torr/cli": "^1.2.3", "psr/log": "^3.0", + "symfony/cache": "^7.1", "symfony/cache-contracts": "^3.5", "symfony/clock": "^7.1", "symfony/config": "^7.1", @@ -98,4 +99,4 @@ "vendor-bin/phpstan/vendor/bin/phpstan analyze -c phpstan.neon . --ansi -v" ] } -} \ No newline at end of file +} diff --git a/config/routes/api.yaml b/config/routes/api.yaml index 25660e8..19ee1bd 100644 --- a/config/routes/api.yaml +++ b/config/routes/api.yaml @@ -1,7 +1,11 @@ hosting.api.live: path: /live - controller: Torr\Hosting\Controller\HealthApiController::liveEndpoint + controller: Torr\Hosting\Controller\HealthApiController::healthCheckEndpoint + defaults: + type: live hosting.api.ready: path: /ready - controller: Torr\Hosting\Controller\HealthApiController::readyEndpoint + controller: Torr\Hosting\Controller\HealthApiController::healthCheckEndpoint + defaults: + type: ready diff --git a/src/Controller/HealthApiController.php b/src/Controller/HealthApiController.php index a0bf033..326d0dd 100644 --- a/src/Controller/HealthApiController.php +++ b/src/Controller/HealthApiController.php @@ -2,9 +2,11 @@ namespace Torr\Hosting\Controller; +use Psr\Cache\CacheItemInterface; +use Psr\Clock\ClockInterface; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Response; +use Symfony\Contracts\Cache\CacheInterface; use Symfony\Contracts\EventDispatcher\EventDispatcherInterface; use Torr\Hosting\Event\HealthCheckLiveEvent; use Torr\Hosting\Event\HealthCheckReadyEvent; @@ -14,47 +16,49 @@ */ class HealthApiController extends AbstractController { + private const string CACHE_KEY = "hosting.health_check.%s"; + /** * */ - public function liveEndpoint ( + public function healthCheckEndpoint ( EventDispatcherInterface $dispatcher, + CacheInterface $cache, + ClockInterface $clock, + string $type, ) : JsonResponse { - return $this->runHealthCheck( - $dispatcher->dispatch(new HealthCheckLiveEvent()), - ); - } + $event = match ($type) + { + "live" => new HealthCheckLiveEvent(), + "ready" => new HealthCheckReadyEvent(), + default => throw $this->createNotFoundException("Unknown health check type \"{$type}\""), + }; - /** - * - */ - public function readyEndpoint ( - EventDispatcherInterface $dispatcher, - ) : Response - { - return $this->runHealthCheck( - $dispatcher->dispatch(new HealthCheckReadyEvent()), + $failedCheck = $cache->get( + \sprintf(self::CACHE_KEY, $type), + static function (CacheItemInterface $item) use ($dispatcher, $event) + { + // cache for at most 60s + $item->expiresAfter(60); + + return $dispatcher->dispatch($event)->getFailedCheck(); + }, ); - } - /** - * - */ - private function runHealthCheck ( - HealthCheckLiveEvent|HealthCheckReadyEvent $event, - ) : JsonResponse - { - if (!$event->isHealthy()) + // check whether any check failed + if (null !== $failedCheck) { return $this->json([ "ok" => false, - "failed" => $event->getFailedCheck(), + "failed" => $failedCheck, + "checked" => $clock->now()->format("c"), ], 503); } return $this->json([ "ok" => true, + "checked" => $clock->now()->format("c"), ]); } } From a51d6492f1422c00647e952e22e788837956a980 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Tue, 22 Oct 2024 16:48:15 +0200 Subject: [PATCH 5/6] Lower priority on doctrine health check --- src/Listener/DoctrineHealthCheckListener.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Listener/DoctrineHealthCheckListener.php b/src/Listener/DoctrineHealthCheckListener.php index 1a0465e..c68d06c 100644 --- a/src/Listener/DoctrineHealthCheckListener.php +++ b/src/Listener/DoctrineHealthCheckListener.php @@ -18,7 +18,7 @@ public function __construct ( private ?EntityManagerInterface $entityManager = null, ) {} - #[AsEventListener] + #[AsEventListener(priority: -100)] public function onLiveCheck (HealthCheckLiveEvent $event) : void { if (null === $this->entityManager || !class_exists(SchemaValidator::class)) From 5ee4c1cba459aa3b2b4bdabad7572884b14a8df7 Mon Sep 17 00:00:00 2001 From: Jannik Zschiesche Date: Wed, 23 Oct 2024 10:41:02 +0200 Subject: [PATCH 6/6] Finalize health URL checks --- config/routes.yaml | 6 +++--- config/routes/{api.yaml => health.yaml} | 0 2 files changed, 3 insertions(+), 3 deletions(-) rename config/routes/{api.yaml => health.yaml} (100%) diff --git a/config/routes.yaml b/config/routes.yaml index 041b506..73dd725 100644 --- a/config/routes.yaml +++ b/config/routes.yaml @@ -1,3 +1,3 @@ -_import.hosting.api: - prefix: /_h - resource: routes/api.yaml +_import.hosting.health: + resource: routes/health.yaml + prefix: /health/ diff --git a/config/routes/api.yaml b/config/routes/health.yaml similarity index 100% rename from config/routes/api.yaml rename to config/routes/health.yaml