├── .github └── workflows │ └── test.yml ├── .gitignore ├── LICENSE.txt ├── README.md ├── VERSION ├── composer.json ├── init.php ├── lib ├── Client.php ├── ConfigurationException.php ├── HttpClient.php ├── Monitor.php ├── Monitors.php └── ValidationException.php ├── phpunit.xml └── tests ├── ClientTest.php ├── MonitorTest.php ├── MonitorsTest.php ├── TestBase.php ├── bootstrap.php └── data └── config.yml /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: [master] 6 | pull_request: 7 | branches: [master] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Install PHP 17 | uses: shivammathur/setup-php@v2 18 | with: 19 | php-version: 7.2 20 | 21 | - name: Validate composer.json and composer.lock 22 | run: composer validate --strict 23 | 24 | - name: Cache Composer packages 25 | id: composer-cache 26 | uses: actions/cache@v2 27 | with: 28 | path: vendor 29 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.lock') }} 30 | restore-keys: | 31 | ${{ runner.os }}-php- 32 | 33 | - name: Install dependencies 34 | run: composer install --prefer-dist --no-progress --no-suggest 35 | 36 | - name: Run test suite 37 | run: composer run-script test 38 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | .DS_Store 3 | /coverage 4 | *.code-workspace 5 | .phpunit.result.cache 6 | cache 7 | composer.lock -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Cronitor.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cronitor PHP Library 2 | 3 | ![Test](https://github.com/cronitorio/cronitor-php/workflows/Test/badge.svg) 4 | 5 | [Cronitor](https://cronitor.io/) provides end-to-end monitoring for background jobs, websites, APIs, and anything else that can send or receive an HTTP request. This library provides convenient access to the Cronitor API from applications written in PHP. See our [API docs](https://cronitor.io/docs/api) for detailed references on configuring monitors and sending telemetry pings. 6 | 7 | 8 | In this guide: 9 | 10 | - [Installation](#Installation) 11 | - [Monitoring Background Jobs](#monitoring-background-jobs) 12 | - [Sending Telemetry Events](#sending-telemetry-events) 13 | - [Configuring Monitors](#configuring-monitors) 14 | - [Package Configuration & Env Vars](#package-configuration) 15 | 16 | ## Installation 17 | 18 | ```bash 19 | composer require cronitor/cronitor-php 20 | ``` 21 | 22 | To use manually, you can include the `init.php` file from source. 23 | 24 | ```php 25 | require_once('/path/to/cronitor-php/init.php'); 26 | ``` 27 | 28 | ### Monitoring Background Jobs 29 | 30 | The `$cronitor->job` function will send telemetry events before calling your function and after it exits. If your function raises an exception a `fail` event will be sent (and the exception re-raised). 31 | 32 | 33 | ```php 34 | $cronitor = new Cronitor\Client('api_key_123'); 35 | 36 | $closureVar = time(); 37 | $cronitor->job('weekly-report-job', function() use ($closureVar){ 38 | new WeeklyReportJob($closureVar)->run(); 39 | }); 40 | ``` 41 | 42 | ## Sending Telemetry Events 43 | 44 | If you want to send a heartbeat events, or want finer control over when/how [telemetry events](https://cronitor.io/docs/telemetry-api) are sent for your jobs, you can create a `Monitor` instance and call the `.ping` method. 45 | 46 | 47 | ```php 48 | $cronitor = new Cronitor\Client('api_key_123'); 49 | 50 | $monitor = $cronitor->monitor('heartbeat-monitor'); 51 | 52 | $monitor->ping(); # a basic heartbeat event 53 | 54 | # optional params can be passed as kwargs 55 | # complete list - https://cronitor.io/docs/telemetry-api#parameters 56 | 57 | $monitor->ping(['state' => 'run']); # a job/process has started 58 | 59 | # a job/process has completed (include metrics for Cronitor to record) 60 | $monitor->ping(['state' => 'complete', 'metrics' => ['count' => 1000, 'error_count' => 17]]); 61 | ``` 62 | 63 | ## Configuring Monitors 64 | 65 | You can configure all of your monitors using a single YAML file. This can be version controlled and synced to Cronitor as part of 66 | a deployment or build process. For details on all of the attributes that can be set, see the [Monitor API](https://cronitor.io/docs/monitor-api) documentation. 67 | 68 | ```php 69 | # read config file and set credentials (if included). 70 | $cronitor->readConfig('./cronitor.yaml'); 71 | 72 | # sync config file's monitors to Cronitor. 73 | $cronitor->applyConfig(); 74 | 75 | # send config file's monitors to Cronitor to validate correctness. 76 | # monitors will not be saved. 77 | $cronitor->validateConfig(); 78 | 79 | # save config to local YAML file (defaults to cronitor.yaml) 80 | $cronitor->generateConfig(); 81 | ``` 82 | 83 | The `cronitor.yaml` file includes three top level keys `jobs`, `checks`, `heartbeats`. You can configure monitors under each key by declaring a monitor `key` and defining [Monitor attributes](https://cronitor.io/docs/monitor-api#attributes) 84 | 85 | ```yaml 86 | jobs: 87 | nightly-database-backup: 88 | schedule: 0 0 * * * 89 | notify: 90 | - devops-alert-pagerduty 91 | assertions: 92 | - metric.duration < 5 minutes 93 | 94 | send-welcome-email: 95 | schedule: every 10 minutes 96 | assertions: 97 | - metric.count > 0 98 | - metric.duration < 30 seconds 99 | 100 | checks: 101 | cronitor-homepage: 102 | request: 103 | url: https://cronitor.io 104 | regions: 105 | - us-east-1 106 | - eu-central-1 107 | - ap-northeast-1 108 | assertions: 109 | - response.code = 200 110 | - response.time < 2s 111 | 112 | cronitor-telemetry-api: 113 | request: 114 | url: https://cronitor.link/ping 115 | assertions: 116 | - response.body contains ok 117 | - response.time < .25s 118 | 119 | heartbeats: 120 | production-deploy: 121 | notify: 122 | alerts: ["deploys-slack"] 123 | events: true # send alert when the event occurs 124 | ``` 125 | 126 | You can also create and update monitors by calling `$cronitor->monitors->put`. For details on all of the attributes that can be set see the Monitor API [documentation)(https://cronitor.io/docs/monitor-api#attributes). 127 | 128 | ```php 129 | $cronitor->monitors->put([ 130 | [ 131 | 'type' => 'job', 132 | 'key' => 'send-customer-invoices', 133 | 'schedule' => '0 0 * * *', 134 | 'assertions' => [ 135 | 'metric.duration < 5 min' 136 | ], 137 | 'notify' => ['devops-alerts-slack'] 138 | ], 139 | [ 140 | 'type' => 'check', 141 | 'key' => 'Cronitor Homepage', 142 | 'schedule' => 'every 45 seconds', 143 | 'request' => [ 144 | 'url' => 'https://cronitor.io' 145 | ] 146 | 'assertions' => [ 147 | 'response.code = 200', 148 | 'response.time < 1.5s', 149 | 'response.json "open_orders" < 2000' 150 | ] 151 | ] 152 | ]) 153 | ``` 154 | 155 | ### Pause, Reset, Delete 156 | 157 | ```php 158 | require 'cronitor' 159 | 160 | $monitor = $cronitor->monitor('heartbeat-monitor'); 161 | 162 | $monitor->pause(24) # pause alerting for 24 hours 163 | $monitor->unpause() # alias for ->pause(0) 164 | $monitor->ok() # manually reset to a passing state alias for $monitor->ping({state: ok}) 165 | $monitor->delete() # destroy the monitor 166 | ``` 167 | 168 | ## Package Configuration 169 | 170 | The package needs to be configured with your account's `API key`, which is available on the [account settings](https://cronitor.io/settings) page. You can also optionally specify an `api_version` and an `environment`. If not provided, your account default is used. These can also be supplied using the environment variables `CRONITOR_API_KEY`, `CRONITOR_API_VERSION`, `CRONITOR_ENVIRONMENT`. 171 | 172 | ```php 173 | $apiKey = 'apiKey123'; 174 | $apiVersion = '2020-10-01'; 175 | $environment = 'staging'; 176 | $cronitor = new Cronitor\Client($apiKey, $apiVersion, $environment); 177 | ``` 178 | 179 | ## Contributing 180 | 181 | Pull requests and features are happily considered! By participating in this project you agree to abide by the [Code of Conduct](http://contributor-covenant.org/version/2/0). 182 | 183 | ### To contribute 184 | 185 | Fork, then clone the repo: 186 | 187 | git clone git@github.com:your-username/cronitor-php.git 188 | 189 | Push to your fork and [submit a pull request](https://github.com/cronitorio/cronitor-php/compare/) 190 | -------------------------------------------------------------------------------- /VERSION: -------------------------------------------------------------------------------- 1 | 1.1.0 2 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cronitor/cronitor-php", 3 | "description": "Cronitor PHP Library", 4 | "keywords": [ 5 | "cronitor", 6 | "api" 7 | ], 8 | "homepage": "https://cronitor.io/", 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Cronitor" 13 | } 14 | ], 15 | "require": { 16 | "php": ">=5.6", 17 | "symfony/yaml": "^4.4|^5.2|^6.0|^7.0" 18 | }, 19 | "require-dev": { 20 | "phpunit/phpunit": "^6.0", 21 | "friendsofphp/php-cs-fixer": "^2.18", 22 | "ext-curl": "*", 23 | "codeception/aspect-mock": "^3.1" 24 | }, 25 | "autoload": { 26 | "psr-4": { 27 | "Cronitor\\": "lib/" 28 | } 29 | }, 30 | "autoload-dev": { 31 | "psr-4": { 32 | "Cronitor\\Tests\\": "tests/" 33 | } 34 | }, 35 | "scripts": { 36 | "test": "vendor/bin/phpunit ./tests" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /init.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey ?: getenv('CRONITOR_API_KEY'); 21 | $this->apiVersion = $apiVersion ?: getenv('CRONITOR_API_VERSION'); 22 | $this->environment = $environment ?: getenv('CRONITOR_ENVIRONMENT') ?: null; 23 | $this->config = getenv('CRONITOR_CONFIG') ?: null; 24 | if ($this->config !== null) { 25 | $this->readConfig(); 26 | } 27 | 28 | $this->monitors = new Monitors($this->apiKey, $this->apiVersion); 29 | } 30 | 31 | public function monitor($key) 32 | { 33 | return new Monitor($key, $this->apiKey, $this->apiVersion, $this->environment); 34 | } 35 | 36 | public function readConfig($path = null, $output = false) 37 | { 38 | $this->config = $path ?: $this->config; 39 | if (!$this->config) { 40 | throw new ConfigurationException("Must include a path by passing a path to readConfig e.g. \$cronitor->readConfig('./cronitor.yaml')"); 41 | } 42 | 43 | $conf = \Symfony\Component\Yaml\Yaml::parseFile($this->config); 44 | 45 | $configKeys = self::getConfigKeys(); 46 | foreach ($conf as $k => $v) { 47 | if (!in_array($k, $configKeys)) { 48 | throw new ConfigurationException("Invalid configuration variable: $k"); 49 | } 50 | } 51 | 52 | $this->apiKey = isset($conf['apiKey']) ? $conf['apiKey'] : $this->apiKey; 53 | $this->apiVersion = isset($conf['apiVersion']) ? $conf['apiVersion'] : $this->apiVersion; 54 | $this->environment = isset($conf['environment']) ? $conf['environment'] : $this->environment; 55 | 56 | if (!$output) { 57 | return; 58 | } 59 | 60 | $monitors = []; 61 | foreach (self::MONITOR_TYPES as $t) { 62 | $pluralType = $this->pluralizeType($t); 63 | $toParse = isset($conf[$t]) ? $conf[$t] : (isset($conf[$pluralType]) ? $conf[$pluralType] : null); 64 | if (!$toParse) { 65 | continue; 66 | } 67 | 68 | if (array_keys($toParse) === range(0, count($toParse) - 1)) { 69 | throw new ConfigurationException('An associative array with keys corresponding to monitor keys is expected.'); 70 | } 71 | 72 | foreach ($toParse as $key => $m) { 73 | $m['key'] = $key; 74 | $m['type'] = $t; 75 | array_push($monitors, $m); 76 | } 77 | } 78 | 79 | $conf['monitors'] = $monitors; 80 | return $conf; 81 | } 82 | 83 | public function applyConfig($rollback = false) 84 | { 85 | try { 86 | $conf = $this->readConfig(null, true); 87 | $params = [ 88 | 'monitors' => isset($conf['monitors']) ? $conf['monitors'] : [], 89 | 'rollback' => $rollback, 90 | ]; 91 | $monitors = $this->monitors->put($params); 92 | echo count($monitors) . " monitors " . ($rollback ? 'validated' : 'synced to Cronitor'); 93 | return true; 94 | } catch (ValidationException $e) { 95 | \error_log($e, 0); 96 | } 97 | } 98 | 99 | public function validateConfig() 100 | { 101 | return $this->applyConfig(true); 102 | } 103 | 104 | public function generateConfig() 105 | { 106 | $configPath = $this->config ?: self::DEFAULT_CONFIG_PATH; 107 | $file = fopen($configPath, 'w'); 108 | try { 109 | $config = Monitor::getYaml($this->apiKey, $this->apiVersion); 110 | fwrite($file, $config); 111 | } finally { 112 | fclose($file); 113 | } 114 | return true; 115 | } 116 | 117 | /** 118 | * @throws Exception 119 | */ 120 | public function job($key, $callback) 121 | { 122 | $monitor = $this->monitor($key); 123 | $series = microtime(true); 124 | $monitor->ping(['state' => 'run', 'series' => $series]); 125 | 126 | try { 127 | $callbackValue = $callback(); 128 | $monitor->ping(['state' => 'complete', 'series' => $series]); 129 | return $callbackValue; 130 | } catch (Exception $e) { 131 | $message = $e->getMessage(); 132 | $truncatedMessage = substr($message, abs(min(0, 1600 - strlen($message)))); 133 | $monitor->ping([ 134 | 'state' => 'fail', 135 | 'message' => $truncatedMessage, 136 | 'series' => $series, 137 | ]); 138 | throw $e; 139 | } 140 | } 141 | 142 | private static function pluralizeType($type) 143 | { 144 | return $type . 's'; 145 | } 146 | 147 | private static function getConfigKeys() 148 | { 149 | return array_merge( 150 | self::BASE_CONFIG_KEYS, 151 | array_map('self::pluralizeType', self::MONITOR_TYPES) 152 | ); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /lib/ConfigurationException.php: -------------------------------------------------------------------------------- 1 | baseUrl = $baseUrl; 14 | $this->apiKey = $apiKey; 15 | $this->apiVersion = $apiVersion; 16 | } 17 | 18 | public function get($path, $params = []) 19 | { 20 | return $this->request($path, 'GET', $params); 21 | } 22 | 23 | public function delete($path, $params = []) 24 | { 25 | return $this->request($path, 'DELETE', $params); 26 | } 27 | 28 | public function put($path, $body = [], $params = []) 29 | { 30 | return $this->request($path, 'PUT', $params, $body); 31 | } 32 | 33 | private function request($path, $httpMethod, $params = [], $body = null) 34 | { 35 | $headers = $this->buildHeaders(isset($params['headers']) ? $params['headers'] : []); 36 | $options = array( 37 | CURLOPT_HTTPHEADER => $headers, 38 | CURLOPT_CUSTOMREQUEST => $httpMethod, 39 | CURLOPT_USERPWD => $this->apiKey . ":", 40 | CURLOPT_TIMEOUT => isset($params['timeout']) ? $params['timeout'] : 5, 41 | CURLOPT_RETURNTRANSFER => true, 42 | CURLOPT_FOLLOWLOCATION => true, 43 | CURLOPT_HEADER => 0, 44 | ); 45 | 46 | $url = $this->baseUrl . $path; 47 | $ch = curl_init($url); 48 | curl_setopt_array($ch, $options); 49 | 50 | if (!is_null($body)) { 51 | curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_SLASHES)); 52 | } 53 | 54 | $content = curl_exec($ch); 55 | $code = curl_getinfo($ch, CURLINFO_HTTP_CODE); 56 | 57 | if ($content === false) { 58 | $error = curl_error($ch); 59 | } 60 | 61 | curl_close($ch); 62 | 63 | return [ 64 | 'code' => $code, 65 | 'content' => $content, 66 | 'error' => isset($error) ? $error : null, 67 | ]; 68 | } 69 | 70 | private function buildHeaders($headers = []) 71 | { 72 | $defaultHeaders = [ 73 | 'Content-Type' => 'application/json', 74 | 'Accept' => 'application/json', 75 | "User-Agent" => 'cronitor-php', 76 | "Cronitor-Version" => $this->apiVersion, 77 | ]; 78 | $mergedHeaders = array_merge($defaultHeaders, $headers); 79 | 80 | return array_map(function ($key) use ($mergedHeaders) { 81 | $value = $mergedHeaders[$key]; 82 | return "$key: $value"; 83 | }, array_keys($mergedHeaders)); 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /lib/Monitor.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 22 | $this->apiVersion = $apiVersion; 23 | $this->key = $key; 24 | $this->env = $env; 25 | 26 | $monitorApiUrl = self::BASE_MONITOR_API_URL . "/$key"; 27 | $this->monitorClient = new HttpClient($monitorApiUrl, $this->apiKey, $this->apiVersion); 28 | } 29 | 30 | public static function put($apiKey, $apiVersion, $params = []) 31 | { 32 | $rollback = isset($params['rollback']) ? $params['rollback'] : false; 33 | unset($params['rollback']); 34 | $monitors = isset($params['monitors']) ? $params['monitors'] : [$params]; 35 | 36 | $client = self::getMonitorHttpClient($apiKey, $apiVersion); 37 | $response = $client->put('', [ 38 | 'monitors' => $monitors, 39 | 'rollback' => $rollback, 40 | ], ['timeout' => 10]); 41 | 42 | $code = $response['code']; 43 | switch ($code) { 44 | case 200: 45 | $out = []; 46 | $data = json_decode($response['content'], true); 47 | 48 | $dataMonitors = isset($data['monitors']) ? $data['monitors'] : []; 49 | foreach ($dataMonitors as &$md) { 50 | $m = new Monitor($md['key']); 51 | $m->data = $md; 52 | array_push($out, $m); 53 | } 54 | return count($out) == 1 ? $out[0] : $out; 55 | break; 56 | case 400: 57 | throw new ValidationException($response['content']); 58 | default: 59 | throw new \Exception("Error connecting to Cronitor: $code"); 60 | } 61 | } 62 | 63 | public static function getYaml($apiKey, $apiVersion) 64 | { 65 | $client = self::getMonitorHttpClient($apiKey, $apiVersion); 66 | $params = ['timeout' => 25, 'headers' => ['Accept' => 'application/yaml']]; 67 | $response = $client->get('.yaml', $params); 68 | $content = $response['content']; 69 | if ($response['code'] == 200) { 70 | return $content; 71 | } 72 | 73 | throw new \Exception("Unexpected error: $content"); 74 | } 75 | 76 | public static function delete($apiKey, $apiVersion, $key) 77 | { 78 | $client = self::getMonitorHttpClient($apiKey, $apiVersion); 79 | $response = $client->delete("/$key", ['timeout' => 10]); 80 | 81 | if ($response['code'] != 204) { 82 | \error_log("Error deleting monitor: $key", 0); 83 | return false; 84 | } 85 | return $response; 86 | } 87 | 88 | public function ping($params = array()) 89 | { 90 | $retryCount = isset($params['retryCount']) ? $params['retryCount'] : 0; 91 | 92 | if (!$this->apiKey) { 93 | \error_log('No API key detected. Set Cronitor.api_key or initialize Monitor with an api_key:', 0); 94 | return false; 95 | } 96 | 97 | try { 98 | $queryString = $this->buildPingQuery($params); 99 | $client = $this->getPingClient($retryCount); 100 | $response = $client->get("?$queryString"); 101 | $responseCode = $response['code']; 102 | 103 | if ($responseCode !== 200) { 104 | \error_log("Cronitor Telemetry Error: $responseCode", 0); 105 | return false; 106 | } 107 | 108 | return true; 109 | } catch (\Exception $e) { 110 | // rescue instances of StandardError i.e. Timeout::Error, SocketError, etc 111 | \error_log("Cronitor Telemetry Error: $e", 0); 112 | if ($retryCount >= self::PING_RETRY_THRESHOLD) { 113 | return false; 114 | } 115 | 116 | // apply a backoff before sending the next ping 117 | sleep($this->calculateSleep($retryCount)); 118 | $this->ping(array_merge($params, ['retryCount' => $retryCount + 1])); 119 | } 120 | } 121 | 122 | public function pause($hours = null) 123 | { 124 | $path = '/pause'; 125 | if (isset($hours)) { 126 | $path .= "/$hours"; 127 | } 128 | 129 | $response = $this->monitorClient->get($path, ['timeout' => 5]); 130 | return $response['code'] >= 200 && $response['code'] <= 299; 131 | } 132 | 133 | public function unpause() 134 | { 135 | return $this->pause(0); 136 | } 137 | 138 | public function getData() 139 | { 140 | if (isset($this->data)) { 141 | return $this->data; 142 | } 143 | 144 | if (!$this->apiKey) { 145 | \error_log('No API key detected. Initialize CronitorClient with a valid API key.', 0); 146 | return null; 147 | } 148 | 149 | $response = $this->monitorClient->get('', ['timeout' => 10]); 150 | $this->data = json_decode($response['content']); 151 | return $this->data; 152 | } 153 | 154 | public function setData($data) 155 | { 156 | $this->data = $data; 157 | return true; 158 | } 159 | 160 | public function ok() 161 | { 162 | return $this->ping(['state' => 'ok']); 163 | } 164 | 165 | private static function getMonitorHttpClient($apiKey, $apiVersion) 166 | { 167 | return new HttpClient(self::BASE_MONITOR_API_URL, $apiKey, $apiVersion); 168 | } 169 | 170 | private function cleanParams($params) 171 | { 172 | $cleanedParams = [ 173 | 'state' => isset($params['state']) ? $params['state'] : null, 174 | 'message' => isset($params['message']) ? $params['message'] : null, 175 | 'series' => isset($params['series']) ? $params['series'] : null, 176 | 'host' => isset($params['host']) ? $params['host'] : gethostname(), 177 | 'metric' => isset($params['metrics']) ? $this->cleanMetrics($params['metrics']) : null, 178 | 'stamp' => microtime(true), 179 | 'env' => isset($params['env']) ? $params['env'] : $this->env, 180 | ]; 181 | 182 | $filteredParams = array_filter($cleanedParams, function ($v) { 183 | return !is_null($v); 184 | }); 185 | 186 | return $filteredParams; 187 | } 188 | 189 | private function cleanMetrics($metrics) 190 | { 191 | return array_map(function ($key) use ($metrics) { 192 | $value = $metrics[$key]; 193 | return "$key:$value"; 194 | }, array_keys($metrics)); 195 | } 196 | 197 | private function getPingClient($retryCount) 198 | { 199 | $url = $retryCount > (self::PING_RETRY_THRESHOLD / 2) ? $this->getFallbackPingApiUrl() : $this->getPingApiUrl(); 200 | return new HttpClient($url, $this->apiKey, $this->apiVersion); 201 | } 202 | private function getPingApiUrl() 203 | { 204 | return self::BASE_PING_API_URL . "/$this->apiKey/$this->key"; 205 | } 206 | 207 | private function getFallbackPingApiUrl() 208 | { 209 | return self::BASE_FALLBACK_PING_API_URL . "/$this->apiKey/$this->key"; 210 | } 211 | 212 | private function buildPingQuery($params) 213 | { 214 | $cleanParams = $this->cleanParams($params); 215 | $metrics = isset($cleanParams['metric']) ? $cleanParams['metric'] : null; 216 | unset($cleanParams['metric']); 217 | 218 | $queryParams = array_map(function ($key) use ($cleanParams) { 219 | $value = $cleanParams[$key]; 220 | return "$key=$value"; 221 | }, array_keys($cleanParams)); 222 | 223 | // format query string array params to non-array format, e.g. metric=foo:1 224 | if ($metrics) { 225 | $metricParams = array_map(function ($v) { 226 | return "metric=$v"; 227 | }, $metrics); 228 | 229 | array_push($queryParams, ...$metricParams); 230 | } 231 | 232 | $query = join('&', $queryParams); 233 | 234 | return $query; 235 | } 236 | 237 | private function calculateSleep($retryCount) 238 | { 239 | $randomFactor = mt_rand(0, 10) / 10; 240 | return $retryCount * 1.5 * $randomFactor; 241 | } 242 | } 243 | -------------------------------------------------------------------------------- /lib/Monitors.php: -------------------------------------------------------------------------------- 1 | apiKey = $apiKey; 13 | $this->apiVersion = $apiVersion; 14 | } 15 | 16 | public function put($params) 17 | { 18 | return Monitor::put($this->apiKey, $this->apiVersion, $params); 19 | } 20 | 21 | public function delete($key) 22 | { 23 | return Monitor::delete($this->apiKey, $this->apiVersion, $key); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /lib/ValidationException.php: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /tests/ClientTest.php: -------------------------------------------------------------------------------- 1 | client = new \Cronitor\Client($this->apiKey, $this->apiVersion, $this->environment); 19 | } 20 | 21 | public function testIsInitializable() 22 | { 23 | $this->assertEquals($this->apiKey, $this->client->apiKey); 24 | $this->assertEquals($this->apiVersion, $this->client->apiVersion); 25 | $this->assertEquals($this->environment, $this->client->environment); 26 | $this->assertInstanceOf(\Cronitor\Monitors::class, $this->client->monitors); 27 | } 28 | 29 | public function testMonitor() 30 | { 31 | $monitorKey = '1234'; 32 | $monitor = $this->client->monitor($monitorKey); 33 | $this->assertInstanceOf(\Cronitor\Monitor::class, $monitor); 34 | } 35 | 36 | public function testReadConfig() 37 | { 38 | $dataConfig = [ 39 | "jobs" => [ 40 | "replenishment-report" => [ 41 | "schedule" => "0 * * * *" 42 | ], 43 | 'data-warehouse-exports' => [ 44 | 'schedule' => '0 0 * * *' 45 | ], 46 | 'welcome-email' => [ 47 | 'schedule' => 'every 10 minutes' 48 | ] 49 | ], 50 | 'checks' => [ 51 | 'cronitor-homepage' => [ 52 | 'request' => [ 53 | 'url' => 'https://cronitor.io' 54 | ], 55 | 'assertions' => ['response.time < 2s'] 56 | ] 57 | ], 58 | 'heartbeats' => [ 59 | 'production-deploy' => [ 60 | 'notify' => [ 61 | 'alerts' => ['default'], 62 | 'events' => [ 63 | 'complete' => true 64 | ] 65 | ] 66 | ] 67 | ], 68 | 'monitors' => [ 69 | [ 70 | 'key' => 'replenishment-report', 71 | 'schedule' => '0 * * * *', 72 | 'type' => 'job' 73 | ], 74 | [ 75 | 'key' => 'data-warehouse-exports', 76 | 'schedule' => '0 0 * * *', 77 | 'type' => 'job' 78 | ], 79 | [ 80 | 'key' => 'welcome-email', 81 | 'schedule' => 'every 10 minutes', 82 | 'type' => 'job' 83 | ], 84 | [ 85 | 'key' => 'production-deploy', 86 | 'type' => 'heartbeat', 87 | "notify" => [ 88 | "alerts" => ["default"], 89 | "events" => ["complete" => true] 90 | ] 91 | ], 92 | [ 93 | 'key' => 'cronitor-homepage', 94 | 'type' => 'check', 95 | 'request' => ['url' => 'https://cronitor.io'], 96 | 'assertions' => ['response.time < 2s'] 97 | ] 98 | ] 99 | ]; 100 | $returnedConfig = $this->client->readConfig('tests/data/config.yml', true); 101 | $this->assertEquals($dataConfig, $returnedConfig); 102 | } 103 | 104 | public function testGenerateConfig() 105 | { 106 | $content = 'yaml'; 107 | test::double('\Cronitor\Monitor', ['getYaml' => $content]); 108 | $this->client->generateConfig(); 109 | $this->assertEquals($content, trim(file_get_contents($this->configPath))); 110 | } 111 | 112 | public function testApplyConfig() 113 | { 114 | test::double('\Cronitor\Monitor', ['put' => []]); 115 | $this->client->readConfig('tests/data/config.yml'); 116 | $this->assertTrue($this->client->applyConfig()); 117 | } 118 | 119 | public function testValidateConfig() 120 | { 121 | test::double('\Cronitor\Monitor', ['put' => []]); 122 | $this->client->readConfig('tests/data/config.yml'); 123 | $this->assertTrue($this->client->validateConfig()); 124 | } 125 | 126 | public function testJob() 127 | { 128 | $callback = function () { 129 | return 'success'; 130 | }; 131 | $jobResult = $this->client->job('1234', $callback); 132 | $this->assertEquals('success', $jobResult); 133 | } 134 | 135 | public function testJobException() 136 | { 137 | $this->expectExceptionMessage('This job Exception'); 138 | $this->expectException(\Exception::class); 139 | $callback = function () { 140 | throw new \Exception('This job Exception'); 141 | }; 142 | 143 | $jobResult = $this->client->job('1234', $callback); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /tests/MonitorTest.php: -------------------------------------------------------------------------------- 1 | monitor = new \Cronitor\Monitor($this->key, $this->apiKey, $this->apiVersion, $this->env); 18 | } 19 | 20 | public function testIsInitializable() 21 | { 22 | $this->assertEquals($this->key, $this->monitor->key); 23 | $this->assertEquals($this->apiKey, $this->monitor->apiKey); 24 | $this->assertEquals($this->apiVersion, $this->monitor->apiVersion); 25 | $this->assertEquals($this->env, $this->monitor->env); 26 | } 27 | 28 | public function testPut() 29 | { 30 | $params = [ 31 | 'monitors' => [], 32 | 'rollback' => true 33 | ]; 34 | $mockResponse = [ 35 | 'code' => 200, 36 | 'content' => "{\"monitors\": [{\"key\": \"$this->key\"}]}" 37 | ]; 38 | $mockHttpClient = test::double('Cronitor\HttpClient', ['put' => $mockResponse]); 39 | 40 | $result = \Cronitor\Monitor::put($this->apiKey, $this->apiVersion, $params); 41 | $this->assertInstanceOf(\Cronitor\Monitor::class, $result); 42 | $this->assertEquals($this->key, $result->key); 43 | } 44 | 45 | public function testDelete() 46 | { 47 | $mockResponse = [ 48 | 'code' => 204, 49 | 'content' => "" 50 | ]; 51 | $mockHttpClient = test::double('Cronitor\HttpClient', ['delete' => $mockResponse]); 52 | 53 | $result = \Cronitor\Monitor::delete($this->apiKey, $this->apiVersion, $this->key); 54 | $this->assertEquals($mockResponse, $result); 55 | } 56 | 57 | public function testGetYaml() 58 | { 59 | $content = "yaml"; 60 | $mockResponse = [ 61 | 'code' => 200, 62 | 'content' => $content 63 | ]; 64 | $mockHttpClient = test::double('Cronitor\HttpClient', ['get' => $mockResponse]); 65 | 66 | $result = \Cronitor\Monitor::getYaml($this->apiKey, $this->apiVersion); 67 | $this->assertEquals($content, $result); 68 | } 69 | 70 | public function testGetYamlFailure() 71 | { 72 | $content = "yaml"; 73 | $mockResponse = [ 74 | 'code' => 500, 75 | 'content' => $content 76 | ]; 77 | $mockHttpClient = test::double('Cronitor\HttpClient', ['get' => $mockResponse]); 78 | $this->expectException(\Exception::class); 79 | $result = \Cronitor\Monitor::getYaml($this->apiKey, $this->apiVersion); 80 | } 81 | 82 | public function testPing() 83 | { 84 | $mockResponse = [ 85 | 'code' => 200, 86 | 'content' => "" 87 | ]; 88 | $mockHttpClient = test::double('Cronitor\HttpClient', ['get' => $mockResponse]); 89 | 90 | $result = $this->monitor->ping([]); 91 | $this->assertTrue($result); 92 | } 93 | 94 | public function testPause() 95 | { 96 | $mockResponse = [ 97 | 'code' => 200, 98 | 'content' => "" 99 | ]; 100 | $mockHttpClient = test::double('Cronitor\HttpClient', ['get' => $mockResponse]); 101 | 102 | $result = $this->monitor->pause(); 103 | $this->assertTrue($result); 104 | } 105 | 106 | public function testUnpause() 107 | { 108 | $mockResponse = [ 109 | 'code' => 200, 110 | 'content' => "" 111 | ]; 112 | $mockHttpClient = test::double('Cronitor\HttpClient', ['get' => $mockResponse]); 113 | 114 | $result = $this->monitor->unpause(); 115 | $this->assertTrue($result); 116 | } 117 | 118 | public function testGetData() 119 | { 120 | $mockResponse = [ 121 | 'code' => 200, 122 | 'content' => "" 123 | ]; 124 | $mockHttpClient = test::double('Cronitor\HttpClient', ['get' => $mockResponse]); 125 | 126 | $result = $this->monitor->getData(); 127 | $this->assertEquals('', $result); 128 | } 129 | 130 | public function testSetData() 131 | { 132 | $data = ["monitors" => []]; 133 | 134 | $result = $this->monitor->setData($data); 135 | $this->assertTrue($result); 136 | $this->assertEquals($data, $this->monitor->getData()); 137 | } 138 | 139 | public function testOk() 140 | { 141 | $mockResponse = [ 142 | 'code' => 200, 143 | 'content' => "" 144 | ]; 145 | $mockHttpClient = test::double('Cronitor\HttpClient', ['get' => $mockResponse]); 146 | 147 | $result = $this->monitor->ok(); 148 | $this->assertTrue($result); 149 | } 150 | 151 | public function testCleanMetrics() 152 | { 153 | $metrics = [ 154 | 'key1' => 'value1', 155 | 'key2' => 'value2', 156 | 'key3' => 'value3' 157 | ]; 158 | 159 | $method = new \ReflectionMethod('\Cronitor\Monitor', 'cleanMetrics'); 160 | $method->setAccessible(true); 161 | 162 | $cleanMetricsResult = $method->invokeArgs($this->monitor, [$metrics]); 163 | 164 | $this->assertSame('key1:value1', $cleanMetricsResult[0]); 165 | $this->assertArrayNotHasKey('key1', $cleanMetricsResult); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /tests/MonitorsTest.php: -------------------------------------------------------------------------------- 1 | monitors = new \Cronitor\Monitors($this->apiKey, $this->apiVersion); 16 | } 17 | 18 | public function testIsInitializable() 19 | { 20 | $this->assertEquals($this->apiKey, $this->monitors->apiKey); 21 | $this->assertEquals($this->apiVersion, $this->monitors->apiVersion); 22 | } 23 | 24 | public function testPut() 25 | { 26 | $monitor = test::double('\Cronitor\Monitor', ['put' => true]); 27 | $this->assertTrue($this->monitors->put([])); 28 | $monitor->verifyInvokedOnce('put'); 29 | } 30 | 31 | public function testDelete() 32 | { 33 | $monitor = test::double('\Cronitor\Monitor', ['delete' => true]); 34 | $this->assertTrue($this->monitors->delete([])); 35 | $monitor->verifyInvokedOnce('delete'); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/TestBase.php: -------------------------------------------------------------------------------- 1 | init([ 11 | 'debug' => true, 12 | 'includePaths' => [__DIR__ . '/../lib'], 13 | 'cacheDir' => __DIR__ . '/../cache' 14 | ]); 15 | -------------------------------------------------------------------------------- /tests/data/config.yml: -------------------------------------------------------------------------------- 1 | jobs: 2 | replenishment-report: 3 | schedule: "0 * * * *" 4 | data-warehouse-exports: 5 | schedule: "0 0 * * *" 6 | welcome-email: 7 | schedule: "every 10 minutes" 8 | 9 | checks: 10 | cronitor-homepage: 11 | request: 12 | url: "https://cronitor.io" 13 | assertions: 14 | - "response.time < 2s" 15 | 16 | heartbeats: 17 | production-deploy: 18 | notify: 19 | alerts: 20 | - default 21 | events: 22 | complete: true 23 | --------------------------------------------------------------------------------