Skip to content

Commit 8fdd557

Browse files
committed
🆕 [Healthcheck] Add initial implementation
1 parent 2c25dcb commit 8fdd557

File tree

10 files changed

+391
-0
lines changed

10 files changed

+391
-0
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<?php
2+
/*
3+
This file will automatically be included before EACH run.
4+
Use it to configure atoum or anything that needs to be done before EACH run.
5+
More information on documentation:
6+
[en] http://docs.atoum.org/en/latest/chapter3.html#configuration-files
7+
[fr] http://docs.atoum.org/fr/latest/lancement_des_tests.html#fichier-de-configuration
8+
*/
9+
use \mageekguy\atoum;
10+
11+
$report = $script->addDefaultReport();
12+
// This will add a green or red logo after each run depending on its status.
13+
$report->addField(new atoum\report\fields\runner\result\logo());
14+
$runner->addTestsFromDirectory(__DIR__.'/tests/Units');
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
vendor
2+
composer.lock
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Ubirak package.
5+
*
6+
* (c) Ubirak team <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Ubirak\Component\Healthcheck;
15+
16+
interface Healthcheck
17+
{
18+
/**
19+
* Informs if a destination is reachable
20+
*
21+
* @param string $destination A destination to join for the health check
22+
*
23+
* @throws InvalidDestination when the destination is not supported by health check implementation.
24+
* @throws HealthcheckFailure when a non expected health check failure occurs.
25+
*/
26+
public function isReachable(string $destination): bool;
27+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Ubirak package.
5+
*
6+
* (c) Ubirak team <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Ubirak\Component\Healthcheck;
15+
16+
final class HealthcheckFailure extends \RuntimeException
17+
{
18+
public static function cannotConnectToUri(string $uri)
19+
{
20+
return new static("Cannot connect to uri ${uri}.");
21+
}
22+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Ubirak package.
5+
*
6+
* (c) Ubirak team <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Ubirak\Component\Healthcheck;
15+
16+
use GuzzleHttp\Psr7\Request;
17+
use Http\Client\HttpClient;
18+
use Psr\Log\LoggerInterface;
19+
use Psr\Log\NullLogger;
20+
21+
final class HttpHealthcheck implements Healthcheck
22+
{
23+
private $httpClient;
24+
25+
private $logger;
26+
27+
public function __construct(HttpClient $httpClient, LoggerInterface $logger = null)
28+
{
29+
$this->httpClient = $httpClient;
30+
$this->logger = $logger ?? new NullLogger();
31+
}
32+
33+
public function isReachable(string $target): bool
34+
{
35+
$this->logger->info('Start HTTP healthcheck', ['target' => $target]);
36+
37+
$response = $this->httpClient->sendRequest(new Request('GET', $target));
38+
39+
$result = 200 === $response->getStatusCode();
40+
$resultAsString = $result ? 'OK' : 'Fail';
41+
42+
$this->logger->info("[${resultAsString}] HTTP healthcheck", ['target' => $target]);
43+
44+
return $result;
45+
}
46+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Ubirak package.
5+
*
6+
* (c) Ubirak team <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Ubirak\Component\Healthcheck;
15+
16+
final class InvalidDestination extends \InvalidArgumentException
17+
{
18+
public static function ofProtocol(string $protocol)
19+
{
20+
return new static("Destination must be a valid ${protocol} uri.");
21+
}
22+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Ubirak package.
5+
*
6+
* (c) Ubirak team <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Ubirak\Component\Healthcheck;
15+
16+
use Tolerance\Operation\Callback;
17+
use Tolerance\Operation\Runner\RetryOperationRunner;
18+
use Tolerance\Operation\Runner\CallbackOperationRunner;
19+
use Tolerance\Waiter\SleepWaiter;
20+
use Tolerance\Waiter\TimeOut;
21+
use Tolerance\Waiter\ExponentialBackOff;
22+
use Psr\Log\LoggerInterface;
23+
use Psr\Log\NullLogger;
24+
25+
final class TcpHealthcheck implements Healthcheck
26+
{
27+
private $maxExecutionTime;
28+
29+
private $initialExponent;
30+
31+
private $step;
32+
33+
private $logger;
34+
35+
/**
36+
* All values are expressed in seconds.
37+
*/
38+
public function __construct(float $initialExponent, float $step, float $maxExecutionTime, LoggerInterface $logger = null)
39+
{
40+
$this->initialExponent = $initialExponent;
41+
$this->step = $step;
42+
$this->maxExecutionTime = $maxExecutionTime;
43+
$this->logger = $logger ?? new NullLogger();
44+
}
45+
46+
public function isReachable(string $destination): bool
47+
{
48+
$this->logger->info('Start TCP healthcheck', ['target' => $destination]);
49+
50+
if (false === filter_var($destination, FILTER_VALIDATE_URL, FILTER_FLAG_HOST_REQUIRED)) {
51+
throw InvalidDestination::ofProtocol('tcp');
52+
}
53+
['host' => $host, 'port' => $port] = parse_url($destination);
54+
55+
$runner = new RetryOperationRunner(
56+
new CallbackOperationRunner(),
57+
new ExponentialBackOff(
58+
new Timeout(new SleepWaiter(), $this->maxExecutionTime),
59+
$this->initialExponent,
60+
$this->step
61+
)
62+
);
63+
$uri = "tcp://${host}:${port}";
64+
65+
try {
66+
$runner->run(new Callback(function () use ($uri) {
67+
$socket = @stream_socket_client($uri, $errno, $errstr, 5);
68+
if (false === $socket) {
69+
throw HealthcheckFailure::cannotConnectToUri($uri);
70+
}
71+
@fclose($socket);
72+
return true;
73+
}));
74+
$this->logger->info('[OK] TCP healthcheck', ['target' => $destination]);
75+
return true;
76+
} catch (\Exception $e) {
77+
$this->logger->info('[Fail] TCP healthcheck', ['target' => $destination]);
78+
return false;
79+
}
80+
}
81+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
{
2+
"name": "ubirak/healthcheck",
3+
"description": "Health checks against various protocols",
4+
"type": "library",
5+
"license": "Apache-2.0",
6+
"authors": [
7+
{
8+
"name": "Ubirak team",
9+
"email": "[email protected]"
10+
}
11+
],
12+
"autoload": {
13+
"psr-4": { "Ubirak\\Component\\Healthcheck\\": "" },
14+
"exclude-from-classmap": ["/tests/"]
15+
},
16+
"autoload-dev": {
17+
"psr-4": {
18+
"Ubirak\\Component\\Tests\\": "tests"
19+
}
20+
},
21+
"minimum-stability": "dev",
22+
"require": {
23+
"php": "^7.1.3",
24+
"guzzlehttp/psr7": "^1.2",
25+
"php-http/client-common": "^1.3",
26+
"tolerance/tolerance": "^0.4.2",
27+
"psr/log": "^1.0"
28+
},
29+
"require-dev": {
30+
"atoum/atoum": "^3.0"
31+
},
32+
"config": {
33+
"preferred-install": {
34+
"*": "dist"
35+
}
36+
}
37+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Ubirak package.
5+
*
6+
* (c) Ubirak team <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
declare(strict_types=1);
13+
14+
namespace Ubirak\Component\Healthcheck\Tests\Units;
15+
16+
use atoum;
17+
18+
class HttpHealthcheck extends atoum
19+
{
20+
/**
21+
* @dataProvider reachability
22+
*/
23+
public function test reachability(string $destination, int $statusCode, bool $expectedReachability)
24+
{
25+
$this
26+
->given(
27+
$httpClient = new \mock\Http\Client\HttpClient(),
28+
$response = new \mock\GuzzleHttp\Psr7\Response(),
29+
$this->calling($httpClient)->sendRequest = $response,
30+
$logger = new \mock\Psr\Log\LoggerInterface(),
31+
$this->newTestedInstance($httpClient, $logger),
32+
$this->calling($response)->getStatusCode = $statusCode
33+
)
34+
->when(
35+
$reachable = $this->testedInstance->isReachable($destination)
36+
)
37+
->then
38+
->boolean($reachable)->isIdenticalTo($expectedReachability)
39+
->mock($logger)->call('info')->twice()
40+
->mock($httpClient)->call('sendRequest')->once()
41+
->mock($response)->call('getStatusCode')->once()
42+
;
43+
}
44+
45+
protected function reachability(): array
46+
{
47+
return [
48+
'reachable on 200' => ['http://localhost:9000/foo', 200, true],
49+
'unreachable on 201' => ['http://localhost:9000/foo', 201, false],
50+
'unreachable on 400' => ['http://localhost:9000/foo', 400, false],
51+
'unreachable on 500' => ['http://localhost:9000/foo', 500, false],
52+
];
53+
}
54+
}

0 commit comments

Comments
 (0)