├── src ├── config │ ├── .gitkeep │ ├── resources │ │ ├── Framework.yml │ │ ├── Horizon.yml │ │ ├── Filesystem.yml │ │ ├── Cache.yml │ │ ├── Redis.yml │ │ ├── RoutesCached.yml │ │ ├── RedisConnectable.yml │ │ ├── AppKey.yml │ │ ├── MemcachedConnectable.yml │ │ ├── ConfigurationCached.yml │ │ ├── PostgreSqlConnectable.yml │ │ ├── ElasticsearchConnectable.yml │ │ ├── DiskSpace.yml │ │ ├── S3.yml │ │ ├── SecurityChecker.yml │ │ ├── MySqlConnectable.yml │ │ ├── Queue.yml │ │ ├── DebugMode.yml │ │ ├── Http.yml │ │ ├── DirectoryPermissions.yml │ │ ├── LocalStorage.yml │ │ ├── EnvExists.yml │ │ ├── LaravelServices.yml │ │ ├── MigrationsUpToDate.yml │ │ ├── RebootRequired.yml │ │ ├── MixManifest.yml │ │ ├── Sendinblue.yml │ │ ├── MailgunConnectable.yml │ │ ├── PackagesUpToDate.yml │ │ ├── Database.yml │ │ ├── DocuSign.yml │ │ ├── CheckoutCom.yml │ │ ├── HealthPanel.yml │ │ ├── API.yml │ │ ├── Certificate.yml │ │ ├── Mail.yml │ │ ├── SeeTickets.yml │ │ ├── Latency.yml │ │ ├── Adyen.yml │ │ ├── ServerUptime.yml │ │ ├── Broadcasting.yml │ │ ├── ServerLoad.yml │ │ ├── MySql.yml │ │ ├── Sshd.yml │ │ ├── Php.yml │ │ ├── NginxServer.yml │ │ ├── RedisServer.yml │ │ ├── NewrelicDeamon.yml │ │ ├── PostgreSqlServer.yml │ │ ├── QueueWorkers.yml │ │ ├── Supervisor.yml │ │ ├── Dynamics.yml │ │ ├── Https.yml │ │ ├── ServerVars.yml │ │ └── Extensions.yml │ └── deprecated │ │ └── Health.yml ├── resources │ ├── views │ │ ├── default │ │ │ ├── email.blade.php │ │ │ ├── panel.blade.php │ │ │ ├── empty-panel.blade.php │ │ │ ├── html.blade.php │ │ │ └── partials │ │ │ │ ├── style.blade.php │ │ │ │ └── well.blade.php │ │ └── html.blade.php │ ├── sass │ │ ├── _variables.scss │ │ └── app.scss │ ├── js │ │ ├── bootstrap.js │ │ ├── mixins │ │ │ └── routes.js │ │ ├── app.js │ │ └── components │ │ │ └── Chart.vue │ └── dist │ │ └── js │ │ └── app.js.LICENSE.txt ├── Checkers │ ├── Https.php │ ├── Framework.php │ ├── Contract.php │ ├── Redis.php │ ├── Extensions.php │ ├── DiskSpace.php │ ├── Writable.php │ ├── Horizon.php │ ├── Expression.php │ ├── SecurityChecker.php │ ├── CloudStorage.php │ ├── Artisan.php │ ├── ServerLoad.php │ ├── Cache.php │ ├── Queue.php │ ├── Filesystem.php │ ├── Health.php │ ├── PortCheck.php │ ├── Mail.php │ ├── Composer.php │ ├── Database.php │ ├── Docusign.php │ ├── ServerVars.php │ ├── MixManifest.php │ ├── Broadcasting.php │ ├── Process.php │ ├── HealthPanel.php │ ├── NotInDebugMode.php │ ├── ServerUptime.php │ ├── Certificate.php │ ├── Ping.php │ ├── Base.php │ └── DirectoryAndFilePresence.php ├── Support │ ├── Constants.php │ ├── Traits │ │ ├── HandleExceptions.php │ │ ├── ImportProperties.php │ │ ├── Routing.php │ │ ├── ToArray.php │ │ └── Database.php │ ├── Jobs │ │ └── TestJob.php │ ├── Broadcasting │ │ ├── server.js │ │ └── pusher.blade.php │ ├── LocallyProtected.php │ ├── Timer.php │ ├── Result.php │ ├── Cache.php │ ├── helpers.php │ ├── ResourceLoader.php │ └── Target.php ├── Events │ ├── RaiseHealthIssue.php │ └── HealthPing.php ├── Http │ ├── Middleware │ │ └── LocallyProtected.php │ └── Controllers │ │ ├── Broadcasting.php │ │ └── Health.php ├── Console │ └── Commands │ │ ├── HealthPanelCommand.php │ │ └── HealthCheckCommand.php ├── Data │ └── Models │ │ └── HealthCheck.php ├── database │ └── migrations │ │ └── 2018_09_09_000001_create_table_health_checks.php ├── Listeners │ └── NotifyHealthIssue.php ├── Service.php └── Notifications │ └── HealthStatus.php ├── .styleci.yml ├── .scrutinizer.yml ├── docs └── images │ ├── json.png │ ├── panel.png │ ├── slack.png │ ├── error-alert.png │ ├── error-hint.png │ ├── error-multi.png │ ├── console-panel.png │ ├── json-resource.png │ ├── error-single-2-columns.png │ └── error-single-4-columns.png ├── .gitignore ├── mix-manifest.json ├── .github ├── dependabot.yml └── workflows │ └── run-tests.yml ├── .prettierrc.yml ├── tests └── PhpUnit │ ├── Service │ ├── TimerTest.php │ └── ServiceTest.php │ ├── bootstrap.php │ └── TestCase.php ├── webpack.mix.js ├── .travis.yml ├── phpunit.xml ├── package.json ├── LICENSE.md ├── composer.json └── CHANGELOG.md /src/config/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: laravel 2 | -------------------------------------------------------------------------------- /src/resources/views/default/email.blade.php: -------------------------------------------------------------------------------- 1 | This is a health check email message. 2 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | environment: 3 | php: 4 | version: 7.2 5 | -------------------------------------------------------------------------------- /docs/images/json.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/health/HEAD/docs/images/json.png -------------------------------------------------------------------------------- /docs/images/panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/health/HEAD/docs/images/panel.png -------------------------------------------------------------------------------- /docs/images/slack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/health/HEAD/docs/images/slack.png -------------------------------------------------------------------------------- /docs/images/error-alert.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/health/HEAD/docs/images/error-alert.png -------------------------------------------------------------------------------- /docs/images/error-hint.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/health/HEAD/docs/images/error-hint.png -------------------------------------------------------------------------------- /docs/images/error-multi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/health/HEAD/docs/images/error-multi.png -------------------------------------------------------------------------------- /docs/images/console-panel.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/health/HEAD/docs/images/console-panel.png -------------------------------------------------------------------------------- /docs/images/json-resource.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/health/HEAD/docs/images/json-resource.png -------------------------------------------------------------------------------- /docs/images/error-single-2-columns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/health/HEAD/docs/images/error-single-2-columns.png -------------------------------------------------------------------------------- /docs/images/error-single-4-columns.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/antonioribeiro/health/HEAD/docs/images/error-single-4-columns.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | .DS_Store 4 | tests/_output/* 5 | coverage 6 | node_modules/ 7 | .phpunit.result.cache 8 | composer.lock 9 | -------------------------------------------------------------------------------- /src/config/resources/Framework.yml: -------------------------------------------------------------------------------- 1 | name: Framework 2 | abbreviation: frmwrk 3 | checker: PragmaRX\Health\Checkers\Framework 4 | notify: true 5 | column_size: 3 6 | -------------------------------------------------------------------------------- /mix-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "/src/resources/dist/js/app.js": "/src/resources/dist/js/app.js", 3 | "/src/resources/dist/css/app.css": "/src/resources/dist/css/app.css" 4 | } 5 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: composer 4 | directory: "/" 5 | schedule: 6 | interval: daily 7 | time: "08:00" 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /src/Checkers/Https.php: -------------------------------------------------------------------------------- 1 | 5 | 9 | 10 | 11 | @stop 12 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | phpVersion: "7.4" 2 | trailingComma: all 3 | printWidth: 80 4 | tabWidth: 4 5 | useTabs: false 6 | singleQuote: true 7 | trailingCommaPHP: false 8 | braceStyle: psr-2 9 | requirePragma: false 10 | insertPragma: false 11 | semi: false 12 | vueIndentScriptAndStyle: true 13 | -------------------------------------------------------------------------------- /src/config/resources/Cache.yml: -------------------------------------------------------------------------------- 1 | name: Cache 2 | abbreviation: csh 3 | checker: PragmaRX\Health\Checkers\Cache 4 | notify: true 5 | error_message: "Cache is not returning cached values." 6 | column_size: 3 7 | targets: 8 | - default: 9 | seconds: 60 10 | key: health-cache-test 11 | -------------------------------------------------------------------------------- /src/config/resources/Redis.yml: -------------------------------------------------------------------------------- 1 | name: Redis 2 | abbreviation: rds 3 | checker: PragmaRX\Health\Checkers\Redis 4 | column_size: 3 5 | notify: true 6 | error_message: "Got a wrong value back from Redis." 7 | targets: 8 | - default: 9 | key: "health:redis:key" 10 | connection: "" 11 | -------------------------------------------------------------------------------- /src/resources/views/html.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ config('health.title') }} 7 | 8 | @yield('head') 9 | 10 | 11 | 12 | @yield('body') 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/Checkers/Framework.php: -------------------------------------------------------------------------------- 1 | makeHealthyResult(); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/config/resources/RoutesCached.yml: -------------------------------------------------------------------------------- 1 | name: Routes Cached 2 | abbreviation: rtcch 3 | checker: PragmaRX\Health\Checkers\Expression 4 | notify: true 5 | column_size: 3 6 | error_message: "Routes are not cached" 7 | targets: 8 | - default: 9 | expression_value: "app()->routesAreCached()" 10 | should_return: true 11 | -------------------------------------------------------------------------------- /src/config/resources/RedisConnectable.yml: -------------------------------------------------------------------------------- 1 | name: Redis Connectable 2 | abbreviation: redisconn 3 | checker: PragmaRX\Health\Checkers\PortCheck 4 | notify: true 5 | column_size: 3 6 | error_message: "Could not connect to %s on port %s" 7 | targets: 8 | - default: 9 | hostname: localhost 10 | port: 6379 11 | timeout: 2 12 | -------------------------------------------------------------------------------- /src/config/resources/AppKey.yml: -------------------------------------------------------------------------------- 1 | name: App Key 2 | abbreviation: appkey 3 | checker: PragmaRX\Health\Checkers\Expression 4 | notify: true 5 | column_size: 3 6 | error_message: "There is no app key defined" 7 | graph_enabled: false 8 | targets: 9 | - default: 10 | expression_value: "config('app.key') !== null" 11 | should_return: true 12 | -------------------------------------------------------------------------------- /src/config/resources/MemcachedConnectable.yml: -------------------------------------------------------------------------------- 1 | name: Memcached Connectable 2 | abbreviation: redisconn 3 | checker: PragmaRX\Health\Checkers\PortCheck 4 | notify: true 5 | column_size: 3 6 | error_message: "Could not connect to %s on port %s" 7 | targets: 8 | - default: 9 | hostname: localhost 10 | port: 11211 11 | timeout: 2 12 | -------------------------------------------------------------------------------- /src/config/resources/ConfigurationCached.yml: -------------------------------------------------------------------------------- 1 | name: Configuration Cached 2 | abbreviation: cfgcch 3 | checker: PragmaRX\Health\Checkers\Expression 4 | notify: true 5 | column_size: 3 6 | error_message: "Configuration is not cached" 7 | targets: 8 | - default: 9 | expression_value: "app()->configurationIsCached()" 10 | should_return: true 11 | -------------------------------------------------------------------------------- /src/config/resources/PostgreSqlConnectable.yml: -------------------------------------------------------------------------------- 1 | name: PostgreSQL Connectable 2 | abbreviation: pstgrsqlsrvrconn 3 | checker: PragmaRX\Health\Checkers\PortCheck 4 | notify: true 5 | column_size: 3 6 | error_message: "Could not connect to %s on port %s" 7 | targets: 8 | - default: 9 | hostname: localhost 10 | port: 5432 11 | timeout: 2 12 | -------------------------------------------------------------------------------- /src/config/resources/ElasticsearchConnectable.yml: -------------------------------------------------------------------------------- 1 | name: Elasticsearch Connectable 2 | abbreviation: redisconn 3 | checker: PragmaRX\Health\Checkers\PortCheck 4 | notify: true 5 | column_size: 3 6 | error_message: "Could not connect to %s on port %s" 7 | targets: 8 | - localhost: 9 | hostname: localhost 10 | port: 9200 11 | timeout: 2 12 | -------------------------------------------------------------------------------- /src/config/resources/DiskSpace.yml: -------------------------------------------------------------------------------- 1 | name: Disk Space 2 | abbreviation: dskspc 3 | checker: PragmaRX\Health\Checkers\DiskSpace 4 | notify: true 5 | column_size: 3 6 | targets: 7 | - default: 8 | name: root 9 | path: "/" 10 | minimum: 1GB 11 | message: 12 | "Volume %s is getting out of space. It has %s when it should have at least %s." 13 | -------------------------------------------------------------------------------- /src/config/resources/S3.yml: -------------------------------------------------------------------------------- 1 | name: S3 2 | abbreviation: s3 3 | checker: PragmaRX\Health\Checkers\CloudStorage 4 | notify: true 5 | column_size: 3 6 | error_message: "Amazon S3 connection is failing." 7 | targets: 8 | - default: 9 | driver: s3 10 | file: pragmarx-health-s3-testfile.txt 11 | contents: "{{ \\Illuminate\\Support\\Str::random(32) }}" 12 | -------------------------------------------------------------------------------- /src/config/resources/SecurityChecker.yml: -------------------------------------------------------------------------------- 1 | name: Packages Security Checker 2 | abbreviation: debug 3 | checker: PragmaRX\Health\Checkers\SecurityChecker 4 | notify: true 5 | column_size: 3 6 | executable: /usr/local/bin/local-php-security-checker 7 | error_message: 8 | "The following packages have vulnerabilities referenced in the SensioLabs security advisories database: %s" 9 | -------------------------------------------------------------------------------- /src/config/resources/MySqlConnectable.yml: -------------------------------------------------------------------------------- 1 | name: MySQL Connectable 2 | abbreviation: mysqlgrsqlsrvrconn 3 | checker: PragmaRX\Health\Checkers\PortCheck 4 | notify: true 5 | column_size: 3 6 | error_message: "Could not connect to %s on port %s" 7 | targets: 8 | - default: 9 | name: localhost 10 | hostname: localhost 11 | port: 3306 12 | timeout: 2 13 | -------------------------------------------------------------------------------- /src/config/resources/Queue.yml: -------------------------------------------------------------------------------- 1 | name: Queue 2 | abbreviation: queue 3 | checker: PragmaRX\Health\Checkers\Queue 4 | error_message: "Queue system is not working properly." 5 | column_size: 3 6 | targets: 7 | - default: 8 | test_job: PragmaRX\Health\Support\Jobs\TestJob 9 | cache_instance: cache 10 | notify: true 11 | connection: "" 12 | queue: "" 13 | -------------------------------------------------------------------------------- /src/config/resources/DebugMode.yml: -------------------------------------------------------------------------------- 1 | name: Debug Mode on Production 2 | abbreviation: debug 3 | checker: PragmaRX\Health\Checkers\Expression 4 | notify: true 5 | column_size: 3 6 | error_message: "Application is in debug mode" 7 | targets: 8 | - default: 9 | expression_value: "!app()->environment('production') || !app('config')->get('app.debug')" 10 | should_return: true 11 | -------------------------------------------------------------------------------- /src/config/resources/Http.yml: -------------------------------------------------------------------------------- 1 | name: Http 2 | abbreviation: http 3 | checker: PragmaRX\Health\Checkers\Http 4 | notify: true 5 | column_size: 3 6 | timeout_message: "[TIMEOUT] A request to %s took %s seconds. Timeout is set to %s seconds." 7 | connection_timeout: 30 8 | roundtrip_timeout: 30 9 | targets: 10 | - default: 11 | urls: 12 | - '{{ config("app.url") }}' 13 | - http://google.com 14 | -------------------------------------------------------------------------------- /src/config/resources/DirectoryPermissions.yml: -------------------------------------------------------------------------------- 1 | name: Directory Permissions 2 | abbreviation: dirperm 3 | checker: PragmaRX\Health\Checkers\Writable 4 | notify: true 5 | column_size: 3 6 | error_message: "%s is not writable" 7 | targets: 8 | - default: 9 | paths: 10 | - "{{ storage_path() }}" 11 | - "{{ storage_path('logs/laravel.log') }}" 12 | - "{{ base_path('bootstrap/cache') }}" 13 | -------------------------------------------------------------------------------- /src/config/resources/LocalStorage.yml: -------------------------------------------------------------------------------- 1 | name: LocalStorage 2 | abbreviation: lclstrg 3 | checker: PragmaRX\Health\Checkers\CloudStorage 4 | notify: true 5 | error_message: "Cloud storage is not retrieving files correctly." 6 | column_size: 3 7 | targets: 8 | - default: 9 | driver: local 10 | file: pragmarx-health-storage-testfile.txt 11 | contents: "{{ \\Illuminate\\Support\\Str::random(32) }}" 12 | -------------------------------------------------------------------------------- /src/config/resources/EnvExists.yml: -------------------------------------------------------------------------------- 1 | name: .env Exists 2 | abbreviation: envexists 3 | checker: PragmaRX\Health\Checkers\DirectoryAndFilePresence 4 | notify: true 5 | error_message: "The .env file does not exists" 6 | column_size: 3 7 | targets: 8 | - default: 9 | file_exists: 10 | - "{{ base_path('.env') }}" 11 | file_do_not_exists: 12 | directory_exists: 13 | directory_do_not_exists: 14 | -------------------------------------------------------------------------------- /src/config/resources/LaravelServices.yml: -------------------------------------------------------------------------------- 1 | name: LaravelServices 2 | abbreviation: lvs 3 | checker: PragmaRX\Health\Checkers\Https 4 | notify: true 5 | column_size: 3 6 | connection_timeout: 2 7 | roundtrip_timeout: 4 8 | timeout_message: "[TIMEOUT] A request to %s took %s seconds. Timeout is set to %s." 9 | targets: 10 | - default: 11 | urls: 12 | - "https://forge.laravel.com" 13 | - "https://envoyer.io" 14 | -------------------------------------------------------------------------------- /tests/PhpUnit/Service/TimerTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(1, (int) Timer::stop()); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/config/resources/MigrationsUpToDate.yml: -------------------------------------------------------------------------------- 1 | name: Migrations Up to Date 2 | abbreviation: debug 3 | checker: PragmaRX\Health\Checkers\Artisan 4 | notify: true 5 | column_size: 3 6 | error_message: "Not all migrations were migrated" 7 | targets: 8 | - default: 9 | command: 10 | name: "migrate" 11 | options: 12 | "--pretend": true 13 | "--force": true 14 | should_return: "Nothing to migrate" 15 | -------------------------------------------------------------------------------- /src/config/resources/RebootRequired.yml: -------------------------------------------------------------------------------- 1 | name: RebootRequired 2 | abbreviation: rbtrqrd 3 | checker: PragmaRX\Health\Checkers\DirectoryAndFilePresence 4 | notify: true 5 | error_message: "A reboot is required in this server (Uptime Checker)" 6 | column_size: 3 7 | targets: 8 | - default: 9 | file_exists: 10 | file_do_not_exists: 11 | - /var/run/reboot-required 12 | directory_exists: 13 | directory_do_not_exists: 14 | -------------------------------------------------------------------------------- /src/resources/sass/_variables.scss: -------------------------------------------------------------------------------- 1 | 2 | // Body 3 | $body-bg: #f8fafc; 4 | 5 | // Typography 6 | $font-family-sans-serif: "Nunito", sans-serif; 7 | $font-size-base: 0.9rem; 8 | $line-height-base: 1.6; 9 | 10 | // Colors 11 | $blue: #635321; 12 | $green: #E5BBFF; 13 | $cyan: #FFF4FC; 14 | $orange: #E89DB4; 15 | $red: #FFA8A4; 16 | 17 | $indigo: #6574cd; 18 | $purple: #9561e2; 19 | $pink: #f66D9b; 20 | $yellow: #ffed4a; 21 | $teal: #4dc0b5; 22 | -------------------------------------------------------------------------------- /src/Checkers/Contract.php: -------------------------------------------------------------------------------- 1 | { 5 | return route.name === name 6 | }) 7 | 8 | let routeUri = route.uri 9 | 10 | Object.entries(params).forEach(([key, value]) => { 11 | routeUri = routeUri.replace('{'+key+'}', value) 12 | }) 13 | 14 | return routeUri 15 | }, 16 | }, 17 | } 18 | -------------------------------------------------------------------------------- /src/Support/Traits/HandleExceptions.php: -------------------------------------------------------------------------------- 1 | failure = $failure; 25 | 26 | $this->channel = $channel; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Support/Jobs/TestJob.php: -------------------------------------------------------------------------------- 1 | each(function ($value, $key) { 17 | $key = Str::camel($key); 18 | 19 | if (! property_exists($this, $key)) { 20 | $this->$key = $value; 21 | } 22 | }); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/config/resources/Database.yml: -------------------------------------------------------------------------------- 1 | name: Database 2 | abbreviation: db 3 | checker: PragmaRX\Health\Checkers\Database 4 | notify: true 5 | column_size: 3 6 | targets: 7 | - users: 8 | type: "find_first_model" 9 | models: 10 | - "{{ config('auth.providers.users.model') }}" 11 | - "query speed": 12 | type: "raw_query" 13 | connection: "default" 14 | query: "select * from users u join password_resets pr on pr.email = u.email order by u.created_at desc" 15 | maximum_time: 0.05 16 | error_message: "Query took %sms when it should have last at most %sms" 17 | -------------------------------------------------------------------------------- /src/config/resources/DocuSign.yml: -------------------------------------------------------------------------------- 1 | name: DocuSign 2 | abbreviation: dcsgn 3 | checker: PragmaRX\Health\Checkers\Docusign 4 | column_size: 3 5 | notify: true 6 | error_message: "A reboot is required in this server (Uptime Checker)" 7 | targets: 8 | - default: 9 | username: "{{ docusign.username }}" 10 | password: "{{ docusign.password }}" 11 | integrator_key: "{{ docusign.integrator_key }}" 12 | debug: "{{ docusign.debug }}" 13 | debug_file: storage/logs/docusign.log 14 | api_host: "{{ docusign.host }}" 15 | not_installed_message: "Docusign is not installed." 16 | -------------------------------------------------------------------------------- /src/resources/views/default/empty-panel.blade.php: -------------------------------------------------------------------------------- 1 | @extends('pragmarx/health::default.html') 2 | 3 | @section('html.body') 4 |
5 |
6 |
7 |

{{ config('health.title') }}

8 |
9 | 10 |
11 |

12 | You list of resources is empty or something wrong happened, please check your logs. 13 |

14 |
15 |
16 |
17 | @stop 18 | -------------------------------------------------------------------------------- /src/config/resources/CheckoutCom.yml: -------------------------------------------------------------------------------- 1 | name: Checkout.com API 2 | abbreviation: https 3 | checker: PragmaRX\Health\Checkers\Https 4 | notify: true 5 | column_size: 3 6 | timeout_message: "[TIMEOUT] A request to %s took %s seconds. Timeout is set to %s seconds." 7 | connection_timeout: 30 8 | roundtrip_timeout: 30 9 | targets: 10 | - default: 11 | urls: 12 | - checkout-com: 13 | url: "https://{{ config('services.checkout_com.api.host') }}/event-types" 14 | method: GET 15 | headers: 16 | Authorization: "{{ config('services.checkout_com.api.keys.secret') }}" 17 | -------------------------------------------------------------------------------- /src/Support/Broadcasting/server.js: -------------------------------------------------------------------------------- 1 | var request = require('request') 2 | var server = require('http').Server() 3 | var io = require('socket.io')(server) 4 | var Redis = require('ioredis') 5 | var redis = new Redis() 6 | 7 | redis.subscribe('pragmarx-health-broadcasting-channel') 8 | 9 | redis.on('message', function(channel, message) { 10 | message = JSON.parse(message) 11 | 12 | console.log('ponging...') 13 | 14 | if (message.event == 'PragmaRX\\Health\\Events\\HealthPing') { 15 | request.get( 16 | message.data.callbackUrl + '?data=' + JSON.stringify(message.data), 17 | ) 18 | } 19 | }) 20 | 21 | server.listen(3000) 22 | -------------------------------------------------------------------------------- /src/config/resources/HealthPanel.yml: -------------------------------------------------------------------------------- 1 | name: Remote Laravel Health Panel 2 | abbreviation: health-panel 3 | checker: PragmaRX\Health\Checkers\HealthPanel 4 | notify: true 5 | column_size: 3 6 | timeout_message: "[TIMEOUT] A request to %s took %s seconds. Timeout is set to %s seconds." 7 | connection_timeout: 5 8 | roundtrip_timeout: 10 9 | targets: 10 | - default: 11 | urls: 12 | - "http://your-site.com/health/check": 13 | method: GET 14 | auth: 15 | - "{{ config('services.whatever.api.username') }}" 16 | - "{{ config('services.whatever.api.password') }}" 17 | - basic 18 | -------------------------------------------------------------------------------- /webpack.mix.js: -------------------------------------------------------------------------------- 1 | const mix = require('laravel-mix') 2 | 3 | /* 4 | |-------------------------------------------------------------------------- 5 | | Mix Asset Management 6 | |-------------------------------------------------------------------------- 7 | | 8 | | Mix provides a clean, fluent API for defining some Webpack build steps 9 | | for your Laravel application. By default, we are compiling the Sass 10 | | file for the application as well as bundling up all the JS files. 11 | | 12 | */ 13 | 14 | mix.js('src/resources/js/app.js', 'src/resources/dist/js').sass( 15 | 'src/resources/sass/app.scss', 16 | 'src/resources/dist/css' 17 | ) 18 | -------------------------------------------------------------------------------- /src/config/resources/API.yml: -------------------------------------------------------------------------------- 1 | name: API Checker 2 | abbreviation: apichecker 3 | checker: PragmaRX\Health\Checkers\Https 4 | notify: true 5 | column_size: 3 6 | timeout_message: "[TIMEOUT] A request to %s took %s seconds. Timeout is set to %s seconds." 7 | connection_timeout: 5 8 | roundtrip_timeout: 10 9 | targets: 10 | - default: 11 | urls: 12 | - https://api.whatever.com/v1.1/authorization/token: 13 | method: POST 14 | auth: 15 | - "{{ config('services.whatever.api.username') }}" 16 | - "{{ config('services.whatever.api.password') }}" 17 | - basic 18 | is_json: true 19 | -------------------------------------------------------------------------------- /src/config/resources/Certificate.yml: -------------------------------------------------------------------------------- 1 | name: Certificate 2 | abbreviation: ccert 3 | checker: PragmaRX\Health\Checkers\Certificate 4 | notify: true 5 | error_message: "Invalid certificate for domain: %s" 6 | column_size: 3 7 | command: "openssl s_client {$options} -connect {$host}:443 2>&1" 8 | verify_string: "Verify return code" 9 | success_string: "Verify return code: 0 (ok)" 10 | targets: 11 | - host1: 12 | options: -tls1 13 | urls: 14 | - laravel.com 15 | - wwwprd: 16 | options: -tls1_1 17 | urls: 18 | - google.com 19 | - revoked: 20 | options: -tls1_2 21 | urls: 22 | - revoked-rsa-dv.ssl.com 23 | -------------------------------------------------------------------------------- /src/config/resources/Mail.yml: -------------------------------------------------------------------------------- 1 | name: Mail 2 | abbreviation: ml 3 | checker: PragmaRX\Health\Checkers\Mail 4 | notify: true 5 | column_size: 3 6 | targets: 7 | - default: 8 | view: "pragmarx/health::default.email" 9 | config: 10 | driver: log 11 | host: mailtrap.io 12 | port: "2525" 13 | from: 14 | address: health@example.com 15 | name: "Health Checker" 16 | encryption: null 17 | username: "{{ mail.username }}" 18 | password: "{{ mail.password }}" 19 | sendmail: "/usr/sbin/sendmail -bs" 20 | to: you-know-who@sink.sendgrid.net 21 | subject: "Health Test mail" 22 | -------------------------------------------------------------------------------- /src/config/resources/SeeTickets.yml: -------------------------------------------------------------------------------- 1 | name: See Tickets API 2 | abbreviation: seetickets 3 | checker: PragmaRX\Health\Checkers\Https 4 | notify: true 5 | column_size: 3 6 | timeout_message: "[TIMEOUT] A request to %s took %s seconds. Timeout is set to %s seconds." 7 | connection_timeout: 5 8 | roundtrip_timeout: 10 9 | targets: 10 | - default: 11 | urls: 12 | - url1: 13 | url: "{{ config('services.see_tickets.api.endpoint') }}" 14 | method: POST 15 | auth: 16 | - "{{ config('services.see_tickets.api.username') }}" 17 | - "{{ config('services.see_tickets.api.password') }}" 18 | - basic 19 | -------------------------------------------------------------------------------- /src/config/resources/Latency.yml: -------------------------------------------------------------------------------- 1 | name: Latency 2 | abbreviation: latency 3 | checker: PragmaRX\Health\Checkers\Ping 4 | notify: true 5 | binary: "{{ config('health.services.ping.bin') }}" 6 | error_message: 7 | 'The host "%s" exceeded the maximum accepted latency on ping: last ping was %s, accepted is %s' 8 | column_size: 3 9 | targets: 10 | - server: 11 | name: rio de janeiro servers 12 | hostname: google.com 13 | accepted_latency: 15 14 | - server: 15 | name: south america servers 16 | hostname: globo.com 17 | accepted_latency: 20 18 | - server: 19 | name: europe servers 20 | hostname: ovh.fr 21 | accepted_latency: 2 22 | -------------------------------------------------------------------------------- /src/Http/Middleware/LocallyProtected.php: -------------------------------------------------------------------------------- 1 | check($request)) { 21 | abort(403, 'Forbidden'); 22 | } 23 | 24 | return $next($request); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/Checkers/Redis.php: -------------------------------------------------------------------------------- 1 | target->key, 21 | $number = random_bytes(80) 22 | ); 23 | 24 | $result = IlluminateRedis::get($key); 25 | 26 | return $this->makeResult( 27 | $number == $result, 28 | $number !== $result ? $this->target->key : '' 29 | ); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/config/resources/Adyen.yml: -------------------------------------------------------------------------------- 1 | name: Adyen 2 | abbreviation: adyen 3 | checker: PragmaRX\Health\Checkers\Https 4 | notify: true 5 | column_size: 3 6 | timeout_message: "[TIMEOUT] A request to %s took %s seconds. Timeout is set to %s seconds." 7 | connection_timeout: 20 8 | roundtrip_timeout: 30 9 | targets: 10 | - default: 11 | urls: 12 | - url1: 13 | url: "{{ config('services.adyen.api_base_url') }}" 14 | method: POST 15 | debug: false 16 | headers: 17 | x-api-key: "{{ config('services.adyen.backend.api_key') }}" 18 | content-type: application/json 19 | form_params: 20 | merchantAccount: "{{ config('services.adyen.merchant_account') }}" 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /src/resources/views/default/html.blade.php: -------------------------------------------------------------------------------- 1 | @extends('pragmarx/health::html') 2 | 3 | @section('head') 4 | 7 | 8 | 11 | @stop 12 | 13 | @section('body') 14 | @include('pragmarx/health::default.partials.style') 15 | 16 | @yield('html.header') 17 | 18 | @yield('html.body') 19 | 20 | @yield('html.footer') 21 | 22 | 25 | 26 | @if (config('app.env') == 'local') 27 | 28 | @endif 29 | @stop 30 | -------------------------------------------------------------------------------- /src/config/resources/ServerUptime.yml: -------------------------------------------------------------------------------- 1 | name: ServerUptime 2 | abbreviation: uptm 3 | checker: PragmaRX\Health\Checkers\ServerUptime 4 | column_size: 3 5 | notify: true 6 | error_message: 7 | 'Looks like your server was recently rebooted, current uptime is now "%s" and it was "%s" before restart.' 8 | targets: 9 | - default: 10 | regex: 11 | '~(?\d{1,2}):(?\d{2})(?::(?\d{2}))?\s+up\s+(?:(?\d+)\s+days?,\s+)?\b(?:(?\d+):)?(?\d+)(?:\s+(?:minute|minutes|min)?)?,\s+(?\d+)?.+?(?\d+.\d+),?\s+(?\d+.\d+),?\s+(?\d+.\d+)~' 12 | command: "uptime 2>&1" 13 | save_to: "{{ storage_path('app') }}/uptime.json" 14 | action_message: "Your server was rebooted (Uptime Checker)" 15 | -------------------------------------------------------------------------------- /src/config/resources/Broadcasting.yml: -------------------------------------------------------------------------------- 1 | name: Broadcasting 2 | abbreviation: brdc 3 | checker: PragmaRX\Health\Checkers\Broadcasting 4 | notify: true 5 | column_size: 3 6 | error_message: 7 | "The broadcasting service did not respond in time, it may be in trouble." 8 | targets: 9 | - default: 10 | channel: pragmarx-health-broadcasting-channel 11 | route_name: pragmarx.health.broadcasting.callback 12 | secret: "{{ \\Illuminate\\Support\\Str::random(32) }}" 13 | timeout: 30 14 | routes: 15 | pragmarx.health.broadcasting.callback: 16 | uri: "/health/broadcasting/callback/{secret}" 17 | controller: PragmaRX\Health\Http\Controllers\Broadcasting 18 | action: callback 19 | save_to: "{{ storage_path('app') }}/broadcasting.json" 20 | -------------------------------------------------------------------------------- /src/Http/Controllers/Broadcasting.php: -------------------------------------------------------------------------------- 1 | get('data'), true)['resource']; 13 | 14 | $checker = $this->instantiateChecker($resource); 15 | 16 | $checker->pong($secret); 17 | } 18 | 19 | /** 20 | * @param $resource 21 | * @return mixed 22 | */ 23 | protected function instantiateChecker($resource) 24 | { 25 | $checkerClass = $resource['checker']; 26 | 27 | $checker = new $checkerClass($resource, []); 28 | 29 | return $checker; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/resources/views/default/partials/style.blade.php: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /src/config/resources/ServerLoad.yml: -------------------------------------------------------------------------------- 1 | name: ServerLoad 2 | abbreviation: load 3 | checker: PragmaRX\Health\Checkers\ServerLoad 4 | error_message: 5 | 'Your server might be overloaded, current server load values are "%s, %s and %s", which are above the threshold values: "%s, %s and %s".' 6 | column_size: 3 7 | notify: true 8 | targets: 9 | - default: 10 | regex: 11 | '~(?\d{1,2}):(?\d{2})(?::(?\d{2}))?\s+up\s+(?:(?\d+)\s+days?,\s+)?\b(?:(?\d+):)?(?\d+)(?:\s+(?:minute|minutes|min)?)?,\s+(?\d+).+?(?\d+.\d+),?\s+(?\d+.\d+),?\s+(?\d+.\d+)~' 12 | command: "uptime 2>&1" 13 | max_load: 14 | load_1: 2 15 | load_5: 1.5 16 | load_15: 1 17 | action_message: "Too much load! (Server Load Checker)" 18 | -------------------------------------------------------------------------------- /src/Checkers/Extensions.php: -------------------------------------------------------------------------------- 1 | target->items); 17 | 18 | $installed = collect(get_loaded_extensions()); 19 | 20 | $alerts = $needed->reject(fn ($value) => $installed->contains($value)); 21 | 22 | if (count($alerts) === 0) { 23 | return $this->makeHealthyResult(); 24 | } 25 | 26 | $problems = collect($alerts)->implode(', '); 27 | 28 | return $this->makeResult( 29 | false, 30 | sprintf($this->target->getErrorMessage(), $problems) 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: trusty 2 | language: php 3 | 4 | php: 5 | - 7.1 6 | - 7.2 7 | - 7.3 8 | - 7.4 9 | - nightly 10 | 11 | sudo: false 12 | 13 | cache: 14 | directories: 15 | - $HOME/.composer/cache 16 | 17 | install: 18 | - | 19 | if [[ "$TRAVIS_PHP_VERSION" == 'nightly' ]]; then 20 | travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist --ignore-platform-reqs 21 | else 22 | travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-dist 23 | fi 24 | 25 | script: 26 | - vendor/bin/phpunit --coverage-text --coverage-clover=coverage.clover 27 | 28 | after_script: 29 | - | 30 | if [[ "$TRAVIS_PHP_VERSION" != 'nightly' ]]; then 31 | wget https://scrutinizer-ci.com/ocular.phar 32 | php ocular.phar code-coverage:upload --format=php-clover coverage.clover 33 | fi 34 | -------------------------------------------------------------------------------- /src/config/resources/MySql.yml: -------------------------------------------------------------------------------- 1 | name: MySql 2 | abbreviation: msql 3 | checker: PragmaRX\Health\Checkers\Process 4 | column_size: 3 5 | notify: true 6 | targets: 7 | - default: 8 | command: "pgrep %s" 9 | method: process_count 10 | process_name: mysqld 11 | pid_file: "" 12 | instances: 13 | minimum: 14 | count: 1 15 | message: 16 | 'Process "%s" has not enough instances running: it has %s, when should have at least %s' 17 | maximum: 18 | count: 20 19 | message: 20 | 'Process "%s" exceeded the maximum number of running instances: it has %s, when should have at most %s' 21 | pid_file_missing_error_message: "Process ID file is missing: %s." 22 | pid_file_missing_not_locked: 23 | "Process ID file is not being used by any process: %s." 24 | -------------------------------------------------------------------------------- /src/config/resources/Sshd.yml: -------------------------------------------------------------------------------- 1 | name: Sshd 2 | abbreviation: sshd 3 | checker: PragmaRX\Health\Checkers\Process 4 | column_size: 3 5 | notify: true 6 | targets: 7 | - default: 8 | command: "pgrep %s" 9 | method: process_count 10 | process_name: sshd 11 | pid_file: "" 12 | instances: 13 | minimum: 14 | count: 1 15 | message: 16 | 'Process "%s" has not enough instances running: it has %s, when should have at least %s' 17 | maximum: 18 | count: 15 19 | message: 20 | 'Process "%s" exceeded the maximum number of running instances: it has %s, when should have at most %s' 21 | pid_file_missing_error_message: "Process ID file is missing: %s." 22 | pid_file_missing_not_locked: 23 | "Process ID file is not being used by any process: %s." 24 | -------------------------------------------------------------------------------- /src/Support/Broadcasting/pusher.blade.php: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Pusher Test 5 | 6 | 21 | 22 | 23 | 24 | Pusher waiting for events... 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/config/resources/Php.yml: -------------------------------------------------------------------------------- 1 | name: Php 2 | abbreviation: php 3 | checker: PragmaRX\Health\Checkers\Process 4 | column_size: 3 5 | notify: true 6 | targets: 7 | - default: 8 | command: "pgrep %s" 9 | method: process_count 10 | process_name: php-fpm 11 | pid_file: /tmp/php7.1-fpm.pid 12 | instances: 13 | minimum: 14 | count: 2 15 | message: 16 | 'Process "%s" has not enough instances running: it has %s, when should have at least %s' 17 | maximum: 18 | count: 20 19 | message: 20 | 'Process "%s" exceeded the maximum number of running instances: it has %s, when should have at most %s' 21 | pid_file_missing_error_message: "Process ID file is missing: %s." 22 | pid_file_missing_not_locked: 23 | "Process ID file is not being used by any process: %s." 24 | -------------------------------------------------------------------------------- /src/config/resources/NginxServer.yml: -------------------------------------------------------------------------------- 1 | name: NginxServer 2 | abbreviation: ngnxsrvr 3 | checker: PragmaRX\Health\Checkers\Process 4 | column_size: 3 5 | notify: true 6 | targets: 7 | - default: 8 | command: "pgrep %s" 9 | method: process_count 10 | process_name: nginx 11 | pid_file: "" 12 | instances: 13 | minimum: 14 | count: 4 15 | message: 16 | 'Process "%s" has not enough instances running: it has %s, when should have at least %s' 17 | maximum: 18 | count: 8 19 | message: 20 | 'Process "%s" exceeded the maximum number of running instances: it has %s, when should have at most %s' 21 | pid_file_missing_error_message: "Process ID file is missing: %s." 22 | pid_file_missing_not_locked: 23 | "Process ID file is not being used by any process: %s." 24 | -------------------------------------------------------------------------------- /src/config/resources/RedisServer.yml: -------------------------------------------------------------------------------- 1 | name: RedisServer 2 | abbreviation: rdssrvr 3 | checker: PragmaRX\Health\Checkers\Process 4 | column_size: 3 5 | notify: true 6 | targets: 7 | - default: 8 | command: "pgrep %s" 9 | method: process_count 10 | process_name: redis-server 11 | pid_file: "" 12 | instances: 13 | minimum: 14 | count: 1 15 | message: 16 | 'Process "%s" has not enough instances running: it has %s, when should have at least %s' 17 | maximum: 18 | count: 20 19 | message: 20 | 'Process "%s" exceeded the maximum number of running instances: it has %s, when should have at most %s' 21 | pid_file_missing_error_message: "Process ID file is missing: %s." 22 | pid_file_missing_not_locked: 23 | "Process ID file is not being used by any process: %s." 24 | -------------------------------------------------------------------------------- /src/Console/Commands/HealthPanelCommand.php: -------------------------------------------------------------------------------- 1 | info('Checking resources and gathering information to build the panel...'); 30 | 31 | return $commands->panel($this); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/config/resources/NewrelicDeamon.yml: -------------------------------------------------------------------------------- 1 | name: NewrelicDeamon 2 | abbreviation: nwrlcdmn 3 | checker: PragmaRX\Health\Checkers\Process 4 | column_size: 3 5 | notify: true 6 | targets: 7 | - default: 8 | command: "pgrep %s" 9 | method: process_count 10 | process_name: newrelic-daemon 11 | pid_file: "" 12 | instances: 13 | minimum: 14 | count: 1 15 | message: 16 | 'Process "%s" has not enough instances running: it has %s, when should have at least %s' 17 | maximum: 18 | count: 4 19 | message: 20 | 'Process "%s" exceeded the maximum number of running instances: it has %s, when should have at most %s' 21 | pid_file_missing_error_message: "Process ID file is missing: %s." 22 | pid_file_missing_not_locked: 23 | "Process ID file is not being used by any process: %s." 24 | -------------------------------------------------------------------------------- /src/config/resources/PostgreSqlServer.yml: -------------------------------------------------------------------------------- 1 | name: PostgreSqlServer 2 | abbreviation: pstgrsqlsrvr 3 | checker: PragmaRX\Health\Checkers\Process 4 | column_size: 3 5 | notify: true 6 | targets: 7 | - default: 8 | command: "pgrep %s" 9 | method: process_count 10 | process_name: postgres 11 | pid_file: "" 12 | instances: 13 | minimum: 14 | count: 4 15 | message: 16 | 'Process "%s" has not enough instances running: it has %s, when should have at least %s' 17 | maximum: 18 | count: 8 19 | message: 20 | 'Process "%s" exceeded the maximum number of running instances: it has %s, when should have at most %s' 21 | pid_file_missing_error_message: "Process ID file is missing: %s." 22 | pid_file_missing_not_locked: 23 | "Process ID file is not being used by any process: %s." 24 | -------------------------------------------------------------------------------- /src/config/resources/QueueWorkers.yml: -------------------------------------------------------------------------------- 1 | name: QueueWorkers 2 | abbreviation: qwrkrs 3 | checker: PragmaRX\Health\Checkers\Process 4 | notify: true 5 | column_size: 3 6 | targets: 7 | - default: 8 | command: "ps aux | grep php | grep queue:work" 9 | method: process_count 10 | process_name: php 11 | pid_file: "" 12 | instances: 13 | minimum: 14 | count: 1 15 | message: 16 | 'Process "%s" has not enough instances running: it has %s, when should have at least %s' 17 | maximum: 18 | count: 3 19 | message: 20 | 'Process "%s" exceeded the maximum number of running instances: it has %s, when should have at most %s' 21 | pid_file_missing_error_message: "Process ID file is missing: %s." 22 | pid_file_missing_not_locked: 23 | "Process ID file is not being used by any process: %s." 24 | -------------------------------------------------------------------------------- /src/config/resources/Supervisor.yml: -------------------------------------------------------------------------------- 1 | name: Supervisor 2 | abbreviation: sprvsr 3 | checker: PragmaRX\Health\Checkers\Process 4 | column_size: 3 5 | notify: true 6 | targets: 7 | - default: 8 | command: "ps aux | grep python | grep supervisord" 9 | method: process_count 10 | process_name: supervisor 11 | pid_file: "" 12 | instances: 13 | minimum: 14 | count: 1 15 | message: 16 | 'Process "%s" has not enough instances running: it has %s, when should have at least %s' 17 | maximum: 18 | count: 3 19 | message: 20 | 'Process "%s" exceeded the maximum number of running instances: it has %s, when should have at most %s' 21 | pid_file_missing_error_message: "Process ID file is missing: %s." 22 | pid_file_missing_not_locked: 23 | "Process ID file is not being used by any process: %s." 24 | -------------------------------------------------------------------------------- /src/Console/Commands/HealthCheckCommand.php: -------------------------------------------------------------------------------- 1 | info('Checking resources...'); 33 | 34 | return $commands->check($this); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/config/resources/Dynamics.yml: -------------------------------------------------------------------------------- 1 | name: Dynamics 365 2 | abbreviation: dynamics 3 | checker: PragmaRX\Health\Checkers\Https 4 | notify: true 5 | column_size: 3 6 | timeout_message: "[TIMEOUT] A request to %s took %s seconds. Timeout is set to %s seconds." 7 | connection_timeout: 5 8 | roundtrip_timeout: 10 9 | targets: 10 | - default: 11 | urls: 12 | - url-from-config: 13 | url: "{{ config('services.newsletter.login_url') }}" 14 | headers: 15 | api-key: "{{ config('services.sendinblue.api_key') }}" 16 | method: POST 17 | form_params: 18 | username: "{{ config('services.newsletter.username') }}" 19 | resource: "{{ config('services.newsletter.api_endpoint') }}" 20 | grant_type: password 21 | password: "{{ config('services.newsletter.password') }}" 22 | client_id: "{{ config('services.newsletter.client_id') }}" 23 | -------------------------------------------------------------------------------- /src/resources/js/app.js: -------------------------------------------------------------------------------- 1 | import routesMixin from './mixins/routes' 2 | 3 | require('./bootstrap') 4 | 5 | window.Vue = require('vue') 6 | 7 | Vue.component('health-panel', require('./components/Panel.vue').default) 8 | 9 | const app = new Vue({ 10 | el: '#app', 11 | 12 | mixins: [routesMixin], 13 | 14 | data: { 15 | config: { loaded: false }, 16 | }, 17 | 18 | methods: { 19 | loadConfig() { 20 | let $this = this 21 | 22 | return axios.get($this.route('pragmarx.health.config')).then(function(response) { 23 | response.data.loaded = true 24 | 25 | $this.config = response.data 26 | 27 | $('.chart').css( 28 | 'height', 29 | $this.config.database.graphs.height + 'px', 30 | ) 31 | }) 32 | }, 33 | }, 34 | 35 | mounted() { 36 | this.loadConfig() 37 | }, 38 | }) 39 | -------------------------------------------------------------------------------- /src/Checkers/DiskSpace.php: -------------------------------------------------------------------------------- 1 | target->path); 17 | 18 | if (! $this->isEnough($free, $this->target->minimum)) { 19 | return $this->makeResult( 20 | false, 21 | sprintf( 22 | $this->target->message, 23 | $this->target->path, 24 | bytes_to_human($free), 25 | $this->target->minimum 26 | ) 27 | ); 28 | } 29 | 30 | return $this->makeHealthyResult(); 31 | } 32 | 33 | public function isEnough($free, $minimum) 34 | { 35 | return $free > human_to_bytes($minimum); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Data/Models/HealthCheck.php: -------------------------------------------------------------------------------- 1 | target->paths as $path) { 20 | if (! $this->getFilesystem()->isWritable($path)) { 21 | return $this->makeResult( 22 | false, 23 | sprintf($this->target->getErrorMessage(), $path) 24 | ); 25 | } 26 | } 27 | 28 | return $this->makeHealthyResult(); 29 | } 30 | 31 | public function getFilesystem() 32 | { 33 | if ($this->filesystem) { 34 | return $this->filesystem; 35 | } 36 | 37 | return $this->filesystem = app(Filesystem::class); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Checkers/Horizon.php: -------------------------------------------------------------------------------- 1 | horizonIsRunning() 17 | ? $this->makeHealthyResult() 18 | : $this->makeResult(false, $this->target->getErrorMessage()); 19 | } 20 | 21 | /** 22 | * Check if Horizon is up. 23 | * 24 | * @return bool 25 | */ 26 | protected function horizonIsRunning() 27 | { 28 | if (! $masters = app(MasterSupervisorRepository::class)->all()) { 29 | return false; 30 | } 31 | 32 | return collect($masters)->contains(function ($master) { 33 | return $master->status === 'paused'; 34 | }) 35 | ? false 36 | : true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Support/LocallyProtected.php: -------------------------------------------------------------------------------- 1 | header('API-Token'); 19 | 20 | try { 21 | $cacheKey = decrypt($token); 22 | } catch (\Throwable $exception) { 23 | return false; 24 | } 25 | 26 | $value = Cache::get($cacheKey); 27 | 28 | return $value === $cacheKey; 29 | } 30 | 31 | public function protect($timeout) 32 | { 33 | $cacheKey = Constants::SERVER_VARS_CACHE_KEY_PREFIX.'-'.Str::random(); 34 | 35 | Cache::put($cacheKey, $cacheKey, $timeout ?? 60); 36 | 37 | return encrypt($cacheKey); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Events/HealthPing.php: -------------------------------------------------------------------------------- 1 | callbackUrl = $callbackUrl; 33 | 34 | $this->channel = $channel; 35 | 36 | $this->target = $target; 37 | } 38 | 39 | /** 40 | * Get the channels the event should broadcast on. 41 | * 42 | * @return array 43 | */ 44 | public function broadcastOn() 45 | { 46 | return [$this->channel]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/Support/Traits/Routing.php: -------------------------------------------------------------------------------- 1 | isset($route['middleware']) ? $route['middleware'] : [], 15 | ]; 16 | 17 | $this->getRouter()->group($attributes, function () use ($route, $name) { 18 | $action = isset($route['controller']) 19 | ? "{$route['controller']}@{$route['action']}" 20 | : $route['action']; 21 | 22 | $this->getRouter()->get($route['uri'], [ 23 | 'as' => $name ?: $route['name'], 24 | 'uses' => $action, 25 | ]); 26 | }); 27 | } 28 | 29 | /** 30 | * Get the current router. 31 | * 32 | * @return mixed 33 | */ 34 | protected function getRouter() 35 | { 36 | return app()->router; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Checkers/Expression.php: -------------------------------------------------------------------------------- 1 | expressionIsOk() 17 | ? $this->makeHealthyResult() 18 | : $this->makeResult(false, $this->target->getErrorMessage()); 19 | } 20 | 21 | public function expressionIsOk() 22 | { 23 | $expressionResult = $this->executeExpression( 24 | $this->target->expressionValue 25 | ); 26 | 27 | if ($this->target->shouldReturn === true) { 28 | return (bool) $expressionResult; 29 | } 30 | 31 | if ($this->target->shouldReturn === false) { 32 | return ! $expressionResult; 33 | } 34 | 35 | return preg_match("|{$this->target->shouldReturn}|", $expressionResult); 36 | } 37 | 38 | public function executeExpression($expression) 39 | { 40 | return eval("return {$expression} ;"); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Checkers/SecurityChecker.php: -------------------------------------------------------------------------------- 1 | getCommand(), $output); 18 | 19 | $alerts = collect(json_decode(collect($output)->join(''), true))->keys(); 20 | 21 | if (count($alerts) === 0) { 22 | return $this->makeHealthyResult(); 23 | } 24 | 25 | $problems = collect($alerts)->implode(', '); 26 | 27 | return $this->makeResult( 28 | false, 29 | sprintf($this->target->getErrorMessage(), $problems) 30 | ); 31 | } 32 | 33 | public function getCommand() 34 | { 35 | if (! file_exists($executable = $this->target->resource->executable)) { 36 | throw new Exception("The security checker executable was not found: $executable"); 37 | } 38 | 39 | return $executable.' -format json '.base_path('composer.lock'); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/PhpUnit/bootstrap.php: -------------------------------------------------------------------------------- 1 | target->driver)->put( 17 | $this->target->file, 18 | $this->target->contents 19 | ); 20 | 21 | $contents = Storage::disk($this->target->driver)->get( 22 | $this->target->file 23 | ); 24 | 25 | Storage::disk($this->target->driver)->delete($this->target->file); 26 | 27 | if ($contents !== $this->target->contents) { 28 | return $this->makeResult( 29 | false, 30 | $this->target->getErrorMessage() 31 | ); 32 | } 33 | 34 | return $this->makeHealthyResult(); 35 | } catch (\Exception $exception) { 36 | report($exception); 37 | 38 | return $this->makeResultFromException($exception); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Checkers/Artisan.php: -------------------------------------------------------------------------------- 1 | executeAndCheck() 17 | ? $this->makeHealthyResult() 18 | : $this->makeResult(false, $this->target->getErrorMessage()); 19 | } 20 | 21 | /** 22 | * @return bool 23 | */ 24 | protected function executeAndCheck() 25 | { 26 | $this->executeArtisan(); 27 | 28 | return $this->checkArtisanOutput(); 29 | } 30 | 31 | /** 32 | * @return bool 33 | */ 34 | protected function checkArtisanOutput() 35 | { 36 | $output = IlluminateArtisan::output(); 37 | 38 | return 39 | $output && preg_match("|{$this->target->shouldReturn}|", $output); 40 | } 41 | 42 | protected function executeArtisan() 43 | { 44 | IlluminateArtisan::call( 45 | $this->target->command['name'], 46 | $this->target->command['options']->toArray() 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/resources/views/default/partials/well.blade.php: -------------------------------------------------------------------------------- 1 |
2 |
7 | @if (config('health.style.button_lines', 'multi') == 'multi') 8 |

