diff --git a/.install/symfony/config/routes/open_conext_monitor.yaml b/.install/symfony/config/routes/open_conext_monitor.yaml index ca03974..856e844 100644 --- a/.install/symfony/config/routes/open_conext_monitor.yaml +++ b/.install/symfony/config/routes/open_conext_monitor.yaml @@ -1,5 +1,4 @@ open_conext_monitor: - resource: "@OpenConextMonitorBundle/Resources/config/routing.yml" - prefix: / - - + resource: "@OpenConextMonitorBundle/src/Controller" + type: attribute + prefix: / diff --git a/README.md b/README.md index a266d65..01e3c63 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A Symfony 5/6/7 bundle that adds an /internal/health and /internal/info endpoint The endpoints return JSON responses. The `/internal/info` endpoint tries to give as much information about the currently installed version of the application as possible. This information is based on the build path of the installation. But also -includes the Symfony environment that is currently active and whether or not the debugger is enabled. +includes the Symfony environment that is currently active and whether the debugger is enabled. The `/internal/health` endpoint reports on the health of the application. This information could be used for example by a load balancer. Example output: @@ -46,9 +46,11 @@ When a health check failed the HTTP Response status code will be 503. And the JS * Include the routing configuration in `config/routes.yaml` by adding: ```yaml open_conext_monitor: - resource: "@OpenConextMonitorBundle/Resources/config/routing.yml" - prefix: / + resource: "@OpenConextMonitorBundle/src/Controller" + type: attribute + prefix: / ``` +_Note: this is currently done by the bundle itself, with an external dependency called https://github.com/endroid/installer_ * Add security exceptions in `config/packages/security.yaml` (if this is required at all) ```yaml @@ -78,24 +80,18 @@ use OpenConext\MonitorBundle\Value\HealthReport; class ApiHealthCheck implements HealthCheckInterface { - /** - * @var MyService - */ - private $testService; - - public function __construct(MyService $service) + public function __construct(private readonly MyService $service) { - $this->testService = $service; } public function check(HealthReportInterface $report): HealthReportInterface { - if (!$this->testService->everythingOk()) { + if (!$this->service->everythingOk()) { // Return a HealthReport with a DOWN status when there are indications the application is not functioning as // intended. You can provide an optional message that is displayed alongside the DOWN status. return HealthReport::buildStatusDown('Not everything is allright.'); } - // By default return the report that was passed along as a parameter to the check method + // By default, return the report that was passed along as a parameter to the check method return $report; } } @@ -104,35 +100,20 @@ class ApiHealthCheck implements HealthCheckInterface registered health checkers. If everything was OK, just return the report that was passed to the method. ### Register the checker -To actually include the home made checker simply tag it with 'surfnet.monitor.health_check' - -Example service definition in `services.yml` - -```yaml -services: - acme.monitor.my_custom_health_check: - class: Acme\AppBundle\HealthCheck\MyCustomHealthCheck - arguments: - - @test_service - tags: - - { name: surfnet.monitor.health_check } -``` +By implementing the `HealthCheckInterface` you can register your own health check. +This interface is tagged automatically, so you don't have to do it yourself. ## Overriding a default HealthCheck To run a custom query with the DoctrineConnectionHealthCheck you will need to override it in your own project. -For example in your ACME bunde that is using the monitor bundle: +For example in your ACME bundle that is using the monitor bundle: -`services.yml` +`services.yaml` ```yaml - # Override the service, service names can be found in `/src/Resources/config/services.yml` - openconext.monitor.database_health_check: + # Override the service in `/src/config/services.yaml` + OpenConext\MonitorBundle\HealthCheck\DoctrineConnectionHealthCheck: # Point to your own implementation of the check class: Acme\GreatSuccessBundle\HealthCheck\DoctrineConnectionHealthCheck - # Do not forget to apply the correct tag - tags: - - { name: openconext.monitor.health_check } - ``` The rest of the service configuration is up to your own needs. You can inject arguments, factory calls and other service features as need be. diff --git a/composer.json b/composer.json index 2e70275..04a3f59 100644 --- a/composer.json +++ b/composer.json @@ -1,25 +1,21 @@ { "name": "openconext/monitor-bundle", "type": "symfony-bundle", - "description": "A Symfony 5/6/7 bundle that facilitates health and info endpoints to a Symfony application.", + "description": "A Symfony 6/7 bundle that facilitates health and info endpoints to a Symfony application.", "keywords": ["SURFnet", "StepUp", "OpenConext", "monitoring", "health"], "license": "Apache-2.0", "minimum-stability": "stable", "require": { "php": ">=8.2, <9.0-dev", - "doctrine/orm": "^2.9", - "doctrine/dbal": "^3.1", + "doctrine/dbal": "^3.1|^4.0", "endroid/installer": "^1.4", - "symfony/dependency-injection": "^5.4|^6.3|^7.0", - "symfony/framework-bundle": "^5.4|^6.3|^7.0", + "symfony/dependency-injection": "^6.3|^7.0", + "symfony/framework-bundle": "^6.3|^7.0", "webmozart/assert": "^1.10" }, "require-dev": { "php-parallel-lint/php-parallel-lint": "^1.3", "phpmd/phpmd": "^2.13", - "matthiasnoback/symfony-config-test": "^4.3", - "phpdocumentor/reflection-docblock": "^5.2", - "phpunit/php-token-stream": "^3.1.3|^4.0.4", "phpunit/phpunit": "^9.6|^10.4", "sebastian/phpcpd": "^4.1|^5.0|^6.0", "squizlabs/php_codesniffer": "^3.6", @@ -28,21 +24,21 @@ }, "autoload": { "psr-4": { - "OpenConext\\MonitorBundle\\": "src" + "OpenConext\\MonitorBundle\\": "src/" } }, "autoload-dev": { "psr-4": { - "App\\Tests\\": "src/Tests/" + "App\\Tests\\": "tests/" } }, "scripts": { "tests": { - "docheader": "vendor/bin/docheader check src/", - "phpcs": "vendor/bin/phpcs src --report=full --standard=phpcs.xml --extensions=php --warning-severity=0", - "phpcpd": "vendor/bin/phpcpd src --exclude=src/Tests/*", + "docheader": "vendor/bin/docheader check src/ tests/", + "phpcs": "vendor/bin/phpcs src tests --report=full --standard=phpcs.xml --extensions=php --warning-severity=0", + "phpcpd": "vendor/bin/phpcpd src", "phpunit": "vendor/bin/phpunit --coverage-text", - "phpmd": "vendor/bin/phpmd src text phpmd.xml --exclude 'src/Tests/*'" + "phpmd": "vendor/bin/phpmd src text phpmd.xml" }, "post-update-cmd": [ "@tests" diff --git a/config/services.yaml b/config/services.yaml new file mode 100644 index 0000000..7317294 --- /dev/null +++ b/config/services.yaml @@ -0,0 +1,8 @@ +services: + _defaults: + autowire: true # Automatically injects dependencies in your services. + autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. + + OpenConext\MonitorBundle\: + resource: '../src/' + exclude: '../src/{Entity,Repository,Tests,Resources,DependencyInjection,OpenConextMonitorBundle.php}' diff --git a/phpunit.xml b/phpunit.xml index ff1f970..18d25eb 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -13,32 +13,29 @@ > - - - - - - - - - + + + + + + - - - src - - - .github - src/Tests - - - - - src/Tests - - - - - + + + src + + + .github + tests + + + + + tests + + + + + diff --git a/src/Controller/HealthController.php b/src/Controller/HealthController.php index e37f091..59f583a 100644 --- a/src/Controller/HealthController.php +++ b/src/Controller/HealthController.php @@ -21,6 +21,7 @@ use OpenConext\MonitorBundle\HealthCheck\HealthCheckChain; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\Routing\Attribute\Route; /** * Display the health state of the application. @@ -35,6 +36,8 @@ public function __construct( ) { } + #[Route('/health', name: 'monitor.health', methods: ['GET'])] + #[Route('/internal/health', name: 'monitor.internal_health', methods: ['GET'])] public function __invoke(): JsonResponse { $statusResponse = $this->healthChecker->check(); diff --git a/src/Controller/InfoController.php b/src/Controller/InfoController.php index c968f40..243ddf9 100644 --- a/src/Controller/InfoController.php +++ b/src/Controller/InfoController.php @@ -19,11 +19,11 @@ namespace OpenConext\MonitorBundle\Controller; use OpenConext\MonitorBundle\Value\BuildInformationFactory; -use OpenConext\MonitorBundle\Value\BuildPathFactory; use OpenConext\MonitorBundle\Value\Information; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; +use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\Routing\Attribute\Route; /** * Display specific information about the application. @@ -43,21 +43,31 @@ class InfoController extends AbstractController { private array $systemInfo = []; + private readonly string $buildPath; public function __construct( - private readonly string $buildPath, + #[Autowire(param: 'kernel.project_dir')] + private readonly string $projectDir, + #[Autowire(param: 'kernel.environment')] private readonly string $environment, + #[Autowire(param: 'kernel.debug')] private readonly bool $debuggerEnabled, + #[Autowire(env: 'default::string:OPENCONEXT_APP_VERSION')] private readonly ?string $version, + #[Autowire(env: 'default::string:OPENCONEXT_GIT_SHA')] private readonly ?string $revision, + #[Autowire(env: 'default::string:OPENCONEXT_COMMIT_DATE')] private readonly ?string $commitDate, ) { + $this->buildPath = basename(realpath($this->projectDir)); if (function_exists('opcache_get_status')) { $this->systemInfo['opcache'] = opcache_get_status(false); } } + #[Route('/info', name: 'monitor.info', methods: ['GET'])] + #[Route('/internal/info', name: 'monitor.internal_info', methods: ['GET'])] public function __invoke(): JsonResponse { $buildInformation = BuildInformationFactory::build( diff --git a/src/DependencyInjection/Compiler/HealthCheckPass.php b/src/DependencyInjection/Compiler/HealthCheckPass.php deleted file mode 100644 index 680fe6c..0000000 --- a/src/DependencyInjection/Compiler/HealthCheckPass.php +++ /dev/null @@ -1,48 +0,0 @@ -has('openconext.monitor.health_check_chain')) { - return; - } - - $definition = $container->findDefinition('openconext.monitor.health_check_chain'); - - // find all service IDs with the openconext.monitor.health_check tag - $taggedServices = $container->findTaggedServiceIds('openconext.monitor.health_check'); - - foreach ($taggedServices as $id => $tags) { - $definition->addMethodCall('addHealthCheck', array(new Reference($id))); - } - } -} diff --git a/src/DependencyInjection/OpenConextMonitorExtension.php b/src/DependencyInjection/OpenConextMonitorExtension.php deleted file mode 100644 index ab1e0c4..0000000 --- a/src/DependencyInjection/OpenConextMonitorExtension.php +++ /dev/null @@ -1,56 +0,0 @@ -load('services.yml'); - - $this->setInfoControllerArguments($container); - } - - private function setInfoControllerArguments(ContainerBuilder $container) - { - // The buildPath is the installation directory of the project. And is derived from the kernel.project_dir - // (which is the app folder). - $rootDir = $container->getParameter('kernel.project_dir'); - $buildPath = basename(realpath($rootDir)); - - $container - ->getDefinition('openconext.monitor.controller.info') - ->replaceArgument(0, $buildPath); - } -} diff --git a/src/HealthCheck/DoctrineConnectionHealthCheck.php b/src/HealthCheck/DoctrineConnectionHealthCheck.php index 3c204d6..af7a97a 100644 --- a/src/HealthCheck/DoctrineConnectionHealthCheck.php +++ b/src/HealthCheck/DoctrineConnectionHealthCheck.php @@ -18,9 +18,10 @@ namespace OpenConext\MonitorBundle\HealthCheck; -use Doctrine\ORM\EntityManager; +use Doctrine\DBAL\Connection; use Exception; use OpenConext\MonitorBundle\Value\HealthReport; +use Symfony\Component\DependencyInjection\Attribute\Autowire; /** * Test if there is a working database connection. @@ -30,34 +31,27 @@ */ class DoctrineConnectionHealthCheck implements HealthCheckInterface { - /** - * @var EntityManager|null - */ - private $entityManager; - /** - * @param EntityManager $entityManager - */ - public function setEntityManager(EntityManager $entityManager) - { - $this->entityManager = $entityManager; + public function __construct( + #[Autowire(service: 'doctrine.dbal.default_connection')] + private readonly ?Connection $connection + ) { } public function check(HealthReportInterface $report): HealthReportInterface { - // Was the entityManager injected? When it is not the project does not use Doctrine ORM - if (!is_null($this->entityManager)) { + if (!is_null($this->connection)) { try { // Get the schema manager and grab the first table to later query on - $sm = $this->entityManager->getConnection()->createSchemaManager(); + $sm = $this->connection->createSchemaManager(); $tables = $sm->listTables(); if (!empty($tables)) { $table = reset($tables); // Perform a light-weight query on the chosen table - $query = 'SELECT * FROM `%s` LIMIT 1'; - $this->entityManager->getConnection()->executeQuery(sprintf($query, $table->getName())); + $query = "SELECT * FROM %s LIMIT 1"; + $this->connection->executeQuery(sprintf($query, $table->getName())); } - } catch (Exception $e) { + } catch (Exception) { return HealthReport::buildStatusDown('Unable to execute a query on the database.'); } } diff --git a/src/HealthCheck/HealthCheckChain.php b/src/HealthCheck/HealthCheckChain.php index acfa308..16e2940 100644 --- a/src/HealthCheck/HealthCheckChain.php +++ b/src/HealthCheck/HealthCheckChain.php @@ -19,25 +19,17 @@ namespace OpenConext\MonitorBundle\HealthCheck; use OpenConext\MonitorBundle\Value\HealthReport; +use Symfony\Component\DependencyInjection\Attribute\AutowireIterator; /** * Collect HealthCheck instances and checks them for UP or DOWN status. */ class HealthCheckChain { - /** - * @var HealthCheckInterface[] - */ - private $checks; - - public function __construct() - { - $this->checks = []; - } - - public function addHealthCheck(HealthCheckInterface $healthCheck) - { - $this->checks[] = $healthCheck; + public function __construct( + #[AutowireIterator(tag: 'surfnet.monitor.health_check')] + private readonly iterable $healthChecks + ) { } /** @@ -46,8 +38,8 @@ public function addHealthCheck(HealthCheckInterface $healthCheck) public function check(): HealthReportInterface { $report = HealthReport::buildStatusUp(); - if (!empty($this->checks)) { - foreach ($this->checks as $check) { + if (!empty($this->healthChecks)) { + foreach ($this->healthChecks as $check) { $report = $check->check($report); if ($report->isDown()) { return $report; diff --git a/src/HealthCheck/HealthCheckInterface.php b/src/HealthCheck/HealthCheckInterface.php index bf49b49..aa002a5 100644 --- a/src/HealthCheck/HealthCheckInterface.php +++ b/src/HealthCheck/HealthCheckInterface.php @@ -18,9 +18,12 @@ namespace OpenConext\MonitorBundle\HealthCheck; +use Symfony\Component\DependencyInjection\Attribute\AutoconfigureTag; + /** * Contract for a HealthCheck. */ +#[AutoconfigureTag('surfnet.monitor.health_check')] interface HealthCheckInterface { public function check(HealthReportInterface $report): HealthReportInterface; diff --git a/src/OpenConextMonitorBundle.php b/src/OpenConextMonitorBundle.php index 21f7513..04238d7 100644 --- a/src/OpenConextMonitorBundle.php +++ b/src/OpenConextMonitorBundle.php @@ -18,14 +18,14 @@ namespace OpenConext\MonitorBundle; -use OpenConext\MonitorBundle\DependencyInjection\Compiler\HealthCheckPass; use Symfony\Component\DependencyInjection\ContainerBuilder; -use Symfony\Component\HttpKernel\Bundle\Bundle; +use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator; +use Symfony\Component\HttpKernel\Bundle\AbstractBundle; -class OpenConextMonitorBundle extends Bundle +class OpenConextMonitorBundle extends AbstractBundle { - public function build(ContainerBuilder $container): void + public function loadExtension(array $config, ContainerConfigurator $container, ContainerBuilder $builder): void { - $container->addCompilerPass(new HealthCheckPass()); + $container->import('../config/services.yaml'); } } diff --git a/src/Resources/config/routing.yml b/src/Resources/config/routing.yml deleted file mode 100644 index fcfda87..0000000 --- a/src/Resources/config/routing.yml +++ /dev/null @@ -1,24 +0,0 @@ -# Deprecated routes: /info and /health are moving to /internal/info and internal/health respectively. -monitor.info: - path: /info - methods: [GET] - defaults: - _controller: openconext.monitor.controller.info - -monitor.health: - path: /health - methods: [GET] - defaults: - _controller: openconext.monitor.controller.health - -monitor.internal_info: - path: /internal/info - methods: [GET] - defaults: - _controller: openconext.monitor.controller.info - -monitor.internal_health: - path: /internal/health - methods: [GET] - defaults: - _controller: openconext.monitor.controller.health diff --git a/src/Resources/config/services.yml b/src/Resources/config/services.yml deleted file mode 100644 index bb09adf..0000000 --- a/src/Resources/config/services.yml +++ /dev/null @@ -1,35 +0,0 @@ -services: - openconext.monitor.controller.info: - class: OpenConext\MonitorBundle\Controller\InfoController - arguments: - - '~' - - '%kernel.environment%' - - '%kernel.debug%' - - '%env(default::string:OPENCONEXT_APP_VERSION)%' - - '%env(default::string:OPENCONEXT_GIT_SHA)%' - - '%env(default::string:OPENCONEXT_COMMIT_DATE)%' - - autowire: true - tags: ['controller.service_arguments', 'container.service_subscriber'] - - openconext.monitor.controller.health: - class: OpenConext\MonitorBundle\Controller\HealthController - arguments: - - '@openconext.monitor.health_check_chain' - autowire: true - tags: ['controller.service_arguments', 'container.service_subscriber'] - - openconext.monitor.health_check_chain: - class: OpenConext\MonitorBundle\HealthCheck\HealthCheckChain - - openconext.monitor.session_health_check: - class: OpenConext\MonitorBundle\HealthCheck\SessionHealthCheck - tags: - - { name: openconext.monitor.health_check } - - openconext.monitor.database_health_check: - class: OpenConext\MonitorBundle\HealthCheck\DoctrineConnectionHealthCheck - calls: - - [ setEntityManager, ['@?doctrine.orm.entity_manager']] - tags: - - { name: openconext.monitor.health_check } diff --git a/src/Tests/HealthCheck/SessionHealthCheckChainTest.php b/tests/HealthCheck/SessionHealthCheckChainTest.php similarity index 88% rename from src/Tests/HealthCheck/SessionHealthCheckChainTest.php rename to tests/HealthCheck/SessionHealthCheckChainTest.php index 9e90502..7d72620 100644 --- a/src/Tests/HealthCheck/SessionHealthCheckChainTest.php +++ b/tests/HealthCheck/SessionHealthCheckChainTest.php @@ -18,6 +18,7 @@ namespace OpenConext\MonitorBundle\Tests\HealthCheck; +use ArrayIterator; use Mockery as m; use OpenConext\MonitorBundle\HealthCheck\HealthCheckChain; use OpenConext\MonitorBundle\HealthCheck\HealthCheckInterface; @@ -57,10 +58,9 @@ public function testChain() ->once() ->andReturn($statusOk); - $chain = new HealthCheckChain(); - $chain->addHealthCheck($checker1); - $chain->addHealthCheck($checker2); - $chain->addHealthCheck($checker3); + $iterator = new ArrayIterator([$checker1, $checker2, $checker3]); + + $chain = new HealthCheckChain($iterator); $result = $chain->check(); $this->assertEquals($statusOk, $result); @@ -74,7 +74,7 @@ public function testChainStopsWhenCheckFails() ->andReturn(false); $statusDown = m::mock(HealthReport::buildStatusDown('Lorem ipsum dolor sit')); - $statusOk + $statusDown ->shouldReceive('isDown') ->andReturn(true); @@ -93,10 +93,9 @@ public function testChainStopsWhenCheckFails() $checker3 = m::mock(HealthCheckInterface::class); $checker3->shouldNotReceive('check'); - $chain = new HealthCheckChain(); - $chain->addHealthCheck($checker1); - $chain->addHealthCheck($checker2); - $chain->addHealthCheck($checker3); + $iterator = new ArrayIterator([$checker1, $checker2, $checker3]); + + $chain = new HealthCheckChain($iterator); $result = $chain->check(); $this->assertEquals($statusDown, $result); diff --git a/src/Tests/Value/BuildInformationFactoryTest.php b/tests/Value/BuildInformationFactoryTest.php similarity index 100% rename from src/Tests/Value/BuildInformationFactoryTest.php rename to tests/Value/BuildInformationFactoryTest.php diff --git a/src/Tests/Value/BuildPathFactoryTest.php b/tests/Value/BuildPathFactoryTest.php similarity index 100% rename from src/Tests/Value/BuildPathFactoryTest.php rename to tests/Value/BuildPathFactoryTest.php diff --git a/src/Tests/Value/EnvVarFactoryTest.php b/tests/Value/EnvVarFactoryTest.php similarity index 100% rename from src/Tests/Value/EnvVarFactoryTest.php rename to tests/Value/EnvVarFactoryTest.php