├── tests ├── commit.txt ├── trailing-newline-commit.txt ├── TestCase.php ├── Checks │ ├── General │ │ ├── EnvHealthCheckTest.php │ │ ├── DebugHealthCheckTest.php │ │ ├── MemcachedHealthCheckTest.php │ │ └── HttpHealthCheckTest.php │ └── Laravel │ │ ├── DatabaseHealthCheckTest.php │ │ └── QueueHealthCheckTest.php ├── HealthCheckTest.php ├── ResultStackTest.php ├── HealthResultTest.php └── HealthzTest.php ├── .gitignore ├── docker-compose.yaml ├── src ├── Exceptions │ ├── HealthFailureException.php │ └── HealthWarningException.php ├── Support │ ├── Stack.php │ ├── HealthzArtisanCommand.php │ └── HealthzServiceProvider.php ├── Checks │ ├── General │ │ ├── DebugHealthCheck.php │ │ ├── EnvHealthCheck.php │ │ ├── MemcachedHealthCheck.php │ │ └── HttpHealthCheck.php │ └── Laravel │ │ ├── DatabaseHealthCheck.php │ │ └── QueueHealthCheck.php ├── ResultStack.php ├── HealthResult.php ├── Healthz.php └── HealthCheck.php ├── phpunit.xml ├── docker └── installComposer.sh ├── .github └── workflows │ └── test.yml ├── LICENSE ├── composer.json ├── Dockerfile ├── templates └── healthz.html └── readme.md /tests/commit.txt: -------------------------------------------------------------------------------- 1 | 2d39ff989b3f18bd332cfc7a5c2a0abea1308e27 -------------------------------------------------------------------------------- /tests/trailing-newline-commit.txt: -------------------------------------------------------------------------------- 1 | 2d39ff989b3f18bd332cfc7a5c2a0abea1308e27 2 | 3 | 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /tests/coverage 3 | composer.lock 4 | .idea 5 | .phpunit.result.cache 6 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests/ 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /docker/installComposer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | EXPECTED_SIGNATURE=$(wget -q -O - https://composer.github.io/installer.sig) 4 | php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" 5 | ACTUAL_SIGNATURE=$(php -r "echo hash_file('sha384', 'composer-setup.php');") 6 | 7 | if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ]; then 8 | >&2 echo 'ERROR: Invalid installer signature' 9 | rm composer-setup.php 10 | exit 1 11 | fi 12 | 13 | php composer-setup.php --install-dir=/usr/local/bin --filename=composer --quiet 14 | RESULT=$? 15 | rm composer-setup.php 16 | if [[ "$RESULT" -eq "0" ]]; then 17 | composer global require hirak/prestissimo 18 | fi; 19 | exit $RESULT 20 | Terms 21 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | on: 4 | push: 5 | pull_request: 6 | types: 7 | - opened 8 | - synchronize 9 | 10 | jobs: 11 | build: 12 | name: PHP ${{ matrix.php }} 13 | 14 | runs-on: ${{ matrix.os }} 15 | 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | php: [ "7.3", "7.4", "8.0", "8.1" ] 20 | os: [ ubuntu-latest ] 21 | 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@master 25 | 26 | - name: Setup PHP 27 | uses: shivammathur/setup-php@master 28 | with: 29 | php-version: ${{ matrix.php }} 30 | 31 | - name: Cache Composer dependencies 32 | uses: actions/cache@v2 33 | with: 34 | path: /tmp/composer-cache 35 | key: ${{ runner.os }}-${{ hashFiles('**/composer.lock') }} 36 | 37 | - name: Install Dependencies 38 | uses: php-actions/composer@master 39 | with: 40 | php_version: ${{ matrix.php }} 41 | 42 | - name: Execute tests 43 | run: vendor/bin/phpunit 44 | -------------------------------------------------------------------------------- /src/Support/Stack.php: -------------------------------------------------------------------------------- 1 | items; 20 | } 21 | 22 | /** 23 | * @param $item 24 | * 25 | * @return $this 26 | */ 27 | public function push($item): self 28 | { 29 | $this->items[] = $item; 30 | 31 | return $this; 32 | } 33 | 34 | /** 35 | * @param array $items 36 | * 37 | * @return $this 38 | */ 39 | public function merge(array $items): self 40 | { 41 | $this->items = array_merge($this->items, $items); 42 | 43 | return $this; 44 | } 45 | 46 | /** 47 | * @param array $items 48 | * 49 | * @return $this 50 | */ 51 | public function replace(array $items): self 52 | { 53 | $this->items = $items; 54 | 55 | return $this; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Generation Tux 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tests/Checks/General/EnvHealthCheckTest.php: -------------------------------------------------------------------------------- 1 | env = new EnvHealthCheck('CUSTOM_ENV'); 17 | } 18 | 19 | /** @test */ 20 | public function instance_of_health_check() 21 | { 22 | $this->assertInstanceOf(HealthCheck::class, $this->env); 23 | } 24 | 25 | /** @test */ 26 | public function sets_the_status_to_the_current_environment() 27 | { 28 | putenv('CUSTOM_ENV=staging'); 29 | $this->env->run(); 30 | $this->assertSame('staging', $this->env->status()); 31 | } 32 | 33 | /** 34 | * @test 35 | */ 36 | public function unknown_environment_emits_a_warning() 37 | { 38 | $this->expectException(\Gentux\Healthz\Exceptions\HealthWarningException::class); 39 | putenv('CUSTOM_ENV='); 40 | $this->env->run(); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Checks/General/DebugHealthCheck.php: -------------------------------------------------------------------------------- 1 | env = $env; 27 | } 28 | 29 | /** 30 | * Check if the app is in debug mode 31 | * 32 | * @return void 33 | * 34 | * @throws HealthWarningException 35 | */ 36 | public function run(): void 37 | { 38 | $debug = getenv($this->env) == 'true'; 39 | 40 | if ($debug) { 41 | throw new HealthWarningException('on'); 42 | } 43 | 44 | $this->setStatus('off'); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/Checks/General/EnvHealthCheck.php: -------------------------------------------------------------------------------- 1 | env = $env; 28 | } 29 | 30 | /** 31 | * Run the health check 32 | * 33 | * @throws HealthWarningException 34 | */ 35 | public function run(): void 36 | { 37 | $env = getenv($this->env) ?: 'UNKNOWN'; 38 | if ($env == 'UNKNOWN') { 39 | throw new HealthWarningException($env); 40 | } 41 | 42 | $this->setStatus($env); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/Checks/General/DebugHealthCheckTest.php: -------------------------------------------------------------------------------- 1 | debug = new DebugHealthCheck(); 13 | } 14 | 15 | /** @test */ 16 | public function instance_of_health_check() 17 | { 18 | $this->assertInstanceOf(HealthCheck::class, $this->debug); 19 | } 20 | 21 | /** @test */ 22 | public function run_sets_the_description_to_off() 23 | { 24 | putenv('APP_DEBUG=false'); 25 | 26 | $this->debug->run(); 27 | $this->assertSame('off', $this->debug->status()); 28 | } 29 | 30 | /** 31 | * @test 32 | */ 33 | public function run_throws_warning_exception_if_debug_is_on() 34 | { 35 | $this->expectException(\Gentux\Healthz\Exceptions\HealthWarningException::class); 36 | $this->debug = new DebugHealthCheck('DEBUG_CUSTOM'); 37 | putenv('DEBUG_CUSTOM=true'); 38 | 39 | $this->debug->run(); 40 | $this->assertSame('on', $this->debug->status()); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/HealthCheckTest.php: -------------------------------------------------------------------------------- 1 | check = new MockCheck(); 20 | $this->checkWithTitle = new MockCheckTitle(); 21 | } 22 | 23 | /** @test */ 24 | public function title_defaults_to_the_class_name() 25 | { 26 | $result = $this->check->title(); 27 | $this->assertSame('MockCheck', $result); 28 | 29 | $result = $this->checkWithTitle->title(); 30 | $this->assertSame('Custom Title', $result); 31 | } 32 | } 33 | 34 | /** 35 | * ---------------------------------------------------------------------- 36 | * Mock Health Checks that extends base abstract class 37 | * ---------------------------------------------------------------------- 38 | */ 39 | 40 | class MockCheck extends HealthCheck 41 | { 42 | public function run() { return 'all good'; } 43 | } 44 | 45 | class MockCheckTitle extends HealthCheck 46 | { 47 | protected $title = 'Custom Title'; 48 | 49 | public function run() { return 'all good'; } 50 | } 51 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "generationtux/healthz", 3 | "description": "Health checks for PHP apps.", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Kyle Ferguson", 8 | "email": "kyle.ferguson@generationtux.com" 9 | }, 10 | { 11 | "name": "Damien Russell", 12 | "email": "christopher.russell@generationtux.com" 13 | }, 14 | { 15 | "name": "Thomas Manley", 16 | "email": "thomas.manley@generationtux.com" 17 | } 18 | ], 19 | "minimum-stability": "dev", 20 | "prefer-stable": true, 21 | "autoload": { 22 | "psr-4": { 23 | "Gentux\\Healthz\\": "src/" 24 | }, 25 | "classmap": [ 26 | "tests/TestCase.php" 27 | ] 28 | }, 29 | "require": { 30 | "php": ">=7.2.5|^8.0", 31 | "guzzlehttp/guzzle": "^6.2|^7.0.1|^7.2", 32 | "aws/aws-sdk-php": "~3.0", 33 | "illuminate/contracts": "^6.0|^7.0|^8.0|^9.0", 34 | "illuminate/database": "^6.0|^7.0|^8.0|^9.0", 35 | "illuminate/queue": "^6.0|7.0|^8.0|^9.0", 36 | "illuminate/console": "^6.0|^7.0|^8.0|^9.0", 37 | "twig/twig": "^3.0" 38 | }, 39 | "require-dev": { 40 | "phpunit/phpunit": "^8.4|^9.3.3", 41 | "mockery/mockery": "~1.3.3|^1.4.2" 42 | }, 43 | "extra": { 44 | "laravel": { 45 | "providers": [ 46 | "Gentux\\Healthz\\Support\\HealthzServiceProvider" 47 | ] 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:8.1-fpm-alpine 2 | 3 | # The following labels need to be set as part of the docker build process. 4 | # org.opencontainers.image.created 5 | # org.opencontainers.image.revision 6 | LABEL org.opencontainers.image.url="https://laravel.com" \ 7 | org.opencontainers.image.documentation="https://github.com/generationtux/php-healthz/blob/master/README.md" \ 8 | org.opencontainers.image.source="https://github.com/generationtux/php-healthz/Dockerfile" \ 9 | org.opencontainers.image.vendor="Generation Tux " \ 10 | org.opencontainers.image.title="Laravel 6.x" \ 11 | org.opencontainers.image.description="PHP built for use with the Laravel/Lumen framework" \ 12 | com.generationtux.php.backend="fpm" 13 | 14 | USER root 15 | 16 | COPY ./docker/installComposer.sh /tmp/installComposer.sh 17 | 18 | RUN apk --no-cache --update add bash ca-certificates libpq postgresql-dev curl git curl git mysql-client unzip wget zip postgresql-client \ 19 | && apk add --no-cache --virtual build-dependencies autoconf build-base g++ make \ 20 | && pecl install redis xdebug-3.1.4 \ 21 | && docker-php-ext-install bcmath opcache pdo_mysql pdo_pgsql pcntl \ 22 | && docker-php-ext-enable bcmath opcache redis xdebug \ 23 | && chmod +x /tmp/installComposer.sh \ 24 | && /tmp/installComposer.sh \ 25 | && chown www-data:www-data /usr/local/bin/composer \ 26 | && apk del --purge autoconf build-dependencies g++ make \ 27 | && chown -R www-data:www-data /var/www 28 | 29 | WORKDIR /var/www 30 | 31 | USER www-data:www-data 32 | -------------------------------------------------------------------------------- /src/ResultStack.php: -------------------------------------------------------------------------------- 1 | items = $results; 26 | } 27 | 28 | /** 29 | * @param HealthResult $result 30 | * 31 | * @return Stack 32 | */ 33 | public function push(HealthResult $result): ResultStack 34 | { 35 | return $this->stackPush($result); 36 | } 37 | 38 | /** 39 | * Determine if any results in the stack have failed 40 | * 41 | * @return bool 42 | */ 43 | public function hasFailures(): bool 44 | { 45 | $hasFailure = false; 46 | foreach ($this->all() as $result) { 47 | if ($result->failed()) { 48 | $hasFailure = true; 49 | break; 50 | } 51 | } 52 | 53 | return $hasFailure; 54 | } 55 | 56 | /** 57 | * Determine if any results in the stack have warnings 58 | * 59 | * @return bool 60 | */ 61 | public function hasWarnings(): bool 62 | { 63 | $hasWarning = false; 64 | foreach($this->all() as $result) { 65 | if ($result->warned()) { 66 | $hasWarning = true; 67 | break; 68 | } 69 | } 70 | 71 | return $hasWarning; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/ResultStackTest.php: -------------------------------------------------------------------------------- 1 | checkPassed = Mockery::mock(HealthResult::class); 24 | $this->checkPassed->shouldReceive('failed')->andReturn(false); 25 | 26 | $this->checkFailed = Mockery::mock(HealthResult::class); 27 | $this->checkFailed->shouldReceive('failed')->andReturn(true); 28 | 29 | $this->checkWarned = Mockery::mock(HealthResult::class); 30 | $this->checkWarned->shouldReceive('warned')->andReturn(true); 31 | $this->checkWarned->shouldReceive('failed')->andReturn(false); 32 | 33 | $this->stack = new ResultStack(); 34 | parent::setUp(); 35 | } 36 | 37 | /** @test */ 38 | public function push_and_check_for_failures() 39 | { 40 | $this->stack->push($this->checkPassed); 41 | $this->assertSame([$this->checkPassed], $this->stack->all()); 42 | $this->assertFalse($this->stack->hasFailures()); 43 | 44 | $this->stack->merge([$this->checkFailed]); 45 | $this->assertSame([$this->checkPassed, $this->checkFailed], $this->stack->all()); 46 | $this->assertTrue($this->stack->hasFailures()); 47 | } 48 | 49 | /** @test */ 50 | public function push_and_check_for_warnings() 51 | { 52 | $this->stack->replace([$this->checkWarned]); 53 | $this->assertFalse($this->stack->hasFailures()); 54 | $this->assertTrue($this->stack->hasWarnings()); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/HealthResultTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('title')->andReturn('Title'); 23 | $check->shouldReceive('description')->andReturn('Description'); 24 | $check->shouldReceive('status')->andReturn('Status'); 25 | 26 | $this->resultSuccess = new HealthResult(HealthResult::RESULT_SUCCESS, $check); 27 | $this->resultWarning = new HealthResult(HealthResult::RESULT_WARNING, $check); 28 | $this->resultFailure = new HealthResult(HealthResult::RESULT_FAILURE, $check); 29 | } 30 | 31 | /** @test */ 32 | public function result_helpers() 33 | { 34 | $this->assertTrue($this->resultSuccess->passed()); 35 | $this->assertFalse($this->resultSuccess->warned()); 36 | $this->assertFalse($this->resultSuccess->failed()); 37 | 38 | $this->assertTrue($this->resultWarning->warned()); 39 | $this->assertFalse($this->resultWarning->passed()); 40 | $this->assertFalse($this->resultWarning->failed()); 41 | 42 | $this->assertTrue($this->resultFailure->failed()); 43 | $this->assertFalse($this->resultFailure->passed()); 44 | $this->assertFalse($this->resultFailure->warned()); 45 | } 46 | 47 | /** @test */ 48 | public function information_about_health_check() 49 | { 50 | $this->assertSame('Title', $this->resultSuccess->title()); 51 | $this->assertSame('Description', $this->resultSuccess->description()); 52 | $this->assertSame('Status', $this->resultSuccess->status()); 53 | $this->assertSame(HealthResult::RESULT_SUCCESS, $this->resultSuccess->result()); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /templates/healthz.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Healthz 4 | 5 | 47 | 48 | 49 | 50 | 51 |
52 |

Health Check

53 | 54 | 65 |
66 | 67 | 68 | -------------------------------------------------------------------------------- /src/Support/HealthzArtisanCommand.php: -------------------------------------------------------------------------------- 1 | checks = $checks; 24 | } 25 | 26 | /** 27 | * Execute the console command 28 | * 29 | * @return int 30 | */ 31 | public function handle(): int 32 | { 33 | if (count($this->checks->all()) == 0) { 34 | $this->comment("No health checks registered. Be sure to register Gentux\Healthz\Healthz in a service provider. See github.com/generationtux/php-healthz for more info."); 35 | return 0; 36 | } 37 | 38 | $results = $this->checks->run(); 39 | foreach ($results->all() as $result) { 40 | $this->outputCheckResult($result); 41 | } 42 | 43 | if ($results->hasFailures()) { 44 | return 1; 45 | } 46 | 47 | return 0; 48 | } 49 | 50 | /** 51 | * Output message about health check result 52 | * 53 | * @param HealthResult $result 54 | * 55 | * @return void 56 | */ 57 | protected function outputCheckResult(HealthResult $result): void 58 | { 59 | $message = $result->title() . ": " . $result->status(); 60 | 61 | switch ($result->result()) { 62 | case HealthResult::RESULT_SUCCESS: 63 | $this->info($message); 64 | break; 65 | case HealthResult::RESULT_WARNING: 66 | $this->warn($message); 67 | break; 68 | case HealthResult::RESULT_FAILURE: 69 | $this->error($message); 70 | break; 71 | default: 72 | $this->comment($message); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/HealthResult.php: -------------------------------------------------------------------------------- 1 | result = $result; 30 | $this->check = $check; 31 | } 32 | 33 | /** 34 | * Determine if the result is a failure 35 | * 36 | * @return bool 37 | */ 38 | public function failed(): bool 39 | { 40 | return $this->result() === self::RESULT_FAILURE; 41 | } 42 | 43 | /** 44 | * Determine if the result is a success 45 | * 46 | * @return bool 47 | */ 48 | public function passed(): bool 49 | { 50 | return $this->result() === self::RESULT_SUCCESS; 51 | } 52 | 53 | /** 54 | * Determine if the result is a warning 55 | * 56 | * @return bool 57 | */ 58 | public function warned(): bool 59 | { 60 | return $this->result() === self::RESULT_WARNING; 61 | } 62 | 63 | /** Getters: information about the health check */ 64 | 65 | /** 66 | * @return string 67 | */ 68 | public function title(): string 69 | { 70 | return $this->check->title(); 71 | } 72 | 73 | /** 74 | * @return null|string 75 | */ 76 | public function description(): ?string 77 | { 78 | return $this->check->description(); 79 | } 80 | 81 | /** 82 | * @return string|null 83 | */ 84 | public function status(): ?string 85 | { 86 | return $this->check->status(); 87 | } 88 | 89 | /** 90 | * @return int 91 | */ 92 | public function result(): int 93 | { 94 | return $this->result; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/Support/HealthzServiceProvider.php: -------------------------------------------------------------------------------- 1 | app, 'post')) { 23 | $this->app->get('/healthz', $this->healthzHandler()); 24 | $this->app->get('/healthz/ui', $this->healthzUIHandler()); 25 | } else { 26 | \Route::get('/healthz', $this->healthzHandler()); 27 | \Route::get('/healthz/ui', $this->healthzUIHandler()); 28 | } 29 | 30 | if ($this->app->runningInConsole()) { 31 | $this->commands([HealthzArtisanCommand::class]); 32 | } 33 | } 34 | 35 | protected function healthzHandler() 36 | { 37 | return function() { 38 | $healthz = app(Healthz::class); 39 | $results = $healthz->run(); 40 | if ($results->hasFailures()) { 41 | return response('fail', 500); 42 | } 43 | 44 | return response('ok', 200); 45 | }; 46 | } 47 | 48 | protected function healthzUIHandler() 49 | { 50 | return function() { 51 | $username = getenv('HEALTHZ_USERNAME'); 52 | $password = getenv('HEALTHZ_PASSWORD'); 53 | if ($username != "") { 54 | if ( 55 | Arr::get($_SERVER, 'PHP_AUTH_USER') !== $username || 56 | Arr::get($_SERVER, 'PHP_AUTH_PW') !== $password 57 | ) { 58 | return response('Invalid credentials', 401, ['WWW-Authenticate' => 'Basic']); 59 | } 60 | } 61 | 62 | $healthz = app(Healthz::class); 63 | $results = $healthz->run(); 64 | $html = $healthz->html($results); 65 | 66 | $status = 200; 67 | if ($results->hasFailures()) { 68 | $status = 500; 69 | } 70 | 71 | return response($html, $status)->header('Content-Type', 'text/html'); 72 | }; 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tests/HealthzTest.php: -------------------------------------------------------------------------------- 1 | check1 = Mockery::mock(HealthCheck::class); 27 | $this->check2 = Mockery::mock(HealthCheck::class); 28 | $this->check3 = Mockery::mock(HealthCheck::class); 29 | 30 | $this->healthz = new Healthz([$this->check1, $this->check2]); 31 | } 32 | 33 | /** @test */ 34 | public function get_set_of_health_checks() 35 | { 36 | $result = $this->healthz->all(); 37 | $this->assertCount(2, $result); 38 | $this->assertSame($this->check1, $result[0]); 39 | $this->assertSame($this->check2, $result[1]); 40 | } 41 | 42 | /** @test */ 43 | public function push_new_health_checks_onto_the_stack() 44 | { 45 | $result = $this->healthz->push($this->check3); 46 | $this->assertSame($this->healthz, $result); 47 | 48 | $this->assertCount(3, $this->healthz->all()); 49 | } 50 | 51 | /** @test */ 52 | public function run_health_checks_and_return_result_stack() 53 | { 54 | $this->healthz->push($this->check3); 55 | 56 | # success 57 | $this->check1->shouldReceive('run'); 58 | 59 | # warning 60 | $this->check2->shouldReceive('run')->andThrow(new HealthWarningException('warning')); 61 | $this->check2->shouldReceive('setStatus')->with('warning')->once(); 62 | 63 | # failure 64 | $this->check3->shouldReceive('run')->andThrow(new Exception('failure')); 65 | $this->check3->shouldReceive('setStatus')->with('failure')->once(); 66 | 67 | $result = $this->healthz->run(); 68 | $this->assertInstanceOf(ResultStack::class, $result); 69 | $this->assertTrue($result->all()[0]->passed()); 70 | $this->assertTrue($result->all()[1]->warned()); 71 | $this->assertTrue($result->all()[2]->failed()); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Healthz.php: -------------------------------------------------------------------------------- 1 | items = $healthChecks; 28 | } 29 | 30 | /** 31 | * Push new health check onto the stack 32 | * 33 | * @param HealthCheck $healthCheck 34 | * 35 | * @return $this 36 | */ 37 | public function push(HealthCheck $healthCheck): self 38 | { 39 | return $this->stackPush($healthCheck); 40 | } 41 | 42 | /** 43 | * Run the health checks in the stack 44 | * 45 | * @return ResultStack 46 | */ 47 | public function run(): ResultStack 48 | { 49 | $results = []; 50 | 51 | foreach($this->all() as $check) { 52 | $resultCode = HealthResult::RESULT_SUCCESS; 53 | 54 | try { 55 | $check->run(); 56 | } catch (Exception $e) { 57 | $check->setStatus($e->getMessage()); 58 | $resultCode = $e instanceof HealthWarningException ? HealthResult::RESULT_WARNING : HealthResult::RESULT_FAILURE; 59 | } 60 | 61 | $results[] = new HealthResult($resultCode, $check); 62 | } 63 | 64 | return new ResultStack($results); 65 | } 66 | 67 | /** 68 | * Generate the HTML view for the health checks 69 | * 70 | * NOTE: this will run the health checks if a result stack is not passed in 71 | * 72 | * @param ResultStack $results 73 | * 74 | * @return string 75 | */ 76 | public function html(ResultStack $results = null): string 77 | { 78 | if ($results === null) { 79 | $results = $this->run(); 80 | } 81 | 82 | $loader = new TwigArrayLoader([ 83 | 'healthz' => file_get_contents(__DIR__ . '/../templates/healthz.html'), 84 | ]); 85 | $twig = new TwigEnvironment($loader); 86 | 87 | return $twig->render('healthz', ['results' => $results->all()]); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /tests/Checks/Laravel/DatabaseHealthCheckTest.php: -------------------------------------------------------------------------------- 1 | manager = Mockery::mock(DatabaseManager::class); 24 | $this->db = new DatabaseHealthCheck($this->manager); 25 | } 26 | 27 | /** @test */ 28 | public function instance_of_health_check() 29 | { 30 | $this->assertInstanceOf(HealthCheck::class, $this->db); 31 | } 32 | 33 | /** @test */ 34 | public function sets_connection_name() 35 | { 36 | $this->assertNull($this->db->connection()); 37 | 38 | $this->db->setConnection('custom'); 39 | $this->assertSame('custom', $this->db->connection()); 40 | } 41 | 42 | /** @test */ 43 | public function if_no_connection_is_set_use_the_description() 44 | { 45 | $description = $this->db->description(); 46 | $this->assertSame('Check the database connection.', $description); # if connection is also null 47 | 48 | $this->db->setConnection('mysql'); 49 | $description = $this->db->description(); 50 | $this->assertSame('mysql', $description); 51 | } 52 | 53 | /** @test */ 54 | public function uses_the_connection_name_set_to_resolve_a_laravel_db_connection() 55 | { 56 | $this->db->setConnection('custom'); 57 | 58 | $conn = Mockery::mock(Connection::class)->makePartial(); 59 | $this->manager->shouldReceive('connection')->with('custom')->once() 60 | ->andReturn($conn); 61 | 62 | $pdo = Mockery::mock(PDO::class); 63 | $conn->shouldReceive('getPdo')->once() 64 | ->andReturn($pdo); 65 | 66 | $this->db->run(); 67 | $status = $this->db->status(); 68 | $this->assertSame('connected', $status); 69 | } 70 | 71 | /** 72 | * @test 73 | */ 74 | public function throws_health_failure_when_laravel_runs_into_trouble() 75 | { 76 | $this->expectException(\Gentux\Healthz\Exceptions\HealthFailureException::class); 77 | $this->manager->shouldReceive('connection')->andThrow(new \Exception()); 78 | $this->db->run(); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Checks/Laravel/DatabaseHealthCheck.php: -------------------------------------------------------------------------------- 1 | db = $db; 37 | 38 | if (!$this->db) { 39 | try { $this->db = app('db'); } catch (Exception $e) { 40 | throw new HealthFailureException('Cannot create instance of Laravel database manager.'); 41 | } 42 | } 43 | } 44 | 45 | /** 46 | * Check database connection 47 | * 48 | * @return void 49 | * 50 | * @throws HealthFailureException 51 | */ 52 | public function run(): void 53 | { 54 | try { 55 | $name = $this->connection(); 56 | $this->db->connection($name)->getPdo(); 57 | } catch (Exception $e) { 58 | throw new HealthFailureException($e->getMessage()); 59 | } 60 | 61 | $this->setStatus('connected'); 62 | } 63 | 64 | /** 65 | * Get the connection name 66 | * 67 | * @return null|string 68 | */ 69 | public function connection(): ?string 70 | { 71 | return $this->connection; 72 | } 73 | 74 | /** 75 | * Set the connection name 76 | * 77 | * @param string $connection 78 | * 79 | * @return void 80 | */ 81 | public function setConnection($connection): void 82 | { 83 | $this->connection = $connection; 84 | } 85 | 86 | /** 87 | * If no description property is defined, use the connection 88 | * name instead ('default' if connection is also null). 89 | * 90 | * @return string 91 | */ 92 | public function description(): string 93 | { 94 | return $this->connection() ?: $this->description;; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/HealthCheck.php: -------------------------------------------------------------------------------- 1 | title; 45 | 46 | if (!$title) { 47 | $classTitle = explode('\\', get_class($this)); 48 | $title = array_pop($classTitle); 49 | } 50 | 51 | return $title; 52 | } 53 | 54 | /** 55 | * Set the title for the health check 56 | * 57 | * @param string $title 58 | * 59 | * @return $this 60 | */ 61 | public function setTitle($title): self 62 | { 63 | $this->title = $title; 64 | 65 | return $this; 66 | } 67 | 68 | /** 69 | * Get description for the health check. 70 | * 71 | * @return null|string 72 | */ 73 | public function description(): ?string 74 | { 75 | return $this->description; 76 | } 77 | 78 | /** 79 | * Set the description for the health check 80 | * 81 | * @param string $description 82 | * 83 | * @return $this 84 | */ 85 | public function setDescription($description): self 86 | { 87 | $this->description = $description; 88 | 89 | return $this; 90 | } 91 | 92 | /** 93 | * Get the status of the health check 94 | * 95 | * NOTE: If an exception is thrown, the status message for a health 96 | * check will be replaced with the exceptions message. 97 | * 98 | * @return null|string 99 | */ 100 | public function status(): ?string 101 | { 102 | return $this->status; 103 | } 104 | 105 | /** 106 | * Set the status of the health check 107 | * 108 | * @param string $status 109 | * 110 | * @return $this 111 | */ 112 | public function setStatus($status): self 113 | { 114 | $this->status = $status; 115 | 116 | return $this; 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /tests/Checks/General/MemcachedHealthCheckTest.php: -------------------------------------------------------------------------------- 1 | memcached = Mockery::mock(Memcached::class); 29 | $this->health = new MemcachedHealthCheck($this->memcached); 30 | } 31 | 32 | /** @test */ 33 | public function instance_of_health_check() 34 | { 35 | $this->assertInstanceOf(HealthCheck::class, $this->health); 36 | } 37 | 38 | /** @test */ 39 | public function add_servers() 40 | { 41 | $this->health->addServer('123.com'); 42 | $this->health->addServer('456.com', 2222, 1); 43 | 44 | $expect = [ 45 | ['123.com', 11211, 0], 46 | ['456.com', 2222, 1], 47 | ]; 48 | $this->assertSame($expect, $this->health->servers()); 49 | } 50 | 51 | /** @test */ 52 | public function set_options() 53 | { 54 | $this->health->setOptions([1 => 'foo']); 55 | $this->assertSame([1 => 'foo'], $this->health->options()); 56 | } 57 | 58 | /** @test */ 59 | public function username_and_password() 60 | { 61 | $this->health->setAuth('user', 'secret'); 62 | 63 | $this->assertSame('user', $this->health->username()); 64 | $this->assertSame('secret', $this->health->password()); 65 | } 66 | 67 | /** @test */ 68 | public function run_builds_memcached_instance_and_tests_connection() 69 | { 70 | $this->health->addServer('123.com'); 71 | $this->health->addServer('456.com', 2222, 1); 72 | $this->health->setAuth('user', 'secret'); 73 | $this->health->setOptions(['foo' => 'bar']); 74 | 75 | # spy on memcached instance 76 | $servers = [ ['123.com', 11211, 0], ['456.com', 2222, 1] ]; 77 | $this->memcached->shouldReceive('addServers')->with($servers)->once(); 78 | $this->memcached->shouldReceive('setOptions')->with(['foo' => 'bar'])->once(); 79 | $this->memcached->shouldReceive('setSaslAuthData')->with('user', 'secret')->once(); 80 | $this->memcached->shouldReceive('set')->with('test.connection', 'success', 1)->once()->andReturn(true); 81 | 82 | $this->health->run(); 83 | $this->assertTrue(true); 84 | } 85 | 86 | /** 87 | * @test 88 | */ 89 | public function run_throws_failure_exception_if_memcached_cant_set_test_value() 90 | { 91 | $this->expectException(\Gentux\Healthz\Exceptions\HealthFailureException::class); 92 | $this->memcached->shouldReceive('set')->with('test.connection', 'success', 1)->once()->andReturn(false); 93 | $this->health->run(); 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tests/Checks/Laravel/QueueHealthCheckTest.php: -------------------------------------------------------------------------------- 1 | manager = Mockery::mock(QueueManager::class); 26 | $this->queue = new QueueHealthCheck($this->manager); 27 | } 28 | 29 | /** @test */ 30 | public function instance_of_health_check() 31 | { 32 | $this->assertInstanceOf(HealthCheck::class, $this->queue); 33 | } 34 | 35 | /** @test */ 36 | public function sets_queue_name() 37 | { 38 | $this->assertNull($this->queue->name()); 39 | 40 | $this->queue->setName('custom'); 41 | $this->assertSame('custom', $this->queue->name()); 42 | } 43 | 44 | /** @test */ 45 | public function if_no_connection_is_set_use_the_description() 46 | { 47 | $description = $this->queue->description(); 48 | $this->assertSame('Check the queue connection.', $description); 49 | 50 | $this->queue->setName('sqs'); 51 | $description = $this->queue->description(); 52 | $this->assertSame('sqs', $description); 53 | } 54 | 55 | /** @test */ 56 | public function checks_connection_status_of_sqs_queue() 57 | { 58 | $this->queue->setName('custom'); 59 | 60 | # laravel sqs queue service 61 | $sqsQueue = Mockery::mock(SqsQueue::class); 62 | $this->manager->shouldReceive('connection')->with('custom')->once()->andReturn($sqsQueue); 63 | 64 | # need url of queue to check attributes on SQS 65 | $sqsQueue->shouldReceive('getQueue')->andReturn('some-queue-url.com');; 66 | 67 | # sqs service to check connection 68 | $sqs = Mockery::mock(SqsClient::class); 69 | $sqsQueue->shouldReceive('getSqs')->andReturn($sqs); 70 | 71 | # make sure a call is made using sqs client to get queue attributes 72 | $sqs->shouldReceive('getQueueAttributes')->with(['QueueUrl' => 'some-queue-url.com'])->once(); 73 | 74 | $this->queue->run(); 75 | $status = $this->queue->status(); 76 | $this->assertSame('connected to SQS', $status); 77 | } 78 | 79 | /** @test */ 80 | public function checks_status_of_sync_queue() 81 | { 82 | $sync = Mockery::mock(SyncQueue::class); 83 | $this->manager->shouldReceive('connection')->andReturn($sync); 84 | 85 | $this->queue->run(); 86 | 87 | $status = $this->queue->status(); 88 | $this->assertSame('connected to Sync queue', $status); 89 | } 90 | 91 | /** 92 | * @test 93 | */ 94 | public function throws_warning_if_queue_driver_is_not_supported() 95 | { 96 | $this->expectException(\Gentux\Healthz\Exceptions\HealthWarningException::class); 97 | $redis = Mockery::mock(RedisQueue::class); 98 | $this->manager->shouldReceive('connection')->andReturn($redis); 99 | 100 | $this->queue->run(); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/Checks/General/MemcachedHealthCheck.php: -------------------------------------------------------------------------------- 1 | memcached = $memcached ?: new Memcached(); 29 | } 30 | 31 | /** 32 | * Check for connection to memcached servers 33 | * 34 | * @return void 35 | * 36 | * @throws HealthFailureException 37 | */ 38 | public function run(): void 39 | { 40 | if (count($this->servers())) { 41 | $this->memcached->addServers($this->servers()); 42 | } 43 | 44 | if (count($this->options())) { 45 | $this->memcached->setOptions($this->options()); 46 | } 47 | 48 | if (!is_null($this->username())) { 49 | $this->memcached->setSaslAuthData($this->username(), $this->password()); 50 | } 51 | 52 | $result = $this->memcached->set('test.connection', 'success', 1); 53 | if (!$result) { 54 | throw new HealthFailureException('Unable to set test value in memcache'); 55 | } 56 | 57 | $this->setStatus('able to set test value in memcache'); 58 | } 59 | 60 | /** 61 | * Add server to check 62 | * 63 | * @param string $server 64 | * @param int $port 65 | * @param int $weight 66 | * 67 | * @return self 68 | */ 69 | public function addServer($server, $port = 11211, $weight = 0): HealthCheck 70 | { 71 | $this->servers[] = [$server, $port, $weight]; 72 | 73 | return $this; 74 | } 75 | 76 | /** 77 | * Get servers 78 | * 79 | * @return array 80 | */ 81 | public function servers(): array 82 | { 83 | return $this->servers; 84 | } 85 | 86 | /** 87 | * Set memcached options 88 | * 89 | * @param array $options 90 | * 91 | * @return self 92 | */ 93 | public function setOptions(array $options): HealthCheck 94 | { 95 | $this->options = $options; 96 | 97 | return $this; 98 | } 99 | 100 | /** 101 | * Get options 102 | * 103 | * @return array 104 | */ 105 | public function options(): array 106 | { 107 | return $this->options; 108 | } 109 | 110 | /** 111 | * Set username and password for servers 112 | * 113 | * @param string $username 114 | * @param string $password 115 | * 116 | * @return self 117 | */ 118 | public function setAuth($username, $password): HealthCheck 119 | { 120 | $this->username = $username; 121 | $this->password = $password; 122 | 123 | return $this; 124 | } 125 | 126 | /** 127 | * Get username 128 | * 129 | * @return string|null 130 | */ 131 | public function username(): ?string 132 | { 133 | return $this->username; 134 | } 135 | 136 | /** 137 | * Get password 138 | * 139 | * @return string|null 140 | */ 141 | public function password(): ?string 142 | { 143 | return $this->password; 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/Checks/Laravel/QueueHealthCheck.php: -------------------------------------------------------------------------------- 1 | queue = $queue; 40 | 41 | if (!$this->queue) { 42 | try { $this->queue = app('queue'); } catch (Exception $e) { 43 | throw new HealthFailureException('Cannot create instance of Laravels queue manager.'); 44 | } 45 | } 46 | } 47 | 48 | /** 49 | * Check database connection 50 | * 51 | * @return void 52 | * 53 | * @throws HealthFailureException 54 | * @throws HealthWarningException 55 | */ 56 | public function run(): void 57 | { 58 | $name = $this->name(); 59 | $queue = $this->queue->connection($name); 60 | 61 | if ($queue instanceof SqsQueue) { 62 | $this->runSqsCheck($queue); 63 | } elseif ($queue instanceof SyncQueue) { 64 | $this->runSyncCheck($queue); 65 | } else { 66 | throw new HealthWarningException('Only SQS and Sync queue drivers supported at this time.'); 67 | } 68 | } 69 | 70 | /** 71 | * Run the health check against an sqs queue 72 | * 73 | * @param SqsQueue $queue 74 | * 75 | * @return void 76 | */ 77 | protected function runSqsCheck(SqsQueue $queue): void 78 | { 79 | $url = $queue->getQueue(null); 80 | $queue->getSqs()->getQueueAttributes(['QueueUrl' => $url]); 81 | 82 | $this->setStatus('connected to SQS'); 83 | } 84 | 85 | /** 86 | * Nothing to really check with a sync queue, will 87 | * just set the status. 88 | * 89 | * @param SyncQueue $queue 90 | * 91 | * @return void 92 | */ 93 | protected function runSyncCheck(SyncQueue $queue): void 94 | { 95 | $this->setStatus('connected to Sync queue'); 96 | } 97 | 98 | /** 99 | * Get the queue name 100 | * 101 | * @return null|string 102 | */ 103 | public function name(): ?string 104 | { 105 | return $this->name; 106 | } 107 | 108 | /** 109 | * Set the queue name 110 | * 111 | * @param string $name 112 | * 113 | * @return void 114 | */ 115 | public function setName($name): void 116 | { 117 | $this->name = $name; 118 | } 119 | 120 | /** 121 | * If no description property is defined, use the queue name instead. 122 | * 123 | * @return string|null 124 | */ 125 | public function description(): ?string 126 | { 127 | return $this->name() ?: $this->description; 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /tests/Checks/General/HttpHealthCheckTest.php: -------------------------------------------------------------------------------- 1 | request = Mockery::mock(Request::class); 29 | $this->guzzle = Mockery::mock(Client::class); 30 | $this->http = new HttpHealthCheck($this->request, 200, [], $this->guzzle); 31 | } 32 | 33 | /** @test */ 34 | public function instance_of_health_check() 35 | { 36 | $this->assertInstanceOf(HealthCheck::class, $this->http); 37 | } 38 | 39 | /** @test */ 40 | public function get_request_to_be_made() 41 | { 42 | $result = $this->http->request(); 43 | $this->assertSame($this->request, $result); 44 | 45 | $newRequest = Mockery::mock(Request::class); 46 | $this->http->setRequest($newRequest); 47 | $this->assertSame($newRequest, $this->http->request()); 48 | } 49 | 50 | /** @test */ 51 | public function gets_expected_status_code_for_request() 52 | { 53 | $result = $this->http->expectedStatusCode(); 54 | $this->assertSame(200, $result); 55 | 56 | $this->http->setExpectedStatusCode(404); 57 | $result = $this->http->expectedStatusCode(); 58 | $this->assertSame(404, $result); 59 | } 60 | 61 | /** @test */ 62 | public function gets_guzzle_options() 63 | { 64 | $result = $this->http->guzzleOptions(); 65 | $this->assertSame([], $result); 66 | 67 | $this->http->setGuzzleOptions(['foo' => 'bar']); 68 | $result = $this->http->guzzleOptions(); 69 | $this->assertSame(['foo' => 'bar'], $result); 70 | } 71 | 72 | /** @test */ 73 | public function run_sends_the_request() 74 | { 75 | $response = Mockery::mock(Response::class); 76 | $response->shouldReceive('getStatusCode')->andReturn(200); 77 | 78 | $this->guzzle->shouldReceive('send')->with($this->request, [])->once()->andReturn($response); 79 | 80 | $this->http->run(); 81 | $this->assertTrue(true); 82 | } 83 | 84 | /** 85 | * @test 86 | */ 87 | public function run_throws_an_exception_if_the_expected_response_code_doesnt_match() 88 | { 89 | $this->expectException(\Gentux\Healthz\Exceptions\HealthFailureException::class); 90 | $response = Mockery::mock(Response::class); 91 | $response->shouldReceive('getStatusCode')->andReturn(201); 92 | 93 | $this->guzzle->shouldReceive('send')->andReturn($response); 94 | 95 | $this->http->run(); 96 | } 97 | 98 | /** @test */ 99 | public function run_catches_guzzle_exceptions_to_compare_status_code() 100 | { 101 | $response = Mockery::mock(Response::class); 102 | $response->shouldReceive('getStatusCode')->andReturn(404); 103 | 104 | $e = Mockery::mock(RequestException::class); 105 | $e->shouldReceive('getResponse')->andReturn($response); 106 | 107 | $this->guzzle->shouldReceive('send')->andThrow($e); 108 | 109 | $this->http->setExpectedStatusCode(404); 110 | $this->http->run(); // no exceptions, should pass 111 | $this->assertTrue(true); 112 | } 113 | 114 | /** @test */ 115 | public function if_no_description_is_set_the_request_uri_is_used() 116 | { 117 | $this->request->shouldReceive('getUri')->andReturn(new Uri('/somewhere')); 118 | $description = $this->http->description(); 119 | 120 | $this->assertSame('/somewhere', $description); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /src/Checks/General/HttpHealthCheck.php: -------------------------------------------------------------------------------- 1 | send */ 29 | protected $guzzleOptions; 30 | 31 | /** @var Client */ 32 | protected $guzzle; 33 | 34 | public function __construct(Request $request, $expectedStatusCode = 200, array $guzzleOptions = [], Client $guzzle = null) 35 | { 36 | $this->request = $request; 37 | $this->expectedStatusCode = $expectedStatusCode; 38 | $this->guzzleOptions = $guzzleOptions; 39 | $this->guzzle = $guzzle ?: new Client($this->guzzleOptions); 40 | } 41 | 42 | /** 43 | * Send the request 44 | * 45 | * @return ResponseInterface 46 | * 47 | * @throws HealthFailureException|RequestException 48 | */ 49 | public function run(): ResponseInterface 50 | { 51 | try { 52 | $response = $this->guzzle()->send( 53 | $this->request(), 54 | $this->guzzleOptions() 55 | ); 56 | } catch (RequestException $e) { 57 | if (!$response = $e->getResponse()) { 58 | throw $e; 59 | } 60 | } 61 | 62 | if ($response->getStatusCode() !== $this->expectedStatusCode()) { 63 | $message = "Status code {$response->getStatusCode()} does not match expected {$this->expectedStatusCode()}"; 64 | throw new HealthFailureException($message); 65 | } 66 | 67 | return $response; 68 | } 69 | 70 | /** 71 | * Get request object to send 72 | * 73 | * @return Request 74 | */ 75 | public function request() 76 | { 77 | return $this->request; 78 | } 79 | 80 | /** 81 | * Set request object to send 82 | * 83 | * @param Request $request 84 | * 85 | * @return $this 86 | */ 87 | public function setRequest(Request $request) 88 | { 89 | $this->request = $request; 90 | 91 | return $this; 92 | } 93 | 94 | /** 95 | * Get the expected status code for the request 96 | * 97 | * @return int 98 | */ 99 | public function expectedStatusCode() 100 | { 101 | return $this->expectedStatusCode; 102 | } 103 | 104 | /** 105 | * Set the expected status code from the request 106 | * 107 | * @param $statusCode 108 | * 109 | * @return $this 110 | */ 111 | public function setExpectedStatusCode($statusCode) 112 | { 113 | $this->expectedStatusCode = $statusCode; 114 | 115 | return $this; 116 | } 117 | 118 | /** 119 | * Get Guzzle client options 120 | * 121 | * @return array 122 | */ 123 | public function guzzleOptions() 124 | { 125 | return $this->guzzleOptions; 126 | } 127 | 128 | /** 129 | * Set options for Guzzle client 130 | * 131 | * These will be passed as the request options on client->send 132 | * @see http://docs.guzzlephp.org/en/latest/request-options.html 133 | * 134 | * @param array $guzzleOptions 135 | * 136 | * @return $this 137 | */ 138 | public function setGuzzleOptions(array $guzzleOptions) 139 | { 140 | $this->guzzleOptions = $guzzleOptions; 141 | 142 | return $this; 143 | } 144 | 145 | /** 146 | * Get Guzzle client instance 147 | * 148 | * @return \GuzzleHttp\Client 149 | */ 150 | public function guzzle() 151 | { 152 | return $this->guzzle; 153 | } 154 | 155 | /** 156 | * If no description is set, we will use the request URL 157 | * 158 | * @return string 159 | */ 160 | public function description(): ?string 161 | { 162 | $description = $this->description; 163 | 164 | if (!$description) { 165 | $description = (string) $this->request()->getUri(); 166 | } 167 | 168 | return $description; 169 | } 170 | } 171 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![Build Test Status](https://github.com/generationtux/php-healthz/actions/workflows/test.yml/badge.svg?event=push) 2 | [![Code Climate](https://codeclimate.com/github/generationtux/php-healthz/badges/gpa.svg)](https://codeclimate.com/github/generationtux/php-healthz) 3 | [![Test Coverage](https://codeclimate.com/github/generationtux/php-healthz/badges/coverage.svg)](https://codeclimate.com/github/generationtux/php-healthz/coverage) 4 | 5 | # PHP Healthz 6 | 7 | Health checking for PHP apps with built-in support for Laravel. 8 | 9 | 10 | 11 | Get an easy overview of the health of your app! Implement a health check endpoint for load balancers, or your own sanity :) Comes with an optional UI and set of pre-configured checks you can use, and is extensible 12 | to add custom health checks to the stack as well. 13 | 14 | - [Setup and usage](#setup) 15 | - [Laravel](#laravel) 16 | - [General PHP](#general-php) 17 | - [Available checks and config](#check-configuration) 18 | - [HTTP](#http-check) 19 | - [Memcached](#memcached-check) 20 | - [Debug](#debug-check) 21 | - [Env)](#env-check) 22 | - [Database (Laravel)](#laravel-database) 23 | - [Queue (Laravel)](#laravel-queue) 24 | - [Creating custom checks](#custom-checks) 25 | 26 | --- 27 | 28 | ## Setup 29 | 30 | ```sh 31 | $ composer require generationtux/healthz 32 | ``` 33 | 34 | ### Laravel < 5.4 35 | 36 | (the following should work with Lumen as well, with minor differences) 37 | 38 | **Add the service provider that will register the default health checks and routes** 39 | 40 | ```php 41 | // config/app.php 42 | 'providers' => [ 43 | Illuminate..., 44 | Gentux\Healthz\Support\HealthzServiceProvider::class, 45 | ] 46 | ``` 47 | 48 | You should be able to visit `/healthz/ui` to see the default Laravel health checks, or run `php artisan healthz` to get a CLI view. 49 | 50 | To add basic auth to the UI page, set the `HEALTHZ_USERNAME` and `HEALTHZ_PASSWORD` environment variables. 51 | Even if the UI has basic auth, the simplified `/healthz` endpoint will always be available to respond with a simple `ok` or `fail` for load balancers and other automated checks to hit. 52 | 53 | **In order to customize the health checks, simply register `Gentux\Healthz\Healthz` in your app service provider (probably `app/Providers/AppServiceProvider.php`) to build a custom Healthz instance.** 54 | 55 | ```php 56 | use Gentux\Healthz\Healthz; 57 | use Illuminate\Support\ServiceProvider; 58 | use Gentux\Healthz\Checks\General\EnvHealthCheck; 59 | use Gentux\Healthz\Checks\Laravel\DatabaseHealthCheck; 60 | 61 | class AppServiceProvider extends ServiceProvider 62 | { 63 | public function register() 64 | { 65 | $this->app->bind(Healthz::class, function () { 66 | $env = new EnvHealthCheck(); 67 | $db = new DatabaseHealthCheck(); 68 | $db->setConnection("non-default"); 69 | 70 | return new Healthz([$env, $db]); 71 | }); 72 | } 73 | } 74 | ``` 75 | 76 | [See more about configuring available checks](#check-configuration) 77 | 78 | --- 79 | 80 | ### General PHP 81 | 82 | **Build an instance of the health check** 83 | 84 | ```php 85 | addServer("127.0.0.1"); 90 | $healthz = new Healthz([$memcached]); 91 | ``` 92 | 93 | **Run the checks and review results** 94 | 95 | ```php 96 | // @var $results Gentux\Healthz\ResultStack 97 | $results = $healthz->run(); 98 | 99 | if ($results->hasFailures()) { 100 | // oh no 101 | } 102 | 103 | if ($results->hasWarnings()) { 104 | // hmm 105 | } 106 | 107 | foreach ($results->all() as $result) { 108 | // @var $result Gentux\Healthz\HealthResult 109 | if ($result->passed() || $result->warned() || $result->failed()) { 110 | echo "it did one of those things at least"; 111 | } 112 | 113 | echo "{$result->title()}: {$result->status()} ({$result->description()})"; 114 | } 115 | ``` 116 | 117 | **Get the UI view** 118 | 119 | ```php 120 | $html = $healthz->html(); 121 | ``` 122 | 123 | --- 124 | 125 | ## Check configuration 126 | 127 | - [HTTP](#http-check) 128 | - [Memcached](#memcached-check) 129 | - [Debug](#debug-check) 130 | - [Env](#env-check) 131 | - [Database (Laravel)](#laravel-database) 132 | - [Queue (Laravel)](#laravel-queue) 133 | 134 | #### HTTP 135 | 136 | 137 | Create a new [Guzzle Request](http://docs.guzzlephp.org/en/latest/psr7.html) to pass to the constuctor of the HTTP health check. 138 | 139 | ```php 140 | use GuzzleHTTP\PSR7\Request; 141 | use Gentux\Healthz\Checks\General\HttpHealthCheck; 142 | 143 | $request = new Request("GET", "http://example.com"); 144 | $httpCheck = new HttpHealthCheck($request); 145 | ``` 146 | 147 | You can optionally pass the expected response status code (defaults to `200`), as well as Guzzle client options. 148 | 149 | ```php 150 | $httpCheck = new HttpHealthCheck($request, 204, [ 151 | "base_url" => "http://example.com", 152 | ]); 153 | ``` 154 | 155 | #### Memcached 156 | 157 | 158 | Create a new Memcached health check and use the methods `addServer` and `setOptions`. 159 | 160 | ```php 161 | use Gentux\Healthz\Checks\General\MemcachedHealthCheck; 162 | 163 | $memcachedCheck = new MemcachedHealthCheck(); 164 | $memcachedCheck->addServer($server, $port = 11211, $weight = 0); 165 | $memcachedCheck->setOptions([]); 166 | ``` 167 | 168 | _See [Memcached setOptions](http://php.net/manual/en/memcached.setoptions.php) for option information._ 169 | 170 | #### Debug 171 | 172 | 173 | Set the environment variable to check if the app is running in debug. If this check fails, it emits a warning. 174 | 175 | ```php 176 | use Gentux\Healthz\Checks\General\DebugHealthCheck; 177 | 178 | $debugCheck = new DebugHealthCheck("APP_DEBUG"); 179 | ``` 180 | 181 | In this case, if `APP_DEBUG` == `'true'` then this check will emit a warning. 182 | 183 | #### Env 184 | 185 | 186 | Provide an environment variable name to check for the apps environment. If the provided env name is found the check will always be successful and simply output the name. If no environment variable is set the check will emit a warning. 187 | 188 | ```php 189 | use Gentux\Healthz\Checks\General\EnvHealthCheck; 190 | 191 | $envCheck = new EnvHealthCheck("APP_ENV"); 192 | ``` 193 | 194 | #### Database (Laravel) 195 | 196 | 197 | This will use Laravel's built in database service to verify connectivity. You may optionally set a connection name to use (will use the default if not provided). 198 | 199 | ```php 200 | use Gentux\Healthz\Checks\Laravel\DatabaseHealthCheck; 201 | 202 | $dbCheck = new DatabaseHealthCheck(); 203 | $dbCheck->setConnection("my_conn"); // optional 204 | ``` 205 | 206 | #### Queue (Laravel) 207 | 208 | 209 | The queue health check currently supports `sync` and `sqs` queues. You may optionally set the queue name to use (will use the default if not specified). 210 | 211 | ```php 212 | use Gentux\Healthz\Checks\Laravel\QueueHealthCheck; 213 | 214 | $queueCheck = new QueueHealthCheck(); 215 | $queueCheck->setName("my_queue"); // optional 216 | ``` 217 | 218 | --- 219 | 220 | ## Custom checks 221 | 222 | _Note: Checks may have one of 3 statuses (`success`, `warning`, or `failure`). Any combination of success and warning and the stack as a whole will be considered to be successful. 223 | Any single failure, however, will consider the stack to be failed._ 224 | 225 | To create a custom health check, you should extend `Gentux\Healthz\HealthCheck` and implement the one abstract method `run()`. 226 | 227 | ```php 228 |