9 | {{ $itemTitle }} 10 |

11 |

12 | @if ($itemSubtitle !== 'default') 13 | {{ $itemSubtitle }} 14 | @else 15 |   16 | @endif 17 |

18 | 19 |

20 | 21 |

22 | @else 23 |

24 | 25 | {{ $itemTitle }} 26 |

27 | @endif 28 |
29 |
30 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 17 | 18 | 19 | ./src/ 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ./tests/PhpUnit 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "npm run development", 5 | "development": "cross-env NODE_ENV=development node_modules/webpack/bin/webpack.js --progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js", 6 | "watch": "npm run development -- --watch", 7 | "watch-poll": "npm run watch -- --watch-poll", 8 | "hot": "cross-env NODE_ENV=development node_modules/webpack-dev-server/bin/webpack-dev-server.js --inline --hot --config=node_modules/laravel-mix/setup/webpack.config.js", 9 | "prod": "npm run production", 10 | "production": "cross-env NODE_ENV=production node_modules/webpack/bin/webpack.js --no-progress --hide-modules --config=node_modules/laravel-mix/setup/webpack.config.js" 11 | }, 12 | "devDependencies": { 13 | "axios": "^0.21.0", 14 | "bootstrap": "^4.5.3", 15 | "chart.js": "^2.9.4", 16 | "cross-env": "^7.0.2", 17 | "jquery": "^3.2", 18 | "laravel-mix": "^5.0.7", 19 | "lodash": "^4.17.20", 20 | "popper.js": "^1.12", 21 | "resolve-url-loader": "^3.1.2", 22 | "sass": "^1.27.0", 23 | "sass-loader": "^10.0.4", 24 | "sweetalert2": "^10.8.1", 25 | "vue": "^2.5.7", 26 | "vue-chartjs": "^3.5.1", 27 | "vue-template-compiler": "^2.6.12" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/resources/js/components/Chart.vue: -------------------------------------------------------------------------------- 1 | 50 | -------------------------------------------------------------------------------- /src/database/migrations/2018_09_09_000001_create_table_health_checks.php: -------------------------------------------------------------------------------- 1 | increments('id'); 17 | 18 | $table->string('resource_name'); 19 | 20 | $table->string('resource_slug')->index(); 21 | 22 | $table->string('target_name'); 23 | 24 | $table->string('target_slug')->index(); 25 | 26 | $table->string('target_display'); 27 | 28 | $table->boolean('healthy'); 29 | 30 | $table->text('error_message')->nullable(); 31 | 32 | $table->float('runtime'); 33 | 34 | $table->string('value')->nullable(); 35 | 36 | $table->string('value_human')->nullable(); 37 | 38 | $table->timestamp('created_at', 0)->index(); 39 | }); 40 | } 41 | 42 | /** 43 | * Reverse the migrations. 44 | * 45 | * @return void 46 | */ 47 | public function down() 48 | { 49 | Schema::drop('health_checks'); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/Checkers/ServerLoad.php: -------------------------------------------------------------------------------- 1 | getCurrentUptime(); 17 | 18 | $inTrouble = 19 | $current['load_1'] > $this->target->maxLoad['load_1'] || 20 | $current['load_5'] > $this->target->maxLoad['load_5'] || 21 | $current['load_15'] > $this->target->maxLoad['load_15']; 22 | 23 | return $this->makeResult(! $inTrouble, $this->makeMessage($current)); 24 | } 25 | 26 | protected function makeMessage($current, $saved = null) 27 | { 28 | $current['load_1'] > $this->target->maxLoad['load_1'] || 29 | $current['load_5'] > $this->target->maxLoad['load_5'] || 30 | $current['load_15'] > $this->target->maxLoad['load_15']; 31 | 32 | return sprintf( 33 | $this->target->getErrorMessage(), 34 | $current['load_1'], 35 | $current['load_5'], 36 | $current['load_15'], 37 | $this->target->maxLoad['load_1'], 38 | $this->target->maxLoad['load_5'], 39 | $this->target->maxLoad['load_15'] 40 | ); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Checkers/Cache.php: -------------------------------------------------------------------------------- 1 | getChecker(); 17 | 18 | $value1 = $this->getCached(); 19 | 20 | $value2 = $this->getCached(); 21 | 22 | if ($value1 !== $value2 || $value2 !== $checker()) { 23 | return $this->makeResult( 24 | false, 25 | $this->target->getErrorMessage() 26 | ); 27 | } 28 | 29 | return $this->makeHealthyResult(); 30 | } catch (\Exception $exception) { 31 | report($exception); 32 | 33 | return $this->makeResultFromException($exception); 34 | } 35 | } 36 | 37 | private function getCached() 38 | { 39 | $checker = $this->getChecker(); 40 | 41 | return IlluminateCache::remember( 42 | $this->target->key, 43 | $this->target->seconds, 44 | $checker 45 | ); 46 | } 47 | 48 | private function getChecker() 49 | { 50 | return function () { 51 | return 'DUMMY DATA'; 52 | }; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /.github/workflows/run-tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | schedule: 6 | - cron: '0 0 * * *' 7 | 8 | jobs: 9 | php-tests: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | php: [8.1, 8.0, 7.4, 7.3] 15 | laravel: [9.*, 8.*] 16 | include: 17 | - laravel: 9.* 18 | testbench: 7.* 19 | - laravel: 8.* 20 | testbench: 6.* 21 | 22 | name: P${{ matrix.php }} - L${{ matrix.laravel }} 23 | 24 | steps: 25 | - name: Checkout code 26 | uses: actions/checkout@v2 27 | 28 | - name: Install dependencies 29 | run: | 30 | git config --global user.email "acr@antoniocarlosribeiro.com"; git config --global user.name "Antonio Ribeiro" 31 | 32 | - name: Install dependencies 33 | run: | 34 | composer require "laravel/framework:${{ matrix.laravel }}" "orchestra/testbench:${{ matrix.testbench }}" --dev --no-interaction --no-suggest 35 | 36 | - name: Execute tests 37 | run: vendor/bin/phpunit 38 | 39 | # after_script: 40 | # - | 41 | # if [[ "$TRAVIS_PHP_VERSION" != 'hhvm' && "$TRAVIS_PHP_VERSION" != '7.0' ]]; then 42 | # wget https://scrutinizer-ci.com/ocular.phar 43 | # php ocular.phar code-coverage:upload --format=php-clover coverage.clover 44 | # fi 45 | # --coverage-clover=coverage.clover 46 | -------------------------------------------------------------------------------- /src/Checkers/Queue.php: -------------------------------------------------------------------------------- 1 | target->connection 19 | ?: app('config')['queue.default']; 20 | 21 | $queue = filled($this->target->queue ?? null) 22 | ? $this->target->queue 23 | : app('config')->get( 24 | "queue.connections.{$connection}.queue", 25 | 'default' 26 | ); 27 | 28 | app('queue')->pushOn( 29 | $queue, 30 | instantiate($this->target->testJob) 31 | ); 32 | 33 | $worker = app('queue.worker'); 34 | 35 | $worker->setCache(instantiate($this->target->cacheInstance)->driver()); 36 | 37 | $worker->runNextJob($connection, $queue, $this->gatherWorkerOptions()); 38 | 39 | return $this->makeResult(true); 40 | } 41 | 42 | /** 43 | * Gather all of the queue worker options as a single object. 44 | * 45 | * @return \Illuminate\Queue\WorkerOptions 46 | */ 47 | protected function gatherWorkerOptions() 48 | { 49 | return new WorkerOptions(0, 0, 0, 0, 0, false); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Antonio Carlos Ribeiro 2 | 3 | All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 8 | 9 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 10 | 11 | 3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 14 | -------------------------------------------------------------------------------- /src/resources/sass/app.scss: -------------------------------------------------------------------------------- 1 | 2 | // Fonts 3 | @import url('https://fonts.googleapis.com/css?family=Ubuntu'); 4 | 5 | // Variables 6 | @import 'variables'; 7 | 8 | // Bootstrap 9 | @import '~bootstrap/scss/bootstrap'; 10 | 11 | body { 12 | background-color: #FFFEEA; 13 | } 14 | 15 | .navbar-laravel { 16 | background-color: rgba(239, 204, 18, 0.14); 17 | box-shadow: 0 2px 4px rgba(0, 0, 0, 0.04); 18 | } 19 | 20 | .title { 21 | font-size: 1.2em; 22 | font-weight: 100; 23 | margin-top: 1px; 24 | margin-bottom: 1px; 25 | position: relative; 26 | } 27 | 28 | .subtitle { 29 | line-height: 10px; 30 | font-size: 1em; 31 | color: blue; 32 | margin-bottom: 1px; 33 | } 34 | 35 | .nav-button { 36 | margin: 0 !important; 37 | } 38 | 39 | .target-card { 40 | border-color: #bdbdbd !important; 41 | background-color: white; 42 | } 43 | 44 | .shadow { 45 | box-shadow: 0 1px 1px 0 rgba(0,0,0,.1); 46 | } 47 | 48 | .color-neutral { 49 | color: blue; 50 | fill: blue; 51 | } 52 | 53 | .color-success { 54 | color: green; 55 | fill: green; 56 | } 57 | 58 | .color-danger { 59 | color: #FFA8C4; 60 | fill: red; 61 | } 62 | 63 | .color-neutral-background { 64 | } 65 | 66 | .color-success-background { 67 | } 68 | 69 | .color-danger-background { 70 | background-color: #FFE5C4; 71 | } 72 | 73 | .info-icon { 74 | margin-top: 7px; 75 | width: 1.8em; 76 | opacity: 0.5; 77 | } 78 | 79 | .chart { 80 | margin-top: 7px; 81 | margin-right: -50px; 82 | margin-left: -7px; 83 | margin-bottom: -7px; 84 | } 85 | -------------------------------------------------------------------------------- /src/Checkers/Filesystem.php: -------------------------------------------------------------------------------- 1 | temporaryFile( 16 | 'health-check-', 17 | 'just testing', 18 | storage_path() 19 | ); 20 | 21 | if (! file_exists($file)) { 22 | return $this->makeResult( 23 | false, 24 | sprintf($this->target->getErrorMessage(), $file) 25 | ); 26 | } 27 | 28 | unlink($file); 29 | 30 | return $this->makeHealthyResult(); 31 | } catch (\Exception $exception) { 32 | return $this->makeResultFromException($exception); 33 | } 34 | } 35 | 36 | /** 37 | * @param $name 38 | * @param $content 39 | * @param null $folder 40 | * @return string 41 | */ 42 | private function temporaryFile($name, $content, $folder = null) 43 | { 44 | $folder = $folder ?: sys_get_temp_dir(); 45 | 46 | $file = tempnam($folder, $name); 47 | 48 | file_put_contents($file, $content); 49 | 50 | register_shutdown_function(function () use ($file) { 51 | if (file_exists($file)) { 52 | unlink($file); 53 | } 54 | }); 55 | 56 | return $file; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/config/resources/Https.yml: -------------------------------------------------------------------------------- 1 | name: Https 2 | abbreviation: https 3 | checker: PragmaRX\Health\Checkers\Https 4 | notify: true 5 | column_size: 3 6 | timeout_message: "[TIMEOUT] A request to %s took %s seconds. Timeout is set to %s seconds." 7 | connection_timeout: 30 8 | roundtrip_timeout: 30 9 | targets: 10 | - default: 11 | urls: 12 | - '{{ config("app.url") }}' 13 | - https://google.com 14 | - https://yahoo.com: 15 | headers: 16 | Authorization: Basic zzz0czBmz2zkzXRpb25sbzVpzzZ1zXRzz2zuZzz6z29hzC1zZWzkzQ== 17 | - https://api.sendinblue.com/v3/account: 18 | headers: 19 | api-key: "{{ config('services.sendinblue.api_key') }}" 20 | method: GET 21 | - url-via-config-1: 22 | url: "{{ config('services.see_tickets.api.endpoint') }}" 23 | method: POST 24 | auth: 25 | - "{{ config('services.see_tickets.api.username') }}" 26 | - "{{ config('services.see_tickets.api.password') }}" 27 | - basic 28 | - url-via-config-2: 29 | url: "{{ config('services.whatever.url') }}" 30 | method: POST 31 | debug: false 32 | form_params: 33 | username: "{{ config('services.whatever.username') }}" 34 | resource: "{{ config('services.whatever.api_endpoint') }}" 35 | grant_type: password 36 | password: "{{ config('services.whatever.password') }}" 37 | client_id: "{{ config('services.whatever.client_id') }}" 38 | -------------------------------------------------------------------------------- /src/Checkers/Health.php: -------------------------------------------------------------------------------- 1 | isHealthy(); 17 | 18 | return $this->makeResult( 19 | $healthy, 20 | $healthy ? '' : $this->target->getErrorMessage() 21 | ); 22 | } 23 | 24 | /** 25 | * Compute health. 26 | * 27 | * @param $previous 28 | * @param $resource 29 | * @return bool 30 | */ 31 | private function computeHealth($previous, $resource) 32 | { 33 | return $resource->isGlobal 34 | ? $previous 35 | : $previous && $this->computeHealthForAllTargets($resource); 36 | } 37 | 38 | /** 39 | * Compute health for targets. 40 | * 41 | * @param $resource 42 | * @return bool 43 | */ 44 | private function computeHealthForAllTargets($resource) 45 | { 46 | return $resource->targets->reduce(function ($carry, $target) { 47 | return $target->result->healthy; 48 | }, true); 49 | } 50 | 51 | /** 52 | * Check if the resource is healty. 53 | * 54 | * @return mixed 55 | */ 56 | protected function isHealthy() 57 | { 58 | $healthy = $this->target->resource->resources->reduce(function ( 59 | $carry, 60 | $item 61 | ) { 62 | return $this->computeHealth($carry, $item); 63 | }, 64 | true); 65 | 66 | return $healthy; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/config/resources/ServerVars.yml: -------------------------------------------------------------------------------- 1 | name: ServerVars 2 | abbreviation: server-vars 3 | checker: PragmaRX\Health\Checkers\ServerVars 4 | error_message: "These $_SERVER vars doesn't match the expected value: %s" 5 | notify: true 6 | column_size: 3 7 | targets: 8 | - default: 9 | config: 10 | route: pragmarx.health.server-vars 11 | cache_timeout: 5 12 | query_string: "query=testing-cdn-enabled-query-strings" 13 | auth: 14 | username: "{{ config('auth.http.user') }}" 15 | password: "{{ config('auth.http.password') }}" 16 | vars: 17 | server_port: 18 | name: SERVER_PORT 19 | operator: equals 20 | value: 443 21 | strict: false 22 | mandatory: true 23 | server_software: 24 | name: SERVER_SOFTWARE 25 | operator: contains 26 | value: "nginx/1.20" 27 | mandatory: true 28 | query_string: 29 | name: QUERY_STRING 30 | operator: contains 31 | value: "query=testing-cdn-enabled-query-strings" 32 | mandatory: true 33 | http_x_forwarded_proto: 34 | name: HTTP_X_FORWARDED_PROTO 35 | operator: equals 36 | value: https 37 | mandatory: false 38 | http_cloudfront_forwarded_proto: 39 | name: HTTP_CLOUDFRONT_FORWARDED_PROTO 40 | operator: equals 41 | value: https 42 | mandatory: false 43 | http_x_forwarded_port: 44 | name: HTTP_X_FORWARDED_PORT 45 | operator: equals 46 | value: 443 47 | strict: false 48 | mandatory: false 49 | -------------------------------------------------------------------------------- /src/config/resources/Extensions.yml: -------------------------------------------------------------------------------- 1 | name: Extensions are installed 2 | abbreviation: extensions-installed 3 | checker: PragmaRX\Health\Checkers\Extensions 4 | notify: true 5 | error_message: 6 | "The following extensions are not installed or enabled: %s" 7 | column_size: 3 8 | targets: 9 | default: 10 | - items: 11 | - Core 12 | - phpdbg_webhelper 13 | - date 14 | - libxml 15 | - openssl 16 | - pcre 17 | - sqlite3 18 | - zlib 19 | - bcmath 20 | - bz2 21 | - calendar 22 | - ctype 23 | - curl 24 | - dba 25 | - dom 26 | - hash 27 | - FFI 28 | - fileinfo 29 | - filter 30 | - ftp 31 | - gd 32 | - gettext 33 | - gmp 34 | - SPL 35 | - iconv 36 | - intl 37 | - json 38 | - ldap 39 | - mbstring 40 | - session 41 | - standard 42 | - odbc 43 | - pcntl 44 | - mysqlnd 45 | - PDO 46 | - pdo_dblib 47 | - pdo_mysql 48 | - PDO_ODBC 49 | - pdo_pgsql 50 | - pdo_sqlite 51 | - pgsql 52 | - Phar 53 | - posix 54 | - pspell 55 | - readline 56 | - Reflection 57 | - mysqli 58 | - shmop 59 | - SimpleXML 60 | - soap 61 | - sockets 62 | - sodium 63 | - exif 64 | - sysvmsg 65 | - sysvsem 66 | - sysvshm 67 | - tidy 68 | - tokenizer 69 | - xml 70 | - xmlreader 71 | - xmlwriter 72 | - xsl 73 | - zip 74 | - redis 75 | - Zend OPcache 76 | - xdebug 77 | -------------------------------------------------------------------------------- /src/Support/Timer.php: -------------------------------------------------------------------------------- 1 | start(); 30 | } 31 | 32 | public static function stopSB() 33 | { 34 | $result = static::isStatic() 35 | ? SBTimer::stop() 36 | : static::getSBInstance()->stop(); 37 | 38 | if (is_object($result)) { 39 | return $result->asSeconds(); 40 | } 41 | 42 | return $result; 43 | } 44 | 45 | public static function isStatic() 46 | { 47 | return static::getStaticMethodNames()->contains('start'); 48 | } 49 | 50 | public static function getSBInstance() 51 | { 52 | if (! empty(static::$sbTimer)) { 53 | return static::$sbTimer; 54 | } 55 | 56 | return static::$sbTimer = new SBTimer(); 57 | } 58 | 59 | public static function getStaticMethodNames() 60 | { 61 | return collect((new \ReflectionClass(SBTimer::class))->getMethods(\ReflectionMethod::IS_STATIC))->map(function ($method) { 62 | return $method->name; 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Listeners/NotifyHealthIssue.php: -------------------------------------------------------------------------------- 1 | map( 19 | function ($item) { 20 | $model = instantiate( 21 | config('health.notifications.users.model') 22 | ); 23 | 24 | $model->email = $item; 25 | 26 | return $model; 27 | } 28 | ); 29 | } 30 | 31 | /** 32 | * Handle the event. 33 | * 34 | * @param RaiseHealthIssue $event 35 | * @return void 36 | * 37 | * @throws ReflectionException 38 | */ 39 | public function handle(RaiseHealthIssue $event) 40 | { 41 | $notifier = config('health.notifications.notifier'); 42 | if ($notifier) { 43 | $notifierClass = new ReflectionClass($notifier); 44 | } else { 45 | $notifierClass = HealthStatus::class; 46 | } 47 | try { 48 | $event->failure->targets->each(function ($target) use ($event, $notifierClass) { 49 | if (! $target->result->healthy) { 50 | Notification::send( 51 | $this->getNotifiableUsers(), 52 | $notifierClass->newInstance($target, $event->channel) 53 | ); 54 | } 55 | }); 56 | } catch (\Exception $exception) { 57 | report($exception); 58 | } catch (\Throwable $error) { 59 | report($error); 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/PhpUnit/TestCase.php: -------------------------------------------------------------------------------- 1 | set('health.resources.enabled', static::ALL_RESOURCES); 69 | 70 | return [HealthServiceProvider::class]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Support/Traits/ToArray.php: -------------------------------------------------------------------------------- 1 | ___toArray($subject, $maxDepth); 27 | } 28 | 29 | /** 30 | * Convert object to array. 31 | * 32 | * @param $subject 33 | * @return array|null 34 | */ 35 | public function ___toArray($subject) 36 | { 37 | $callback = $this->__getToArrayCallBack(); 38 | 39 | return $callback($subject); 40 | } 41 | 42 | /** 43 | * Generate a callback to transform object to array. 44 | * 45 | * @return \Closure 46 | */ 47 | public function __getToArrayCallBack() 48 | { 49 | return function ($subject) { 50 | static::$__depth++; 51 | 52 | if ($subject instanceof Arrayable) { 53 | $subject = $subject->toArray(); 54 | } 55 | 56 | if (is_object($subject)) { 57 | $subject = get_object_vars($subject); 58 | } 59 | 60 | if (is_array($subject)) { 61 | if (static::$__depth <= static::$__maxDepth) { 62 | $subject = array_map( 63 | $this->__getToArrayCallBack(), 64 | $subject 65 | ); 66 | } else { 67 | $subject = null; 68 | } 69 | } 70 | 71 | static::$__depth--; 72 | 73 | return $subject; 74 | }; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Support/Result.php: -------------------------------------------------------------------------------- 1 | healthy = $healthy; 49 | 50 | $this->errorMessage = $errorMessage; 51 | 52 | // Currently the status is inferred from the $healthy flag until full support is added. 53 | $this->status = $healthy ? self::OK : self::CRITICAL; 54 | } 55 | 56 | /** 57 | * Value setter. 58 | * 59 | * @param mixed $value 60 | * @return Result 61 | */ 62 | public function setValue($value) 63 | { 64 | $this->value = $value; 65 | 66 | return $this; 67 | } 68 | 69 | /** 70 | * Value for humans setter. 71 | * 72 | * @param string $valueHuman 73 | * @return Result 74 | */ 75 | public function setValueHuman(string $valueHuman) 76 | { 77 | $this->valueHuman = $valueHuman; 78 | 79 | return $this; 80 | } 81 | 82 | /** 83 | * Get the result's status of the check. 84 | * 85 | * @return string one of the consts e.g. result::OK 86 | */ 87 | public function getStatus(): string 88 | { 89 | return $this->status; 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Checkers/PortCheck.php: -------------------------------------------------------------------------------- 1 | portIsNotConnectable( 18 | $ipAddress = ip_address_from_hostname($this->target->hostname) 19 | ) 20 | ) { 21 | return $this->makeFinalResult($ipAddress); 22 | } 23 | 24 | return $this->makeHealthyResult(); 25 | } 26 | 27 | /** 28 | * Get hostname and IP. 29 | * 30 | * @param $hostname 31 | * @return mixed 32 | */ 33 | protected function hosnameAndIp($hostname, $ipAdress) 34 | { 35 | return $hostname.($hostname != $ipAdress ? " ({$ipAdress})" : ''); 36 | } 37 | 38 | /** 39 | * Make the result. 40 | * 41 | * @param bool $ipAddress 42 | * @return Result 43 | */ 44 | protected function makeFinalResult($ipAddress) 45 | { 46 | return $this->target->setResult( 47 | $this->makeResult( 48 | false, 49 | sprintf( 50 | $this->target->getErrorMessage(), 51 | $this->hosnameAndIp($this->target->hostname, $ipAddress), 52 | $this->target->port 53 | ) 54 | ) 55 | )->getResult(); 56 | } 57 | 58 | public function portCheck($ipAddress, $port, $timeout) 59 | { 60 | $fp = @fsockopen($ipAddress, $port, $errno, $errstr, $timeout); 61 | 62 | if (gettype($fp) !== 'resource') { 63 | return false; 64 | } 65 | 66 | fclose($fp); 67 | 68 | return true; 69 | } 70 | 71 | /** 72 | * @param $ipAddress 73 | * @return bool 74 | */ 75 | protected function portIsNotConnectable($ipAddress) 76 | { 77 | return ! $this->portCheck( 78 | $ipAddress, 79 | $this->target->port, 80 | $this->target->timeout ?? 1 81 | ); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pragmarx/health", 3 | "description": "Laravel Server & App Health Monitor and Notifier", 4 | "keywords": [ 5 | "health", 6 | "laravel", 7 | "pragmarx", 8 | "notifications", 9 | "panel", 10 | "monitor", 11 | "server", 12 | "app" 13 | ], 14 | "license": "BSD-3-Clause", 15 | "authors": [ 16 | { 17 | "name": "Antonio Carlos Ribeiro", 18 | "email": "acr@antoniocarlosribeiro.com", 19 | "role": "Creator & Designer" 20 | } 21 | ], 22 | "require": { 23 | "php": ">=7.3", 24 | "illuminate/support": ">=8.0", 25 | "pragmarx/yaml": ">=0.1", 26 | "phpunit/php-timer": "^1.0|^2.0|^3.0|^4.0|^5.0|^6.0", 27 | "ext-json": "*" 28 | }, 29 | "require-dev": { 30 | "phpunit/phpunit": ">=6.5", 31 | "laravel/laravel": ">=8.0", 32 | "orchestra/testbench": "5.*|6.*|7.*|8.*|9.*", 33 | "guzzlehttp/guzzle": ">=6.0", 34 | "docusign/esign-client": ">=2.0", 35 | "predis/predis": ">=1.0", 36 | "nesbot/carbon": ">=1.34", 37 | "laravel/framework": "9.*|10.*|11.*" 38 | }, 39 | "suggest": { 40 | "guzzlehttp/guzzle": ">=6.0", 41 | "docusign/esign-client": ">=2.0", 42 | "predis/predis": ">=1.0", 43 | "league/flysystem-aws-s3-v3": ">=1.0", 44 | "sensiolabs/security-checker": ">=4.1", 45 | "spatie/ssl-certificate": ">=1.0" 46 | }, 47 | "autoload": { 48 | "files": [ 49 | "src/Support/helpers.php" 50 | ], 51 | "psr-4": { 52 | "PragmaRX\\Health\\": "src/" 53 | } 54 | }, 55 | "autoload-dev": { 56 | "psr-4": { 57 | "PragmaRX\\Health\\Tests\\PhpUnit\\": "tests/PhpUnit/" 58 | } 59 | }, 60 | "extra": { 61 | "laravel": { 62 | "providers": [ 63 | "PragmaRX\\Health\\ServiceProvider" 64 | ] 65 | } 66 | }, 67 | "scripts": { 68 | "test": [ 69 | "@composer install", 70 | "vendor/bin/phpunit" 71 | ] 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Checkers/Mail.php: -------------------------------------------------------------------------------- 1 | checkMail(); 26 | } 27 | 28 | /** 29 | * Configure mail for testing. 30 | */ 31 | private function configureMail() 32 | { 33 | $this->mailConfiguration = config('mail'); 34 | 35 | config(['mail' => $this->target->config->toArray()]); 36 | } 37 | 38 | /** 39 | * Send a test e-mail. 40 | */ 41 | private function checkMail() 42 | { 43 | $this->configureMail(); 44 | 45 | try { 46 | $this->sendMail(); 47 | 48 | $result = $this->makeHealthyResult(); 49 | } catch (\Exception $exception) { 50 | report($exception); 51 | 52 | $result = $this->makeResultFromException($exception); 53 | } 54 | 55 | $this->restoreMailConfiguration(); 56 | 57 | return $result; 58 | } 59 | 60 | /** 61 | * Restore mail configuration. 62 | */ 63 | private function restoreMailConfiguration() 64 | { 65 | config(['mail' => $this->mailConfiguration]); 66 | } 67 | 68 | /** 69 | * Send a test e-mail message. 70 | */ 71 | private function sendMail() 72 | { 73 | IlluminateMail::send($this->target->view, [], function ($message) { 74 | $fromAddress = Arr::get($this->target->config, 'from.address'); 75 | 76 | $message->returnPath($fromAddress); 77 | 78 | $message->cc($fromAddress); 79 | 80 | $message->bcc($fromAddress); 81 | 82 | $message->replyTo($fromAddress); 83 | 84 | $message->to($this->target->to); 85 | 86 | $message->subject($this->target->subject); 87 | }); 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Support/Traits/Database.php: -------------------------------------------------------------------------------- 1 | database = $this->__load(); 15 | } 16 | 17 | /** 18 | * Load cache. 19 | * 20 | * @return \Illuminate\Support\Collection 21 | */ 22 | public function __load() 23 | { 24 | if (! file_exists($file = $this->getDatabaseFileName())) { 25 | return collect(); 26 | } 27 | 28 | return collect(json_decode(file_get_contents($file), true)); 29 | } 30 | 31 | /** 32 | * Get cache filename. 33 | * 34 | * @return string|null 35 | */ 36 | protected function getDatabaseFileName() 37 | { 38 | return $this->target->saveTo; 39 | } 40 | 41 | /** 42 | * Check if database is enabled. 43 | * 44 | * @return bool 45 | */ 46 | protected function databaseEnabled() 47 | { 48 | return config('health.database.enabled'); 49 | } 50 | 51 | protected function saveResultsToDatabase($target, $result) 52 | { 53 | $model = config('health.database.model', HealthCheck::class); 54 | 55 | $model::create([ 56 | 'resource_name' => $resource = $target->resource->name, 57 | 'resource_slug' => $target->resource->slug, 58 | 'target_name' => $target->name, 59 | 'target_slug' => Str::slug($target->name), 60 | 'target_display' => $target->display, 61 | 'healthy' => $result->healthy, 62 | 'error_message' => $result->errorMessage, 63 | 'runtime' => $result->elapsedTime ?? 0, 64 | 'value' => $result->value, 65 | 'value_human' => $result->valueHuman, 66 | ]); 67 | 68 | return $model::where([ 69 | 'resource_slug' => $target->resource->slug, 70 | 'target_name' => $target->name, 71 | ]) 72 | ->orderBy('created_at', 'desc') 73 | ->take(config('health.database.max_records')) 74 | ->get(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/Checkers/Composer.php: -------------------------------------------------------------------------------- 1 | executeCommand(); 18 | 19 | if ($outdated->count() > $this->target->resource->shouldCountAtMost) { 20 | return $this->makeResult( 21 | false, 22 | sprintf($this->target->getErrorMessage(), $outdated->count()) 23 | ); 24 | } 25 | 26 | return $this->makeHealthyResult(); 27 | } 28 | 29 | /** 30 | * Convert output to array. 31 | * 32 | * @param string $output 33 | * @return \Illuminate\Support\Collection|mixed 34 | */ 35 | protected function outputToCollection(string $output) 36 | { 37 | if ($this->target->resource->jsonResult) { 38 | return collect(json_decode($output, true) ?? collect([])); 39 | } 40 | 41 | return $output; 42 | } 43 | 44 | /** 45 | * Get the ping binary. 46 | */ 47 | protected function makeCommand() 48 | { 49 | return [ 50 | sprintf( 51 | '%s %s', 52 | $this->target->resource->binary, 53 | $this->target->resource->command 54 | ), 55 | ]; 56 | } 57 | 58 | /** 59 | * Execute the Composer command. 60 | * 61 | * @return \Illuminate\Support\Collection|mixed 62 | */ 63 | protected function executeCommand() 64 | { 65 | $process = new SymfonyProcess( 66 | $this->makeCommand(), 67 | $this->target->workingDir 68 | ); 69 | 70 | $process->run(); 71 | 72 | $output = $this->outputToCollection($process->getOutput()); 73 | 74 | if ($output->count() == 0) { 75 | return $output; 76 | } 77 | 78 | if ($rootItem = $this->target->resource->rootItem) { 79 | return collect($output[$rootItem]); 80 | } 81 | 82 | return $output; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Checkers/Database.php: -------------------------------------------------------------------------------- 1 | target->type) { 18 | case 'find_first_model': 19 | return $this->findFirstModel(); 20 | case 'raw_query': 21 | return $this->rawQuery(); 22 | } 23 | 24 | throw new \Exception( 25 | "Target type '{$this->target->type}' does not exists" 26 | ); 27 | } catch (\Exception $exception) { 28 | report($exception); 29 | 30 | return $this->makeResultFromException($exception); 31 | } 32 | } 33 | 34 | protected function findFirstModel() 35 | { 36 | collect($this->target->models)->each(function ($model) { 37 | instantiate($model)->first(); 38 | }); 39 | 40 | return $this->makeHealthyResult(); 41 | } 42 | 43 | protected function getConnectionName() 44 | { 45 | return $this->target->connection == 'default' 46 | ? config('database.default') 47 | : $this->target->connection; 48 | } 49 | 50 | protected function rawQuery() 51 | { 52 | Timer::start(); 53 | 54 | DB::connection($this->getConnectionName())->select($this->target->query); 55 | 56 | $took = round(Timer::stop(), 5); 57 | $tookHuman = "{$took}s"; 58 | 59 | $this->target->setDisplay($this->target->name." ({$tookHuman})"); 60 | 61 | $result = 62 | $took > $this->target->maximumTime 63 | ? $this->makeResult( 64 | false, 65 | sprintf( 66 | $this->target->errorMessage, 67 | $took, 68 | $this->target->maximumTime 69 | ) 70 | ) 71 | : $this->makeHealthyResult(); 72 | 73 | $result->setValue($took)->setValueHuman($tookHuman); 74 | 75 | return $result; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/resources/dist/js/app.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.6.0 (https://getbootstrap.com/) 3 | * Copyright 2011-2021 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/main/LICENSE) 5 | */ 6 | 7 | /*! 8 | * Chart.js v2.9.4 9 | * https://www.chartjs.org 10 | * (c) 2020 Chart.js Contributors 11 | * Released under the MIT License 12 | */ 13 | 14 | /*! 15 | * Sizzle CSS Selector Engine v2.3.6 16 | * https://sizzlejs.com/ 17 | * 18 | * Copyright JS Foundation and other contributors 19 | * Released under the MIT license 20 | * https://js.foundation/ 21 | * 22 | * Date: 2021-02-16 23 | */ 24 | 25 | /*! 26 | * Vue.js v2.6.14 27 | * (c) 2014-2021 Evan You 28 | * Released under the MIT License. 29 | */ 30 | 31 | /*! 32 | * jQuery JavaScript Library v3.6.0 33 | * https://jquery.com/ 34 | * 35 | * Includes Sizzle.js 36 | * https://sizzlejs.com/ 37 | * 38 | * Copyright OpenJS Foundation and other contributors 39 | * Released under the MIT license 40 | * https://jquery.org/license 41 | * 42 | * Date: 2021-03-02T17:08Z 43 | */ 44 | 45 | /** 46 | * @license 47 | * Lodash 48 | * Copyright OpenJS Foundation and other contributors 49 | * Released under MIT license 50 | * Based on Underscore.js 1.8.3 51 | * Copyright Jeremy Ashkenas, DocumentCloud and Investigative Reporters & Editors 52 | */ 53 | 54 | /**! 55 | * @fileOverview Kickass library to create and place poppers near their reference elements. 56 | * @version 1.16.1 57 | * @license 58 | * Copyright (c) 2016 Federico Zivolo and contributors 59 | * 60 | * Permission is hereby granted, free of charge, to any person obtaining a copy 61 | * of this software and associated documentation files (the "Software"), to deal 62 | * in the Software without restriction, including without limitation the rights 63 | * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 64 | * copies of the Software, and to permit persons to whom the Software is 65 | * furnished to do so, subject to the following conditions: 66 | * 67 | * The above copyright notice and this permission notice shall be included in all 68 | * copies or substantial portions of the Software. 69 | * 70 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 71 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 72 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 73 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 74 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 75 | * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 76 | * SOFTWARE. 77 | */ 78 | 79 | //! moment.js 80 | 81 | //! moment.js locale configuration 82 | -------------------------------------------------------------------------------- /src/Checkers/Docusign.php: -------------------------------------------------------------------------------- 1 | docusignIsNotInstalled()) { 20 | return $this->makeResult(false, $this->target->notInstalledMessage); 21 | } 22 | 23 | if (! $this->login()) { 24 | throw new DomainException( 25 | 'Unable to authenticate to the DocuSign Api' 26 | ); 27 | } 28 | 29 | return $this->makeHealthyResult(); 30 | } 31 | 32 | private function docusignIsNotInstalled() 33 | { 34 | return ! class_exists(ApiClient::class); 35 | } 36 | 37 | private function getAccountIdFromLogin($login) 38 | { 39 | return $login->getLoginAccounts()[0]->getAccountId(); 40 | } 41 | 42 | /** 43 | * @param $config 44 | * @return ApiClient 45 | */ 46 | protected function getApiClient($config) 47 | { 48 | return new ApiClient($config); 49 | } 50 | 51 | /** 52 | * @param $config 53 | * @return AuthenticationApi 54 | */ 55 | protected function getAuthApi($config) 56 | { 57 | return new AuthenticationApi($this->getApiClient($config)); 58 | } 59 | 60 | /** 61 | * @return ApiClient 62 | */ 63 | protected function getConfig() 64 | { 65 | return (new Configuration()) 66 | ->setDebug($this->target->debug) 67 | ->setDebugFile($this->makeFileName($this->target->debugFile)) 68 | ->setHost($this->target->apiHost) 69 | ->addDefaultHeader( 70 | 'X-DocuSign-Authentication', 71 | json_encode([ 72 | 'Username' => $this->target->username, 73 | 'Password' => $this->target->password, 74 | 'IntegratorKey' => $this->target->integratorKey, 75 | ]) 76 | ); 77 | } 78 | 79 | /** 80 | * @param $config 81 | * @return \DocuSign\eSign\Model\LoginInformation 82 | */ 83 | protected function getLoginInformation($config) 84 | { 85 | return $this->getAuthApi($config)->login($this->getLoginOptions()); 86 | } 87 | 88 | /** 89 | * @return LoginOptions 90 | */ 91 | protected function getLoginOptions() 92 | { 93 | return new LoginOptions(); 94 | } 95 | 96 | /** 97 | * @return mixed 98 | */ 99 | protected function login() 100 | { 101 | return $this->getAccountIdFromLogin( 102 | $this->getLoginInformation($this->getConfig()) 103 | ); 104 | } 105 | 106 | private function makeFileName($file) 107 | { 108 | if (is_absolute_path($file)) { 109 | return $file; 110 | } 111 | 112 | return base_path($file); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Checkers/ServerVars.php: -------------------------------------------------------------------------------- 1 | requestServerVars(); 23 | 24 | collect($this->target->config['vars'])->each(function ($var) { 25 | $this->checkVar($var); 26 | }); 27 | 28 | return blank($this->errors) 29 | ? $this->makeHealthyResult() 30 | : $this->makeResult(false, sprintf($this->target->resource->errorMessage, implode('; ', $this->errors))); 31 | } 32 | 33 | public function requestServerVars() 34 | { 35 | $url = $this->makeUrl(); 36 | 37 | $bearer = (new LocallyProtected())->protect($this->target->config['cache_timeout'] ?? 60); 38 | 39 | $guzze = new Guzzle($this->getAuthorization()); 40 | 41 | $response = $guzze->request('GET', $url, [ 42 | 'headers' => ['API-Token' => $bearer], 43 | ]); 44 | 45 | if (($code = $response->getStatusCode()) !== 200) { 46 | throw new \Exception("Request to {$url} returned a status code {$code}"); 47 | } 48 | 49 | $this->response = json_decode((string) $response->getBody(), true); 50 | } 51 | 52 | public function checkVar($var) 53 | { 54 | if (blank($this->response[$var['name']] ?? null)) { 55 | if ($var['mandatory']) { 56 | $this->errors[] = "{$var['name']} is empty"; 57 | } 58 | 59 | return; 60 | } 61 | 62 | $got = $this->response[$var['name']]; 63 | 64 | $expected = $var['value']; 65 | 66 | if (! $this->compare($var, $expected, $got)) { 67 | $this->errors[] = "{$var['name']}: expected '{$expected}' but got '{$got}'"; 68 | } 69 | } 70 | 71 | public function compare($var, $expected, $got) 72 | { 73 | $operator = $var['operator'] ?? 'equals'; 74 | 75 | $strict = $var['strict'] ?? true; 76 | 77 | if ($operator === 'equals') { 78 | return $strict ? $expected === $got : $expected == $got; 79 | } 80 | 81 | if ($operator === 'contains') { 82 | return Str::contains($got, $expected); 83 | } 84 | 85 | throw new \Exception("Operator '$operator' is not supported."); 86 | } 87 | 88 | public function makeUrl() 89 | { 90 | $url = route($this->target->config['route']); 91 | 92 | if ($queryString = $this->target->config['query_string']) { 93 | $url .= "?$queryString"; 94 | } 95 | 96 | return $url; 97 | } 98 | 99 | public function getAuthorization() 100 | { 101 | if (blank($auth = $this->target->config['auth'] ?? null)) { 102 | return []; 103 | } 104 | 105 | return ['auth' => [$auth['username'], $auth['password']]]; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Checkers/MixManifest.php: -------------------------------------------------------------------------------- 1 | checkFilePresence($file = $this->getManifestFileName()); 21 | 22 | $loaded = $this->loadJson($file); 23 | 24 | foreach ($loaded as $item => $asset) { 25 | if (! $this->ignored($item)) { 26 | $this->checkFilePresence(base_path($this->target->assetsRoot.$asset)); 27 | } 28 | } 29 | 30 | return $this->makeHealthyResult(); 31 | } 32 | 33 | /** 34 | * Get the mix manifest file name. 35 | * 36 | * @return string 37 | */ 38 | public function getManifestFileName() 39 | { 40 | $file = $this->target->file; 41 | 42 | if (Str::startsWith($file, '/')) { 43 | return $file; 44 | } 45 | 46 | return base_path($file); 47 | } 48 | 49 | /** 50 | * Check presence of a file in disk. 51 | * 52 | * @return string 53 | */ 54 | public function checkFilePresence($fileName) 55 | { 56 | if (! file_exists($fileName)) { 57 | throw new Exception("File doesn't exist: ".$fileName); 58 | } 59 | } 60 | 61 | /** 62 | * Load manifest. 63 | * 64 | * @return string 65 | */ 66 | public function loadJson($fileName) 67 | { 68 | $contents = file_get_contents($fileName); 69 | 70 | if (blank($fileName)) { 71 | throw new Exception('Manifest is empty or permission to read the file was denied: '.$fileName); 72 | } 73 | 74 | $contents = json_decode($contents, true); 75 | 76 | if (json_last_error() !== \JSON_ERROR_NONE) { 77 | throw new Exception('Error parsing manifest: '.$fileName.' - Error: '.$this->getJSONErrorMessage(json_last_error())); 78 | } 79 | 80 | return $contents; 81 | } 82 | 83 | /** 84 | * Snippet took from Symfony/Translation. 85 | * 86 | * Translates JSON_ERROR_* constant into meaningful message. 87 | */ 88 | private function getJSONErrorMessage(int $errorCode): string 89 | { 90 | switch ($errorCode) { 91 | case \JSON_ERROR_DEPTH: 92 | return 'Maximum stack depth exceeded'; 93 | case \JSON_ERROR_STATE_MISMATCH: 94 | return 'Underflow or the modes mismatch'; 95 | case \JSON_ERROR_CTRL_CHAR: 96 | return 'Unexpected control character found'; 97 | case \JSON_ERROR_SYNTAX: 98 | return 'Syntax error, malformed JSON'; 99 | case \JSON_ERROR_UTF8: 100 | return 'Malformed UTF-8 characters, possibly incorrectly encoded'; 101 | default: 102 | return 'Unknown error'; 103 | } 104 | } 105 | 106 | /** 107 | * Load manifest. 108 | * 109 | * @return string 110 | */ 111 | public function ignored($item) 112 | { 113 | if (! isset($this->target->ignoreItems)) { 114 | return false; 115 | } 116 | 117 | return $this->target->ignoreItems->contains($item); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/Support/Cache.php: -------------------------------------------------------------------------------- 1 | getSeconds() !== false; 22 | } 23 | 24 | /** 25 | * Flush cache. 26 | * 27 | * @param bool $force 28 | * @param null $key 29 | */ 30 | public function flush($force = false, $key = null) 31 | { 32 | if (! isset($this->flushed[$key]) && ($force || $this->needsToFlush())) { 33 | try { 34 | $this->forceFlush($key); 35 | } catch (Exception $exception) { 36 | report($exception); 37 | } 38 | 39 | $this->flushed[$key] = true; 40 | } 41 | } 42 | 43 | /** 44 | * Force cache flush. 45 | * 46 | * @param string|null $key 47 | */ 48 | protected function forceFlush($key = null) 49 | { 50 | IlluminateCache::forget($key ?? config('health.cache.key')); 51 | } 52 | 53 | /** 54 | * Get cache seconds. 55 | * 56 | * @return mixed 57 | */ 58 | public function getSeconds() 59 | { 60 | return config('health.cache.seconds'); 61 | } 62 | 63 | /** 64 | * Get the request. 65 | * 66 | * @return \Illuminate\Foundation\Application|mixed 67 | */ 68 | protected function getCurrentRequest() 69 | { 70 | return instantiate(Request::class); 71 | } 72 | 73 | /** 74 | * Get cached resources. 75 | * 76 | * @return \Illuminate\Support\Collection 77 | */ 78 | public function getCachedResources() 79 | { 80 | $this->flush(); 81 | 82 | return $this->cacheIsEnabled() 83 | ? IlluminateCache::get(config('health.cache.key'), collect()) 84 | : collect(); 85 | } 86 | 87 | /** 88 | * Check if cache needs to be flushed. 89 | * 90 | * @return bool 91 | */ 92 | protected function needsToFlush() 93 | { 94 | return 95 | $this->cacheIsEnabled() && $this->getCurrentRequest()->get('flush'); 96 | } 97 | 98 | /** 99 | * Cache all resources. 100 | * 101 | * @param Collection $resources 102 | * @return Collection 103 | */ 104 | public function cacheResources($resources) 105 | { 106 | if ($this->cacheIsEnabled()) { 107 | IlluminateCache::put( 108 | config('health.cache.key'), 109 | $resources, 110 | $this->getSeconds() 111 | ); 112 | } 113 | 114 | return $resources; 115 | } 116 | 117 | /** 118 | * Get an item from the cache, or store the default value. 119 | * 120 | * @param string $key 121 | * @param \Closure $callback 122 | * @return mixed 123 | */ 124 | public function remember($key, \Closure $callback) 125 | { 126 | if (! $this->cacheIsEnabled()) { 127 | return $callback(); 128 | } 129 | 130 | $key = config('health.cache.key').'|resource:'.$key; 131 | 132 | $this->flush(false, $key); 133 | 134 | return IlluminateCache::remember($key, $this->getSeconds(), $callback); 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/Checkers/Broadcasting.php: -------------------------------------------------------------------------------- 1 | target->routes->each(function ($route, $name) { 18 | $this->registerRoute($route, $name); 19 | }); 20 | } 21 | 22 | /** 23 | * Check resource. 24 | * 25 | * @return Result 26 | */ 27 | public function check() 28 | { 29 | $this->loadDatabase(); 30 | 31 | $this->bootRouter(); 32 | 33 | $isHealthy = ! $this->pingTimedout(); 34 | 35 | $this->createPing(); 36 | 37 | $this->dispatchEvent(); 38 | 39 | return $this->makeResult($isHealthy, $this->target->getErrorMessage()); 40 | } 41 | 42 | /** 43 | * Dispatch event. 44 | */ 45 | protected function dispatchEvent() 46 | { 47 | event( 48 | new HealthPing( 49 | $this->target->channel, 50 | route($this->target->routeName, [$this->target->secret]), 51 | $this->target 52 | ) 53 | ); 54 | } 55 | 56 | /** 57 | * Create and persist ping. 58 | */ 59 | protected function createPing() 60 | { 61 | $this->database->push($this->createPingRow()); 62 | 63 | $this->persist(); 64 | } 65 | 66 | /** 67 | * Create ping row array. 68 | * 69 | * @return array 70 | */ 71 | protected function createPingRow() 72 | { 73 | return [ 74 | 'pinged_at' => Carbon::now(), 75 | 'ponged_at' => null, 76 | 'secret' => $this->target->secret, 77 | ]; 78 | } 79 | 80 | /** 81 | * Parse date. 82 | * 83 | * @param $date 84 | * @return Carbon 85 | */ 86 | protected function parseDate($date) 87 | { 88 | return Carbon::parse($date['date'], $date['timezone']); 89 | } 90 | 91 | /** 92 | * Create and persist pong. 93 | * 94 | * @param $secret 95 | */ 96 | public function pong($secret) 97 | { 98 | $this->database = $this->database->map(function ($item) use ($secret) { 99 | if ($item['secret'] == $secret) { 100 | $item['ponged_at'] = Carbon::now(); 101 | } 102 | 103 | return $item; 104 | }); 105 | 106 | $this->persist(); 107 | } 108 | 109 | /** 110 | * Check if a ping timed out. 111 | * 112 | * @return bool 113 | */ 114 | protected function pingTimedout() 115 | { 116 | $timedout = false; 117 | 118 | $this->database = $this->database->filter(function ($item) use ( 119 | &$timedout 120 | ) { 121 | if (! $item['ponged_at']) { 122 | if ( 123 | Carbon::now()->diffInSeconds( 124 | $this->parseDate($item['pinged_at']) 125 | ) > $this->target->timeout 126 | ) { 127 | $timedout = true; 128 | 129 | return false; 130 | } 131 | 132 | return true; 133 | } 134 | 135 | return false; 136 | }); 137 | 138 | return $timedout; 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/Checkers/Process.php: -------------------------------------------------------------------------------- 1 | checkMinMax($this->getProcessesRunningCount()); 22 | 23 | if (! empty($message)) { 24 | return $this->makeResult(false, $message); 25 | } 26 | 27 | return $this->makeHealthyResult(); 28 | } 29 | 30 | private function checkMinMax($processes) 31 | { 32 | return $this->buildMessage('minimum', $processes) 33 | ?: $this->buildMessage('maximum', $processes); 34 | } 35 | 36 | private function buildMessage($type, $processes) 37 | { 38 | $instances = $this->target->instances; 39 | 40 | if (! $count = (int) $instances[$type]['count']) { 41 | return ''; 42 | } 43 | 44 | if ($type == 'minimum') { 45 | $diff = $processes - $count; 46 | } else { 47 | $diff = $count - $processes; 48 | } 49 | 50 | if ($diff < 0) { 51 | return sprintf( 52 | $instances[$type]['message'], 53 | $this->target->processName, 54 | $processes, 55 | $count 56 | ); 57 | } 58 | } 59 | 60 | private function checkPidFile() 61 | { 62 | return $this->processPidFileIsLocked() ? 1 : 0; 63 | } 64 | 65 | private function countRunningProcesses() 66 | { 67 | if ($command = $this->getCommand()) { 68 | exec($command, $count); 69 | 70 | return count($count); 71 | } 72 | 73 | return 0; 74 | } 75 | 76 | /** 77 | * @param $file 78 | * @return bool 79 | */ 80 | private function checkPidFileExistence($file) 81 | { 82 | if (file_exists($file)) { 83 | return 1; 84 | } 85 | 86 | throw new DomainException( 87 | sprintf($this->target->pidFileMissingErrorMessage, $file) 88 | ); 89 | } 90 | 91 | /** 92 | * @param $file 93 | * @return bool 94 | */ 95 | private function checkPidFileLockState($file) 96 | { 97 | try { 98 | $locked = flock($filePointer = fopen($file, 'r+'), LOCK_EX); 99 | 100 | flock($filePointer, LOCK_UN); 101 | 102 | fclose($filePointer); 103 | 104 | if (! $locked) { 105 | throw new DomainException( 106 | sprintf($this->target->pid_file_missing_not_locked, $file) 107 | ); 108 | } 109 | } catch (\Exception $exception) { 110 | report($exception); 111 | } 112 | } 113 | 114 | /** 115 | * @return bool 116 | */ 117 | private function processPidFileIsLocked() 118 | { 119 | $file = $this->target->pidFile; 120 | 121 | $this->checkPidFileExistence($file); 122 | 123 | $this->checkPidFileLockState($file); 124 | 125 | return 1; 126 | } 127 | 128 | private function getCommand() 129 | { 130 | $command = $this->target->command; 131 | 132 | $name = $this->target->processName; 133 | 134 | if ($command && $name) { 135 | return sprintf($command, $name); 136 | } 137 | } 138 | 139 | private function getProcessesRunningCount() 140 | { 141 | if ($this->target->method == static::METHOD_PROCESS_COUNT) { 142 | return $this->countRunningProcesses(); 143 | } 144 | 145 | if ($this->target->method == static::METHOD_PID_FILE) { 146 | return $this->checkPidFile(); 147 | } 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/Checkers/HealthPanel.php: -------------------------------------------------------------------------------- 1 | getResourceUrlArray(); 40 | 41 | $first = collect($resources)->first(); 42 | 43 | if (filled($first)) { 44 | $this->target->setDisplay("{$first['url']}"); 45 | } 46 | 47 | try { 48 | foreach ($resources as $url) { 49 | [$healthy, $message] = $this->checkHealthPanel($url); 50 | 51 | if (! $healthy) { 52 | return $this->makeResult(false, $message); 53 | } 54 | } 55 | 56 | return $this->makeHealthyResult(); 57 | } catch (\Exception $exception) { 58 | report($exception); 59 | 60 | return $this->makeResultFromException($exception); 61 | } 62 | } 63 | 64 | /** 65 | * Get array of resource urls. 66 | * 67 | * @return array 68 | */ 69 | private function getResourceUrlArray() 70 | { 71 | $urls = $this->target->urls; 72 | 73 | if (! is_a($urls, Collection::class)) { 74 | $urls = collect($urls); 75 | } 76 | 77 | $result = collect(); 78 | 79 | $index = 0; 80 | 81 | foreach ($urls as $urlGroup) { 82 | foreach ($urlGroup as $url => $values) { 83 | if (blank($values['url'] ?? null)) { 84 | $values['url'] = $url; 85 | } 86 | 87 | $result[$index] = $values; 88 | 89 | $index++; 90 | } 91 | } 92 | 93 | return $result->toArray(); 94 | } 95 | 96 | /** 97 | * HTTP Checker. 98 | * 99 | * @return Result 100 | */ 101 | public function checkHealthPanel($url) 102 | { 103 | $resources = $this->getJson($url); 104 | 105 | if ($resources === null) { 106 | throw new \Exception('Error reading Health Panel json from '.$url['url']); 107 | } 108 | 109 | $messages = []; 110 | 111 | foreach ($resources as $resource) { 112 | foreach ($resource['targets'] as $target) { 113 | if (! $target['result']['healthy']) { 114 | $messages[] = $resource['name']; 115 | } 116 | } 117 | } 118 | 119 | if (count($messages) > 0) { 120 | throw new Exception('The following resources are failing: '.join(', ', $messages).'.'); 121 | } 122 | 123 | return [true, null]; 124 | } 125 | 126 | /** 127 | * Send an http request and fetch the panel json. 128 | * 129 | * @param $url 130 | * @return mixed|\Psr\Http\Message\ResponseInterface 131 | * 132 | * @throws \GuzzleHttp\Exception\GuzzleException 133 | */ 134 | private function fetchJson($url, $parameters = []) 135 | { 136 | $this->url = $url; 137 | 138 | return (new Guzzle())->request( 139 | $parameters['method'], 140 | $this->url, 141 | array_merge($this->getConnectionOptions($this->secure), $parameters) 142 | ); 143 | } 144 | 145 | public function getJson($parameters) 146 | { 147 | $url = $parameters['url']; 148 | 149 | unset($parameters['url']); 150 | 151 | return json_decode((string) $this->fetchJson($url, $parameters)->getBody(), true); 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/Support/helpers.php: -------------------------------------------------------------------------------- 1 | newInstanceArgs((array) $parameters); 19 | } 20 | 21 | return app($abstract); 22 | } 23 | } 24 | 25 | if (! function_exists('package_dir')) { 26 | /** 27 | * Get the package root directory. 28 | * 29 | * @return string 30 | */ 31 | function package_dir() 32 | { 33 | $reflector = new ReflectionClass(Service::class); 34 | 35 | return dirname($reflector->getFileName()); 36 | } 37 | } 38 | 39 | if (! function_exists('package_resources_dir')) { 40 | /** 41 | * Get package resources directory. 42 | * 43 | * @return string 44 | */ 45 | function package_resources_dir() 46 | { 47 | return 48 | package_dir(). 49 | DIRECTORY_SEPARATOR. 50 | 'config'. 51 | DIRECTORY_SEPARATOR. 52 | 'resources'; 53 | } 54 | } 55 | 56 | if (! function_exists('is_absolute_path')) { 57 | /** 58 | * Check if string is absolute path. 59 | * 60 | * @param $path 61 | * @return string 62 | */ 63 | function is_absolute_path($path) 64 | { 65 | // Optional wrapper(s). 66 | $regExp = 67 | // Optional root prefix. 68 | '%^(?(?:[[:print:]]{2,}://)*)'. 69 | '(?(?:[[:alpha:]]:/|/)?)'. 70 | // Actual path. 71 | '(?(?:[[:print:]]*))$%'; 72 | 73 | $parts = []; 74 | 75 | preg_match($regExp, $path, $parts); 76 | 77 | if ('' !== $parts['root']) { 78 | return true; 79 | } 80 | 81 | return false; 82 | } 83 | } 84 | 85 | if (! function_exists('bytes_to_human')) { 86 | /** 87 | * Convert bytes to human readable. 88 | * 89 | * @return string 90 | */ 91 | function bytes_to_human($bytes) 92 | { 93 | $base = log($bytes) / log(1024); 94 | 95 | $suffix = ['', 'KB', 'MB', 'GB', 'TB']; 96 | 97 | $f_base = floor($base); 98 | 99 | return round(pow(1024, $base - floor($base)), 1).$suffix[$f_base]; 100 | } 101 | } 102 | 103 | if (! function_exists('human_to_bytes')) { 104 | /** 105 | * Convert bytes to human readable. 106 | * 107 | * @return string 108 | */ 109 | function human_to_bytes($str) 110 | { 111 | $str = trim($str); 112 | 113 | $num = (float) $str; 114 | 115 | if (strtoupper(substr($str, -1)) == 'B') { 116 | $str = substr($str, 0, -1); 117 | } 118 | 119 | switch (strtoupper(substr($str, -1))) { 120 | case 'P': 121 | $num *= 1024; 122 | case 'T': 123 | $num *= 1024; 124 | case 'G': 125 | $num *= 1024; 126 | case 'M': 127 | $num *= 1024; 128 | case 'K': 129 | $num *= 1024; 130 | } 131 | 132 | return $num; 133 | } 134 | } 135 | 136 | if (! function_exists('ip_address_from_hostname')) { 137 | function ip_address_from_hostname($host) 138 | { 139 | if ( 140 | filter_var( 141 | $host, 142 | FILTER_VALIDATE_IP, 143 | FILTER_FLAG_NO_PRIV_RANGE | FILTER_FLAG_NO_RES_RANGE 144 | ) 145 | ) { 146 | return $host; 147 | } 148 | 149 | if (filter_var($host, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)) { 150 | return gethostbyname($host); 151 | } 152 | 153 | return false; 154 | } 155 | } 156 | -------------------------------------------------------------------------------- /src/Checkers/NotInDebugMode.php: -------------------------------------------------------------------------------- 1 | getCurrentUptime(); 20 | 21 | $this->persist($current); 22 | 23 | $rebooted = 24 | ($cs = $this->uptimeInSeconds($current)) < 25 | ($ss = $this->uptimeInSeconds($this->database)); 26 | 27 | return $this->makeResult( 28 | ! $rebooted, 29 | $this->makeMessage($current, $this->database) 30 | ); 31 | } 32 | 33 | /** 34 | * Execute command to get an uptime string. 35 | * 36 | * @return mixed|string 37 | * 38 | * @throws DomainException 39 | */ 40 | private function executeUptimeCommand() 41 | { 42 | $error = exec($this->resource->command, $system_string, $output); 43 | 44 | if ($output !== 0) { 45 | throw new DomainException((string) $error); 46 | } 47 | 48 | return (! is_array($system_string) || empty($system_string)) 49 | ? '' 50 | : $system_string[0]; 51 | } 52 | 53 | /** 54 | * Get current uptime. 55 | * 56 | * @return static 57 | * 58 | * @throws DomainException 59 | */ 60 | protected function getCurrentUptime() 61 | { 62 | return $this->parseUptimeString($this->executeUptimeCommand()); 63 | } 64 | 65 | /** 66 | * Normalize uptime matches. 67 | * 68 | * @param $matches 69 | * @return \Illuminate\Support\Collection 70 | */ 71 | protected function normalizeMatches($matches) 72 | { 73 | return collect($matches) 74 | ->filter(function ($item, $key) { 75 | return ! is_numeric($key); 76 | }) 77 | ->map(function ($item, $key) { 78 | $return = $item[0]; 79 | 80 | if (Str::startsWith($key, 'load')) { 81 | $return = floatval($return); 82 | } elseif (is_numeric($return)) { 83 | $return = (int) $return; 84 | } elseif (empty($return)) { 85 | $return = null; 86 | } 87 | 88 | return $return; 89 | }) 90 | ->toArray(); 91 | } 92 | 93 | /** 94 | * Parse the uptime string. 95 | * 96 | * @param $system_string 97 | * @return array 98 | */ 99 | protected function parseUptimeString($system_string) 100 | { 101 | $matches = []; 102 | 103 | preg_match( 104 | $this->resource['regex'], 105 | $system_string, 106 | $matches, 107 | PREG_OFFSET_CAPTURE 108 | ); 109 | 110 | $matches = $this->normalizeMatches($matches); 111 | 112 | $matches['uptime_string'] = $system_string; 113 | 114 | return $matches; 115 | } 116 | 117 | /** 118 | * Convert uptime to seconds. 119 | * 120 | * @param $date 121 | * @return int 122 | */ 123 | protected function uptimeInSeconds($date) 124 | { 125 | return 126 | (isset($date['up_days']) ? $date['up_days'] * 24 * 60 : 0) + 127 | (isset($date['up_hours']) ? $date['up_hours'] * 60 : 0) + 128 | (isset($date['up_minutes']) ? $date['up_minutes'] : 0); 129 | } 130 | 131 | /** 132 | * Make uptime message. 133 | * 134 | * @param $current 135 | * @param $saved 136 | * @return string 137 | */ 138 | protected function makeMessage($current, $saved = null) 139 | { 140 | $current = $this->toUptimeString($current); 141 | 142 | $saved = $this->toUptimeString($saved); 143 | 144 | return sprintf($this->target->getErrorMessage(), $current, $saved); 145 | } 146 | 147 | /** 148 | * Convert uptime to human readable string. 149 | * 150 | * @param $uptime 151 | * @return string 152 | */ 153 | public function toUptimeString($uptime) 154 | { 155 | return (string) CarbonInterval::days( 156 | isset($uptime['up_days']) ? $uptime['up_days'] : 0 157 | ) 158 | ->hours(isset($uptime['up_hours']) ? $uptime['up_hours'] : 0) 159 | ->minutes(isset($uptime['up_minutes']) ? $uptime['up_minutes'] : 0); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /src/Checkers/ServerUptime.php: -------------------------------------------------------------------------------- 1 | loadDatabase(); 23 | 24 | $current = $this->getCurrentUptime(); 25 | 26 | $this->persist($current); 27 | 28 | $rebooted = 29 | ($cs = $this->uptimeInSeconds($current)) < 30 | ($ss = $this->uptimeInSeconds($this->database)); 31 | 32 | return $this->makeResult( 33 | ! $rebooted, 34 | $this->makeMessage($current, $this->database) 35 | ); 36 | } 37 | 38 | /** 39 | * Execute command to get an uptime string. 40 | * 41 | * @return mixed|string 42 | * 43 | * @throws DomainException 44 | */ 45 | private function executeUptimeCommand() 46 | { 47 | $error = exec($this->target->command, $system_string, $output); 48 | 49 | if ($output !== 0) { 50 | throw new DomainException((string) $error); 51 | } 52 | 53 | return (! is_array($system_string) || empty($system_string)) 54 | ? '' 55 | : $system_string[0]; 56 | } 57 | 58 | /** 59 | * Get current uptime. 60 | * 61 | * @return array 62 | * 63 | * @throws DomainException 64 | */ 65 | protected function getCurrentUptime() 66 | { 67 | return $this->parseUptimeString($this->executeUptimeCommand()); 68 | } 69 | 70 | /** 71 | * Normalize uptime matches. 72 | * 73 | * @param $matches 74 | * @return \Illuminate\Support\Collection 75 | */ 76 | protected function normalizeMatches($matches) 77 | { 78 | return collect($matches) 79 | ->filter(function ($item, $key) { 80 | return ! is_numeric($key); 81 | }) 82 | ->map(function ($item, $key) { 83 | $return = $item[0]; 84 | 85 | if (Str::startsWith($key, 'load')) { 86 | $return = floatval($return); 87 | } elseif (is_numeric($return)) { 88 | $return = (int) $return; 89 | } elseif (empty($return)) { 90 | $return = null; 91 | } 92 | 93 | return $return; 94 | }) 95 | ->toArray(); 96 | } 97 | 98 | /** 99 | * Parse the uptime string. 100 | * 101 | * @param $system_string 102 | * @return array 103 | */ 104 | protected function parseUptimeString($system_string) 105 | { 106 | $matches = []; 107 | 108 | preg_match( 109 | $this->target->regex, 110 | $system_string, 111 | $matches, 112 | PREG_OFFSET_CAPTURE 113 | ); 114 | 115 | $matches = $this->normalizeMatches($matches); 116 | 117 | $matches['uptime_string'] = $system_string; 118 | 119 | return $matches; 120 | } 121 | 122 | /** 123 | * Convert uptime to seconds. 124 | * 125 | * @param $date 126 | * @return int 127 | */ 128 | protected function uptimeInSeconds($date) 129 | { 130 | return 131 | (isset($date['up_days']) ? $date['up_days'] * 24 * 60 : 0) + 132 | (isset($date['up_hours']) ? $date['up_hours'] * 60 : 0) + 133 | (isset($date['up_minutes']) ? $date['up_minutes'] : 0); 134 | } 135 | 136 | /** 137 | * Make uptime message. 138 | * 139 | * @param $current 140 | * @param $saved 141 | * @return string 142 | */ 143 | protected function makeMessage($current, $saved = null) 144 | { 145 | $current = $this->toUptimeString($current); 146 | 147 | $saved = $this->toUptimeString($saved); 148 | 149 | return sprintf($this->target->getErrorMessage(), $current, $saved); 150 | } 151 | 152 | /** 153 | * Convert uptime to human readable string. 154 | * 155 | * @param $uptime 156 | * @return string 157 | */ 158 | public function toUptimeString($uptime) 159 | { 160 | return (string) CarbonInterval::days( 161 | isset($uptime['up_days']) ? $uptime['up_days'] : 0 162 | ) 163 | ->hours(isset($uptime['up_hours']) ? $uptime['up_hours'] : 0) 164 | ->minutes(isset($uptime['up_minutes']) ? $uptime['up_minutes'] : 0); 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /src/Service.php: -------------------------------------------------------------------------------- 1 | resourceChecker = $resourceChecker; 30 | 31 | $this->cache = $cache; 32 | } 33 | 34 | /** 35 | * Check Resources. 36 | * 37 | * @param bool $force 38 | * @return \Illuminate\Support\Collection 39 | * 40 | * @throws \Exception 41 | */ 42 | public function checkResources($force = false) 43 | { 44 | return $this->resourceChecker->checkResources($force); 45 | } 46 | 47 | /** 48 | * Check one resource. 49 | * 50 | * @param $slug 51 | * @return array 52 | * 53 | * @throws \Exception 54 | */ 55 | public function checkResource($slug) 56 | { 57 | return $this->resourceChecker->checkResource($slug); 58 | } 59 | 60 | /** 61 | * Get services health. 62 | * 63 | * @return mixed 64 | * 65 | * @throws \Exception 66 | */ 67 | public function health() 68 | { 69 | return $this->checkResources(); 70 | } 71 | 72 | /** 73 | * Get resources. 74 | * 75 | * @return mixed 76 | * 77 | * @throws \Exception 78 | */ 79 | public function getResources() 80 | { 81 | return $this->resourceChecker->getResources(); 82 | } 83 | 84 | /** 85 | * Get a silent checker and notifier closure. 86 | * 87 | * @return \Closure 88 | */ 89 | public function getSilentChecker() 90 | { 91 | return function () { 92 | return $this->checkResources(); 93 | }; 94 | } 95 | 96 | /** 97 | * Make a string result of all resources. 98 | * 99 | * @param $string 100 | * @param string Result::status $resultStatus 101 | * @return string 102 | */ 103 | private function makeString($string, $resultStatus) 104 | { 105 | // To preserve current ok/fail behaviour, it will override the result 106 | // status string with 'fail', when the status is critical, and if the 107 | // fail string was set. 108 | if ( 109 | $resultStatus === Result::CRITICAL 110 | && is_null(config('health.string.'.strtolower($resultStatus))) 111 | && null !== config('health.string.fail') 112 | ) { 113 | $resultStatus = 'fail'; 114 | } 115 | $resultStatusOutput = config('health.string.'.strtolower($resultStatus)); 116 | 117 | // If not defined, it should use the default string for the status. 118 | return $string.($resultStatusOutput ?? $resultStatus); 119 | } 120 | 121 | /** 122 | * Check and get a resource. 123 | * 124 | * @param $name 125 | * @return mixed 126 | * 127 | * @throws \Exception 128 | */ 129 | public function resource($slug) 130 | { 131 | return $this->checkResource($slug); 132 | } 133 | 134 | /** 135 | * Set the action. 136 | * 137 | * @param $action 138 | */ 139 | public function setAction($action) 140 | { 141 | $this->resourceChecker->setCurrentAction($action); 142 | } 143 | 144 | /** 145 | * @return mixed 146 | * 147 | * @throws \Exception 148 | */ 149 | public function string(?string $filters = '') 150 | { 151 | // If filters are required, return "" for results that should not be included. 152 | if (! empty($filters)) { 153 | $filters = explode(',', strtolower($filters)); 154 | } 155 | 156 | return collect($this->health())->reduce(function ($current, $resource) use ($filters) { 157 | $resourceStatus = $resource->getStatus(); 158 | 159 | if (! empty($filters) && ! in_array(strtolower($resourceStatus), $filters)) { 160 | return $current; 161 | } 162 | 163 | return 164 | $current. 165 | ($current ? config('health.string.glue') : ''). 166 | $this->makeString( 167 | $resource->abbreviation, 168 | $resourceStatus 169 | ); 170 | }); 171 | } 172 | 173 | /** 174 | * Set the caller. 175 | * 176 | * @param string $caller 177 | */ 178 | public function setCaller($caller) 179 | { 180 | $this->resourceChecker->setCurrentCaller($caller); 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /src/Checkers/Certificate.php: -------------------------------------------------------------------------------- 1 | getResourceUrlArray(); 20 | 21 | $first = collect($resources)->first(); 22 | 23 | if (filled($first)) { 24 | $this->target->setDisplay("{$first}"); 25 | } 26 | 27 | try { 28 | foreach ($resources as $url) { 29 | [$healthy, $message] = $this->checkCertificate($url); 30 | 31 | if (! $healthy) { 32 | return $this->makeResult(false, $message); 33 | } 34 | } 35 | 36 | return $this->makeHealthyResult(); 37 | } catch (\Exception $exception) { 38 | report($exception); 39 | 40 | return $this->makeResultFromException($exception); 41 | } 42 | } 43 | 44 | /** 45 | * HTTP Checker. 46 | * 47 | * @return Result 48 | */ 49 | public function checkCertificate($url) 50 | { 51 | return $this->checkHostCertificate($this->getHost($url)); 52 | } 53 | 54 | /** 55 | * Get the error message. 56 | * 57 | * @return string 58 | */ 59 | protected function getErrorMessage($host) 60 | { 61 | return sprintf($this->target->resource->errorMessage, $host); 62 | } 63 | 64 | /** 65 | * Get the error message. 66 | * 67 | * @return string 68 | */ 69 | protected function getHost($url) 70 | { 71 | $parsed = parse_url($url); 72 | 73 | if (isset($parsed['host'])) { 74 | return $parsed['host']; 75 | } 76 | 77 | return $url; 78 | } 79 | 80 | /** 81 | * Get array of resource urls. 82 | * 83 | * @return array 84 | */ 85 | private function getResourceUrlArray() 86 | { 87 | if (is_a($this->target->urls, Collection::class)) { 88 | return $this->target->urls->toArray(); 89 | } 90 | 91 | return (array) $this->target->urls; 92 | } 93 | 94 | public function checkHostCertificate($host) 95 | { 96 | $result = collect([ 97 | 'openssl' => $this->checkCertificateWithOpenSSL($host), 98 | 99 | 'package' => [ 100 | SslCertificate::createForHostName($host)->isValid(), 101 | 'Invalid certificate', 102 | ], 103 | 104 | 'php' => $this->checkCertificateWithPhp($host), 105 | ]) 106 | ->filter(function ($result) { 107 | return $result[0] === false; 108 | }) 109 | ->first(); 110 | 111 | if ($result === null) { 112 | return [true, '']; 113 | } 114 | 115 | return $result; 116 | } 117 | 118 | public function checkCertificateWithOpenSSL($host) 119 | { 120 | exec($this->makeCommand($host), $output); 121 | 122 | $result = collect($output) 123 | ->filter( 124 | function ($line) { 125 | return Str::contains( 126 | $line, 127 | $this->target->resource->verifyString 128 | ); 129 | } 130 | ) 131 | ->first(); 132 | 133 | if (blank($result)) { 134 | $output = blank($output) ? 'Unkown openssl error' : $output; 135 | 136 | return [false, json_encode($output)]; 137 | } 138 | 139 | return [ 140 | trim($result) == $this->target->resource->successString, 141 | $result, 142 | ]; 143 | } 144 | 145 | public function checkCertificateWithPhp($host) 146 | { 147 | try { 148 | $get = stream_context_create([ 149 | 'ssl' => ['capture_peer_cert' => true], 150 | ]); 151 | 152 | $read = stream_socket_client( 153 | 'ssl://'.$host.':443', 154 | $errno, 155 | $errstr, 156 | 30, 157 | STREAM_CLIENT_CONNECT, 158 | $get 159 | ); 160 | } catch (\Exception $exception) { 161 | return [false, $exception->getMessage()]; 162 | } 163 | 164 | return [true, '']; 165 | } 166 | 167 | /** 168 | * @param $host 169 | * @return string|string[] 170 | */ 171 | protected function makeCommand($host) 172 | { 173 | $command = $this->target->resource->command; 174 | 175 | $command = str_replace('{$options}', $this->target->options, $command); 176 | 177 | $command = str_replace('{$host}', $host, $command); 178 | 179 | return $command; 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /src/Checkers/Ping.php: -------------------------------------------------------------------------------- 1 | pingBinFactory(); 23 | 24 | $ipAddress = ip_address_from_hostname($this->target->hostname); 25 | 26 | $latency = $this->ping($ipAddress); 27 | 28 | $latencyFormatted = $latency ? "{$latency}ms" : 'error!'; 29 | 30 | $this->target->setDisplay( 31 | "{$this->target->name} ({$latencyFormatted})" 32 | ); 33 | 34 | if ($latency === false || $latency > $this->target->acceptedLatency) { 35 | $result = $this->makeResult( 36 | false, 37 | sprintf( 38 | $this->target->getErrorMessage(), 39 | $this->hosnameAndIp($this->target->hostname, $ipAddress), 40 | $latency === false ? '[ping error]' : $latency, 41 | $this->target->acceptedLatency 42 | ) 43 | ); 44 | } else { 45 | $result = $this->makeHealthyResult(); 46 | } 47 | 48 | return $result->setValue($latency)->setValueHuman("{$latency}ms"); 49 | } 50 | 51 | public function ping($hostname, $timeout = 5, $ttl = 128) 52 | { 53 | $this->host = $hostname; 54 | $this->ttl = $ttl; 55 | $this->timeout = $timeout; 56 | 57 | return $this->pingExec(); 58 | } 59 | 60 | /** 61 | * @param $hostname 62 | * @return mixed 63 | */ 64 | protected function hosnameAndIp($hostname, $ipAdress) 65 | { 66 | return $hostname.($hostname != $ipAdress ? " ({$ipAdress})" : ''); 67 | } 68 | 69 | /** 70 | * The exec method uses the possibly insecure exec() function, which passes 71 | * the input to the system. This is potentially VERY dangerous if you pass in 72 | * any user-submitted data. Be SURE you sanitize your inputs! 73 | * 74 | * @return int 75 | * Latency, in ms. 76 | */ 77 | private function pingExec() 78 | { 79 | $latency = false; 80 | 81 | $ttl = escapeshellcmd($this->ttl); 82 | $timeout = escapeshellcmd($this->timeout); 83 | $host = escapeshellcmd($this->host); 84 | 85 | // Exec string for Windows-based systems. 86 | if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') { 87 | // -n = number of pings; -i = ttl; -w = timeout (in milliseconds). 88 | $exec_string = 89 | $this->pingBin. 90 | ' -n 1 -i '. 91 | $ttl. 92 | ' -w '. 93 | $timeout * 1000 . 94 | ' '. 95 | $host; 96 | } elseif (strtoupper(PHP_OS) === 'DARWIN') { 97 | // Exec string for Darwin based systems (OS X). 98 | // -n = numeric output; -c = number of pings; -m = ttl; -t = timeout. 99 | $exec_string = 100 | $this->pingBin. 101 | ' -n -c 1 -m '. 102 | $ttl. 103 | ' -t '. 104 | $timeout. 105 | ' '. 106 | $host; 107 | } else { 108 | // Exec string for other UNIX-based systems (Linux). 109 | // -n = numeric output; -c = number of pings; -t = ttl; -W = timeout 110 | $exec_string = 111 | $this->pingBin. 112 | ' -n -c 1 -t '. 113 | $ttl. 114 | ' -W '. 115 | $timeout. 116 | ' '. 117 | $host. 118 | ' 2>&1'; 119 | } 120 | 121 | exec($exec_string, $output, $return); 122 | 123 | // Strip empty lines and reorder the indexes from 0 (to make results more 124 | // uniform across OS versions). 125 | $this->commandOutput = implode($output); 126 | $output = array_values(array_filter($output)); 127 | 128 | // If the result line in the output is not empty, parse it. 129 | if (! empty($output[1])) { 130 | // Search for a 'time' value in the result line. 131 | $response = preg_match( 132 | "/time(?:=|<)(?