├── .editorconfig ├── .gitignore ├── LICENSE ├── README.md ├── composer.json ├── prometheus_files └── example_config.yml ├── src ├── Client.php ├── Counter.php ├── Gauge.php ├── Histogram.php ├── Metric.php ├── PrometheusException.php └── Registry.php └── test ├── counter_test.php ├── guage_test.php └── histogram_test.php /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | indent_size = 4 7 | indent_style = tab 8 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | metrics.proto 2 | vendor -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Bryan Peterson 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | PHP Client For Prometheus 2 | ========================= 3 | 4 | An unofficial client for Prometheus (http://prometheus.io/) written in PHP. 5 | This fork is nearly identical to @TimeZynk fork except it includes a test script for both the counter and histogram. 6 | In addition a function for pushing the serialized client data to a prometheus push gateway with curl is provided. 7 | Lastly setup instructions for Prometheus, a Prometheus push gateway, and some example queries are provided with links. 8 | 9 | *Note* It is strongly recommended that you read through some of the Prometheus documentation if you haven't already. 10 | 11 | https://prometheus.io/docs/introduction/getting_started/ 12 | 13 | 14 | Requirements 15 | ------------ 16 | 17 | A Prometheus server running on the localhost running with the provided .yml file. 18 | A Prometheus push gateway running on the localhost listening on port 9091. 19 | A php installation with the curl extension enabled... look up how to do this if you don't know. 20 | 21 | 22 | Setup Prometheus 23 | ================ 24 | 25 | Lets launch the prometheus server so that it listens only to the push gateway that we will setup in the next step. 26 | We purposefully don't have prometheus listen to itself so it's easier to find the data we send to it later. 27 | 28 | - Place the "example_config.yml" file next to where you have the Prometheus server installed. 29 | - If Prometheus is running then kill the server. 30 | - **OPTIONAL** remove the /data/ folder where prometheus is storing it's data. Only do this if you want to remove any previous 31 | testing data you have done so far. 32 | - Run this command. 33 | ./prometheus -config.file=no_prom.yml 34 | 35 | Prometheus is now running and is configured to pull from the localhost at port 9091. Let's give it something to pull! 36 | 37 | 38 | Setup Prometheus Push Gateway 39 | ============================== 40 | 41 | The Push Gateway allows short lived applications that would otherwise be a pain to try and pull data from to still be tracked. 42 | In addition it simplifies the task of sending the data we want to track to Prometheus at relatively minor cost. 43 | The prometheus documentation covers the advantages of PULL vs PUSH logging. 44 | 45 | *Push Gateway Link* https://github.com/prometheus/pushgateway 46 | 47 | - Follow the instructions at the prometheus pushgateway git to get the gateway installed and running on localhost. 48 | - That's it! 49 | 50 | 51 | Supported Metrics 52 | ================= 53 | 54 | Before explaining how to use the client it's important to understand what metrics the PHP client supports. 55 | 56 | - *Counter* This metric can only be incremented positevely by 1 or more. 57 | - *Gauge* This metric can be incremented both postively and negatively. This has some drawbacks in limiting the usefullnes 58 | of certain querying functions. 59 | - *Histogram* This metric is a collection of buckets that counts how many data points fell into each bucket 60 | and a sum of all the values of those data points. 61 | - Summaries are not supported. 62 | 63 | It's highly recommended to view the Prometheus documentations "Concepts => Data Model" to learn about the intricacies these metrics. 64 | 65 | 66 | Using PHP Push Gateway Client 67 | ============================== 68 | 69 | This fork has tried to simplify the process of using the client so that no outside libraries are required and everything should work 70 | right out of the box. 71 | 72 | The only file that we need to include in order to start using the client is the Client.php. 73 | ```php 74 | require_once dirname(__FILE__) . '/../src/Client.php'; 75 | ``` 76 | 77 | Creating a new client is easy. Since the PHP client lives in the Prometheus namespace we must include that when creating 78 | a new client. In addition the client must be passed a list of options. Currently the only valid option is 'base_uri'. 79 | If you don't plan on using the built in "pushMetrics" function, you may set this to an empty string. 80 | ```php 81 | $client = new Prometheus\Client('http://localhost:9091/metrics/job/'); 82 | ``` 83 | 84 | Next we tell the client to create a new metric. Here we are creating a new *Counter*. 85 | ```php 86 | $counter = $client->newCounter([ 87 | 'namespace' => 'php_client', 88 | 'subsystem' => 'testing', 89 | 'name' => 'counter', 90 | 'help' => 'Testing the PHPClients Counter', 91 | ]); 92 | ``` 93 | 94 | We can use the new counter to increment different things. Here we pretend to be counting the status_codes returned by an 95 | imaginary server to clients for the "home.php" page. 96 | ```php 97 | $counter->increment( [ 'url' => 'home.php', 'status_code' => 200 ], rand( 1, 50 ) ); 98 | $counter->increment( [ 'url' => 'home.php', 'status_code' => 404 ], rand( 1, 50 ) ); 99 | ``` 100 | 101 | Once we have gathered enough data we tell the client to send the metrics to the Prometheus Push Gateway. 102 | We can either send the data right to the gateway. Or we may specify a job, or a job and an instance of that job. 103 | The documentation at the Prometheus Push Gateway Git covers the specifics of what happens when setting jobs 104 | and instances and will not be covered here. 105 | ```php 106 | $client->pushMetrics( "pretend_server", $job_id ); 107 | ``` 108 | 109 | Here is the above code all in one snippet. 110 | 111 | ```php 112 | require_once dirname(__FILE__) . '/../src/Client.php'; 113 | 114 | $client = new Prometheus\Client('http://localhost:9091/metrics/job/'); 115 | 116 | $counter = $client->newCounter([ 117 | 'namespace' => 'php_client', 118 | 'subsystem' => 'testing', 119 | 'name' => 'counter', 120 | 'help' => 'Testing the PHPClients Counter', 121 | ]); 122 | 123 | $counter->increment( [ 'url' => 'home.php', 'status_code' => 200 ], rand( 1, 50 ) ); 124 | $counter->increment( [ 'url' => 'home.php', 'status_code' => 404 ], rand( 1, 50 ) ); 125 | 126 | $client->pushMetrics( "pretend_server", "test_instance" ); 127 | ``` 128 | 129 | Going Further 130 | ============= 131 | 132 | Go ahead and run the /test/histogram_test.php function for a minute or two. The output is mundane so go get some coffee. 133 | Now you can navigate to the "http://localhost:9090/graph" in a web browser and execute the following query. 134 | 135 | rate(meta_data_tv_elements_per_hit_sum[5m]) / rate(meta_data_tv_elements_per_hit_count[5m]) 136 | 137 | Adjust the graph to show over the past 15m. This is now a graph that would accurately show how many elements per hit an 138 | imaginary web scraper is pulling from from two different domains. 139 | 140 | 141 | Just Serialized Data 142 | ===================== 143 | 144 | If you want to see the data that is being sent to the server so you can expose it through a server or do whatever you wish 145 | you can simply call the serialize() function from the client. 146 | ```php 147 | echo $client->serialize(); 148 | ``` 149 | 150 | 151 | 152 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lazyshot/prometheus-php", 3 | "description": "Prometheus metrics client for PHP", 4 | "version": "0.3.0", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Bryan Peterson", 9 | "email": "lazyshot@gmail.com" 10 | } 11 | ], 12 | "require": { 13 | "php": "^7.1" 14 | }, 15 | "autoload": { 16 | "psr-4": { 17 | "Prometheus\\": "src/" 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /prometheus_files/example_config.yml: -------------------------------------------------------------------------------- 1 | global: 2 | scrape_interval: 15s # By default, scrape targets every 15 seconds. 3 | evaluation_interval: 15s # By default, scrape targets every 15 seconds. 4 | # scrape_timeout is set to the global default (10s). 5 | 6 | # Attach these extra labels to all timeseries collected by this Prometheus instance. 7 | external_labels: 8 | monitor: 'codelab-monitor' 9 | 10 | rule_files: 11 | - 'prometheus.rules' 12 | 13 | scrape_configs: 14 | - job_name: 'prometheus_pushgateway' 15 | 16 | # Override the global default and scrape targets from this job every 5 seconds. 17 | scrape_interval: 5s 18 | scrape_timeout: 10s 19 | 20 | target_groups: 21 | - targets: ['localhost:9091'] 22 | labels: 23 | group: 'production' 24 | -------------------------------------------------------------------------------- /src/Client.php: -------------------------------------------------------------------------------- 1 | registry = new Registry; 13 | 14 | if (null === $base_uri) 15 | throw new PrometheusException("Prometheus requires a base_uri option, which points to the pushgateway"); 16 | 17 | $this->base_uri = $base_uri; 18 | 19 | // TODO: Allow option for requiring http basic authentication 20 | } 21 | 22 | public function newCounter(array $opts = []) : Counter 23 | { 24 | return $this->register(new Counter($opts)); 25 | } 26 | 27 | public function newGauge(array $opts = []) : Gauge 28 | { 29 | return $this->register(new Gauge($opts)); 30 | } 31 | 32 | public function newHistogram(array $opts = []) : Histogram 33 | { 34 | return $this->register(new Histogram($opts)); 35 | } 36 | 37 | private function register(Metric $metric) : Metric 38 | { 39 | return $this->registry->register($metric); 40 | } 41 | 42 | public function getMetric($metric) : ?Metric 43 | { 44 | return $this->registry->getMetric($metric); 45 | } 46 | 47 | public function serialize() : string 48 | { 49 | 50 | $body = ""; 51 | 52 | foreach ($this->registry->getMetrics() as $metric) { 53 | $body .= $metric->serialize() . "\n"; 54 | } 55 | 56 | return $body; 57 | } 58 | 59 | 60 | function pushMetrics(string $job = null, string $instance = null) 61 | { 62 | $url = $this->base_uri; 63 | 64 | if ($instance && !$job) throw new PrometheusException("Instance passed but job was set to null. Job must be set to a non empty string."); 65 | if (!is_null($job) && $job == "") throw new PrometheusException("Job was set to an empty string. Job must be set to a non empty string."); 66 | elseif (!is_null($instance) && $instance == "") throw new PrometheusException("Instance was set to an empty string. If Instance is set it must be a non empty string."); 67 | 68 | if ($job) $url .= $job; 69 | if ($instance) $url .= "/instance/" . $instance; 70 | 71 | $ch = \curl_init($url); 72 | 73 | \curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); 74 | \curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PUT"); 75 | \curl_setopt($ch, CURLOPT_TIMEOUT, 1); 76 | \curl_setopt($ch, CURLOPT_POSTFIELDS, $this->serialize()); 77 | 78 | if (\curl_exec($ch) === false) { 79 | throw new PrometheusException("Error sending metrics to push gateway: " . \curl_error($ch)); 80 | } 81 | 82 | $this->registry->cleanup(); 83 | 84 | #TODO: Can the pushgateway return a 200 on successful PUT? 85 | # Currently it returns nothing no matter what, lame 86 | } 87 | } 88 | 89 | -------------------------------------------------------------------------------- /src/Counter.php: -------------------------------------------------------------------------------- 1 | hashLabels($labels); 25 | 26 | if (!isset($this->values[$hash])) 27 | $this->values[$hash] = $this->defaultValue(); 28 | 29 | $this->values[$hash] += $by; 30 | 31 | return $this->values[$hash]; 32 | } 33 | 34 | public function decrement(array $labels = [], $by = 1) : int 35 | { 36 | $this->increment($labels, -1 * $by); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Gauge.php: -------------------------------------------------------------------------------- 1 | hashLabels($labels); 25 | if (!isset($this->values[$hash])) 26 | $this->values[$hash] = $this->defaultValue(); 27 | 28 | $this->values[$hash] = $val; 29 | 30 | return $this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Histogram.php: -------------------------------------------------------------------------------- 1 | buckets = isset($opts['buckets']) ? $opts['buckets'] : [1, 2, 3]; 11 | } 12 | 13 | public function type() : string 14 | { 15 | return "histogram"; 16 | } 17 | 18 | public function defaultValue() : int 19 | { 20 | return 0; 21 | } 22 | 23 | public function getBuckets() : array 24 | { 25 | return $this->buckets; 26 | } 27 | 28 | public function observe(array $labels, float $value) : void 29 | { 30 | $labels["__suffix"] = "_bucket"; 31 | foreach ($this->buckets as $bucket) { 32 | $labels["le"] = $bucket; 33 | $hash = $this->hashLabels($labels); 34 | if (!isset($this->values[$hash])) { 35 | $this->values[$hash] = $this->defaultValue(); 36 | } 37 | if ($value <= $bucket) { 38 | $this->values[$hash] += 1; 39 | } 40 | } 41 | $labels["le"] = '+Inf'; 42 | $hash = $this->hashLabels($labels); 43 | if (!isset($this->values[$hash])) { 44 | $this->values[$hash] = $this->defaultValue(); 45 | } 46 | $this->values[$hash] += 1; 47 | unset($labels["le"]); 48 | 49 | 50 | $labels["__suffix"] = "_count"; 51 | $hash = $this->hashLabels($labels); 52 | if (!isset($this->values[$hash])) { 53 | $this->values[$hash] = $this->defaultValue(); 54 | } 55 | $this->values[$hash] += 1; 56 | 57 | 58 | $labels["__suffix"] = "_sum"; 59 | $hash = $this->hashLabels($labels); 60 | if (!isset($this->values[$hash])) { 61 | $this->values[$hash] = $this->defaultValue(); 62 | } 63 | $this->values[$hash] += $value; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Metric.php: -------------------------------------------------------------------------------- 1 | opts = $opts; 22 | $this->name = isset($opts['name']) ? $opts['name'] : ''; 23 | $this->namespace = isset($opts['namespace']) ? $opts['namespace'] : ''; 24 | $this->subsystem = isset($opts['subsystem']) ? $opts['subsystem'] : ''; 25 | $this->help = isset($opts['help']) ? $opts['help'] : ''; 26 | 27 | if (empty($this->name)) throw new PrometheusException("A name is required for a metric"); 28 | if (empty($this->help)) throw new PrometheusException("A help is required for a metric"); 29 | 30 | $this->full_name = implode('_', [$this->namespace, $this->subsystem, $this->name]); 31 | 32 | $this->values = []; 33 | } 34 | 35 | public function values() : array 36 | { 37 | $values = []; 38 | foreach ($this->values as $hash => $val) { 39 | $values [] = [$this->labels[$hash], $val]; 40 | } 41 | 42 | return $values; 43 | } 44 | 45 | public function get(array $labels = []) 46 | { 47 | $hash = $this->hashLabels($labels); 48 | 49 | return $this->values[$hash] ? : $this->defaultValue(); 50 | } 51 | 52 | public function defaultValue() 53 | { 54 | return null; 55 | } 56 | 57 | abstract public function type() : string; 58 | 59 | public function serialize() : string 60 | { 61 | $tbr = []; 62 | $tbr [] = "# HELP " . $this->full_name . " " . $this->help; 63 | $tbr [] = "# TYPE " . $this->full_name . " " . $this->type(); 64 | 65 | foreach ($this->values() as $val) { 66 | list($labels, $value) = $val; 67 | $label_pairs = []; 68 | $suffix = isset($labels['__suffix']) ? $labels['__suffix'] : ''; 69 | unset($labels['__suffix']); 70 | 71 | foreach ($labels as $k => $v) { 72 | $v = str_replace("\"", "\\\"", $v); 73 | $v = str_replace("\n", "\\n", $v); 74 | $v = str_replace("\\", "\\\\", $v); 75 | $label_pairs [] = "$k=\"$v\""; 76 | } 77 | $tbr [] = $this->full_name . $suffix . "{" . implode(",", $label_pairs) . "} " . $value; 78 | } 79 | 80 | return implode("\n", $tbr); 81 | } 82 | 83 | protected function hashLabels(array $labels = []) : string 84 | { 85 | $hash = md5(json_encode($labels, JSON_FORCE_OBJECT)); 86 | $this->labels[$hash] = $labels; 87 | 88 | // TODO: save to memcached 89 | 90 | return $hash; 91 | } 92 | 93 | public function getLabels() : array 94 | { 95 | /* For debugging only */ 96 | return $this->labels; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/PrometheusException.php: -------------------------------------------------------------------------------- 1 | full_name; 13 | 14 | if (isset($this->metrics[$name])) { 15 | throw new PrometheusException("Metric name must be unique"); 16 | } 17 | 18 | $this->metrics[$name] = $metric; 19 | 20 | return $metric; 21 | } 22 | 23 | public function cleanup() 24 | { 25 | $this->metrics = []; 26 | } 27 | 28 | public function getMetric($metric) : ?Metric 29 | { 30 | return $this->metrics[$metric] ?? null; 31 | } 32 | 33 | public function getMetrics() : array 34 | { 35 | return $this->metrics; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/counter_test.php: -------------------------------------------------------------------------------- 1 | newCounter( 9 | [ 10 | 'namespace' => 'php_client', 11 | 'subsystem' => 'testing', 12 | 'name' => 'counter', 13 | 'help' => 'Testing the PHPClients Counter', 14 | ] 15 | ); 16 | 17 | $job_id = uniqid(); 18 | while (true) { 19 | $counter->increment(['url' => 'home.php', 'status_code' => 200], rand(1, 50)); 20 | $counter->increment(['url' => 'home.php', 'status_code' => 404], rand(1, 50)); 21 | 22 | $client->pushMetrics("pretend_server", $job_id); 23 | 24 | $sleepTime = rand(1, 20); 25 | echo "sleeping $sleepTime\n"; 26 | sleep($sleepTime); 27 | } 28 | -------------------------------------------------------------------------------- /test/guage_test.php: -------------------------------------------------------------------------------- 1 | newGauge( 9 | [ 10 | 'namespace' => 'php_client', 11 | 'subsystem' => 'testing', 12 | 'name' => 'Guage', 13 | 'help' => 'Testing the PHPClients guage', 14 | ] 15 | ); 16 | 17 | $job_id = uniqid(); 18 | while (true) { 19 | $guage->set(['key1' => 'val1'], rand(1, 50)); 20 | $guage->set(['key2' => 'val2'], rand(1, 50)); 21 | 22 | echo "attempting a push\n"; 23 | $client->pushMetrics("pretend_server", $job_id); 24 | echo "push done\n"; 25 | $sleepTime = rand(1, 5); 26 | echo "sleeping $sleepTime\n"; 27 | sleep($sleepTime); 28 | } 29 | -------------------------------------------------------------------------------- /test/histogram_test.php: -------------------------------------------------------------------------------- 1 | newHistogram( 9 | [ 10 | 'namespace' => 'meta_data', 11 | 'subsystem' => 'tv', 12 | 'name' => 'elements_per_hit', 13 | 'help' => 'Testing the PHPClients Histogram', 14 | 'buckets' => [10, 25, 50, 75, 100], 15 | ] 16 | ); 17 | 18 | while (true) { 19 | $histogram->observe(['domain' => 'hulu'], rand(0, 100)); 20 | $histogram->observe(['domain' => 'crunchyroll'], rand(0, 100)); 21 | 22 | $client->pushMetrics("foreign_language", "test_server_0"); 23 | 24 | echo "sleeping 5\n"; 25 | sleep(5); 26 | } 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------