Skip to content
Merged
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,16 @@ symfony_health_check:
ping_checks:
- id: symfony_health_check.status_up_check
```
To perform redis check you need use provide its dsn in the config:
```yaml
symfony_health_check:
health_checks:
...
- id: symfony_health_check.redis_check

redis_dsn: 'redis://localhost:6379'
```

Change response code:
- default response code is 200.
- determine your custom response code in case of some check fails (Response code must be a valid HTTP status code)
Expand Down
4 changes: 3 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,9 @@
"symfony/phpunit-bridge": "^3.4 || ^4.1.12 || ^5.0 || ^6.0 || ^7.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
"symfony/browser-kit": "^3.4 || ^4.4 || ^5.0 || ^6.0 || ^7.0"
"symfony/browser-kit": "^3.4 || ^4.4 || ^5.0 || ^6.0 || ^7.0",
"symfony/cache": "^3.4 || ^4.4 || ^5.0 || ^6.0 || ^7.0",
"predis/predis": "^2.3"
},
"autoload": {
"psr-4": {
Expand Down
24 changes: 24 additions & 0 deletions src/Adapter/RedisAdapterWrapper.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?php

declare(strict_types=1);

namespace SymfonyHealthCheckBundle\Adapter;

use Symfony\Component\Cache\Adapter\RedisAdapter;

/**
* @codeCoverageIgnore - simple wrapper of static methods for adapter class
*/
class RedisAdapterWrapper
{
/**
* @param string $dsn
* @param array $options
*
* @return \Predis\ClientInterface|\Redis|\RedisArray|\RedisCluster|\Relay\Relay
*/
public function createConnection(string $dsn, array $options = []): object
{
return RedisAdapter::createConnection($dsn, $options);
}
}
107 changes: 107 additions & 0 deletions src/Check/RedisCheck.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
<?php

declare(strict_types=1);

namespace SymfonyHealthCheckBundle\Check;

use SymfonyHealthCheckBundle\Dto\Response;
use SymfonyHealthCheckBundle\Adapter\RedisAdapterWrapper;

class RedisCheck implements CheckInterface
{
private const CHECK_RESULT_NAME = 'redis_check';

private RedisAdapterWrapper $redisAdapter;
private string $redisDsn;

public function __construct(RedisAdapterWrapper $redisAdapter, string $redisDsn)
{
$this->redisAdapter = $redisAdapter;
$this->redisDsn = $redisDsn;
}

public function check(): Response
{
try {
$redisConnection = $this->redisAdapter->createConnection($this->redisDsn);

switch (true) {
case $redisConnection instanceof \Redis:
$result = $this->checkForDefaultRedisClient($redisConnection);

break;
case $redisConnection instanceof \Predis\ClientInterface:
$result = $this->checkForPredisClient($redisConnection);

break;
case $redisConnection instanceof \RedisArray:
$result = $this->checkForRedisArrayClient($redisConnection);

break;
default:
throw new \RuntimeException(sprintf(
'Unsupported Redis client type: %s',
get_class($redisConnection),
));
}

if (!$result) {
return new Response(self::CHECK_RESULT_NAME, false, 'Redis ping failed.');
}

return new Response(self::CHECK_RESULT_NAME, true, 'ok');
} catch (\Throwable $e) {
return new Response(self::CHECK_RESULT_NAME, false, $e->getMessage());
}
}

private function checkForDefaultRedisClient(\Redis $client): bool
{
$response = $client->ping();

if (is_bool($response)) {
return $response;
}

return $this->isValidPingResponse($response);
}

private function checkForPredisClient(\Predis\ClientInterface $client): bool
{
$response = $client->ping();

if (is_bool($response)) {
return $response;
}

return $this->isValidPingResponse($response);
}

private function checkForRedisArrayClient(\RedisArray $client): bool
{
$response = $client->ping();

if (is_bool($response)) {
return $response;
}

// invalid configuration, RedisClient have different response, than one, provided by RedisArray in fact.
// @phpstan-ignore-next-line
foreach ($response as $pingResult) {
if (is_bool($pingResult)) {
continue;
}

if (!$this->isValidPingResponse($pingResult)) {
return false;
}
}

return true;
}

private function isValidPingResponse(string $response): bool
{
return in_array(strtolower($response), ['pong', '+pong'], true);
}
}
9 changes: 9 additions & 0 deletions src/DependencyInjection/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,15 @@ public function getConfigTreeBuilder(): TreeBuilder
->thenInvalid('The health_error_response_code must be valid HTTP status code or null.')
->end()
->end()
->variableNode('redis_dsn')
->defaultValue(null)
->validate()
->ifTrue(function ($value) {
return $value !== null && !is_string($value);
})
->thenInvalid('The redis_dsn must be a string or null.')
->end()
->end()
->arrayNode('health_checks')
->prototype('array')
->children()
Expand Down
15 changes: 15 additions & 0 deletions src/DependencyInjection/SymfonyHealthCheckExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace SymfonyHealthCheckBundle\DependencyInjection;

use Composer\InstalledVersions;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;
Expand All @@ -23,6 +24,9 @@
$loader->load('controller.xml');

$this->loadHealthChecks($config, $loader, $container);

$container->getDefinition('symfony_health_check.redis_check')
->setArgument(1, $config['redis_dsn']);
}

private function loadHealthChecks(
Expand All @@ -34,6 +38,17 @@

$healthCheckCollection = $container->findDefinition(HealthController::class);

$usedChecks = array_column(array_merge($config['health_checks'], $config['ping_checks']), 'id');
if (in_array('symfony_health_check.redis_check', $usedChecks)) {
if (!InstalledVersions::isInstalled('symfony/cache')) {
throw new \RuntimeException('To use RedisCheck you need to install symfony/cache package.');

Check warning on line 44 in src/DependencyInjection/SymfonyHealthCheckExtension.php

View check run for this annotation

Codecov / codecov/patch

src/DependencyInjection/SymfonyHealthCheckExtension.php#L44

Added line #L44 was not covered by tests
}

if (empty($config['redis_dsn'])) {
throw new \RuntimeException('To use RedisCheck you need to configure redis_dsn parameter.');
}
}

foreach ($config['health_checks'] as $healthCheckConfig) {
$healthCheckDefinition = new Reference($healthCheckConfig['id']);
$healthCheckCollection->addMethodCall('addHealthCheck', [$healthCheckDefinition]);
Expand Down
5 changes: 5 additions & 0 deletions src/Resources/config/health_checks.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
http://symfony.com/schema/dic/services/services-1.0.xsd">
<services>
<defaults autowire="true" autoconfigure="true" public="true" />
<service id="symfony_health_check.redis_adapter_wrapper" class="SymfonyHealthCheckBundle\Adapter\RedisAdapterWrapper" public="false"/>

<service id="symfony_health_check.doctrine_check" class="SymfonyHealthCheckBundle\Check\DoctrineORMCheck">
<argument type="service" id="service_container"/>
<deprecated package="macpaw/symfony-health-check-bundle" version="1.4.2">The "%service_id%" service alias is deprecated, use symfony_health_check.doctrine_orm_check instead</deprecated>
Expand All @@ -15,6 +17,9 @@
<service id="symfony_health_check.doctrine_odm_check" class="SymfonyHealthCheckBundle\Check\DoctrineODMCheck">
<argument type="service" id="service_container"/>
</service>
<service id="symfony_health_check.redis_check" class="SymfonyHealthCheckBundle\Check\RedisCheck">
<argument type="service" id="symfony_health_check.redis_adapter_wrapper"/>
</service>
<service id="symfony_health_check.environment_check" class="SymfonyHealthCheckBundle\Check\EnvironmentCheck">
<argument type="service" id="service_container"/>
</service>
Expand Down
32 changes: 32 additions & 0 deletions tests/Integration/DependencyInjection/ConfigurationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ public function testProcessConfigurationWithDefaultConfiguration(): void
$expectedBundleDefaultConfig = [
'ping_error_response_code' => null,
'health_error_response_code' => null,
'redis_dsn' => null,
'health_checks' => [],
'ping_checks' => [],
];
Expand All @@ -40,6 +41,7 @@ public function testProcessConfigurationHealthChecks(): void
'ping_checks' => [],
'ping_error_response_code' => null,
'health_error_response_code' => null,
'redis_dsn' => null,
];
$new = ['health_checks' => [
['id' => 'symfony_health_check.doctrine_check']
Expand All @@ -60,6 +62,7 @@ public function testProcessConfigurationPing(): void
],
'ping_error_response_code' => null,
'health_error_response_code' => null,
'redis_dsn' => null,
];
$new = ['health_checks' => [], 'ping_checks' => [
['id' => 'symfony_health_check.doctrine_check']
Expand All @@ -82,6 +85,7 @@ public function testProcessConfigurationPingAndHealthChecks(): void
],
'ping_error_response_code' => null,
'health_error_response_code' => null,
'redis_dsn' => null,
];
$new = [
'health_checks' => [['id' => 'symfony_health_check.doctrine_check']],
Expand All @@ -105,6 +109,7 @@ public function testProcessConfigurationCustomErrorCode(): void
],
'ping_error_response_code' => 404,
'health_error_response_code' => 500,
'redis_dsn' => null,
];
$new = [
'health_checks' => [['id' => 'symfony_health_check.doctrine_check']],
Expand All @@ -119,6 +124,33 @@ public function testProcessConfigurationCustomErrorCode(): void
);
}

public function testItProcessConfigurationWithRedisDsn(): void
{
$expectedConfig = [
'health_checks' => [
['id' => 'symfony_health_check.doctrine_check']
],
'ping_checks' => [
['id' => 'symfony_health_check.doctrine_check']
],
'ping_error_response_code' => 404,
'health_error_response_code' => 500,
'redis_dsn' => 'redis://redis',
];
$new = [
'health_checks' => [['id' => 'symfony_health_check.doctrine_check']],
'ping_checks' => [['id' => 'symfony_health_check.doctrine_check']],
'ping_error_response_code' => 404,
'health_error_response_code' => 500,
'redis_dsn' => 'redis://redis',
];

self::assertSame(
$expectedConfig,
$this->processConfiguration($new)
);
}

private function processConfiguration(array $values): array
{
$processor = new Processor();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
symfony_health_check:
health_checks:
- id: symfony_health_check.redis_check
ping_checks:
- id: symfony_health_check.doctrine_check
Original file line number Diff line number Diff line change
Expand Up @@ -46,18 +46,28 @@ public function testWithEmptyConfigPing(): void
}
}

public function testWithEmptyRedisDsnConfig(): void
{
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('To use RedisCheck you need to configure redis_dsn parameter.');

$this->createContainerFromFixture('error_redis_check_bundle_config');
}

public function testWithFullConfig(): void
{
$container = $this->createContainerFromFixture('filled_bundle_config');

self::assertCount(8, $container->getDefinitions());
self::assertCount(10, $container->getDefinitions());
self::assertArrayHasKey(HealthController::class, $container->getDefinitions());
self::assertArrayHasKey(PingController::class, $container->getDefinitions());
self::assertArrayHasKey('symfony_health_check.doctrine_check', $container->getDefinitions()); #deprecated
self::assertArrayHasKey('symfony_health_check.doctrine_orm_check', $container->getDefinitions());
self::assertArrayHasKey('symfony_health_check.doctrine_odm_check', $container->getDefinitions());
self::assertArrayHasKey('symfony_health_check.environment_check', $container->getDefinitions());
self::assertArrayHasKey('symfony_health_check.status_up_check', $container->getDefinitions());
self::assertArrayHasKey('symfony_health_check.redis_check', $container->getDefinitions());
self::assertArrayHasKey('symfony_health_check.redis_adapter_wrapper', $container->getDefinitions());
}

private function createContainerFromFixture(string $fixtureFile): ContainerBuilder
Expand Down
Loading