├── .gitattributes ├── .gitignore ├── .php_cs ├── .styleci.yml ├── .travis.yml ├── LICENSE ├── README.md ├── changelog.md ├── composer.json ├── config └── prometheus.php ├── phpunit.php ├── phpunit.xml ├── src ├── CollectorInterface.php ├── DatabaseServiceProvider.php ├── GuzzleMiddleware.php ├── GuzzleServiceProvider.php ├── MetricsController.php ├── PrometheusExporter.php ├── PrometheusFacade.php ├── PrometheusServiceProvider.php ├── RouteMiddleware.php └── StorageAdapterFactory.php └── tests ├── DatabaseServiceProviderTest.php ├── GuzzleMiddlewareTest.php ├── GuzzleServiceProviderTest.php ├── MetricsControllerTest.php ├── PrometheusExporterTest.php ├── PrometheusServiceProviderTest.php ├── RouteMiddlewareTest.php └── StorageAdapterFactoryTest.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /examples export-ignore 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.lock 3 | vendor 4 | bin 5 | coverage 6 | coverage.xml 7 | .php_cs.cache 8 | examples/lumen-app/vendor 9 | .phpunit.result.cache 10 | -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | files() 4 | ->name('*.php') 5 | ->in(__DIR__ . '/src') 6 | ->in(__DIR__ . '/tests') 7 | ; 8 | return PhpCsFixer\Config::create() 9 | ->setUsingCache(true) 10 | ->setRiskyAllowed(true) 11 | ->setRules([ 12 | // default rules 13 | '@PSR2' => true, 14 | // Alias 15 | // ArrayNotation 16 | 'array_syntax' => ['syntax' => 'short'], 17 | 'trailing_comma_in_multiline_array' => true, 18 | // Basic 19 | // Casing 20 | // CastNotation 21 | 'cast_spaces' => true, 22 | // ClassNotation 23 | // Comment 24 | 'no_empty_comment' => true, 25 | // ControlStructure 26 | 'no_useless_else' => true, 27 | // DoctrineAnnotation 28 | // FunctionNotation 29 | 'return_type_declaration' => ['space_before' => 'one'], 30 | // Import 31 | 'no_leading_import_slash' => true, 32 | 'no_unused_imports' => true, 33 | 'ordered_imports' => true, 34 | // LanguageConstruct 35 | 'declare_equal_normalize' => ['space' => 'single'], 36 | 'dir_constant' => true, 37 | // ListNotation 38 | // NamespaceNotation 39 | // Naming 40 | 'no_homoglyph_names' => true, 41 | // Operator 42 | 'binary_operator_spaces' => true, 43 | 'concat_space' => ['spacing' => 'one'], 44 | // PhpTag 45 | 'blank_line_after_opening_tag' => true, 46 | // PhpUnit 47 | // Phpdoc 48 | 'phpdoc_align' => true, 49 | 'no_empty_phpdoc' => true, 50 | 'phpdoc_scalar' => true, 51 | 'phpdoc_separation' => true, 52 | 'phpdoc_types_order' => ['null_adjustment' => 'always_last', 'sort_algorithm' => 'none'], 53 | // ReturnNotation 54 | // Semicolon 55 | 'no_empty_statement' => true, 56 | // Strict 57 | // StringNotation 58 | // Whitespace 59 | 'blank_line_before_statement' => ['statements' => ['declare', 'return']], 60 | 'method_chaining_indentation' => true, 61 | 'no_extra_consecutive_blank_lines' => true, 62 | 'no_spaces_around_offset' => true, 63 | 'no_whitespace_in_blank_line' => true, 64 | ]) 65 | ->setFinder($finder) 66 | ; 67 | -------------------------------------------------------------------------------- /.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 | - 8.0 5 | 6 | before_install: 7 | - echo "extension = redis.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 8 | 9 | before_script: 10 | - composer install 11 | 12 | script: ./vendor/bin/phpunit --configuration phpunit.xml 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Taxibeat 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 | # Pyr 2 | 3 | A prometheus exporter package for Lumen and Laravel. 4 | 5 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 6 | 7 | ## Introduction 8 | 9 | Prometheus is a time-series database with a UI and sophisticated querying language (PromQL) that can scrape metrics, counters, gauges and histograms over HTTP. 10 | 11 | This package is a wrapper bridging [beatlabs/prometheus_client_php](https://github.com/beatlabs/prometheus_client_php) (a fork of [jimdo/prometheus_client_php](https://github.com/jimdo/prometheus_client_php)) into Lumen and Laravel. 12 | 13 | ## Example 14 | 15 | Head to [examples/lumen-app](https://github.com/beatlabs/pyr/tree/example-application/examples/lumen-app) 16 | to check out our awesome example application. 17 | To get it you'll have to clone the [Pyr](https://github.com/beatlabs/pyr/) repo, as the example 18 | is not included when downloaded from composer. 19 | 20 | The example is a full project containing it's own `README.md` so you can check the 21 | library's functionality and the way it's intended to be used. 22 | 23 | 24 | ## Installation 25 | 26 | Add the repository to composer.json 27 | ```composer.json 28 | "repositories": [ 29 | { 30 | "type": "vcs", 31 | "url": "https://github.com/beatlabs/pyr" 32 | } 33 | ], 34 | ``` 35 | 36 | Install the package via composer 37 | ```bash 38 | composer require beatlabs/pyr 39 | ``` 40 | 41 | After that you may enable facades and register the facade in your application's `bootstrap/app.php` 42 | ```php 43 | $userAliases = [ 44 | // ... 45 | Beat\Pyr\PrometheusFacade::class => 'Prometheus', 46 | ]; 47 | $app->withFacades(true, $userAliases); 48 | ``` 49 | 50 | Then you should register the service provider in `bootstrap/app.php` 51 | ```php 52 | $app->register(Beat\Pyr\PrometheusServiceProvider::class); 53 | ``` 54 | 55 | Please see below for instructions on how to enable metrics on Application routes, Guzzle calls and SQL queries. 56 | 57 | ## Configuration 58 | 59 | The package has a default configuration which uses the following environment variables. 60 | ``` 61 | PYR_NAMESPACE=app 62 | 63 | PYR_METRICS_ROUTE_ENABLED=true 64 | PYR_METRICS_ROUTE_PATH=metrics 65 | PYR_METRICS_ROUTE_MIDDLEWARE=null 66 | PYR_COLLECT_FULL_SQL_QUERY=true 67 | PYR_STORAGE_ADAPTER=memory 68 | 69 | PYR_REDIS_HOST=localhost 70 | PYR_REDIS_PORT=6379 71 | PYR_REDIS_TIMEOUT=0.1 72 | PYR_REDIS_READ_TIMEOUT=10 73 | PYR_REDIS_PERSISTENT_CONNECTIONS=0 74 | PYR_REDIS_PREFIX=PYR_ 75 | ``` 76 | 77 | To customize the configuration values you can either override the environment variables above (usually this is done in your application's `.env` file), or you can copy the included [prometheus.php](config/prometheus.php) 78 | to `config/prometheus.php`, edit it and use it in your application as follows: 79 | ```php 80 | $app->loadComponent('prometheus', [ 81 | Beat\Pyr\PrometheusServiceProvider::class 82 | ]); 83 | ``` 84 | 85 | ## Metrics 86 | 87 | The package allows you to observe metrics on: 88 | 89 | * Application routes. Metrics on request method, request path and status code. 90 | * Guzzle calls. Metrics on request method, URI and status code. 91 | * SQL queries. Metrics on SQL query and query type. 92 | 93 | In order to observe metrics in application routes (the time between a request and response), 94 | you should register the following middleware in your application's `bootstrap/app.php`: 95 | ```php 96 | $app->middleware([ 97 | Beat\Pyr\RouteMiddleware::class, 98 | ]); 99 | ``` 100 | 101 | The labels exported are 102 | 103 | ```php 104 | [ 105 | 'method', 106 | 'route', 107 | 'status_code', 108 | ] 109 | ``` 110 | 111 | To observe Guzzle metrics, you should register the following provider in `bootstrap/app.php`: 112 | ```php 113 | $app->register(Beat\Pyr\GuzzleServiceProvider::class); 114 | ``` 115 | 116 | The labels exported are 117 | 118 | ```php 119 | [ 120 | 'method', 121 | 'external_endpoint', 122 | 'status_code' 123 | ] 124 | ``` 125 | 126 | To observe SQL metrics, you should register the following provider in `bootstrap/app.php`: 127 | ```php 128 | $app->register(Beat\Pyr\DatabaseServiceProvider::class); 129 | ``` 130 | 131 | The labels exported are 132 | 133 | ```php 134 | [ 135 | 'query', 136 | 'query_type', 137 | ] 138 | ``` 139 | 140 | Note: you can disable logging the full query by turning off the configuration of `PYR_COLLECT_FULL_SQL_QUERY`. 141 | 142 | ### Storage Adapters 143 | 144 | The storage adapter is used to persist metrics across requests. The `memory` adapter is enabled by default, meaning 145 | data will only be persisted across the current request. 146 | 147 | We recommend using the `redis` or `apc` adapter in production 148 | environments. Of course your installation has to provide a Redis or APC implementation. 149 | 150 | The `PYR_STORAGE_ADAPTER` environment variable is used to specify the storage adapter. 151 | 152 | If `redis` is used, the `PYR_REDIS_HOST` and `PYR_REDIS_PORT` vars also need to be configured. Optionally you can change the `PYR_REDIS_TIMEOUT`, `PYR_REDIS_READ_TIMEOUT` and `PYR_REDIS_PERSISTENT_CONNECTIONS` variables. 153 | 154 | ## Exporting Metrics 155 | 156 | The package adds a `/metrics` endpoint, enabled by default, which exposes all metrics gathered by collectors. 157 | 158 | This can be turned on/off using the `PYR_METRICS_ROUTE_ENABLED` environment variable, 159 | and can also be changed using the `PYR_METRICS_ROUTE_PATH` environment variable. 160 | 161 | ## Collectors 162 | 163 | A collector is a class, implementing the [CollectorInterface](src/CollectorInterface.php), which is responsible for 164 | collecting data for one or many metrics. 165 | 166 | Please see the [Example](#Collector) included below. 167 | 168 | You can auto-load your collectors by adding them to the `collectors` array in the `prometheus.php` config. 169 | 170 | ## Examples 171 | 172 | ### Example usage 173 | 174 | This is an example usage for a Lumen application 175 | 176 | ```php 177 | // retrieve the exporter (you can also use app('prometheus') or Prometheus::getFacadeRoot()) 178 | $exporter = app(\Beat\Pyr\PrometheusExporter::class); 179 | 180 | // register a new collector 181 | $collector = new \My\New\Collector(); 182 | $exporter->registerCollector($collector); 183 | 184 | // retrieve all collectors 185 | var_dump($exporter->getCollectors()); 186 | 187 | // retrieve a collector by name 188 | $collector = $exporter->getCollector('user'); 189 | 190 | // export all metrics 191 | // this is called automatically when the /metrics end-point is hit 192 | var_dump($exporter->export()); 193 | 194 | // the following methods can be used to create and interact with counters, gauges and histograms directly 195 | // these methods will typically be called by collectors, but can be used to register any custom metrics directly, 196 | // without the need of a collector 197 | 198 | // create a counter 199 | $counter = $exporter->registerCounter('search_requests_total', 'The total number of search requests.'); 200 | $counter->inc(); // increment by 1 201 | $counter->incBy(2); 202 | 203 | // create a counter (with labels) 204 | $counter = $exporter->registerCounter('search_requests_total', 'The total number of search requests.', ['request_type']); 205 | $counter->inc(['GET']); // increment by 1 206 | $counter->incBy(2, ['GET']); 207 | 208 | // retrieve a counter 209 | $counter = $exporter->getCounter('search_requests_total'); 210 | 211 | // create a gauge 212 | $gauge = $exporter->registerGauge('users_online_total', 'The total number of users online.'); 213 | $gauge->inc(); // increment by 1 214 | $gauge->incBy(2); 215 | $gauge->dec(); // decrement by 1 216 | $gauge->decBy(2); 217 | $gauge->set(36); 218 | 219 | // create a gauge (with labels) 220 | $gauge = $exporter->registerGauge('users_online_total', 'The total number of users online.', ['group']); 221 | $gauge->inc(['staff']); // increment by 1 222 | $gauge->incBy(2, ['staff']); 223 | $gauge->dec(['staff']); // decrement by 1 224 | $gauge->decBy(2, ['staff']); 225 | $gauge->set(36, ['staff']); 226 | 227 | // retrieve a gauge 228 | $counter = $exporter->getGauge('users_online_total'); 229 | 230 | // create a histogram 231 | $histogram = $exporter->registerHistogram( 232 | 'response_time_seconds', 233 | 'The response time of a request.', 234 | [], 235 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0] 236 | ); 237 | // the buckets must be in asc order 238 | // 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 239 | $histogram->observe(5.0); 240 | 241 | // create a histogram (with labels) 242 | $histogram = $exporter->registerHistogram( 243 | 'response_time_seconds', 244 | 'The response time of a request.', 245 | ['request_type'], 246 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0] 247 | ); 248 | // the buckets must be in asc order 249 | // 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 250 | $histogram->observe(5.0, ['GET']); 251 | 252 | // retrieve a histogram 253 | $counter = $exporter->getHistogram('response_time_seconds'); 254 | ``` 255 | 256 | ### Collector 257 | 258 | This is an example collector implementation: 259 | 260 | ```php 261 | registerCounter('search_requests_total', 'The total number of search requests.'); 293 | * ``` 294 | * 295 | * @param PrometheusExporter $exporter 296 | */ 297 | public function registerMetrics(PrometheusExporter $exporter) : void 298 | { 299 | $this->usersRegisteredGauge = $exporter->registerGauge( 300 | 'users_registered_total', 301 | 'The total number of registered users.', 302 | ['group'] 303 | ); 304 | } 305 | 306 | /** 307 | * Collect metrics data, if need be, before exporting. 308 | * 309 | * As an example, this may be used to perform time consuming database queries and set the value of a counter 310 | * or gauge. 311 | */ 312 | public function collect() : void 313 | { 314 | // retrieve the total number of staff users registered 315 | // eg: $totalUsers = Users::where('group', 'staff')->count(); 316 | $this->usersRegisteredGauge->set(36, ['staff']); 317 | 318 | // retrieve the total number of regular users registered 319 | // eg: $totalUsers = Users::where('group', 'regular')->count(); 320 | $this->usersRegisteredGauge->set(192, ['regular']); 321 | } 322 | } 323 | ``` 324 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 1.0.1 - 2017-08-30 4 | 5 | * Fix config retrieval of `prometheus.storage_adapters` 6 | 7 | ## 1.0.0 - 2017-07-27 8 | 9 | * Initial release 10 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "beatlabs/pyr", 3 | "description": "A Prometheus bridge for Lumen", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Beat" 8 | } 9 | ], 10 | "require": { 11 | "php": "^8.0", 12 | "illuminate/support": "^9.0", 13 | "illuminate/routing": "^9.0", 14 | "beatlabs/prometheus_client_php": "dev-master#v0.9.4" 15 | }, 16 | "repositories": [ 17 | { 18 | "type": "vcs", 19 | "url": "https://github.com/beatlabs/prometheus_client_php" 20 | } 21 | ], 22 | "autoload": { 23 | "psr-4": { 24 | "Beat\\Pyr\\": "src/", 25 | "Tests\\": "tests/" 26 | } 27 | }, 28 | "require-dev": { 29 | "phpunit/phpunit": "^9.5", 30 | "mockery/mockery": "^1.0", 31 | "friendsofphp/php-cs-fixer": "^3.8", 32 | "orchestra/testbench": "^7.5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /config/prometheus.php: -------------------------------------------------------------------------------- 1 | env('PYR_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('PYR_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('PYR_METRICS_ROUTE_PATH', 'metrics'), 41 | 42 | /* 43 | |-------------------------------------------------------------------------- 44 | | Storage Adapter 45 | |-------------------------------------------------------------------------- 46 | | 47 | | The storage adapter to use. 48 | | 49 | | Supported: "memory", "redis", "apc" 50 | | 51 | */ 52 | 53 | 'storage_adapter' => env('PYR_STORAGE_ADAPTER', 'memory'), 54 | 55 | /* 56 | |-------------------------------------------------------------------------- 57 | | Storage Adapters 58 | |-------------------------------------------------------------------------- 59 | | 60 | | The storage adapter configs. 61 | | 62 | */ 63 | 64 | 'storage_adapters' => [ 65 | 66 | 'redis' => [ 67 | 'host' => env('PYR_REDIS_HOST', 'localhost'), 68 | 'port' => env('PYR_REDIS_PORT', 6379), 69 | 'timeout' => env('PYR_REDIS_TIMEOUT', 0.1), 70 | 'read_timeout' => env('PYR_REDIS_READ_TIMEOUT', 10), 71 | 'persistent_connections' => env('PYR_REDIS_PERSISTENT_CONNECTIONS', false), 72 | 'prefix' => env('PYR_REDIS_PREFIX', 'PYR_'), 73 | ], 74 | 75 | ], 76 | 77 | /* 78 | |-------------------------------------------------------------------------- 79 | | Collect full SQL query 80 | |-------------------------------------------------------------------------- 81 | | 82 | | Indicates whether we should collect the full SQL query or not. 83 | | 84 | */ 85 | 86 | 'collect_full_sql_query' => env('PYR_COLLECT_FULL_SQL_QUERY', true), 87 | 88 | /* 89 | |-------------------------------------------------------------------------- 90 | | Collectors 91 | |-------------------------------------------------------------------------- 92 | | 93 | | The collectors specified here will be auto-registered in the exporter. 94 | | 95 | */ 96 | 97 | 'collectors' => [ 98 | // \Your\ExporterClass::class, 99 | ], 100 | 101 | 'default_metric_labels' => [ 102 | //Set your default env vars labels here 103 | ] 104 | ]; 105 | -------------------------------------------------------------------------------- /phpunit.php: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | ./tests/ 16 | 17 | 18 | 19 | 20 | ./src/ 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/CollectorInterface.php: -------------------------------------------------------------------------------- 1 | registerCounter('search_requests_total', 'The total number of search requests.'); 23 | * ``` 24 | * 25 | * @param PrometheusExporter $exporter 26 | */ 27 | public function registerMetrics(PrometheusExporter $exporter) : void; 28 | 29 | /** 30 | * Collect metrics data, if need be, before exporting. 31 | * 32 | * As an example, this may be used to perform time consuming database queries and set the value of a counter 33 | * or gauge. 34 | */ 35 | public function collect() : void; 36 | } 37 | -------------------------------------------------------------------------------- /src/DatabaseServiceProvider.php: -------------------------------------------------------------------------------- 1 | sql, ' ')); 19 | $labels = array_values(array_filter([ 20 | config('prometheus.collect_full_sql_query') ? $query->sql : null, 21 | $type 22 | ])); 23 | $this->app->get('prometheus.sql.histogram')->observe($query->time, $labels); 24 | }); 25 | } 26 | 27 | /** 28 | * Register bindings in the container. 29 | * 30 | * @return void 31 | */ 32 | public function register() : void 33 | { 34 | $this->app->singleton('prometheus.sql.histogram', function ($app) { 35 | return $app['prometheus']->getOrRegisterHistogram( 36 | 'mysql_query_duration', 37 | 'MySQL query duration histogram', 38 | array_values(array_filter([ 39 | config('prometheus.collect_full_sql_query') ? 'query' : null, 40 | 'query_type' 41 | ])) 42 | ); 43 | }); 44 | } 45 | 46 | /** 47 | * Get the services provided by the provider. 48 | * 49 | * @return array 50 | */ 51 | public function provides() : array 52 | { 53 | return [ 54 | 'prometheus.sql.histogram', 55 | ]; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/GuzzleMiddleware.php: -------------------------------------------------------------------------------- 1 | histogram = $histogram; 26 | } 27 | 28 | /** 29 | * Middleware that calculates the duration of a guzzle request. 30 | * After calculation it sends metrics to prometheus. 31 | * 32 | * @param callable $handler 33 | * 34 | * @return callable Returns a function that accepts the next handler. 35 | */ 36 | public function __invoke(callable $handler) : callable 37 | { 38 | return function (Request $request, array $options) use ($handler) { 39 | $start = microtime(true); 40 | return $handler($request, $options)->then( 41 | function (Response $response) use ($request, $start, $options) { 42 | $path = !empty($options['actionURI']) ? $options['actionURI'] : self::NO_ROUTE; 43 | 44 | $this->histogram->observe( 45 | microtime(true) - $start, 46 | [ 47 | $request->getMethod(), 48 | $request->getUri()->getHost(), 49 | $response->getStatusCode(), 50 | $path 51 | ] 52 | ); 53 | return $response; 54 | } 55 | ); 56 | }; 57 | } 58 | } -------------------------------------------------------------------------------- /src/GuzzleServiceProvider.php: -------------------------------------------------------------------------------- 1 | app->singleton('prometheus.guzzle.client.histogram', function ($app) { 22 | return $app['prometheus']->getOrRegisterHistogram( 23 | 'guzzle_response_duration', 24 | 'Guzzle response duration histogram', 25 | ['method', 'external_endpoint', 'status_code', 'path'] 26 | ); 27 | }); 28 | $this->app->singleton('prometheus.guzzle.handler', function ($app) { 29 | return new CurlHandler(); 30 | }); 31 | $this->app->singleton('prometheus.guzzle.middleware', function ($app) { 32 | return new GuzzleMiddleware($app['prometheus.guzzle.client.histogram']); 33 | }); 34 | $this->app->singleton('prometheus.guzzle.handler-stack', function ($app) { 35 | $stack = HandlerStack::create($app['prometheus.guzzle.handler']); 36 | $stack->push($app['prometheus.guzzle.middleware']); 37 | return $stack; 38 | }); 39 | $this->app->singleton('prometheus.guzzle.client', function ($app) { 40 | return new Client(['handler' => $app['prometheus.guzzle.handler-stack']]); 41 | }); 42 | } 43 | 44 | /** 45 | * Get the services provided by the provider. 46 | * 47 | * @return array 48 | */ 49 | public function provides() : array 50 | { 51 | return [ 52 | 'prometheus.guzzle.client', 53 | 'prometheus.guzzle.handler-stack', 54 | 'prometheus.guzzle.middleware', 55 | 'prometheus.guzzle.handler', 56 | 'prometheus.guzzle.client.histogram', 57 | ]; 58 | } 59 | } -------------------------------------------------------------------------------- /src/MetricsController.php: -------------------------------------------------------------------------------- 1 | responseFactory = $responseFactory; 31 | $this->prometheusExporter = $prometheusExporter; 32 | } 33 | 34 | /** 35 | * GET /metrics 36 | * 37 | * The route path is configurable in the prometheus.metrics_route_path config var, or the 38 | * PYR_METRICS_ROUTE_PATH env var. 39 | * 40 | * @return Response 41 | */ 42 | public function getMetrics() : Response 43 | { 44 | $metrics = $this->prometheusExporter->export(); 45 | 46 | $renderer = new RenderTextFormat(); 47 | $result = $renderer->render($metrics); 48 | 49 | return $this->responseFactory->make($result, 200, ['Content-Type' => RenderTextFormat::MIME_TYPE]); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/PrometheusExporter.php: -------------------------------------------------------------------------------- 1 | namespace = $namespace; 39 | $this->prometheus = $prometheus; 40 | 41 | foreach ($collectors as $collector) { 42 | /* @var CollectorInterface $collector */ 43 | $this->registerCollector($collector); 44 | } 45 | } 46 | 47 | /** 48 | * Return the metric namespace. 49 | * 50 | * @return string 51 | */ 52 | public function getNamespace() : string 53 | { 54 | return $this->namespace; 55 | } 56 | 57 | /** 58 | * Return the CollectorRegistry. 59 | * 60 | * @return CollectorRegistry 61 | */ 62 | public function getPrometheus() : CollectorRegistry 63 | { 64 | return $this->prometheus; 65 | } 66 | 67 | /** 68 | * Register a collector. 69 | * 70 | * @param CollectorInterface $collector 71 | */ 72 | public function registerCollector(CollectorInterface $collector) : void 73 | { 74 | $name = $collector->getName(); 75 | 76 | if (!isset($this->collectors[$name])) { 77 | $this->collectors[$name] = $collector; 78 | 79 | $collector->registerMetrics($this); 80 | } 81 | } 82 | 83 | /** 84 | * Return all collectors. 85 | * 86 | * @return array 87 | */ 88 | public function getCollectors() : array 89 | { 90 | return $this->collectors; 91 | } 92 | 93 | /** 94 | * Return a collector by name. 95 | * 96 | * @param string $name 97 | * 98 | * @return CollectorInterface 99 | */ 100 | public function getCollector($name) : CollectorInterface 101 | { 102 | if (!isset($this->collectors[$name])) { 103 | throw new InvalidArgumentException(sprintf('The collector "%s" is not registered.', $name)); 104 | } 105 | 106 | return $this->collectors[$name]; 107 | } 108 | 109 | /** 110 | * Register a counter. 111 | * 112 | * @param string $name 113 | * @param string $help 114 | * @param array $labels 115 | * 116 | * @return Counter 117 | * 118 | * @see https://prometheus.io/docs/concepts/metric_types/#counter 119 | */ 120 | public function registerCounter($name, $help, $labels = []) : Counter 121 | { 122 | return $this->prometheus->registerCounter($this->namespace, $name, $help, $labels); 123 | } 124 | 125 | /** 126 | * Return a counter. 127 | * 128 | * @param string $name 129 | * 130 | * @return Counter 131 | */ 132 | public function getCounter($name) : Counter 133 | { 134 | return $this->prometheus->getCounter($this->namespace, $name); 135 | } 136 | 137 | /** 138 | * Return or register a counter. 139 | * 140 | * @param string $name 141 | * @param string $help 142 | * @param array $labels 143 | * 144 | * @return Counter 145 | * 146 | * @see https://prometheus.io/docs/concepts/metric_types/#counter 147 | */ 148 | public function getOrRegisterCounter($name, $help, $labels = []) : Counter 149 | { 150 | return $this->prometheus->getOrRegisterCounter($this->namespace, $name, $help, $labels); 151 | } 152 | 153 | /** 154 | * Register a gauge. 155 | * 156 | * @param string $name 157 | * @param string $help 158 | * @param array $labels 159 | * 160 | * @return Gauge 161 | * 162 | * @see https://prometheus.io/docs/concepts/metric_types/#gauge 163 | */ 164 | public function registerGauge($name, $help, $labels = []) : Gauge 165 | { 166 | return $this->prometheus->registerGauge($this->namespace, $name, $help, $labels); 167 | } 168 | 169 | /** 170 | * Return a gauge. 171 | * 172 | * @param string $name 173 | * 174 | * @return Gauge 175 | */ 176 | public function getGauge($name) : Gauge 177 | { 178 | return $this->prometheus->getGauge($this->namespace, $name); 179 | } 180 | 181 | /** 182 | * Return or register a gauge. 183 | * 184 | * @param string $name 185 | * @param string $help 186 | * @param array $labels 187 | * 188 | * @return Gauge 189 | * 190 | * @see https://prometheus.io/docs/concepts/metric_types/#gauge 191 | */ 192 | public function getOrRegisterGauge($name, $help, $labels = []) : Gauge 193 | { 194 | return $this->prometheus->getOrRegisterGauge($this->namespace, $name, $help, $labels); 195 | } 196 | 197 | /** 198 | * Register a histogram. 199 | * 200 | * @param string $name 201 | * @param string $help 202 | * @param array $labels 203 | * @param array $buckets 204 | * 205 | * @return Histogram 206 | * 207 | * @see https://prometheus.io/docs/concepts/metric_types/#histogram 208 | */ 209 | public function registerHistogram($name, $help, $labels = [], $buckets = null) : Histogram 210 | { 211 | return $this->prometheus->registerHistogram($this->namespace, $name, $help, $labels, $buckets); 212 | } 213 | 214 | /** 215 | * Return a histogram. 216 | * 217 | * @param string $name 218 | * 219 | * @return Histogram 220 | */ 221 | public function getHistogram($name) : Histogram 222 | { 223 | return $this->prometheus->getHistogram($this->namespace, $name); 224 | } 225 | 226 | /** 227 | * Return or register a histogram. 228 | * 229 | * @param string $name 230 | * @param string $help 231 | * @param array $labels 232 | * @param array $buckets 233 | * 234 | * @return Histogram 235 | * 236 | * @see https://prometheus.io/docs/concepts/metric_types/#histogram 237 | */ 238 | public function getOrRegisterHistogram($name, $help, $labels = [], $buckets = null) : Histogram 239 | { 240 | return $this->prometheus->getOrRegisterHistogram($this->namespace, $name, $help, $labels, $buckets); 241 | } 242 | 243 | /** 244 | * Export the metrics from all collectors. 245 | * 246 | * @return MetricFamilySamples[] 247 | */ 248 | public function export() : array 249 | { 250 | foreach ($this->collectors as $collector) { 251 | /* @var CollectorInterface $collector */ 252 | $collector->collect(); 253 | } 254 | 255 | return $this->prometheus->getMetricFamilySamples(); 256 | } 257 | } 258 | -------------------------------------------------------------------------------- /src/PrometheusFacade.php: -------------------------------------------------------------------------------- 1 | publishes([ 22 | __DIR__ . '/../config/prometheus.php' => $this->configPath('prometheus.php'), 23 | ]); 24 | $this->loadRoutes(); 25 | /* @var PrometheusExporter $exporter */ 26 | $exporter = $this->app->make(PrometheusExporter::class); 27 | foreach (config('prometheus.collectors') as $class) { 28 | $collector = $this->app->make($class); 29 | $exporter->registerCollector($collector); 30 | } 31 | } 32 | 33 | /** 34 | * Register bindings in the container. 35 | */ 36 | public function register() : void 37 | { 38 | $this->mergeConfigFrom(__DIR__ . '/../config/prometheus.php', 'prometheus'); 39 | 40 | $this->app->singleton(PrometheusExporter::class, function ($app) { 41 | $adapter = $app['prometheus.storage_adapter']; 42 | $defaultMetricLabels = config('prometheus.default_metric_labels'); 43 | $prometheus = new CollectorRegistry($adapter); 44 | $prometheus->applyDefaultLabels($defaultMetricLabels); 45 | 46 | return new PrometheusExporter(config('prometheus.namespace'), $prometheus); 47 | }); 48 | $this->app->alias(PrometheusExporter::class, 'prometheus'); 49 | 50 | $this->app->bind('prometheus.storage_adapter_factory', function () { 51 | return new StorageAdapterFactory(); 52 | }); 53 | 54 | $this->app->bind(Adapter::class, function ($app) { 55 | /* @var StorageAdapterFactory $factory */ 56 | $factory = $app['prometheus.storage_adapter_factory']; 57 | $driver = config('prometheus.storage_adapter'); 58 | $configs = config('prometheus.storage_adapters'); 59 | $config = Arr::get($configs, $driver, []); 60 | 61 | return $factory->make($driver, $config); 62 | }); 63 | $this->app->alias(Adapter::class, 'prometheus.storage_adapter'); 64 | } 65 | 66 | /** 67 | * Get the services provided by the provider. 68 | * 69 | * @return array 70 | */ 71 | public function provides() : array 72 | { 73 | return [ 74 | 'prometheus', 75 | 'prometheus.storage_adapter', 76 | 'prometheus.storage_adapter_factory', 77 | ]; 78 | } 79 | 80 | private function loadRoutes() 81 | { 82 | if (!config('prometheus.metrics_route_enabled')) { 83 | return; 84 | } 85 | 86 | $router = $this->app['router']; 87 | 88 | /** @var Route $route */ 89 | $isLumen = Str::contains($this->app->version(), 'Lumen'); 90 | if ($isLumen) { 91 | $router->get( 92 | config('prometheus.metrics_route_path'), 93 | [ 94 | 'as' => 'metrics', 95 | 'uses' => MetricsController::class . '@getMetrics', 96 | ] 97 | ); 98 | } else { 99 | $router->get( 100 | config('prometheus.metrics_route_path'), 101 | MetricsController::class . '@getMetrics' 102 | )->name('metrics'); 103 | } 104 | } 105 | 106 | private function configPath($path) : string 107 | { 108 | return $this->app->basePath() . ($path ? DIRECTORY_SEPARATOR . $path : ''); 109 | } 110 | } 111 | -------------------------------------------------------------------------------- /src/RouteMiddleware.php: -------------------------------------------------------------------------------- 1 | getOrRegisterHistogram( 31 | 'response_time_seconds', 32 | 'It observes response time.', 33 | [ 34 | 'method', 35 | 'route', 36 | 'status_code', 37 | ] 38 | ); 39 | /** @var Histogram $histogram */ 40 | $histogram->observe( 41 | $duration, 42 | [ 43 | $request->method(), 44 | $request->getPathInfo(), 45 | $response->getStatusCode(), 46 | ] 47 | ); 48 | return $response; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/StorageAdapterFactory.php: -------------------------------------------------------------------------------- 1 | makeRedisAdapter($config); 30 | case 'apc': 31 | return new APC(); 32 | } 33 | 34 | throw new InvalidArgumentException(sprintf('The driver [%s] is not supported.', $driver)); 35 | } 36 | 37 | /** 38 | * Factory a redis storage adapter. 39 | * 40 | * @param array $config 41 | * 42 | * @return Redis 43 | */ 44 | protected function makeRedisAdapter(array $config) : Redis 45 | { 46 | if (isset($config['prefix'])) { 47 | Redis::setPrefix($config['prefix']); 48 | } 49 | 50 | return new Redis($config); 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tests/DatabaseServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | 14 | */ 15 | class DatabaseServiceProviderTest extends TestCase 16 | { 17 | public function testServiceProviderWithDefaultConfig() : void 18 | { 19 | $this->createTestTable(); 20 | 21 | /* @var \Prometheus\Histogram $histogram */ 22 | $histogram = $this->app->get('prometheus.sql.histogram'); 23 | $this->assertInstanceOf(Histogram::class, $histogram); 24 | $this->assertSame(['query', 'query_type'], $histogram->getLabelNames()); 25 | $this->assertSame('app_mysql_query_duration', $histogram->getName()); 26 | $this->assertSame('MySQL query duration histogram', $histogram->getHelp()); 27 | 28 | /* @var PrometheusExporter $prometheus */ 29 | $prometheus = $this->app->get('prometheus'); 30 | $export = $prometheus->export(); 31 | $this->assertCount(1, $export); 32 | 33 | /* @var \Prometheus\MetricFamilySamples $samples */ 34 | $samples = $export[0]; 35 | $this->assertInstanceOf(MetricFamilySamples::class, $samples); 36 | $this->assertSame(['query', 'query_type'], $samples->getLabelNames()); 37 | $this->assertSame('app_mysql_query_duration', $samples->getName()); 38 | $this->assertSame('MySQL query duration histogram', $samples->getHelp()); 39 | } 40 | 41 | public function testServiceProviderWithoutCollectingFullSqlQueries() 42 | { 43 | $this->app->get('config')->set('prometheus.collect_full_sql_query', false); 44 | $this->createTestTable(); 45 | 46 | /* @var \Prometheus\Histogram $histogram */ 47 | $histogram = $this->app->get('prometheus.sql.histogram'); 48 | $this->assertInstanceOf(Histogram::class, $histogram); 49 | $this->assertSame(['query_type'], $histogram->getLabelNames()); 50 | 51 | /* @var PrometheusExporter $prometheus */ 52 | $prometheus = $this->app->get('prometheus'); 53 | $export = $prometheus->export(); 54 | $this->assertCount(1, $export); 55 | 56 | /* @var \Prometheus\MetricFamilySamples $samples */ 57 | $samples = $export[0]; 58 | $this->assertInstanceOf(MetricFamilySamples::class, $samples); 59 | $this->assertSame(['query_type'], $samples->getLabelNames()); 60 | } 61 | 62 | protected function createTestTable() 63 | { 64 | Schema::connection('test')->create('test', function($table) 65 | { 66 | $table->increments('id'); 67 | $table->timestamps(); 68 | }); 69 | } 70 | 71 | protected function getPackageProviders($app) : array 72 | { 73 | return [ 74 | PrometheusServiceProvider::class, 75 | DatabaseServiceProvider::class 76 | ]; 77 | } 78 | 79 | protected function getEnvironmentSetUp($app) 80 | { 81 | $app['config']->set('database.default', 'test'); 82 | $app['config']->set('database.connections.test', [ 83 | 'driver' => 'sqlite', 84 | 'database' => ':memory:', 85 | 'prefix' => '', 86 | ]); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /tests/GuzzleMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('observe')->andReturnUsing($observe); 27 | $middleware = new GuzzleMiddleware($histogram); 28 | $stack = new HandlerStack(); 29 | $stack->setHandler(new MockHandler([new Response()])); 30 | $stack->push($middleware); 31 | $client = new Client(['handler' => $stack]); 32 | $client->get('http://example.org'); 33 | $this->assertGreaterThan(0, $value); 34 | $this->assertSame(['GET', 'example.org', 200, 'Undefined'], $labels); 35 | } 36 | 37 | public function testMiddlewareWithOptions() 38 | { 39 | $value = null; 40 | $labels = null; 41 | $observe = function (float $time, array $data) use (&$value, &$labels) { 42 | $value = $time; 43 | $labels = $data; 44 | }; 45 | $histogram = Mockery::mock(Histogram::class); 46 | $histogram->shouldReceive('observe')->andReturnUsing($observe); 47 | $middleware = new GuzzleMiddleware($histogram); 48 | $stack = new HandlerStack(); 49 | $stack->setHandler(new MockHandler([new Response()])); 50 | $stack->push($middleware); 51 | $client = new Client(['handler' => $stack]); 52 | 53 | $options = [ 54 | 'actionURI' => '/test' 55 | ]; 56 | $client->get('http://example.org', $options); 57 | $this->assertGreaterThan(0, $value); 58 | $this->assertSame(['GET', 'example.org', 200, '/test'], $labels); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/GuzzleServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class GuzzleServiceProviderTest extends TestCase 19 | { 20 | public function testServiceProvidersShouldHaveCorrectClasses() : void 21 | { 22 | $this->assertInstanceOf(Client::class, $this->app->get('prometheus.guzzle.client')); 23 | $this->assertInstanceOf(CurlHandler::class, $this->app->get('prometheus.guzzle.handler')); 24 | $this->assertInstanceOf(GuzzleMiddleware::class, $this->app->get('prometheus.guzzle.middleware')); 25 | $this->assertInstanceOf(HandlerStack::class, $this->app->get('prometheus.guzzle.handler-stack')); 26 | $this->assertInstanceOf(Histogram::class, $this->app->get('prometheus.guzzle.client.histogram')); 27 | } 28 | 29 | public function testHistogramShouldHaveCorrectData() 30 | { 31 | /* @var \Prometheus\Histogram $histogram */ 32 | $histogram = $this->app->get('prometheus.guzzle.client.histogram'); 33 | $this->assertInstanceOf(Histogram::class, $histogram); 34 | $this->assertSame(['method', 'external_endpoint', 'status_code', 'path'], $histogram->getLabelNames()); 35 | $this->assertSame('app_guzzle_response_duration', $histogram->getName()); 36 | $this->assertSame('Guzzle response duration histogram', $histogram->getHelp()); 37 | } 38 | 39 | public function testGuzzleClientShouldCallHandlerStack() 40 | { 41 | $response = new Response(200, ['X-Foo' => 'Bar']); 42 | $this->app->singleton('prometheus.guzzle.handler', function ($app) use ($response) { 43 | return new MockHandler([$response]); 44 | }); 45 | /* @var Client $guzzleClient */ 46 | $guzzleClient = $this->app->get('prometheus.guzzle.client'); 47 | $response = $guzzleClient->request('GET', '/'); 48 | $this->assertNotEmpty($response); 49 | } 50 | 51 | protected function getPackageProviders($app) : array 52 | { 53 | return [ 54 | PrometheusServiceProvider::class, 55 | GuzzleServiceProvider::class 56 | ]; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/MetricsControllerTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class MetricsControllerTest extends TestCase 19 | { 20 | /** 21 | * @var ResponseFactory|Mockery\MockInterface 22 | */ 23 | private $responseFactory; 24 | 25 | /** 26 | * @var PrometheusExporter|Mockery\MockInterface 27 | */ 28 | private $exporter; 29 | 30 | /** 31 | * @var MetricsController 32 | */ 33 | private $controller; 34 | 35 | public function setUp() : void 36 | { 37 | parent::setUp(); 38 | 39 | $this->responseFactory = Mockery::mock(ResponseFactory::class); 40 | $this->exporter = Mockery::mock(PrometheusExporter::class); 41 | $this->controller = new MetricsController($this->responseFactory, $this->exporter); 42 | } 43 | 44 | public function testGetMetrics() : void 45 | { 46 | $mockResponse = Mockery::mock(Response::class); 47 | $this->responseFactory->shouldReceive('make') 48 | ->once() 49 | ->withArgs([ 50 | "\n", 51 | 200, 52 | ['Content-Type' => RenderTextFormat::MIME_TYPE], 53 | ]) 54 | ->andReturn($mockResponse); 55 | $this->exporter->shouldReceive('export') 56 | ->once() 57 | ->andReturn([]); 58 | 59 | $actualResponse = $this->controller->getMetrics(); 60 | $this->assertSame($mockResponse, $actualResponse); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/PrometheusExporterTest.php: -------------------------------------------------------------------------------- 1 | assertEquals('app', $exporter->getNamespace()); 23 | $this->assertSame($registry, $exporter->getPrometheus()); 24 | } 25 | 26 | public function testConstructWithCollectors() : void 27 | { 28 | $collector1 = Mockery::mock(CollectorInterface::class); 29 | $collector1->shouldReceive('getName') 30 | ->once() 31 | ->andReturn('users'); 32 | $collector1->shouldReceive('registerMetrics') 33 | ->once() 34 | ->with(Mockery::type(PrometheusExporter::class)); 35 | $collector2 = Mockery::mock(CollectorInterface::class); 36 | $collector2->shouldReceive('getName') 37 | ->once() 38 | ->andReturn('search_requests'); 39 | $collector2->shouldReceive('registerMetrics') 40 | ->once() 41 | ->with(Mockery::type(PrometheusExporter::class)); 42 | 43 | $registry = Mockery::mock(CollectorRegistry::class); 44 | $exporter = new PrometheusExporter('app', $registry, [$collector1, $collector2]); 45 | 46 | $collectors = $exporter->getCollectors(); 47 | $this->assertCount(2, $collectors); 48 | $this->assertArrayHasKey('users', $collectors); 49 | $this->assertArrayHasKey('search_requests', $collectors); 50 | $this->assertSame($collector1, $collectors['users']); 51 | $this->assertSame($collector2, $collectors['search_requests']); 52 | } 53 | 54 | public function testRegisterCollector() : void 55 | { 56 | $registry = Mockery::mock(CollectorRegistry::class); 57 | $exporter = new PrometheusExporter('app', $registry); 58 | 59 | $this->assertEmpty($exporter->getCollectors()); 60 | 61 | $collector = Mockery::mock(CollectorInterface::class); 62 | $collector->shouldReceive('getName') 63 | ->once() 64 | ->andReturn('users'); 65 | $collector->shouldReceive('registerMetrics') 66 | ->once() 67 | ->with($exporter); 68 | 69 | $exporter->registerCollector($collector); 70 | 71 | $collectors = $exporter->getCollectors(); 72 | $this->assertCount(1, $collectors); 73 | $this->assertArrayHasKey('users', $collectors); 74 | $this->assertSame($collector, $collectors['users']); 75 | } 76 | 77 | public function testRegisterCollectorWhenCollectorIsAlreadyRegistered() : void 78 | { 79 | $registry = Mockery::mock(CollectorRegistry::class); 80 | $exporter = new PrometheusExporter('app', $registry); 81 | 82 | $this->assertEmpty($exporter->getCollectors()); 83 | 84 | $collector = Mockery::mock(CollectorInterface::class); 85 | $collector->shouldReceive('getName') 86 | ->andReturn('users'); 87 | $collector->shouldReceive('registerMetrics') 88 | ->once() 89 | ->with($exporter); 90 | 91 | $exporter->registerCollector($collector); 92 | 93 | $collectors = $exporter->getCollectors(); 94 | $this->assertCount(1, $collectors); 95 | $this->assertArrayHasKey('users', $collectors); 96 | $this->assertSame($collector, $collectors['users']); 97 | 98 | $exporter->registerCollector($collector); 99 | 100 | $collectors = $exporter->getCollectors(); 101 | $this->assertCount(1, $collectors); 102 | $this->assertArrayHasKey('users', $collectors); 103 | $this->assertSame($collector, $collectors['users']); 104 | } 105 | 106 | public function testGetCollector() : void 107 | { 108 | $registry = Mockery::mock(CollectorRegistry::class); 109 | $exporter = new PrometheusExporter('app', $registry); 110 | 111 | $this->assertEmpty($exporter->getCollectors()); 112 | 113 | $collector = Mockery::mock(CollectorInterface::class); 114 | $collector->shouldReceive('getName') 115 | ->once() 116 | ->andReturn('users'); 117 | $collector->shouldReceive('registerMetrics') 118 | ->once() 119 | ->with($exporter); 120 | 121 | $exporter->registerCollector($collector); 122 | 123 | $c = $exporter->getCollector('users'); 124 | $this->assertSame($collector, $c); 125 | } 126 | 127 | public function testGetCollectorWhenCollectorIsNotRegistered() : void 128 | { 129 | $this->expectException(\InvalidArgumentException::class); 130 | $this->expectExceptionMessage('The collector "test" is not registered.'); 131 | 132 | $registry = Mockery::mock(CollectorRegistry::class); 133 | $exporter = new PrometheusExporter('app', $registry); 134 | 135 | $exporter->getCollector('test'); 136 | } 137 | 138 | public function testRegisterCounter() : void 139 | { 140 | $counter = Mockery::mock(Counter::class); 141 | 142 | $registry = Mockery::mock(CollectorRegistry::class); 143 | $registry->shouldReceive('registerCounter') 144 | ->once() 145 | ->withArgs([ 146 | 'app', 147 | 'search_requests_total', 148 | 'The total number of search requests.', 149 | ['request_type'], 150 | ]) 151 | ->andReturn($counter); 152 | 153 | $exporter = new PrometheusExporter('app', $registry); 154 | 155 | $c = $exporter->registerCounter( 156 | 'search_requests_total', 157 | 'The total number of search requests.', 158 | ['request_type'] 159 | ); 160 | $this->assertSame($counter, $c); 161 | } 162 | 163 | public function testGetCounter() : void 164 | { 165 | $counter = Mockery::mock(Counter::class); 166 | 167 | $registry = Mockery::mock(CollectorRegistry::class); 168 | $registry->shouldReceive('getCounter') 169 | ->once() 170 | ->withArgs([ 171 | 'app', 172 | 'search_requests_total', 173 | ]) 174 | ->andReturn($counter); 175 | 176 | $exporter = new PrometheusExporter('app', $registry); 177 | 178 | $c = $exporter->getCounter('search_requests_total'); 179 | $this->assertSame($counter, $c); 180 | } 181 | 182 | public function testGetOrRegisterCounter() : void 183 | { 184 | $counter = Mockery::mock(Counter::class); 185 | 186 | $registry = Mockery::mock(CollectorRegistry::class); 187 | $registry->shouldReceive('getOrRegisterCounter') 188 | ->once() 189 | ->withArgs([ 190 | 'app', 191 | 'search_requests_total', 192 | 'The total number of search requests.', 193 | ['request_type'], 194 | ]) 195 | ->andReturn($counter); 196 | 197 | $exporter = new PrometheusExporter('app', $registry); 198 | 199 | $c = $exporter->getOrRegisterCounter( 200 | 'search_requests_total', 201 | 'The total number of search requests.', 202 | ['request_type'] 203 | ); 204 | $this->assertSame($counter, $c); 205 | } 206 | 207 | public function testRegisterGauge() : void 208 | { 209 | $gauge = Mockery::mock(Gauge::class); 210 | 211 | $registry = Mockery::mock(CollectorRegistry::class); 212 | $registry->shouldReceive('registerGauge') 213 | ->once() 214 | ->withArgs([ 215 | 'app', 216 | 'users_online_total', 217 | 'The total number of users online.', 218 | ['group'], 219 | ]) 220 | ->andReturn($gauge); 221 | 222 | $exporter = new PrometheusExporter('app', $registry); 223 | 224 | $g = $exporter->registerGauge( 225 | 'users_online_total', 226 | 'The total number of users online.', 227 | ['group'] 228 | ); 229 | $this->assertSame($gauge, $g); 230 | } 231 | 232 | public function testGetGauge() : void 233 | { 234 | $gauge = Mockery::mock(Gauge::class); 235 | 236 | $registry = Mockery::mock(CollectorRegistry::class); 237 | $registry->shouldReceive('getGauge') 238 | ->once() 239 | ->withArgs([ 240 | 'app', 241 | 'users_online_total', 242 | ]) 243 | ->andReturn($gauge); 244 | 245 | $exporter = new PrometheusExporter('app', $registry); 246 | 247 | $g = $exporter->getGauge('users_online_total'); 248 | $this->assertSame($gauge, $g); 249 | } 250 | 251 | public function testGetOrRegisterGauge() : void 252 | { 253 | $gauge = Mockery::mock(Gauge::class); 254 | 255 | $registry = Mockery::mock(CollectorRegistry::class); 256 | $registry->shouldReceive('getOrRegisterGauge') 257 | ->once() 258 | ->withArgs([ 259 | 'app', 260 | 'users_online_total', 261 | 'The total number of users online.', 262 | ['group'], 263 | ]) 264 | ->andReturn($gauge); 265 | 266 | $exporter = new PrometheusExporter('app', $registry); 267 | 268 | $g = $exporter->getOrRegisterGauge( 269 | 'users_online_total', 270 | 'The total number of users online.', 271 | ['group'] 272 | ); 273 | $this->assertSame($gauge, $g); 274 | } 275 | 276 | public function testRegisterHistogram() : void 277 | { 278 | $histogram = Mockery::mock(Histogram::class); 279 | 280 | $registry = Mockery::mock(CollectorRegistry::class); 281 | $registry->shouldReceive('registerHistogram') 282 | ->once() 283 | ->withArgs([ 284 | 'app', 285 | 'response_time_seconds', 286 | 'The response time of a request.', 287 | ['request_type'], 288 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0], 289 | ]) 290 | ->andReturn($histogram); 291 | 292 | $exporter = new PrometheusExporter('app', $registry); 293 | 294 | $h = $exporter->registerHistogram( 295 | 'response_time_seconds', 296 | 'The response time of a request.', 297 | ['request_type'], 298 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0] 299 | ); 300 | $this->assertSame($histogram, $h); 301 | } 302 | 303 | public function testGetHistogram() : void 304 | { 305 | $histogram = Mockery::mock(Histogram::class); 306 | 307 | $registry = Mockery::mock(CollectorRegistry::class); 308 | $registry->shouldReceive('getHistogram') 309 | ->once() 310 | ->withArgs([ 311 | 'app', 312 | 'response_time_seconds', 313 | ]) 314 | ->andReturn($histogram); 315 | 316 | $exporter = new PrometheusExporter('app', $registry); 317 | 318 | $h = $exporter->getHistogram('response_time_seconds'); 319 | $this->assertSame($histogram, $h); 320 | } 321 | 322 | public function testGetOrRegisterHistogram() : void 323 | { 324 | $histogram = Mockery::mock(Histogram::class); 325 | 326 | $registry = Mockery::mock(CollectorRegistry::class); 327 | $registry->shouldReceive('getOrRegisterHistogram') 328 | ->once() 329 | ->withArgs([ 330 | 'app', 331 | 'response_time_seconds', 332 | 'The response time of a request.', 333 | ['request_type'], 334 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0], 335 | ]) 336 | ->andReturn($histogram); 337 | 338 | $exporter = new PrometheusExporter('app', $registry); 339 | 340 | $h = $exporter->getOrRegisterHistogram( 341 | 'response_time_seconds', 342 | 'The response time of a request.', 343 | ['request_type'], 344 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0] 345 | ); 346 | $this->assertSame($histogram, $h); 347 | } 348 | 349 | public function testExport() : void 350 | { 351 | $samples = ['meh']; 352 | 353 | $registry = Mockery::mock(CollectorRegistry::class); 354 | $registry->shouldReceive('getMetricFamilySamples') 355 | ->once() 356 | ->andReturn($samples); 357 | 358 | $exporter = new PrometheusExporter('app', $registry); 359 | 360 | $collector1 = Mockery::mock(CollectorInterface::class); 361 | $collector1->shouldReceive('getName') 362 | ->once() 363 | ->andReturn('users'); 364 | $collector1->shouldReceive('registerMetrics') 365 | ->once() 366 | ->with($exporter); 367 | $collector1->shouldReceive('collect') 368 | ->once(); 369 | 370 | $exporter->registerCollector($collector1); 371 | 372 | $collector2 = Mockery::mock(CollectorInterface::class); 373 | $collector2->shouldReceive('getName') 374 | ->once() 375 | ->andReturn('search_requests'); 376 | $collector2->shouldReceive('registerMetrics') 377 | ->once() 378 | ->with($exporter); 379 | $collector2->shouldReceive('collect') 380 | ->once(); 381 | 382 | $exporter->registerCollector($collector2); 383 | 384 | $s = $exporter->export(); 385 | $this->assertSame($samples, $s); 386 | } 387 | } 388 | -------------------------------------------------------------------------------- /tests/PrometheusServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | 12 | */ 13 | class PrometheusServiceProviderTest extends TestCase 14 | { 15 | public function testServiceProvider() : void 16 | { 17 | $this->assertInstanceOf(Adapter::class, $this->app[Adapter::class]); 18 | $this->assertInstanceOf(PrometheusExporter::class, $this->app[PrometheusExporter::class]); 19 | $this->assertInstanceOf(StorageAdapterFactory::class, $this->app[StorageAdapterFactory::class]); 20 | 21 | $this->assertInstanceOf(Adapter::class, $this->app->get('prometheus.storage_adapter')); 22 | $this->assertInstanceOf(PrometheusExporter::class, $this->app->get('prometheus')); 23 | $this->assertInstanceOf(StorageAdapterFactory::class, $this->app->get('prometheus.storage_adapter_factory')); 24 | 25 | /* @var \Illuminate\Support\Facades\Route $router */ 26 | $router = $this->app['router']; 27 | $this->assertNotEmpty($router->get('metrics')); 28 | 29 | /* @var \Illuminate\Support\Facades\Config $config */ 30 | $config = $this->app['config']; 31 | $this->assertTrue($config->get('prometheus.metrics_route_enabled')); 32 | $this->assertEmpty($config->get('prometheus.metrics_route_middleware')); 33 | $this->assertSame([], $config->get('prometheus.collectors')); 34 | $this->assertSame([], $config->get('prometheus.default_metric_labels')); 35 | $this->assertEquals('memory', $config->get('prometheus.storage_adapter')); 36 | } 37 | 38 | protected function getPackageProviders($app) : array 39 | { 40 | return [PrometheusServiceProvider::class]; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/RouteMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('observe')->andReturnUsing($observe); 24 | 25 | $prometheus = \Mockery::mock(PrometheusExporter::class); 26 | $prometheus->shouldReceive('getOrRegisterHistogram')->andReturn($histogram); 27 | app()['prometheus'] = $prometheus; 28 | 29 | $request = new Request(); 30 | $expectedResponse = new Response(); 31 | $next = function (Request $request) use ($expectedResponse) { 32 | return $expectedResponse; 33 | }; 34 | $middleware = new RouteMiddleware(); 35 | $actualResponse = $middleware->handle($request, $next); 36 | 37 | $this->assertSame($expectedResponse, $actualResponse); 38 | $this->assertGreaterThan(0, $value); 39 | $this->assertSame(['GET', '/', 200], $labels); 40 | } 41 | } -------------------------------------------------------------------------------- /tests/StorageAdapterFactoryTest.php: -------------------------------------------------------------------------------- 1 | 16 | */ 17 | class StorageAdapterFactoryTest extends TestCase 18 | { 19 | /** 20 | * @var StorageAdapterFactory 21 | */ 22 | private $factory; 23 | 24 | public function setUp() : void 25 | { 26 | parent::setUp(); 27 | $this->factory = new StorageAdapterFactory(); 28 | } 29 | 30 | public function testMakeMemoryAdapter() : void 31 | { 32 | $adapter = $this->factory->make('memory'); 33 | $this->assertInstanceOf(InMemory::class, $adapter); 34 | } 35 | 36 | public function testMakeApcAdapter() : void 37 | { 38 | $adapter = $this->factory->make('apc'); 39 | $this->assertInstanceOf(APC::class, $adapter); 40 | } 41 | 42 | public function testMakeRedisAdapter() : void 43 | { 44 | $adapter = $this->factory->make('redis'); 45 | $this->assertInstanceOf(Redis::class, $adapter); 46 | } 47 | 48 | public function testMakeInvalidAdapter() : void 49 | { 50 | $this->expectException(InvalidArgumentException::class); 51 | $this->expectExceptionMessage('The driver [moo] is not supported.'); 52 | $this->factory->make('moo'); 53 | } 54 | } 55 | --------------------------------------------------------------------------------