├── .github └── CODEOWNERS ├── .gitignore ├── .styleci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── changelog.md ├── composer.json ├── config └── prometheus.php ├── phpunit.php ├── phpunit.xml ├── src ├── CollectorInterface.php ├── ExampleCollector.php ├── MetricsController.php ├── PrometheusExporter.php ├── PrometheusFacade.php ├── PrometheusServiceProvider.php ├── StorageAdapterFactory.php └── routes.php └── tests ├── MetricsControllerTest.php ├── PrometheusExporterTest.php └── StorageAdapterFactoryTest.php /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # Lines starting with '#' are comments. 2 | # Each line is a file pattern followed by one or more owners. 3 | # More details are here: https://help.github.com/articles/about-codeowners/ 4 | # The '*' pattern is global owners. 5 | # Order is important. The last matching pattern has the most precedence. 6 | 7 | * @Superbalist/core 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.lock 3 | vendor 4 | bin 5 | coverage 6 | coverage.xml -------------------------------------------------------------------------------- /.styleci.yml: -------------------------------------------------------------------------------- 1 | preset: psr2 2 | 3 | enabled: 4 | - alpha_ordered_imports 5 | - binary_operator_spaces 6 | - blank_line_after_opening_tag 7 | - cast_spaces 8 | - concat_with_spaces 9 | - const_visibility_required 10 | - declare_equal_normalize 11 | - function_typehint_space 12 | - hash_to_slash_comment 13 | - heredoc_to_nowdoc 14 | - include 15 | - lowercase_cast 16 | - method_separation 17 | - native_function_casing 18 | - new_with_braces 19 | - no_blank_lines_after_class_opening 20 | - no_blank_lines_after_phpdoc 21 | - no_blank_lines_after_return 22 | - no_blank_lines_after_throw 23 | - no_blank_lines_between_imports 24 | - no_blank_lines_between_traits 25 | - no_empty_statement 26 | - no_extra_consecutive_blank_lines 27 | - no_leading_import_slash 28 | - no_leading_namespace_whitespace 29 | - no_multiline_whitespace_around_double_arrow 30 | - no_short_bool_cast 31 | - no_short_echo_tag 32 | - no_singleline_whitespace_before_semicolons 33 | - no_spaces_inside_offset 34 | - no_spaces_outside_offset 35 | - no_trailing_comma_in_list_call 36 | - no_trailing_comma_in_singleline_array 37 | - no_unneeded_control_parentheses 38 | - no_unreachable_default_argument_value 39 | - no_unused_imports 40 | - no_useless_return 41 | - no_whitespace_before_comma_in_array 42 | - no_whitespace_in_blank_line 43 | - normalize_index_brace 44 | - object_operator_without_whitespace 45 | - phpdoc_add_missing_param_annotation 46 | - phpdoc_indent 47 | - phpdoc_inline_tag 48 | - phpdoc_link_to_see 49 | - phpdoc_no_access 50 | - phpdoc_no_empty_return 51 | - phpdoc_no_package 52 | - phpdoc_order 53 | - phpdoc_property 54 | - phpdoc_scalar 55 | - phpdoc_separation 56 | - phpdoc_single_line_var_spacing 57 | - phpdoc_to_comment 58 | - phpdoc_trim 59 | - phpdoc_type_to_var 60 | - phpdoc_types 61 | - phpdoc_var_without_name 62 | - print_to_echo 63 | - self_accessor 64 | - short_array_syntax 65 | - short_scalar_cast 66 | - single_blank_line_before_namespace 67 | - single_quote 68 | - space_after_semicolon 69 | - standardize_not_equals 70 | - ternary_operator_spaces 71 | - trailing_comma_in_multiline_array 72 | - trim_array_spaces 73 | - unalign_double_arrow 74 | - unalign_equals 75 | - unary_operator_spaces 76 | - whitespace_after_comma_in_array 77 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.6 5 | - 7.0 6 | - 7.1 7 | - 7.2 8 | - 7.3 9 | 10 | before_install: 11 | - echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 12 | 13 | before_script: 14 | - composer install 15 | 16 | script: ./vendor/bin/phpunit --configuration phpunit.xml 17 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Superbalist.com a division of Takealot Online (Pty) Ltd 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 | # laravel-prometheus-exporter 2 | 3 | A prometheus exporter for Laravel. 4 | 5 | [![Author](http://img.shields.io/badge/author-@superbalist-blue.svg?style=flat-square)](https://twitter.com/superbalist) 6 | [![Build Status](https://img.shields.io/travis/Superbalist/laravel-prometheus-exporter/master.svg?style=flat-square)](https://travis-ci.org/Superbalist/laravel-prometheus-exporter) 7 | [![StyleCI](https://styleci.io/repos/98516814/shield?branch=master)](https://styleci.io/repos/98516814) 8 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 9 | [![Packagist Version](https://img.shields.io/packagist/v/superbalist/laravel-prometheus-exporter.svg?style=flat-square)](https://packagist.org/packages/superbalist/laravel-prometheus-exporter) 10 | [![Total Downloads](https://img.shields.io/packagist/dt/superbalist/laravel-prometheus-exporter.svg?style=flat-square)](https://packagist.org/packages/superbalist/laravel-prometheus-exporter) 11 | 12 | This package is a wrapper bridging [jimdo/prometheus_client_php](https://github.com/Jimdo/prometheus_client_php) into Laravel. 13 | 14 | ## Installation 15 | 16 | ```bash 17 | composer require superbalist/laravel-prometheus-exporter 18 | ``` 19 | 20 | Register the service provider in app.php 21 | ```php 22 | 'providers' => [ 23 | // ... 24 | Superbalist\LaravelPrometheusExporter\PrometheusServiceProvider::class, 25 | ] 26 | ``` 27 | 28 | Register the facade in app.php 29 | ```php 30 | 'aliases' => [ 31 | // ... 32 | 'Prometheus' => Superbalist\LaravelPrometheusExporter\PrometheusFacade::class, 33 | ] 34 | ``` 35 | 36 | ## Configuration 37 | 38 | The package has a default configuration which uses the following environment variables. 39 | ``` 40 | PROMETHEUS_NAMESPACE=app 41 | 42 | PROMETHEUS_METRICS_ROUTE_ENABLED=true 43 | PROMETHEUS_METRICS_ROUTE_PATH=metrics 44 | PROMETHEUS_METRICS_ROUTE_MIDDLEWARE=null 45 | 46 | PROMETHEUS_STORAGE_ADAPTER=memory 47 | 48 | REDIS_HOST=localhost 49 | REDIS_PORT=6379 50 | PROMETHEUS_REDIS_PREFIX=PROMETHEUS_ 51 | ``` 52 | 53 | To customize the configuration file, publish the package configuration using Artisan. 54 | ```bash 55 | php artisan vendor:publish --provider="Superbalist\LaravelPrometheusExporter\PrometheusServiceProvider" 56 | ``` 57 | 58 | You can then edit the generated config at `app/config/prometheus.php`. 59 | 60 | ### Storage Adapters 61 | 62 | The storage adapter is used to persist metrics across requests. The `memory` adapter is enabled by default, meaning 63 | data will only be persisted across the current request. We recommend using the `redis` or `apc` adapter in production 64 | environments. 65 | 66 | The `PROMETHEUS_STORAGE_ADAPTER` env var is used to specify the storage adapter. 67 | 68 | If `redis` is used, the `REDIS_HOST` and `REDIS_PORT` vars also need to be configured. 69 | 70 | ### Exporting Metrics 71 | 72 | The package adds a `/metrics` end-point, enabled by default, which exposes all metrics gathered by collectors. 73 | 74 | This can be turned on/off using the `PROMETHEUS_METRICS_ROUTE_ENABLED` var, and can also be changed using the 75 | `PROMETHEUS_METRICS_ROUTE_PATH` var. 76 | 77 | If you would like to protect this end-point, you can write any custom middleware and enable it using 78 | `PROMETHEUS_METRICS_ROUTE_MIDDLEWARE`. 79 | 80 | ### Collectors 81 | 82 | A collector is a class, implementing the [CollectorInterface](src/CollectorInterface.php), which is responsible for 83 | collecting data for one or many metrics. 84 | 85 | Please see the [ExampleCollector](src/ExampleCollector.php) included in this repository. 86 | 87 | You can auto-load your collectors by adding them to the `collectors` array in the prometheus.php config. 88 | 89 | ## Usage 90 | 91 | ```php 92 | // retrieve the exporter 93 | $exporter = app(\Superbalist\LaravelPrometheusExporter::class); 94 | // or 95 | $exporter = app('prometheus'); 96 | // or 97 | $exporter = Prometheus::getFacadeRoot(); 98 | 99 | // register a new collector 100 | $collector = new \My\New\Collector(); 101 | $exporter->registerCollector($collector); 102 | 103 | // retrieve all collectors 104 | var_dump($exporter->getCollectors()); 105 | 106 | // retrieve a collector by name 107 | $collector = $exporter->getCollector('user'); 108 | 109 | // export all metrics 110 | // this is called automatically when the /metrics end-point is hit 111 | var_dump($exporter->export()); 112 | 113 | // the following methods can be used to create and interact with counters, gauges and histograms directly 114 | // these methods will typically be called by collectors, but can be used to register any custom metrics directly, 115 | // without the need of a collector 116 | 117 | // create a counter 118 | $counter = $exporter->registerCounter('search_requests_total', 'The total number of search requests.'); 119 | $counter->inc(); // increment by 1 120 | $counter->incBy(2); 121 | 122 | // create a counter (with labels) 123 | $counter = $exporter->registerCounter('search_requests_total', 'The total number of search requests.', ['request_type']); 124 | $counter->inc(['GET']); // increment by 1 125 | $counter->incBy(2, ['GET']); 126 | 127 | // retrieve a counter 128 | $counter = $exporter->getCounter('search_requests_total'); 129 | 130 | // create a gauge 131 | $gauge = $exporter->registerGauge('users_online_total', 'The total number of users online.'); 132 | $gauge->inc(); // increment by 1 133 | $gauge->incBy(2); 134 | $gauge->dec(); // decrement by 1 135 | $gauge->decBy(2); 136 | $gauge->set(36); 137 | 138 | // create a gauge (with labels) 139 | $gauge = $exporter->registerGauge('users_online_total', 'The total number of users online.', ['group']); 140 | $gauge->inc(['staff']); // increment by 1 141 | $gauge->incBy(2, ['staff']); 142 | $gauge->dec(['staff']); // decrement by 1 143 | $gauge->decBy(2, ['staff']); 144 | $gauge->set(36, ['staff']); 145 | 146 | // retrieve a gauge 147 | $counter = $exporter->getGauge('users_online_total'); 148 | 149 | // create a histogram 150 | $histogram = $exporter->registerHistogram( 151 | 'response_time_seconds', 152 | 'The response time of a request.', 153 | [], 154 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0] 155 | ); 156 | // the buckets must be in asc order 157 | // if buckets aren't specified, the default 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0 buckets will be used 158 | $histogram->observe(5.0); 159 | 160 | // create a histogram (with labels) 161 | $histogram = $exporter->registerHistogram( 162 | 'response_time_seconds', 163 | 'The response time of a request.', 164 | ['request_type'], 165 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0] 166 | ); 167 | // the buckets must be in asc order 168 | // if buckets aren't specified, the default 0.005, 0.01, 0.025, 0.05, 0.075, 0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0 buckets will be used 169 | $histogram->observe(5.0, ['GET']); 170 | 171 | // retrieve a histogram 172 | $counter = $exporter->getHistogram('response_time_seconds'); 173 | ``` 174 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.5 - 2020-03-16 4 | 5 | * Update call to array_get helper to use the Illuminate\Support\Arr class 6 | * Enable Laravel 7 compatability 7 | 8 | ## 1.0.4 - 2019-10-17 9 | 10 | * Enable Laravel 6.0 compatability 11 | 12 | ## 1.0.3 - 2019-02-11 13 | 14 | * Enable package discovery for laravel 5.5+ 15 | 16 | ## 1.0.2 - 2019-01-16 17 | 18 | * Add compatability with Laravel Lumen (make named route configurable) 19 | 20 | ## 1.0.1 - 2017-08-30 21 | 22 | * Fix config retrieval of `prometheus.storage_adapters` 23 | 24 | ## 1.0.0 - 2017-07-27 25 | 26 | * Initial release 27 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "superbalist/laravel-prometheus-exporter", 3 | "description": "A prometheus exporter for Laravel", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Superbalist.com a division of Takealot Online (Pty) Ltd", 8 | "email": "info@superbalist.com" 9 | } 10 | ], 11 | "require": { 12 | "php": ">=5.6.0", 13 | "illuminate/support": "^5.3 || ^6.0 || ^7.0 || ^8.0", 14 | "illuminate/routing": "^5.3 || ^6.0 || ^7.0 || ^8.0", 15 | "jimdo/prometheus_client_php": "^0.9.0" 16 | }, 17 | "autoload": { 18 | "psr-4": { 19 | "Superbalist\\LaravelPrometheusExporter\\": "src/", 20 | "Tests\\": "tests/" 21 | } 22 | }, 23 | "extra": { 24 | "branch-alias": { 25 | "dev-master": "1.0-dev" 26 | }, 27 | "laravel": { 28 | "providers": [ 29 | "Superbalist\\LaravelPrometheusExporter\\PrometheusServiceProvider" 30 | ], 31 | "aliases": { 32 | "Prometheus": "Superbalist\\LaravelPrometheusExporter\\PrometheusFacade" 33 | } 34 | } 35 | }, 36 | "require-dev": { 37 | "phpunit/phpunit": "^5.5", 38 | "mockery/mockery": "^0.9.5" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /config/prometheus.php: -------------------------------------------------------------------------------- 1 | env('PROMETHEUS_NAMESPACE', 'app'), 16 | 17 | /* 18 | |-------------------------------------------------------------------------- 19 | | Metrics Route Enabled? 20 | |-------------------------------------------------------------------------- 21 | | 22 | | If enabled, a /metrics route will be registered to export prometheus 23 | | metrics. 24 | | 25 | */ 26 | 27 | 'metrics_route_enabled' => env('PROMETHEUS_METRICS_ROUTE_ENABLED', true), 28 | 29 | /* 30 | |-------------------------------------------------------------------------- 31 | | Metrics Route Path 32 | |-------------------------------------------------------------------------- 33 | | 34 | | The path at which prometheus metrics are exported. 35 | | 36 | | This is only applicable if metrics_route_enabled is set to true. 37 | | 38 | */ 39 | 40 | 'metrics_route_path' => env('PROMETHEUS_METRICS_ROUTE_PATH', 'metrics'), 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Metrics Route Name 45 | |-------------------------------------------------------------------------- 46 | | 47 | | Route Parh name aliase. 48 | | 49 | | This is only applicable if metrics_route_enabled is set to true. 50 | | 51 | */ 52 | 53 | 'metrics_route_name' => env('PROMETHEUS_METRICS_ROUTE_NAME', 'metrics'), 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Metrics Route Middleware 58 | |-------------------------------------------------------------------------- 59 | | 60 | | The middleware to assign to the metrics route. 61 | | 62 | | This can be used to protect the /metrics end-point to authenticated users, 63 | | a specific ip address, etc. 64 | | You are responsible for writing the middleware and implementing any 65 | | business logic needed by your application. 66 | | 67 | */ 68 | 69 | 'metrics_route_middleware' => env('PROMETHEUS_METRICS_ROUTE_MIDDLEWARE'), 70 | 71 | /* 72 | |-------------------------------------------------------------------------- 73 | | Storage Adapter 74 | |-------------------------------------------------------------------------- 75 | | 76 | | The storage adapter to use. 77 | | 78 | | Supported: "memory", "redis", "apc" 79 | | 80 | */ 81 | 82 | 'storage_adapter' => env('PROMETHEUS_STORAGE_ADAPTER', 'memory'), 83 | 84 | /* 85 | |-------------------------------------------------------------------------- 86 | | Storage Adapters 87 | |-------------------------------------------------------------------------- 88 | | 89 | | The storage adapter configs. 90 | | 91 | */ 92 | 93 | 'storage_adapters' => [ 94 | 95 | 'redis' => [ 96 | 'host' => env('REDIS_HOST', 'localhost'), 97 | 'port' => env('REDIS_PORT', 6379), 98 | 'timeout' => 0.1, 99 | 'read_timeout' => 10, 100 | 'persistent_connections' => false, 101 | 'prefix' => env('PROMETHEUS_REDIS_PREFIX', 'PROMETHEUS_'), 102 | ], 103 | 104 | ], 105 | 106 | /* 107 | |-------------------------------------------------------------------------- 108 | | Collectors 109 | |-------------------------------------------------------------------------- 110 | | 111 | | The collectors specified here will be auto-registered in the exporter. 112 | | 113 | */ 114 | 115 | 'collectors' => [ 116 | // \Your\ExporterClass::class, 117 | ], 118 | 119 | ]; 120 | -------------------------------------------------------------------------------- /phpunit.php: -------------------------------------------------------------------------------- 1 | 2 | 14 | 15 | 16 | ./tests/ 17 | 18 | 19 | 20 | 21 | ./src/ 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/CollectorInterface.php: -------------------------------------------------------------------------------- 1 | registerCounter('search_requests_total', 'The total number of search requests.'); 21 | * ``` 22 | * 23 | * @param PrometheusExporter $exporter 24 | */ 25 | public function registerMetrics(PrometheusExporter $exporter); 26 | 27 | /** 28 | * Collect metrics data, if need be, before exporting. 29 | * 30 | * As an example, this may be used to perform time consuming database queries and set the value of a counter 31 | * or gauge. 32 | */ 33 | public function collect(); 34 | } 35 | -------------------------------------------------------------------------------- /src/ExampleCollector.php: -------------------------------------------------------------------------------- 1 | registerCounter('search_requests_total', 'The total number of search requests.'); 31 | * ``` 32 | * 33 | * @param PrometheusExporter $exporter 34 | */ 35 | public function registerMetrics(PrometheusExporter $exporter) 36 | { 37 | $this->usersRegisteredGauge = $exporter->registerGauge( 38 | 'users_registered_total', 39 | 'The total number of registered users.', 40 | ['group'] 41 | ); 42 | } 43 | 44 | /** 45 | * Collect metrics data, if need be, before exporting. 46 | * 47 | * As an example, this may be used to perform time consuming database queries and set the value of a counter 48 | * or gauge. 49 | */ 50 | public function collect() 51 | { 52 | // retrieve the total number of staff users registered 53 | // eg: $totalUsers = Users::where('group', 'staff')->count(); 54 | $this->usersRegisteredGauge->set(36, ['staff']); 55 | 56 | // retrieve the total number of regular users registered 57 | // eg: $totalUsers = Users::where('group', 'regular')->count(); 58 | $this->usersRegisteredGauge->set(192, ['regular']); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/MetricsController.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 28 | $this->prometheusExporter = $prometheusExporter; 29 | } 30 | 31 | /** 32 | * @return ResponseFactory 33 | */ 34 | public function getResponseFactory() 35 | { 36 | return $this->responseFactory; 37 | } 38 | 39 | /** 40 | * @return PrometheusExporter 41 | */ 42 | public function getPrometheusExporter() 43 | { 44 | return $this->prometheusExporter; 45 | } 46 | 47 | /** 48 | * GET /metrics 49 | * 50 | * The route path is configurable in the prometheus.metrics_route_path config var, or the 51 | * PROMETHEUS_METRICS_ROUTE_PATH env var. 52 | * 53 | * @return \Symfony\Component\HttpFoundation\Response 54 | */ 55 | public function getMetrics() 56 | { 57 | $metrics = $this->prometheusExporter->export(); 58 | 59 | $renderer = new RenderTextFormat(); 60 | $result = $renderer->render($metrics); 61 | 62 | return $this->responseFactory->make($result, 200, ['Content-Type' => RenderTextFormat::MIME_TYPE]); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/PrometheusExporter.php: -------------------------------------------------------------------------------- 1 | namespace = $namespace; 33 | $this->prometheus = $prometheus; 34 | 35 | foreach ($collectors as $collector) { 36 | /* @var CollectorInterface $collector */ 37 | $this->registerCollector($collector); 38 | } 39 | } 40 | 41 | /** 42 | * Return the metric namespace. 43 | * 44 | * @return string 45 | */ 46 | public function getNamespace() 47 | { 48 | return $this->namespace; 49 | } 50 | 51 | /** 52 | * Return the CollectorRegistry. 53 | * 54 | * @return CollectorRegistry 55 | */ 56 | public function getPrometheus() 57 | { 58 | return $this->prometheus; 59 | } 60 | 61 | /** 62 | * Register a collector. 63 | * 64 | * @param CollectorInterface $collector 65 | */ 66 | public function registerCollector(CollectorInterface $collector) 67 | { 68 | $name = $collector->getName(); 69 | 70 | if (!isset($this->collectors[$name])) { 71 | $this->collectors[$name] = $collector; 72 | 73 | $collector->registerMetrics($this); 74 | } 75 | } 76 | 77 | /** 78 | * Return all collectors. 79 | * 80 | * @return array 81 | */ 82 | public function getCollectors() 83 | { 84 | return $this->collectors; 85 | } 86 | 87 | /** 88 | * Return a collector by name. 89 | * 90 | * @param string $name 91 | * 92 | * @return CollectorInterface 93 | */ 94 | public function getCollector($name) 95 | { 96 | if (!isset($this->collectors[$name])) { 97 | throw new InvalidArgumentException(sprintf('The collector "%s" is not registered.', $name)); 98 | } 99 | 100 | return $this->collectors[$name]; 101 | } 102 | 103 | /** 104 | * Register a counter. 105 | * 106 | * @param string $name 107 | * @param string $help 108 | * @param array $labels 109 | * 110 | * @return \Prometheus\Counter 111 | * 112 | * @see https://prometheus.io/docs/concepts/metric_types/#counter 113 | */ 114 | public function registerCounter($name, $help, $labels = []) 115 | { 116 | return $this->prometheus->registerCounter($this->namespace, $name, $help, $labels); 117 | } 118 | 119 | /** 120 | * Return a counter. 121 | * 122 | * @param string $name 123 | * 124 | * @return \Prometheus\Counter 125 | */ 126 | public function getCounter($name) 127 | { 128 | return $this->prometheus->getCounter($this->namespace, $name); 129 | } 130 | 131 | /** 132 | * Return or register a counter. 133 | * 134 | * @param string $name 135 | * @param string $help 136 | * @param array $labels 137 | * 138 | * @return \Prometheus\Counter 139 | * 140 | * @see https://prometheus.io/docs/concepts/metric_types/#counter 141 | */ 142 | public function getOrRegisterCounter($name, $help, $labels = []) 143 | { 144 | return $this->prometheus->getOrRegisterCounter($this->namespace, $name, $help, $labels); 145 | } 146 | 147 | /** 148 | * Register a gauge. 149 | * 150 | * @param string $name 151 | * @param string $help 152 | * @param array $labels 153 | * 154 | * @return \Prometheus\Gauge 155 | * 156 | * @see https://prometheus.io/docs/concepts/metric_types/#gauge 157 | */ 158 | public function registerGauge($name, $help, $labels = []) 159 | { 160 | return $this->prometheus->registerGauge($this->namespace, $name, $help, $labels); 161 | } 162 | 163 | /** 164 | * Return a gauge. 165 | * 166 | * @param string $name 167 | * 168 | * @return \Prometheus\Gauge 169 | */ 170 | public function getGauge($name) 171 | { 172 | return $this->prometheus->getGauge($this->namespace, $name); 173 | } 174 | 175 | /** 176 | * Return or register a gauge. 177 | * 178 | * @param string $name 179 | * @param string $help 180 | * @param array $labels 181 | * 182 | * @return \Prometheus\Gauge 183 | * 184 | * @see https://prometheus.io/docs/concepts/metric_types/#gauge 185 | */ 186 | public function getOrRegisterGauge($name, $help, $labels = []) 187 | { 188 | return $this->prometheus->getOrRegisterGauge($this->namespace, $name, $help, $labels); 189 | } 190 | 191 | /** 192 | * Register a histogram. 193 | * 194 | * @param string $name 195 | * @param string $help 196 | * @param array $labels 197 | * @param array $buckets 198 | * 199 | * @return \Prometheus\Histogram 200 | * 201 | * @see https://prometheus.io/docs/concepts/metric_types/#histogram 202 | */ 203 | public function registerHistogram($name, $help, $labels = [], $buckets = null) 204 | { 205 | return $this->prometheus->registerHistogram($this->namespace, $name, $help, $labels, $buckets); 206 | } 207 | 208 | /** 209 | * Return a histogram. 210 | * 211 | * @param string $name 212 | * 213 | * @return \Prometheus\Histogram 214 | */ 215 | public function getHistogram($name) 216 | { 217 | return $this->prometheus->getHistogram($this->namespace, $name); 218 | } 219 | 220 | /** 221 | * Return or register a histogram. 222 | * 223 | * @param string $name 224 | * @param string $help 225 | * @param array $labels 226 | * @param array $buckets 227 | * 228 | * @return \Prometheus\Histogram 229 | * 230 | * @see https://prometheus.io/docs/concepts/metric_types/#histogram 231 | */ 232 | public function getOrRegisterHistogram($name, $help, $labels = [], $buckets = null) 233 | { 234 | return $this->prometheus->getOrRegisterHistogram($this->namespace, $name, $help, $labels, $buckets); 235 | } 236 | 237 | /** 238 | * Export the metrics from all collectors. 239 | * 240 | * @return \Prometheus\MetricFamilySamples[] 241 | */ 242 | public function export() 243 | { 244 | foreach ($this->collectors as $collector) { 245 | /* @var CollectorInterface $collector */ 246 | $collector->collect(); 247 | } 248 | 249 | return $this->prometheus->getMetricFamilySamples(); 250 | } 251 | } 252 | -------------------------------------------------------------------------------- /src/PrometheusFacade.php: -------------------------------------------------------------------------------- 1 | publishes([ 18 | __DIR__ . '/../config/prometheus.php' => config_path('prometheus.php'), 19 | ]); 20 | 21 | if (config('prometheus.metrics_route_enabled')) { 22 | $this->loadRoutesFrom(__DIR__ . '/routes.php'); 23 | } 24 | 25 | $exporter = $this->app->make(PrometheusExporter::class); /* @var PrometheusExporter $exporter */ 26 | foreach (config('prometheus.collectors') as $class) { 27 | $collector = $this->app->make($class); 28 | $exporter->registerCollector($collector); 29 | } 30 | } 31 | 32 | /** 33 | * Register bindings in the container. 34 | */ 35 | public function register() 36 | { 37 | $this->mergeConfigFrom(__DIR__ . '/../config/prometheus.php', 'prometheus'); 38 | 39 | $this->app->singleton(PrometheusExporter::class, function ($app) { 40 | $adapter = $app['prometheus.storage_adapter']; 41 | $prometheus = new CollectorRegistry($adapter); 42 | return new PrometheusExporter(config('prometheus.namespace'), $prometheus); 43 | }); 44 | $this->app->alias(PrometheusExporter::class, 'prometheus'); 45 | 46 | $this->app->bind('prometheus.storage_adapter_factory', function () { 47 | return new StorageAdapterFactory(); 48 | }); 49 | 50 | $this->app->bind(Adapter::class, function ($app) { 51 | $factory = $app['prometheus.storage_adapter_factory']; /** @var StorageAdapterFactory $factory */ 52 | $driver = config('prometheus.storage_adapter'); 53 | $configs = config('prometheus.storage_adapters'); 54 | $config = Arr::get($configs, $driver, []); 55 | return $factory->make($driver, $config); 56 | }); 57 | $this->app->alias(Adapter::class, 'prometheus.storage_adapter'); 58 | } 59 | 60 | /** 61 | * Get the services provided by the provider. 62 | * 63 | * @return array 64 | */ 65 | public function provides() 66 | { 67 | return [ 68 | 'prometheus', 69 | 'prometheus.storage_adapter_factory', 70 | 'prometheus.storage_adapter', 71 | ]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/StorageAdapterFactory.php: -------------------------------------------------------------------------------- 1 | makeRedisAdapter($config); 28 | case 'apc': 29 | return new APC(); 30 | } 31 | 32 | throw new InvalidArgumentException(sprintf('The driver [%s] is not supported.', $driver)); 33 | } 34 | 35 | /** 36 | * Factory a redis storage adapter. 37 | * 38 | * @param array $config 39 | * 40 | * @return Redis 41 | */ 42 | protected function makeRedisAdapter(array $config) 43 | { 44 | if (isset($config['prefix'])) { 45 | Redis::setPrefix($config['prefix']); 46 | } 47 | return new Redis($config); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/routes.php: -------------------------------------------------------------------------------- 1 | name($name); 11 | } 12 | 13 | $middleware = config('prometheus.metrics_route_middleware'); 14 | 15 | if ($middleware) { 16 | $route->middleware($middleware); 17 | } 18 | -------------------------------------------------------------------------------- /tests/MetricsControllerTest.php: -------------------------------------------------------------------------------- 1 | assertSame($responseFactory, $controller->getResponseFactory()); 21 | $this->assertSame($exporter, $controller->getPrometheusExporter()); 22 | } 23 | 24 | public function testGetMetrics() 25 | { 26 | $response = Mockery::mock(Response::class); 27 | 28 | $responseFactory = Mockery::mock(ResponseFactory::class); 29 | $responseFactory->shouldReceive('make') 30 | ->once() 31 | ->withArgs([ 32 | "\n", 33 | 200, 34 | ['Content-Type' => RenderTextFormat::MIME_TYPE], 35 | ]) 36 | ->andReturn($response); 37 | 38 | $exporter = Mockery::mock(PrometheusExporter::class); 39 | $exporter->shouldReceive('export') 40 | ->once() 41 | ->andReturn([]); 42 | 43 | $controller = new MetricsController($responseFactory, $exporter); 44 | 45 | $r = $controller->getMetrics(); 46 | $this->assertSame($response, $r); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/PrometheusExporterTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('app', $exporter->getNamespace()); 21 | $this->assertSame($registry, $exporter->getPrometheus()); 22 | } 23 | 24 | public function testConstructWithCollectors() 25 | { 26 | $collector1 = Mockery::mock(CollectorInterface::class); 27 | $collector1->shouldReceive('getName') 28 | ->once() 29 | ->andReturn('users'); 30 | $collector1->shouldReceive('registerMetrics') 31 | ->once() 32 | ->with(Mockery::type(PrometheusExporter::class)); 33 | $collector2 = Mockery::mock(CollectorInterface::class); 34 | $collector2->shouldReceive('getName') 35 | ->once() 36 | ->andReturn('search_requests'); 37 | $collector2->shouldReceive('registerMetrics') 38 | ->once() 39 | ->with(Mockery::type(PrometheusExporter::class)); 40 | 41 | $registry = Mockery::mock(CollectorRegistry::class); 42 | $exporter = new PrometheusExporter('app', $registry, [$collector1, $collector2]); 43 | 44 | $collectors = $exporter->getCollectors(); 45 | $this->assertCount(2, $collectors); 46 | $this->assertArrayHasKey('users', $collectors); 47 | $this->assertArrayHasKey('search_requests', $collectors); 48 | $this->assertSame($collector1, $collectors['users']); 49 | $this->assertSame($collector2, $collectors['search_requests']); 50 | } 51 | 52 | public function testRegisterCollector() 53 | { 54 | $registry = Mockery::mock(CollectorRegistry::class); 55 | $exporter = new PrometheusExporter('app', $registry); 56 | 57 | $this->assertEmpty($exporter->getCollectors()); 58 | 59 | $collector = Mockery::mock(CollectorInterface::class); 60 | $collector->shouldReceive('getName') 61 | ->once() 62 | ->andReturn('users'); 63 | $collector->shouldReceive('registerMetrics') 64 | ->once() 65 | ->with($exporter); 66 | 67 | $exporter->registerCollector($collector); 68 | 69 | $collectors = $exporter->getCollectors(); 70 | $this->assertCount(1, $collectors); 71 | $this->assertArrayHasKey('users', $collectors); 72 | $this->assertSame($collector, $collectors['users']); 73 | } 74 | 75 | public function testRegisterCollectorWhenCollectorIsAlreadyRegistered() 76 | { 77 | $registry = Mockery::mock(CollectorRegistry::class); 78 | $exporter = new PrometheusExporter('app', $registry); 79 | 80 | $this->assertEmpty($exporter->getCollectors()); 81 | 82 | $collector = Mockery::mock(CollectorInterface::class); 83 | $collector->shouldReceive('getName') 84 | ->once() 85 | ->andReturn('users'); 86 | $collector->shouldReceive('registerMetrics') 87 | ->once() 88 | ->with($exporter); 89 | 90 | $exporter->registerCollector($collector); 91 | 92 | $collectors = $exporter->getCollectors(); 93 | $this->assertCount(1, $collectors); 94 | $this->assertArrayHasKey('users', $collectors); 95 | $this->assertSame($collector, $collectors['users']); 96 | 97 | $exporter->registerCollector($collector); 98 | 99 | $collectors = $exporter->getCollectors(); 100 | $this->assertCount(1, $collectors); 101 | $this->assertArrayHasKey('users', $collectors); 102 | $this->assertSame($collector, $collectors['users']); 103 | } 104 | 105 | public function testGetCollector() 106 | { 107 | $registry = Mockery::mock(CollectorRegistry::class); 108 | $exporter = new PrometheusExporter('app', $registry); 109 | 110 | $this->assertEmpty($exporter->getCollectors()); 111 | 112 | $collector = Mockery::mock(CollectorInterface::class); 113 | $collector->shouldReceive('getName') 114 | ->once() 115 | ->andReturn('users'); 116 | $collector->shouldReceive('registerMetrics') 117 | ->once() 118 | ->with($exporter); 119 | 120 | $exporter->registerCollector($collector); 121 | 122 | $c = $exporter->getCollector('users'); 123 | $this->assertSame($collector, $c); 124 | } 125 | 126 | public function testGetCollectorWhenCollectorIsNotRegistered() 127 | { 128 | $this->expectException(\InvalidArgumentException::class); 129 | $this->expectExceptionMessage('The collector "test" is not registered.'); 130 | 131 | $registry = Mockery::mock(CollectorRegistry::class); 132 | $exporter = new PrometheusExporter('app', $registry); 133 | 134 | $exporter->getCollector('test'); 135 | } 136 | 137 | public function testRegisterCounter() 138 | { 139 | $counter = Mockery::mock(Counter::class); 140 | 141 | $registry = Mockery::mock(CollectorRegistry::class); 142 | $registry->shouldReceive('registerCounter') 143 | ->once() 144 | ->withArgs([ 145 | 'app', 146 | 'search_requests_total', 147 | 'The total number of search requests.', 148 | ['request_type'], 149 | ]) 150 | ->andReturn($counter); 151 | 152 | $exporter = new PrometheusExporter('app', $registry); 153 | 154 | $c = $exporter->registerCounter( 155 | 'search_requests_total', 156 | 'The total number of search requests.', 157 | ['request_type'] 158 | ); 159 | $this->assertSame($counter, $c); 160 | } 161 | 162 | public function testGetCounter() 163 | { 164 | $counter = Mockery::mock(Counter::class); 165 | 166 | $registry = Mockery::mock(CollectorRegistry::class); 167 | $registry->shouldReceive('getCounter') 168 | ->once() 169 | ->withArgs([ 170 | 'app', 171 | 'search_requests_total', 172 | ]) 173 | ->andReturn($counter); 174 | 175 | $exporter = new PrometheusExporter('app', $registry); 176 | 177 | $c = $exporter->getCounter('search_requests_total'); 178 | $this->assertSame($counter, $c); 179 | } 180 | 181 | public function testGetOrRegisterCounter() 182 | { 183 | $counter = Mockery::mock(Counter::class); 184 | 185 | $registry = Mockery::mock(CollectorRegistry::class); 186 | $registry->shouldReceive('getOrRegisterCounter') 187 | ->once() 188 | ->withArgs([ 189 | 'app', 190 | 'search_requests_total', 191 | 'The total number of search requests.', 192 | ['request_type'], 193 | ]) 194 | ->andReturn($counter); 195 | 196 | $exporter = new PrometheusExporter('app', $registry); 197 | 198 | $c = $exporter->getOrRegisterCounter( 199 | 'search_requests_total', 200 | 'The total number of search requests.', 201 | ['request_type'] 202 | ); 203 | $this->assertSame($counter, $c); 204 | } 205 | 206 | public function testRegisterGauge() 207 | { 208 | $gauge = Mockery::mock(Gauge::class); 209 | 210 | $registry = Mockery::mock(CollectorRegistry::class); 211 | $registry->shouldReceive('registerGauge') 212 | ->once() 213 | ->withArgs([ 214 | 'app', 215 | 'users_online_total', 216 | 'The total number of users online.', 217 | ['group'], 218 | ]) 219 | ->andReturn($gauge); 220 | 221 | $exporter = new PrometheusExporter('app', $registry); 222 | 223 | $g = $exporter->registerGauge( 224 | 'users_online_total', 225 | 'The total number of users online.', 226 | ['group'] 227 | ); 228 | $this->assertSame($gauge, $g); 229 | } 230 | 231 | public function testGetGauge() 232 | { 233 | $gauge = Mockery::mock(Gauge::class); 234 | 235 | $registry = Mockery::mock(CollectorRegistry::class); 236 | $registry->shouldReceive('getGauge') 237 | ->once() 238 | ->withArgs([ 239 | 'app', 240 | 'users_online_total', 241 | ]) 242 | ->andReturn($gauge); 243 | 244 | $exporter = new PrometheusExporter('app', $registry); 245 | 246 | $g = $exporter->getGauge('users_online_total'); 247 | $this->assertSame($gauge, $g); 248 | } 249 | 250 | public function testGetOrRegisterGauge() 251 | { 252 | $gauge = Mockery::mock(Gauge::class); 253 | 254 | $registry = Mockery::mock(CollectorRegistry::class); 255 | $registry->shouldReceive('getOrRegisterGauge') 256 | ->once() 257 | ->withArgs([ 258 | 'app', 259 | 'users_online_total', 260 | 'The total number of users online.', 261 | ['group'], 262 | ]) 263 | ->andReturn($gauge); 264 | 265 | $exporter = new PrometheusExporter('app', $registry); 266 | 267 | $g = $exporter->getOrRegisterGauge( 268 | 'users_online_total', 269 | 'The total number of users online.', 270 | ['group'] 271 | ); 272 | $this->assertSame($gauge, $g); 273 | } 274 | 275 | public function testRegisterHistogram() 276 | { 277 | $histogram = Mockery::mock(Histogram::class); 278 | 279 | $registry = Mockery::mock(CollectorRegistry::class); 280 | $registry->shouldReceive('registerHistogram') 281 | ->once() 282 | ->withArgs([ 283 | 'app', 284 | 'response_time_seconds', 285 | 'The response time of a request.', 286 | ['request_type'], 287 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0], 288 | ]) 289 | ->andReturn($histogram); 290 | 291 | $exporter = new PrometheusExporter('app', $registry); 292 | 293 | $h = $exporter->registerHistogram( 294 | 'response_time_seconds', 295 | 'The response time of a request.', 296 | ['request_type'], 297 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0] 298 | ); 299 | $this->assertSame($histogram, $h); 300 | } 301 | 302 | public function testGetHistogram() 303 | { 304 | $histogram = Mockery::mock(Histogram::class); 305 | 306 | $registry = Mockery::mock(CollectorRegistry::class); 307 | $registry->shouldReceive('getHistogram') 308 | ->once() 309 | ->withArgs([ 310 | 'app', 311 | 'response_time_seconds', 312 | ]) 313 | ->andReturn($histogram); 314 | 315 | $exporter = new PrometheusExporter('app', $registry); 316 | 317 | $h = $exporter->getHistogram('response_time_seconds'); 318 | $this->assertSame($histogram, $h); 319 | } 320 | 321 | public function testGetOrRegisterHistogram() 322 | { 323 | $histogram = Mockery::mock(Histogram::class); 324 | 325 | $registry = Mockery::mock(CollectorRegistry::class); 326 | $registry->shouldReceive('getOrRegisterHistogram') 327 | ->once() 328 | ->withArgs([ 329 | 'app', 330 | 'response_time_seconds', 331 | 'The response time of a request.', 332 | ['request_type'], 333 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0], 334 | ]) 335 | ->andReturn($histogram); 336 | 337 | $exporter = new PrometheusExporter('app', $registry); 338 | 339 | $h = $exporter->getOrRegisterHistogram( 340 | 'response_time_seconds', 341 | 'The response time of a request.', 342 | ['request_type'], 343 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0] 344 | ); 345 | $this->assertSame($histogram, $h); 346 | } 347 | 348 | public function testExport() 349 | { 350 | $samples = ['meh']; 351 | 352 | $registry = Mockery::mock(CollectorRegistry::class); 353 | $registry->shouldReceive('getMetricFamilySamples') 354 | ->once() 355 | ->andReturn($samples); 356 | 357 | $exporter = new PrometheusExporter('app', $registry); 358 | 359 | $collector1 = Mockery::mock(CollectorInterface::class); 360 | $collector1->shouldReceive('getName') 361 | ->once() 362 | ->andReturn('users'); 363 | $collector1->shouldReceive('registerMetrics') 364 | ->once() 365 | ->with($exporter); 366 | $collector1->shouldReceive('collect') 367 | ->once(); 368 | 369 | $exporter->registerCollector($collector1); 370 | 371 | $collector2 = Mockery::mock(CollectorInterface::class); 372 | $collector2->shouldReceive('getName') 373 | ->once() 374 | ->andReturn('search_requests'); 375 | $collector2->shouldReceive('registerMetrics') 376 | ->once() 377 | ->with($exporter); 378 | $collector2->shouldReceive('collect') 379 | ->once(); 380 | 381 | $exporter->registerCollector($collector2); 382 | 383 | $s = $exporter->export(); 384 | $this->assertSame($samples, $s); 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /tests/StorageAdapterFactoryTest.php: -------------------------------------------------------------------------------- 1 | make('memory'); 17 | $this->assertInstanceOf(InMemory::class, $adapter); 18 | } 19 | 20 | public function testMakeApcAdapter() 21 | { 22 | $factory = new StorageAdapterFactory(); 23 | $adapter = $factory->make('apc'); 24 | $this->assertInstanceOf(APC::class, $adapter); 25 | } 26 | 27 | public function testMakeRedisAdapter() 28 | { 29 | $factory = new StorageAdapterFactory(); 30 | $adapter = $factory->make('redis'); 31 | $this->assertInstanceOf(Redis::class, $adapter); 32 | } 33 | 34 | public function testMakeInvalidAdapter() 35 | { 36 | $this->expectException(\InvalidArgumentException::class); 37 | $this->expectExceptionMessage('The driver [moo] is not supported.'); 38 | 39 | $factory = new StorageAdapterFactory(); 40 | $factory->make('moo'); 41 | } 42 | } 43 | --------------------------------------------------------------------------------