├── .gitattributes ├── .github └── workflows │ └── main.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── changelog.md ├── composer.json ├── config └── prometheus.php ├── phpunit.xml.dist ├── src ├── CollectorInterface.php ├── DatabaseServiceProvider.php ├── GuzzleMiddleware.php ├── GuzzleServiceProvider.php ├── MetricsController.php ├── PrometheusExporter.php ├── PrometheusFacade.php ├── PrometheusLaravelRouteMiddleware.php ├── PrometheusLumenRouteMiddleware.php ├── PrometheusServiceProvider.php └── StorageAdapterFactory.php └── tests ├── DatabaseServiceProviderTest.php ├── Fixture └── MetricSamplesSpec.php ├── GuzzleMiddlewareTest.php ├── GuzzleServiceProviderTest.php ├── MetricsControllerTest.php ├── PrometheusExporterTest.php ├── PrometheusServiceProviderTest.php ├── RouteMiddlewareTest.php └── StorageAdapterFactoryTest.php /.gitattributes: -------------------------------------------------------------------------------- 1 | /examples export-ignore 2 | /bin export-ignore 3 | /.docker export-ignore 4 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Unit Tests 2 | 3 | on: 4 | pull_request: 5 | push: 6 | branches: [ master ] 7 | 8 | jobs: 9 | build: 10 | strategy: 11 | matrix: 12 | php: [ "8.3" ] 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - name: Checkout code 17 | uses: actions/checkout@v2 18 | 19 | - name: Setup PHP 20 | uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | 24 | - name: Install Composer dependencies 25 | run: composer install --prefer-dist --no-interaction 26 | 27 | - name: PHPUnit 28 | run: php vendor/bin/phpunit 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | composer.lock 3 | vendor 4 | coverage 5 | coverage.xml 6 | .php_cs.cache 7 | examples/lumen-app/vendor 8 | .phpunit.cache 9 | .phpunit.result.cache 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Superbalist.com a division of Takealot Online (Pty) Ltd 4 | Copyright (c) 2017 Taxibeat 5 | Copyright (c) 2019 Arquivei 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in all 15 | copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PHP_VERSION ?= 8.3 2 | PHP ?= bin/php -d "xdebug.mode=off" 3 | COMPOSER ?= bin/composer 4 | 5 | .PHONY: setup 6 | setup: 7 | DOCKER_BUILDKIT=1 docker build -f .docker/Dockerfile -t laravel-prometheus-exporter:8.3 --build-arg PHP_VERSION=8.3 . 8 | 9 | .PHONY: vendor 10 | vendor: 11 | PHP_VERSION=$(PHP_VERSION) $(COMPOSER) install 12 | 13 | .PHONY: tests 14 | tests: 15 | PHP_VERSION=$(PHP_VERSION) $(PHP) vendor/bin/phpunit 16 | 17 | .PHONY: ci-local 18 | ci-local: ci-local-8.0 ci-local-8.1 19 | 20 | .PHONY: ci-local-% 21 | ci-local-%: 22 | rm -rf composer.lock vendor/ .phpunit.cache/ 23 | 24 | PHP_VERSION=${*} $(COMPOSER) install 25 | PHP_VERSION=${*} $(PHP) vendor/bin/phpunit 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel (and Lumen) Prometheus Exporter 2 | 3 | A prometheus exporter package for Laravel and Lumen. 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 [jimdo/prometheus_client_php](https://github.com/jimdo/prometheus_client_php) into Laravel and Lumen. 12 | 13 | ## Example 14 | 15 | Head to [examples/lumen-app](https://github.com/arquivei/laravel-prometheus-exporter/tree/example-application/examples/lumen-app) 16 | to check out our awesome example application. 17 | To get it you'll have to clone the [Laravel Prometheus Exporter](https://github.com/arquivei/laravel-prometheus-exporter/) 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/arquivei/laravel-prometheus-exporter" 32 | } 33 | ], 34 | ``` 35 | 36 | Install the package via composer 37 | ```bash 38 | composer require arquivei/laravel-prometheus-exporter 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 | Arquivei\LaravelPrometheusExporter\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(Arquivei\LaravelPrometheusExporter\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 | PROMETHEUS_NAMESPACE=app 62 | 63 | PROMETHEUS_METRICS_ROUTE_ENABLED=true 64 | PROMETHEUS_METRICS_ROUTE_PATH=metrics 65 | PROMETHEUS_METRICS_ROUTE_MIDDLEWARE=null 66 | PROMETHEUS_COLLECT_FULL_SQL_QUERY=true 67 | PROMETHEUS_STORAGE_ADAPTER=memory 68 | 69 | PROMETHEUS_REDIS_HOST=localhost 70 | PROMETHEUS_REDIS_PORT=6379 71 | PROMETHEUS_REDIS_TIMEOUT=0.1 72 | PROMETHEUS_REDIS_READ_TIMEOUT=10 73 | PROMETHEUS_REDIS_PERSISTENT_CONNECTIONS=0 74 | PROMETHEUS_REDIS_PREFIX=PROMETHEUS_ 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 | Arquivei\LaravelPrometheusExporter\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 | Arquivei\LaravelPrometheusExporter\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(Arquivei\LaravelPrometheusExporter\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(Arquivei\LaravelPrometheusExporter\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 `PROMETHEUS_COLLECT_FULL_SQL_QUERY`. 141 | 142 | ### Standard metrics 143 | 144 | When using the Arquivei\LaravelPrometheusExporter\PrometheusLaravelRouteMiddleware 145 | middleware, there are two metrics that get exported automatically: 146 | 147 | - execution_count: number of executions 148 | - execution_latency_seconds: latency of executions in seconds 149 | 150 | The execution_count metric is a counter. 151 | 152 | The execution_latency_seconds metric is a histogram with the following values as buckets: 153 | 154 | - 0, 155 | - 0.005, 156 | - 0.01, 157 | - 0.025, 158 | - 0.05, 159 | - 0.075, 160 | - 0.1, 161 | - 0.25, 162 | - 0.5, 163 | - 0.75, 164 | - 1, 165 | - 1.5, 166 | - 2, 167 | - 2.5, 168 | - 5, 169 | - 7.5, 170 | - 10, 171 | - 20, 172 | - 30, 173 | - 40, 174 | - 50, 175 | - 60 176 | 177 | Both metrics have the following labels: 178 | 179 | - owner: person or team responsible for the system 180 | - domain: domain of the system 181 | - system: system's name 182 | - component: name of the controller that handled the request 183 | - operation: method inside the controller that handled the request (action) 184 | - error: error message, if any, NONE otherwise 185 | - error_class: HTTP status text, if any errors, NONE otherwise 186 | 187 | Labels not set will be exported as an empty string. 188 | 189 | Owner, domain, and system can be set by the following environment variables: 190 | 191 | - PROMETHEUS_STANDARD_METRICS_OWNER for owner 192 | - PROMETHEUS_STANDARD_METRICS_DOMAIN for domain 193 | - PROMETHEUS_STANDARD_METRICS_SYSTEM for system 194 | 195 | Or using the config/prometheus.php file: 196 | 197 | - standard_metrics.owner for owner 198 | - standard_metrics.domain for domain 199 | - standard_metrics.system for system 200 | 201 | ### Storage Adapters 202 | 203 | The storage adapter is used to persist metrics across requests. The `memory` adapter is enabled by default, meaning 204 | data will only be persisted across the current request. 205 | 206 | We recommend using the `redis` or `apc` adapter in production 207 | environments. Of course your installation has to provide a Redis or APC implementation. 208 | 209 | The `PROMETHEUS_STORAGE_ADAPTER` environment variable is used to specify the storage adapter. 210 | 211 | If `redis` is used, the `PROMETHEUS_REDIS_HOST` and `PROMETHEUS_REDIS_PORT` vars also need to be configured. Optionally you can change the `PROMETHEUS_REDIS_TIMEOUT`, `PROMETHEUS_REDIS_READ_TIMEOUT` and `PROMETHEUS_REDIS_PERSISTENT_CONNECTIONS` variables. 212 | 213 | ## Exporting Metrics 214 | 215 | The package adds a `/metrics` endpoint, enabled by default, which exposes all metrics gathered by collectors. 216 | 217 | This can be turned on/off using the `PROMETHEUS_METRICS_ROUTE_ENABLED` environment variable, 218 | and can also be changed using the `PROMETHEUS_METRICS_ROUTE_PATH` environment variable. 219 | 220 | ## Collectors 221 | 222 | A collector is a class, implementing the [CollectorInterface](src/CollectorInterface.php), which is responsible for 223 | collecting data for one or many metrics. 224 | 225 | Please see the [Example](#Collector) included below. 226 | 227 | You can auto-load your collectors by adding them to the `collectors` array in the `prometheus.php` config. 228 | 229 | ## Examples 230 | 231 | ### Example usage 232 | 233 | This is an example usage for a Lumen application 234 | 235 | ```php 236 | // retrieve the exporter (you can also use app('prometheus') or Prometheus::getFacadeRoot()) 237 | $exporter = app(\Arquivei\LaravelPrometheusExporter\PrometheusExporter::class); 238 | 239 | // register a new collector 240 | $collector = new \My\New\Collector(); 241 | $exporter->registerCollector($collector); 242 | 243 | // retrieve all collectors 244 | var_dump($exporter->getCollectors()); 245 | 246 | // retrieve a collector by name 247 | $collector = $exporter->getCollector('user'); 248 | 249 | // export all metrics 250 | // this is called automatically when the /metrics end-point is hit 251 | var_dump($exporter->export()); 252 | 253 | // the following methods can be used to create and interact with counters, gauges and histograms directly 254 | // these methods will typically be called by collectors, but can be used to register any custom metrics directly, 255 | // without the need of a collector 256 | 257 | // create a counter 258 | $counter = $exporter->registerCounter('search_requests_total', 'The total number of search requests.'); 259 | $counter->inc(); // increment by 1 260 | $counter->incBy(2); 261 | 262 | // create a counter (with labels) 263 | $counter = $exporter->registerCounter('search_requests_total', 'The total number of search requests.', ['request_type']); 264 | $counter->inc(['GET']); // increment by 1 265 | $counter->incBy(2, ['GET']); 266 | 267 | // retrieve a counter 268 | $counter = $exporter->getCounter('search_requests_total'); 269 | 270 | // create a gauge 271 | $gauge = $exporter->registerGauge('users_online_total', 'The total number of users online.'); 272 | $gauge->inc(); // increment by 1 273 | $gauge->incBy(2); 274 | $gauge->dec(); // decrement by 1 275 | $gauge->decBy(2); 276 | $gauge->set(36); 277 | 278 | // create a gauge (with labels) 279 | $gauge = $exporter->registerGauge('users_online_total', 'The total number of users online.', ['group']); 280 | $gauge->inc(['staff']); // increment by 1 281 | $gauge->incBy(2, ['staff']); 282 | $gauge->dec(['staff']); // decrement by 1 283 | $gauge->decBy(2, ['staff']); 284 | $gauge->set(36, ['staff']); 285 | 286 | // retrieve a gauge 287 | $counter = $exporter->getGauge('users_online_total'); 288 | 289 | // create a histogram 290 | $histogram = $exporter->registerHistogram( 291 | 'response_time_seconds', 292 | 'The response time of a request.', 293 | [], 294 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0] 295 | ); 296 | // the buckets must be in asc order 297 | // 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 298 | $histogram->observe(5.0); 299 | 300 | // create a histogram (with labels) 301 | $histogram = $exporter->registerHistogram( 302 | 'response_time_seconds', 303 | 'The response time of a request.', 304 | ['request_type'], 305 | [0.1, 0.25, 0.5, 0.75, 1.0, 2.5, 5.0, 7.5, 10.0] 306 | ); 307 | // the buckets must be in asc order 308 | // 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 309 | $histogram->observe(5.0, ['GET']); 310 | 311 | // retrieve a histogram 312 | $counter = $exporter->getHistogram('response_time_seconds'); 313 | ``` 314 | 315 | ### Collector 316 | 317 | This is an example collector implementation: 318 | 319 | ```php 320 | registerCounter('search_requests_total', 'The total number of search requests.'); 352 | * ``` 353 | * 354 | * @param PrometheusExporter $exporter 355 | */ 356 | public function registerMetrics(PrometheusExporter $exporter) : void 357 | { 358 | $this->usersRegisteredGauge = $exporter->registerGauge( 359 | 'users_registered_total', 360 | 'The total number of registered users.', 361 | ['group'] 362 | ); 363 | } 364 | 365 | /** 366 | * Collect metrics data, if need be, before exporting. 367 | * 368 | * As an example, this may be used to perform time consuming database queries and set the value of a counter 369 | * or gauge. 370 | */ 371 | public function collect() : void 372 | { 373 | // retrieve the total number of staff users registered 374 | // eg: $totalUsers = Users::where('group', 'staff')->count(); 375 | $this->usersRegisteredGauge->set(36, ['staff']); 376 | 377 | // retrieve the total number of regular users registered 378 | // eg: $totalUsers = Users::where('group', 'regular')->count(); 379 | $this->usersRegisteredGauge->set(192, ['regular']); 380 | } 381 | } 382 | ``` 383 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 5 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 6 | 7 | ## [2.0.0] - 2020-02-12 8 | ### Added 9 | - Add PHP 7.2 support. 10 | - Add PHP 8 support. 11 | - Add pipeline for running automated tests. 12 | - Add makefile and docker images for development purposes. 13 | - Add support for Guzzle 7. 14 | - Add default metric `php_version`. 15 | 16 | ### Changed 17 | - Replace `endclothing/prometheus_client_php` dependency with `promphp/prometheus_client_php` . 18 | - Move collectors instantiation from the boot method to the callable that creates 19 | the prometheus exporter. This means that the collectors are instantiated only when the 20 | exporter is required, preventing the need of a redis connection when just executing 21 | `php artisan`, for example. 22 | 23 | ### Removed 24 | - Drop Laravel 5.x support. 25 | 26 | ## [1.0.1] - 2017-08-30 27 | ### Changed 28 | - Fix config retrieval of `prometheus.storage_adapters` 29 | 30 | ## [1.0.0] - 2017-07-27 31 | ### Added 32 | - Initial release 33 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "arquivei/laravel-prometheus-exporter", 3 | "description": "A Prometheus exporter for Laravel and Lumen", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Arquivei" 8 | } 9 | ], 10 | "require": { 11 | "php": "^8.0 || ^8.1 || ^8.2 || ^8.3", 12 | "guzzlehttp/guzzle": "^7.4.2", 13 | "illuminate/routing": "^11.0", 14 | "illuminate/support": "^11.0", 15 | "promphp/prometheus_client_php": "^2.6.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^11.0.0", 19 | "mockery/mockery": "^1.5.0", 20 | "orchestra/testbench": "^9.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "Arquivei\\LaravelPrometheusExporter\\": "src/", 25 | "Arquivei\\LaravelPrometheusExporter\\Tests\\": "tests/" 26 | } 27 | }, 28 | "extra": { 29 | "branch-alias": { 30 | "dev-master": "1.0-dev" 31 | } 32 | }, 33 | "config": { 34 | "sort-packages": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /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 | | Storage Adapter 45 | |-------------------------------------------------------------------------- 46 | | 47 | | The storage adapter to use. 48 | | 49 | | Supported: "memory", "redis", "apc" 50 | | 51 | */ 52 | 53 | 'storage_adapter' => env('PROMETHEUS_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('PROMETHEUS_REDIS_HOST', 'localhost'), 68 | 'port' => env('PROMETHEUS_REDIS_PORT', 6379), 69 | 'database' => env('PROMETHEUS_REDIS_DATABASE', 0), 70 | 'timeout' => env('PROMETHEUS_REDIS_TIMEOUT', 0.1), 71 | 'read_timeout' => env('PROMETHEUS_REDIS_READ_TIMEOUT', 10), 72 | 'persistent_connections' => env('PROMETHEUS_REDIS_PERSISTENT_CONNECTIONS', false), 73 | 'prefix' => env('PROMETHEUS_REDIS_PREFIX', 'PROMETHEUS_'), 74 | ], 75 | 76 | ], 77 | 78 | /* 79 | |-------------------------------------------------------------------------- 80 | | Collect full SQL query 81 | |-------------------------------------------------------------------------- 82 | | 83 | | Indicates whether we should collect the full SQL query or not. 84 | | 85 | */ 86 | 87 | 'collect_full_sql_query' => env('PROMETHEUS_COLLECT_FULL_SQL_QUERY', false), 88 | 89 | /* 90 | |-------------------------------------------------------------------------- 91 | | Collectors 92 | |-------------------------------------------------------------------------- 93 | | 94 | | The collectors specified here will be auto-registered in the exporter. 95 | | 96 | */ 97 | 98 | 'collectors' => [ 99 | // \Your\ExporterClass::class, 100 | ], 101 | 102 | /* 103 | |-------------------------------------------------------------------------- 104 | | Buckets config 105 | |-------------------------------------------------------------------------- 106 | | 107 | | The buckets config specified here will be passed to the histogram generator 108 | | in the prometheus client. You can configure it as an array of time bounds. 109 | | Default value is null. 110 | | 111 | */ 112 | 113 | 'routes_buckets' => null, 114 | 'sql_buckets' => null, 115 | 'guzzle_buckets' => null, 116 | 117 | 118 | 'standard_metrics' => [ 119 | 'owner' => env('PROMETHEUS_STANDARD_METRICS_OWNER'), 120 | 'domain' => env('PROMETHEUS_STANDARD_METRICS_DOMAIN'), 121 | 'system' => env('PROMETHEUS_STANDARD_METRICS_SYSTEM') 122 | ], 123 | ]; 124 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | 16 | tests 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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, ' ')); 20 | if (config('prometheus.collect_full_sql_query')) { 21 | $querySql = $this->cleanupSqlString((string)$query->sql); 22 | } 23 | $labels = array_values(array_filter([ 24 | $querySql, 25 | $type 26 | ])); 27 | $this->app->get('prometheus.sql.histogram')->observe($query->time, $labels); 28 | }); 29 | } 30 | 31 | /** 32 | * Register bindings in the container. 33 | * 34 | * @return void 35 | */ 36 | public function register() : void 37 | { 38 | $this->app->singleton('prometheus.sql.histogram', function ($app) { 39 | return $app['prometheus']->getOrRegisterHistogram( 40 | 'sql_query_duration', 41 | 'SQL query duration histogram', 42 | array_values(array_filter([ 43 | 'query', 44 | 'query_type' 45 | ])), 46 | config('prometheus.sql_buckets') ?? null 47 | ); 48 | }); 49 | } 50 | 51 | /** 52 | * Get the services provided by the provider. 53 | * 54 | * @return array 55 | */ 56 | public function provides() : array 57 | { 58 | return [ 59 | 'prometheus.sql.histogram', 60 | ]; 61 | } 62 | 63 | /** 64 | * Cleans the SQL string for registering the metric. 65 | * Removes repetitive question marks and simplifies "VALUES" clauses. 66 | * 67 | * @return string 68 | */ 69 | private function cleanupSqlString(string $sql): string 70 | { 71 | $sql = preg_replace('/(VALUES\s*)(\([^\)]*+\)[,\s]*+)++/i', '$1()', $sql); 72 | $sql = preg_replace('/(\s*\?\s*,?\s*){2,}/i', '?', $sql); 73 | $sql = str_replace('"', '', $sql); 74 | 75 | return empty($sql) ? '[error]' : $sql; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/GuzzleMiddleware.php: -------------------------------------------------------------------------------- 1 | histogram = $histogram; 24 | } 25 | 26 | /** 27 | * Middleware that calculates the duration of a guzzle request. 28 | * After calculation it sends metrics to prometheus. 29 | * 30 | * @param callable $handler 31 | * 32 | * @return callable Returns a function that accepts the next handler. 33 | */ 34 | public function __invoke(callable $handler) : callable 35 | { 36 | return function (Request $request, array $options) use ($handler) { 37 | $start = microtime(true); 38 | return $handler($request, $options)->then( 39 | function (Response $response) use ($request, $start) { 40 | $this->histogram->observe( 41 | microtime(true) - $start, 42 | [ 43 | $request->getMethod(), 44 | $request->getUri()->getHost(), 45 | $response->getStatusCode(), 46 | ] 47 | ); 48 | return $response; 49 | } 50 | ); 51 | }; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /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'], 26 | config('prometheus.guzzle_buckets') ?? null 27 | ); 28 | }); 29 | $this->app->singleton('prometheus.guzzle.handler', function ($app) { 30 | return new CurlHandler(); 31 | }); 32 | $this->app->singleton('prometheus.guzzle.middleware', function ($app) { 33 | return new GuzzleMiddleware($app['prometheus.guzzle.client.histogram']); 34 | }); 35 | $this->app->singleton('prometheus.guzzle.handler-stack', function ($app) { 36 | $stack = HandlerStack::create($app['prometheus.guzzle.handler']); 37 | $stack->push($app['prometheus.guzzle.middleware']); 38 | return $stack; 39 | }); 40 | $this->app->singleton('prometheus.guzzle.client', function ($app) { 41 | return new Client(['handler' => $app['prometheus.guzzle.handler-stack']]); 42 | }); 43 | } 44 | 45 | /** 46 | * Get the services provided by the provider. 47 | * 48 | * @return array 49 | */ 50 | public function provides() : array 51 | { 52 | return [ 53 | 'prometheus.guzzle.client', 54 | 'prometheus.guzzle.handler-stack', 55 | 'prometheus.guzzle.middleware', 56 | 'prometheus.guzzle.handler', 57 | 'prometheus.guzzle.client.histogram', 58 | ]; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /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 | * PROMETHEUS_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 | /** 259 | * @param string $name 260 | * @param string $help 261 | * @param array $labels 262 | * 263 | * @return Counter 264 | */ 265 | public function getOrRegisterNamelessCounter(string $name, string $help, array $labels = []): Counter 266 | { 267 | return $this->prometheus->getOrRegisterCounter("", $name, $help, $labels); 268 | } 269 | 270 | /** 271 | * @param string $name 272 | * @param string $help 273 | * @param array $labels 274 | * @param array|null $buckets 275 | * 276 | * @return Histogram 277 | */ 278 | public function getOrRegisterNamelessHistogram( 279 | string $name, 280 | string $help, 281 | array $labels = [], 282 | array $buckets = null 283 | ): Histogram { 284 | return $this->prometheus->getOrRegisterHistogram("", $name, $help, $labels, $buckets); 285 | } 286 | } 287 | -------------------------------------------------------------------------------- /src/PrometheusFacade.php: -------------------------------------------------------------------------------- 1 | getMatchedRoute($request); 50 | 51 | $start = microtime(true); 52 | /** @var Response $response */ 53 | $response = $next($request); 54 | $duration = microtime(true) - $start; 55 | /** @var PrometheusExporter $exporter */ 56 | 57 | $exporter = app('prometheus'); 58 | $histogram = $exporter->getOrRegisterHistogram( 59 | 'response_time_seconds', 60 | 'It observes response time.', 61 | [ 62 | 'method', 63 | 'route', 64 | 'status_code', 65 | ], 66 | config('prometheus.guzzle_buckets') ?? null 67 | ); 68 | /** @var Histogram $histogram */ 69 | $histogram->observe( 70 | $duration, 71 | [ 72 | $request->method(), 73 | $matchedRoute->uri(), 74 | $response->getStatusCode(), 75 | ] 76 | ); 77 | 78 | $labels = $this->getLabels($request, $response); 79 | 80 | $executionCounter = $exporter->getOrRegisterNamelessCounter( 81 | 'execution_count', 82 | 'Counter of system execution', 83 | ['owner', 'domain', 'system', 'component', 'operation', 'error', 'error_class'] 84 | ); 85 | $executionCounter->inc($labels); 86 | 87 | $latencyHistogram = $exporter->getOrRegisterNamelessHistogram( 88 | 'execution_latency_seconds', 89 | 'Latency os system execution in seconds', 90 | ['owner', 'domain', 'system', 'component', 'operation', 'error', 'error_class'], 91 | self::BUCKETS 92 | ); 93 | 94 | $latencyHistogram->observe( 95 | $duration, 96 | $labels 97 | ); 98 | 99 | return $response; 100 | } 101 | 102 | public function getMatchedRoute(Request $request) 103 | { 104 | $routeCollection = RouteFacade::getRoutes(); 105 | return $routeCollection->match($request); 106 | } 107 | 108 | private function getLabels(Request $request, Response $response): array 109 | { 110 | return array_merge( 111 | $this->getConfigLabels(), 112 | $this->getComponentOperationLabels($request), 113 | $this->getErrorLabels($response) 114 | ); 115 | } 116 | 117 | private function getConfigLabels(): array 118 | { 119 | return [ 120 | 'owner' => config('prometheus.standard_metrics.owner'), 121 | 'domain' => config('prometheus.standard_metrics.domain'), 122 | 'system' => config('prometheus.standard_metrics.system'), 123 | ]; 124 | } 125 | 126 | private function getComponentOperationLabels(Request $request): array 127 | { 128 | $route = $this->getMatchedRoute($request); 129 | $controllerAction = $route->getActionName(); 130 | $component = class_basename(explode('@', $controllerAction)[0]); 131 | $operation = explode('@', $controllerAction)[1] ?? null; 132 | 133 | return [ 134 | 'component' => $component, 135 | 'operation' => $operation 136 | ]; 137 | } 138 | 139 | private function getErrorLabels(Response $response): array 140 | { 141 | $error = self::NONE_ERROR; 142 | $errorClass = self::NONE_ERROR; 143 | 144 | if ($response->isClientError() || $response->isServerError()) { 145 | $errorClass = (string)$response->getStatusCode(); 146 | $error = Response::$statusTexts[$response->getStatusCode()] ?? 'Unknown error'; 147 | } 148 | 149 | return [ 150 | 'error' => $error, 151 | 'error_class' => $errorClass, 152 | ]; 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/PrometheusLumenRouteMiddleware.php: -------------------------------------------------------------------------------- 1 | add( 19 | new Route( 20 | $route['method'], 21 | $route['uri'], 22 | $route['action'] 23 | ) 24 | ); 25 | } 26 | return $routeCollection->match($request); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/PrometheusServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 21 | __DIR__ . '/../config/prometheus.php' => $this->configPath('prometheus.php'), 22 | ]); 23 | $this->loadRoutes(); 24 | } 25 | 26 | /** 27 | * Register bindings in the container. 28 | */ 29 | public function register() : void 30 | { 31 | $this->mergeConfigFrom(__DIR__ . '/../config/prometheus.php', 'prometheus'); 32 | 33 | $this->app->singleton(PrometheusExporter::class, function ($app) { 34 | $adapter = $app['prometheus.storage_adapter']; 35 | $prometheus = new CollectorRegistry($adapter, true); 36 | $exporter = new PrometheusExporter(config('prometheus.namespace'), $prometheus); 37 | foreach (config('prometheus.collectors') as $collectorClass) { 38 | $collector = $this->app->make($collectorClass); 39 | $exporter->registerCollector($collector); 40 | } 41 | return $exporter; 42 | }); 43 | $this->app->alias(PrometheusExporter::class, 'prometheus'); 44 | 45 | $this->app->bind('prometheus.storage_adapter_factory', function () { 46 | return new StorageAdapterFactory(); 47 | }); 48 | 49 | $this->app->bind(Adapter::class, function ($app) { 50 | /* @var StorageAdapterFactory $factory */ 51 | $factory = $app['prometheus.storage_adapter_factory']; 52 | $driver = config('prometheus.storage_adapter'); 53 | $configs = config('prometheus.storage_adapters'); 54 | $config = Arr::get($configs, $driver, []); 55 | 56 | return $factory->make($driver, $config); 57 | }); 58 | $this->app->alias(Adapter::class, 'prometheus.storage_adapter'); 59 | } 60 | 61 | /** 62 | * Get the services provided by the provider. 63 | * 64 | * @return array 65 | */ 66 | public function provides() : array 67 | { 68 | return [ 69 | 'prometheus', 70 | 'prometheus.storage_adapter', 71 | 'prometheus.storage_adapter_factory', 72 | ]; 73 | } 74 | 75 | private function loadRoutes() 76 | { 77 | if (!config('prometheus.metrics_route_enabled')) { 78 | return; 79 | } 80 | 81 | $router = $this->app['router']; 82 | 83 | /** @var Route $route */ 84 | $isLumen = mb_strpos($this->app->version(), 'Lumen') !== false; 85 | if ($isLumen) { 86 | $router->get( 87 | config('prometheus.metrics_route_path'), 88 | [ 89 | 'as' => 'metrics', 90 | 'uses' => MetricsController::class . '@getMetrics', 91 | ] 92 | ); 93 | } else { 94 | $router->get( 95 | config('prometheus.metrics_route_path'), 96 | MetricsController::class . '@getMetrics' 97 | )->name('metrics'); 98 | } 99 | } 100 | 101 | private function configPath($path) : string 102 | { 103 | return $this->app->basePath() . ($path ? DIRECTORY_SEPARATOR . $path : ''); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /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 | 18 | */ 19 | class DatabaseServiceProviderTest extends TestCase 20 | { 21 | private $createdTable = false; 22 | 23 | public function testServiceProviderWithDefaultConfig() : void 24 | { 25 | $this->createTestTable(); 26 | 27 | /* @var \Prometheus\Histogram $histogram */ 28 | $histogram = $this->app->get('prometheus.sql.histogram'); 29 | $this->assertInstanceOf(Histogram::class, $histogram); 30 | $this->assertSame(['query', 'query_type'], $histogram->getLabelNames()); 31 | $this->assertSame('app_sql_query_duration', $histogram->getName()); 32 | $this->assertSame('SQL query duration histogram', $histogram->getHelp()); 33 | 34 | /* @var PrometheusExporter $prometheus */ 35 | $prometheus = $this->app->get('prometheus'); 36 | $export = $prometheus->export(); 37 | 38 | $this->assertContainsSamplesMatching( 39 | $export, 40 | MetricSamplesSpec::create() 41 | ->withName('app_sql_query_duration') 42 | ->withLabelNames(['query', 'query_type']) 43 | ->withHelp('SQL query duration histogram') 44 | ); 45 | } 46 | 47 | public function testServiceProviderWithoutCollectingFullSqlQueries() 48 | { 49 | $this->app->get('config')->set('prometheus.collect_full_sql_query', false); 50 | $this->createTestTable(); 51 | 52 | /* @var \Prometheus\Histogram $histogram */ 53 | $histogram = $this->app->get('prometheus.sql.histogram'); 54 | $this->assertInstanceOf(Histogram::class, $histogram); 55 | $this->assertSame(['query', 'query_type'], $histogram->getLabelNames()); 56 | 57 | /* @var PrometheusExporter $prometheus */ 58 | $prometheus = $this->app->get('prometheus'); 59 | $export = $prometheus->export(); 60 | $this->assertContainsSamplesMatching( 61 | $export, 62 | MetricSamplesSpec::create() 63 | ->withLabelNames(['query', 'query_type']) 64 | ); 65 | } 66 | 67 | protected function createTestTable() 68 | { 69 | $this->createdTable = false; 70 | Schema::connection('test')->create('test', function($table) 71 | { 72 | $table->increments('id'); 73 | $table->timestamps(); 74 | $this->createdTable = true; 75 | }); 76 | 77 | while (!$this->createdTable) { 78 | continue; 79 | } 80 | } 81 | 82 | protected function getPackageProviders($app) : array 83 | { 84 | return [ 85 | PrometheusServiceProvider::class, 86 | DatabaseServiceProvider::class 87 | ]; 88 | } 89 | 90 | protected function getEnvironmentSetUp($app) 91 | { 92 | $app['config']->set('database.default', 'test'); 93 | $app['config']->set('database.connections.test', [ 94 | 'driver' => 'sqlite', 95 | 'database' => ':memory:', 96 | 'prefix' => '', 97 | ]); 98 | } 99 | 100 | private function assertContainsSamplesMatching(array $samples, MetricSamplesSpec $spec, int $count = 1): void 101 | { 102 | $matched = array_filter($samples, [$spec, 'matches']); 103 | $this->assertCount($count, $matched); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tests/Fixture/MetricSamplesSpec.php: -------------------------------------------------------------------------------- 1 | name = $name; 29 | return $spec; 30 | } 31 | 32 | public function withType(string $type): self 33 | { 34 | $spec = clone $this; 35 | $spec->type = $type; 36 | return $spec; 37 | } 38 | 39 | public function withHelp(string $help): self 40 | { 41 | $spec = clone $this; 42 | $spec->help = $help; 43 | return $spec; 44 | } 45 | 46 | public function withLabelNames(array $labelNames): self 47 | { 48 | $spec = clone $this; 49 | $spec->labelNames = $labelNames; 50 | return $spec; 51 | } 52 | 53 | public function matches(MetricFamilySamples $samples): bool 54 | { 55 | if ( 56 | !is_null($this->name) 57 | && $samples->getName() !== $this->name 58 | ) { 59 | return false; 60 | } 61 | 62 | if ( 63 | !is_null($this->type) 64 | && $samples->getType() !== $this->type 65 | ) { 66 | return false; 67 | } 68 | 69 | if ( 70 | !is_null($this->help) 71 | && $samples->getHelp() !== $this->help 72 | ) { 73 | return false; 74 | } 75 | 76 | if ( 77 | !is_null($this->labelNames) 78 | && !$this->arraysMatch($this->labelNames, $samples->getLabelNames()) 79 | ) { 80 | return false; 81 | } 82 | 83 | return true; 84 | } 85 | 86 | private function arraysMatch(array $first, array $second): bool 87 | { 88 | sort($first); 89 | sort($second); 90 | 91 | return $first === $second; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /tests/GuzzleMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('observe')->andReturnUsing($observe); 28 | $middleware = new GuzzleMiddleware($histogram); 29 | $stack = new HandlerStack(); 30 | $stack->setHandler(new MockHandler([new Response()])); 31 | $stack->push($middleware); 32 | $client = new Client(['handler' => $stack]); 33 | $client->get('http://example.org'); 34 | $this->assertGreaterThan(0, $value); 35 | $this->assertSame(['GET', 'example.org', 200], $labels); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/GuzzleServiceProviderTest.php: -------------------------------------------------------------------------------- 1 | 20 | */ 21 | class GuzzleServiceProviderTest extends TestCase 22 | { 23 | public function testServiceProvidersShouldHaveCorrectClasses() : void 24 | { 25 | $this->assertInstanceOf(Client::class, $this->app->get('prometheus.guzzle.client')); 26 | $this->assertInstanceOf(CurlHandler::class, $this->app->get('prometheus.guzzle.handler')); 27 | $this->assertInstanceOf(GuzzleMiddleware::class, $this->app->get('prometheus.guzzle.middleware')); 28 | $this->assertInstanceOf(HandlerStack::class, $this->app->get('prometheus.guzzle.handler-stack')); 29 | $this->assertInstanceOf(Histogram::class, $this->app->get('prometheus.guzzle.client.histogram')); 30 | } 31 | 32 | public function testHistogramShouldHaveCorrectData() 33 | { 34 | /* @var \Prometheus\Histogram $histogram */ 35 | $histogram = $this->app->get('prometheus.guzzle.client.histogram'); 36 | $this->assertInstanceOf(Histogram::class, $histogram); 37 | $this->assertSame(['method', 'external_endpoint', 'status_code'], $histogram->getLabelNames()); 38 | $this->assertSame('app_guzzle_response_duration', $histogram->getName()); 39 | $this->assertSame('Guzzle response duration histogram', $histogram->getHelp()); 40 | } 41 | 42 | public function testGuzzleClientShouldCallHandlerStack() 43 | { 44 | $response = new Response(200, ['X-Foo' => 'Bar']); 45 | $this->app->singleton('prometheus.guzzle.handler', function ($app) use ($response) { 46 | return new MockHandler([$response]); 47 | }); 48 | /* @var Client $guzzleClient */ 49 | $guzzleClient = $this->app->get('prometheus.guzzle.client'); 50 | $response = $guzzleClient->request('GET', '/'); 51 | $this->assertNotEmpty($response); 52 | } 53 | 54 | protected function getPackageProviders($app) : array 55 | { 56 | return [ 57 | PrometheusServiceProvider::class, 58 | GuzzleServiceProvider::class 59 | ]; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /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 | 15 | */ 16 | class PrometheusServiceProviderTest extends TestCase 17 | { 18 | public function testServiceProvider() : void 19 | { 20 | $this->assertInstanceOf(Adapter::class, $this->app[Adapter::class]); 21 | $this->assertInstanceOf(PrometheusExporter::class, $this->app[PrometheusExporter::class]); 22 | $this->assertInstanceOf(StorageAdapterFactory::class, $this->app[StorageAdapterFactory::class]); 23 | 24 | $this->assertInstanceOf(Adapter::class, $this->app->get('prometheus.storage_adapter')); 25 | $this->assertInstanceOf(PrometheusExporter::class, $this->app->get('prometheus')); 26 | $this->assertInstanceOf(StorageAdapterFactory::class, $this->app->get('prometheus.storage_adapter_factory')); 27 | 28 | /* @var \Illuminate\Support\Facades\Route $router */ 29 | $router = $this->app['router']; 30 | $this->assertNotEmpty($router->get('metrics')); 31 | 32 | /* @var \Illuminate\Support\Facades\Config $config */ 33 | $config = $this->app['config']; 34 | $this->assertTrue($config->get('prometheus.metrics_route_enabled')); 35 | $this->assertEmpty($config->get('prometheus.metrics_route_middleware')); 36 | $this->assertSame([], $config->get('prometheus.collectors')); 37 | $this->assertEquals('memory', $config->get('prometheus.storage_adapter')); 38 | } 39 | 40 | protected function getPackageProviders($app) : array 41 | { 42 | return [PrometheusServiceProvider::class]; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /tests/RouteMiddlewareTest.php: -------------------------------------------------------------------------------- 1 | shouldReceive('observe')->andReturnUsing($observe); 26 | 27 | $prometheus = \Mockery::mock(PrometheusExporter::class); 28 | $prometheus->shouldReceive('getOrRegisterHistogram')->andReturn($histogram); 29 | app()['prometheus'] = $prometheus; 30 | 31 | $request = new Request(); 32 | $expectedResponse = new Response(); 33 | $next = function (Request $request) use ($expectedResponse) { 34 | return $expectedResponse; 35 | }; 36 | 37 | $matchedRouteMock = \Mockery::mock(Route::class); 38 | $matchedRouteMock->shouldReceive('uri')->andReturn('/test/route'); 39 | 40 | $middleware = \Mockery::mock('Arquivei\LaravelPrometheusExporter\PrometheusLumenRouteMiddleware[getMatchedRoute]'); 41 | $middleware->shouldReceive('getMatchedRoute')->andReturn($matchedRouteMock); 42 | $actualResponse = $middleware->handle($request, $next); 43 | 44 | $this->assertSame($expectedResponse, $actualResponse); 45 | $this->assertGreaterThan(0, $value); 46 | $this->assertSame(['GET', '/test/route', 200], $labels); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/StorageAdapterFactoryTest.php: -------------------------------------------------------------------------------- 1 | 17 | */ 18 | class StorageAdapterFactoryTest extends TestCase 19 | { 20 | /** 21 | * @var StorageAdapterFactory 22 | */ 23 | private $factory; 24 | 25 | public function setUp(): void 26 | { 27 | parent::setUp(); 28 | $this->factory = new StorageAdapterFactory(); 29 | } 30 | 31 | public function testMakeMemoryAdapter(): void 32 | { 33 | $adapter = $this->factory->make('memory'); 34 | $this->assertInstanceOf(InMemory::class, $adapter); 35 | } 36 | 37 | public function testMakeApcAdapter(): void 38 | { 39 | if (!extension_loaded('apcu')) { 40 | $this->expectExceptionObject(new StorageException('APCu extension is not loaded')); 41 | } 42 | if (\function_exists('apcu_enabled') && !apcu_enabled()) { 43 | $this->expectExceptionObject(new StorageException('APCu is not enabled')); 44 | } 45 | 46 | $adapter = $this->factory->make('apc'); 47 | $this->assertInstanceOf(APC::class, $adapter); 48 | } 49 | 50 | public function testMakeRedisAdapter(): void 51 | { 52 | $adapter = $this->factory->make('redis'); 53 | $this->assertInstanceOf(Redis::class, $adapter); 54 | } 55 | 56 | public function testMakeInvalidAdapter(): void 57 | { 58 | $this->expectException(InvalidArgumentException::class); 59 | $this->expectExceptionMessage('The driver [moo] is not supported.'); 60 | $this->factory->make('moo'); 61 | } 62 | } 63 | --------------------------------------------------------------------------------