├── .coveralls.yml ├── .editorconfig ├── .gitignore ├── .travis.yml ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json ├── docker-compose.yml ├── docker └── fpm │ ├── Dockerfile │ ├── apc.ini │ └── entrypoint.sh ├── phpunit.xml ├── src ├── Config │ └── config.php ├── Contract │ └── PrometheusExporterContract.php ├── Controller │ ├── LaravelController.php │ └── LumenController.php ├── Middleware │ └── RequestPerRoute.php ├── PrometheusExporter.php ├── Provider │ └── PrometheusExporterServiceProvider.php └── Routes │ └── routes.php └── tests ├── Middleware └── RequestPerRouteTest.php ├── PrometheusExporterTest.php ├── Provider ├── ApcAdapterTest.php ├── InMemoryAdapterTest.php ├── PushAdapterTest.php └── RedisAdapterTest.php └── TestCase.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: build/logs/clover.xml 3 | json_path: build/logs/coveralls-upload.json -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 4 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false 13 | 14 | [*.yml] 15 | indent_style = space 16 | indent_size = 2 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | vendor 3 | composer.lock 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | sudo: true 4 | 5 | services: 6 | - docker 7 | 8 | php: 9 | - "7.2" 10 | - "7.3" 11 | 12 | install: 13 | - travis_retry composer install --no-interaction --no-suggest 14 | - wget -c -nc --retry-connrefused --tries=0 https://github.com/php-coveralls/php-coveralls/releases/download/v2.0.0/php-coveralls.phar -O coveralls.phar 15 | - chmod +x coveralls.phar 16 | - php coveralls.phar --version 17 | 18 | before_script: 19 | - mkdir -p build/logs 20 | - ls -al 21 | - yes | pecl install apcu apcu_bc-beta 22 | - echo "extension=apcu.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 23 | - echo "extension=apc.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 24 | - echo "apc.enable_cli=1" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini 25 | 26 | script: 27 | - ./vendor/bin/phpunit --coverage-clover build/logs/clover.xml 28 | - composer check-style 29 | 30 | after_success: 31 | - travis_retry php coveralls.phar -v 32 | - bash <(curl -s https://codecov.io/bash) 33 | 34 | cache: 35 | directories: 36 | - vendor 37 | - $HOME/.cache/composer 38 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via Pull Requests on [Github](https://github.com/triadev/LaravelPrometheusExporter). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - Check the code style with ``$ composer check-style`` and fix it with ``$ composer fix-style``. 11 | 12 | - **Add tests!** 13 | 14 | - **Document any change in behaviour** 15 | 16 | - **Create feature branches** 17 | 18 | - **One pull request per feature** 19 | 20 | ## Running Tests 21 | 22 | ``` bash 23 | $ composer test 24 | ``` 25 | 26 | **Happy coding**! 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Christopher Lorke 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. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LaravelPrometheusExporter 2 | 3 | [![Software license][ico-license]](LICENSE) 4 | [![Travis][ico-travis]][link-travis] 5 | [![Coveralls](https://coveralls.io/repos/github/triadev/LaravelPrometheusExporter/badge.svg?branch=master)](https://coveralls.io/github/triadev/LaravelPrometheusExporter?branch=master) 6 | [![CodeCov](https://codecov.io/gh/triadev/LaravelPrometheusExporter/branch/master/graph/badge.svg)](https://codecov.io/gh/triadev/LaravelPrometheusExporter) 7 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/triadev/LaravelPrometheusExporter/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/triadev/LaravelPrometheusExporter/?branch=master) 8 | [![Latest stable][ico-version-stable]][link-packagist] 9 | [![Monthly installs][ico-downloads-monthly]][link-downloads] 10 | [![Total Downloads](https://img.shields.io/packagist/dt/triadev/laravel-prometheus-exporter.svg?style=flat-square)](https://packagist.org/packages/triadev/laravel-prometheus-exporter) 11 | [![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/triadev/LaravelPrometheusExporter.svg)](http://isitmaintained.com/project/triadev/LaravelPrometheusExporter "Average time to resolve an issue") 12 | [![Percentage of issues still open](http://isitmaintained.com/badge/open/triadev/LaravelPrometheusExporter.svg)](http://isitmaintained.com/project/triadev/LaravelPrometheusExporter "Percentage of issues still open") 13 | 14 | A laravel and lumen service provider to export metrics for prometheus. 15 | 16 | ## Supported laravel versions 17 | [![Laravel 5.6][icon-l56]][link-laravel] 18 | [![Laravel 5.7][icon-l57]][link-laravel] 19 | [![Laravel 5.8][icon-l58]][link-laravel] 20 | [![Laravel 6.0][icon-l60]][link-laravel] 21 | 22 | ## Supported lumen versions 23 | [![Lumen 5.6][icon-lumen56]][link-lumen] 24 | [![Lumen 5.7][icon-lumen57]][link-lumen] 25 | [![Lumen 5.8][icon-lumen58]][link-lumen] 26 | [![Lumen 6.0][icon-lumen60]][link-lumen] 27 | 28 | ## Main features 29 | - Metrics with APC 30 | - Metrics with Redis 31 | - Metrics with InMemory 32 | - Metrics with the push gateway 33 | - Request per route middleware (total and duration metrics) 34 | 35 | ## Installation 36 | 37 | ### Composer 38 | > composer require triadev/laravel-prometheus-exporter 39 | 40 | ### Application 41 | 42 | The package is registered through the package discovery of laravel and Composer. 43 | >https://laravel.com/docs/5.8/packages 44 | 45 | Once installed you can now publish your config file and set your correct configuration for using the package. 46 | ```php 47 | php artisan vendor:publish --provider="Triadev\PrometheusExporter\Provider\PrometheusExporterServiceProvider" --tag="config" 48 | ``` 49 | 50 | This will create a file ```config/prometheus-exporter.php```. 51 | 52 | ## Configuration 53 | | Key | Env | Value | Description | Default | 54 | |:-------------:|:-------------:|:-------------:|:-----:|:-----:| 55 | | adapter | PROMETHEUS_ADAPTER | STRING | apc, redis, inmemory or push | apc | 56 | | namespace | --- | STRING | default: app | app | 57 | | namespace_http | --- | STRING | namespace for "RequestPerRoute-Middleware metrics" | http | 58 | | redis.host | PROMETHEUS_REDIS_HOST, REDIS_HOST | STRING | redis host | 127.0.0.1 59 | | redis.port | PROMETHEUS_REDIS_PORT, REDIS_PORT | INTEGER | redis port | 6379 | 60 | | redis.password | PROMETHEUS_REDIS_PASSWORD, REDIS_PASSWORD | STRING | redis password | null | 61 | | redis.timeout | --- | FLOAT | redis timeout | 0.1 | 62 | | redis.read_timeout | --- | INTEGER | redis read timeout | 10 | 63 | | push_gateway.address | PROMETHEUS_PUSH_GATEWAY_ADDRESS | STRING | push gateway address | localhost:9091 | 64 | | buckets_per_route | --- | STRING | histogram buckets for "RequestPerRoute-Middleware" | --- | 65 | 66 | ### buckets_per_route 67 | ``` 68 | 'buckets_per_route' => [ 69 | ROUTE-NAME => [10,20,50,100,200], 70 | ... 71 | ] 72 | ``` 73 | 74 | ## Usage 75 | 76 | ### Get metrics 77 | 78 | #### Laravel 79 | When you are using laravel you can use the default http endpoint: 80 | >triadev/pe/metrics 81 | 82 | Of course you can also register your own route. Here is an example: 83 | ``` 84 | Route::get( 85 | ROUTE, 86 | \Triadev\PrometheusExporter\Controller\LaravelController::class . '@metrics' 87 | ); 88 | ``` 89 | 90 | #### Lumen 91 | When you are using lumen you must register the route on your own. Here is an example: 92 | ``` 93 | Route::get( 94 | ROUTE, 95 | \Triadev\PrometheusExporter\Controller\LumenController::class . '@metrics' 96 | ); 97 | ``` 98 | 99 | ### Middleware 100 | 101 | #### RequestPerRoute 102 | A middleware to build metrics for "request_total" and "requests_latency_milliseconds" per route. 103 | 104 | ##### Alias 105 | >lpe.requestPerRoute 106 | 107 | ##### Metrics 108 | 1. requests_total (inc) 109 | 2. requests_latency_milliseconds (histogram) 110 | 111 | ##### Example 112 | ```php 113 | $router->get('/test/route', function () { 114 | return 'valid'; 115 | })->middleware('lpe.requestPerRoute'); 116 | ``` 117 | 118 | >app_requests_latency_milliseconds_bucket{route="/test/route",method="GET",status_code="200",le="0.005"} 0 119 | >... 120 | >app_requests_latency_milliseconds_count{route="/test/route",method="GET",status_code="200"} 1 121 | >app_requests_latency_milliseconds_sum{route="/test/route",method="GET",status_code="200"} 6 122 | >app_requests_total{route="/test/route",method="GET",status_code="200"} 1 123 | 124 | ## Roadmap 125 | - histogram buckets per route (RequestPerRoute) 126 | 127 | ## Reporting Issues 128 | If you do find an issue, please feel free to report it with GitHub's bug tracker for this project. 129 | 130 | Alternatively, fork the project and make a pull request. :) 131 | 132 | ## Testing 133 | 1. docker-compose up 134 | 2. docker exec fpm ./vendor/phpunit/phpunit/phpunit 135 | 136 | ## Contributing 137 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 138 | 139 | ## Credits 140 | - [Christopher Lorke][link-author] 141 | - [All Contributors][link-contributors] 142 | 143 | ## Other 144 | 145 | ### Project related links 146 | - [Wiki](https://github.com/triadev/LaravelPrometheusExporter/wiki) 147 | - [Issue tracker](https://github.com/triadev/LaravelPrometheusExporter/issues) 148 | 149 | ### Author 150 | - [Christopher Lorke](mailto:christopher.lorke@gmx.de) 151 | 152 | ### License 153 | The code for LaravelPrometheusExporter is distributed under the terms of the MIT license (see [LICENSE](LICENSE)). 154 | 155 | [ico-license]: https://img.shields.io/github/license/triadev/LaravelPrometheusExporter.svg?style=flat-square 156 | [ico-version-stable]: https://img.shields.io/packagist/v/triadev/laravel-prometheus-exporter.svg?style=flat-square 157 | [ico-downloads-monthly]: https://img.shields.io/packagist/dm/triadev/laravel-prometheus-exporter.svg?style=flat-square 158 | [ico-travis]: https://travis-ci.org/triadev/LaravelPrometheusExporter.svg?branch=master 159 | 160 | [link-packagist]: https://packagist.org/packages/triadev/laravel-prometheus-exporter 161 | [link-downloads]: https://packagist.org/packages/triadev/laravel-prometheus-exporter/stats 162 | [link-travis]: https://travis-ci.org/triadev/LaravelPrometheusExporter 163 | [link-scrutinizer]: https://scrutinizer-ci.com/g/triadev/LaravelPrometheusExporter/badges/quality-score.png?b=master 164 | 165 | [icon-l56]: https://img.shields.io/badge/Laravel-5.6-brightgreen.svg?style=flat-square 166 | [icon-l57]: https://img.shields.io/badge/Laravel-5.7-brightgreen.svg?style=flat-square 167 | [icon-l58]: https://img.shields.io/badge/Laravel-5.8-brightgreen.svg?style=flat-square 168 | [icon-l60]: https://img.shields.io/badge/Laravel-6.0-brightgreen.svg?style=flat-square 169 | 170 | [icon-lumen56]: https://img.shields.io/badge/Lumen-5.6-brightgreen.svg?style=flat-square 171 | [icon-lumen57]: https://img.shields.io/badge/Lumen-5.7-brightgreen.svg?style=flat-square 172 | [icon-lumen58]: https://img.shields.io/badge/Lumen-5.8-brightgreen.svg?style=flat-square 173 | [icon-lumen60]: https://img.shields.io/badge/Lumen-6.0-brightgreen.svg?style=flat-square 174 | 175 | [link-laravel]: https://laravel.com 176 | [link-lumen]: https://lumen.laravel.com 177 | [link-author]: https://github.com/triadev 178 | [link-contributors]: ../../contributors 179 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "triadev/laravel-prometheus-exporter", 3 | "description": "A laravel and lumen service provider to export metrics for prometheus.", 4 | "keywords": [ 5 | "Laravel", 6 | "Prometheus", 7 | "Metrics" 8 | ], 9 | "license": "MIT", 10 | "authors": [ 11 | { 12 | "name": "Christopher Lorke", 13 | "email": "christopher.lorke@gmx.de" 14 | } 15 | ], 16 | "require": { 17 | "php": ">=7.2", 18 | "endclothing/prometheus_client_php": "^1.0", 19 | "illuminate/support": "^5.6|^6.0" 20 | }, 21 | "require-dev": { 22 | "phpunit/phpunit": "~7.0", 23 | "fzaninotto/faker": "~1.4", 24 | "mockery/mockery": "~1.0", 25 | "orchestra/testbench": "~3.0", 26 | "laravel/framework": "^5.6|^6.0", 27 | "laravel/lumen-framework": "^5.6|^6.0", 28 | "squizlabs/php_codesniffer": "^3.0" 29 | }, 30 | "suggest": { 31 | "ext-redis": "Required if using Redis.", 32 | "ext-apc": "Required if using APCu." 33 | }, 34 | "autoload": { 35 | "psr-4": { 36 | "Triadev\\PrometheusExporter\\": "src/" 37 | } 38 | }, 39 | "autoload-dev": { 40 | "classmap": [ 41 | "tests/" 42 | ] 43 | }, 44 | "extra": { 45 | "laravel": { 46 | "providers": [ 47 | "Triadev\\PrometheusExporter\\Provider\\PrometheusExporterServiceProvider" 48 | ] 49 | } 50 | }, 51 | "config": { 52 | "preferred-install": "dist", 53 | "sort-packages": true, 54 | "optimize-autoloader": true, 55 | "secure-http": false 56 | }, 57 | "minimum-stability": "dev", 58 | "prefer-stable": true, 59 | "scripts": { 60 | "test": "phpunit", 61 | "check-style": "phpcs -p --standard=PSR2 src --ignore=src/Database/**,src/Config/*", 62 | "fix-style": "phpcbf -p --standard=PSR2 src --ignore=src/Database/**,src/Config/*" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | services: 3 | fpm: 4 | container_name: fpm 5 | build: 6 | context: . 7 | dockerfile: docker/fpm/Dockerfile 8 | ports: 9 | - "9000:9000" 10 | volumes: 11 | - .:/var/www/html:delegated 12 | -------------------------------------------------------------------------------- /docker/fpm/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM php:7.1-fpm-alpine 2 | 3 | RUN apk add --no-cache --update \ 4 | bash \ 5 | openssh && \ 6 | apk add --no-cache --virtual .build-deps $PHPIZE_DEPS \ 7 | autoconf \ 8 | build-base \ 9 | pcre-dev \ 10 | zlib-dev \ 11 | icu-dev \ 12 | file && \ 13 | docker-php-ext-install pdo_mysql && \ 14 | pecl channel-update pecl.php.net && \ 15 | pecl install apcu apcu_bc-beta && \ 16 | pecl install redis-3.1.1 && docker-php-ext-enable redis && \ 17 | docker-php-ext-enable apcu && \ 18 | docker-php-ext-enable apc && \ 19 | docker-php-ext-install pcntl && \ 20 | docker-php-ext-install posix && \ 21 | rm -f /usr/local/etc/php/conf.d/docker-php-ext-apc.ini && \ 22 | rm -f /usr/local/etc/php/conf.d/docker-php-ext-apcu.ini && \ 23 | curl -sS https://getcomposer.org/installer \ 24 | | php -- --install-dir=/usr/local/bin --filename=composer && \ 25 | apk del .build-deps && \ 26 | rm -rf /tmp/* /var/tmp/* 27 | 28 | # config 29 | COPY ./docker/fpm/apc.ini /usr/local/etc/php/conf.d/apc.ini 30 | 31 | # copy entrypoint 32 | COPY ./docker/fpm/entrypoint.sh /entrypoint.sh 33 | RUN chmod a+x /entrypoint.sh 34 | 35 | # add log file 36 | RUN mkdir -p /var/www/logs && chown www-data:www-data /var/www/logs 37 | 38 | WORKDIR /var/www/html 39 | 40 | # add project 41 | COPY . /var/www/html/ 42 | 43 | ARG user="www-data" 44 | 45 | ENTRYPOINT ["/entrypoint.sh"] 46 | CMD ["php-fpm", "-R"] 47 | -------------------------------------------------------------------------------- /docker/fpm/apc.ini: -------------------------------------------------------------------------------- 1 | extension = apcu.so 2 | extension = apc.so 3 | 4 | apc.enabled=1 5 | apc.enable_cli=1 -------------------------------------------------------------------------------- /docker/fpm/entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | exec "$@" 4 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | ./tests 13 | 14 | 15 | 16 | 17 | ./src 18 | 19 | ./src/Routes 20 | ./src/Contract 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /src/Config/config.php: -------------------------------------------------------------------------------- 1 | env('PROMETHEUS_ADAPTER', 'apc'), 5 | 6 | 'namespace' => 'app', 7 | 8 | 'namespace_http' => 'http', 9 | 10 | 'redis' => [ 11 | 'host' => env('PROMETHEUS_REDIS_HOST', env('REDIS_HOST', '127.0.0.1')), 12 | 'port' => env('PROMETHEUS_REDIS_PORT', env('REDIS_PORT', 6379)), 13 | 'password' => env('PROMETHEUS_REDIS_PASSWORD', env('REDIS_PASSWORD', null)), 14 | 'timeout' => 0.1, // in seconds 15 | 'read_timeout' => 10, // in seconds 16 | 'persistent_connections' => false, 17 | ], 18 | 19 | 'push_gateway' => [ 20 | 'address' => env('PROMETHEUS_PUSH_GATEWAY_ADDRESS', 'localhost:9091') 21 | ], 22 | 23 | 'buckets_per_route' => [] 24 | ]; 25 | -------------------------------------------------------------------------------- /src/Contract/PrometheusExporterContract.php: -------------------------------------------------------------------------------- 1 | prometheusExporter = $prometheusExporter; 24 | } 25 | 26 | /** 27 | * metrics 28 | * 29 | * Expose metrics for prometheus 30 | * 31 | * @return Response 32 | */ 33 | public function metrics() : Response 34 | { 35 | $renderer = new RenderTextFormat(); 36 | 37 | return Response::create( 38 | $renderer->render($this->prometheusExporter->getMetricFamilySamples()) 39 | )->header('Content-Type', RenderTextFormat::MIME_TYPE); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Controller/LumenController.php: -------------------------------------------------------------------------------- 1 | prometheusExporter = $prometheusExporter; 24 | } 25 | 26 | /** 27 | * metrics 28 | * 29 | * Expose metrics for prometheus 30 | * 31 | * @return Response 32 | */ 33 | public function metrics() : Response 34 | { 35 | $renderer = new RenderTextFormat(); 36 | 37 | return \response()->make( 38 | $renderer->render($this->prometheusExporter->getMetricFamilySamples()) 39 | )->header('Content-Type', RenderTextFormat::MIME_TYPE); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Middleware/RequestPerRoute.php: -------------------------------------------------------------------------------- 1 | prometheusExporter = $prometheusExporter; 22 | } 23 | 24 | /** 25 | * Handle an incoming request. 26 | * 27 | * @param Request $request 28 | * @param \Closure $next 29 | * @return mixed 30 | * 31 | * @throws MetricsRegistrationException 32 | */ 33 | public function handle(Request $request, Closure $next) 34 | { 35 | $start = microtime(true); 36 | 37 | /** @var Response $response */ 38 | $response = $next($request); 39 | 40 | $durationMilliseconds = (microtime(true) - $start) * 1000.0; 41 | 42 | $path = $request->path(); 43 | $method = $request->getMethod(); 44 | $status = $response->getStatusCode(); 45 | 46 | $this->requestCountMetric($path, $method, $status); 47 | $this->requestLatencyMetric($path, $method, $status, $durationMilliseconds); 48 | 49 | return $response; 50 | } 51 | 52 | /** 53 | * @param string $routeName 54 | * @param string $method 55 | * @param int $status 56 | * 57 | * @throws MetricsRegistrationException 58 | */ 59 | private function requestCountMetric(string $routeName, string $method, int $status) 60 | { 61 | $this->prometheusExporter->incCounter( 62 | 'requests_total', 63 | 'the number of http requests', 64 | config('prometheus_exporter.namespace_http'), 65 | [ 66 | 'route', 67 | 'method', 68 | 'status_code' 69 | ], 70 | [ 71 | $routeName, 72 | $method, 73 | $status 74 | ] 75 | ); 76 | } 77 | 78 | /** 79 | * @param string $routeName 80 | * @param string $method 81 | * @param int $status 82 | * @param int $duration 83 | * 84 | * @throws MetricsRegistrationException 85 | */ 86 | private function requestLatencyMetric(string $routeName, string $method, int $status, int $duration) 87 | { 88 | $bucketsPerRoute = null; 89 | 90 | if ($bucketsPerRouteConfig = config('prometheus-exporter.buckets_per_route')) { 91 | $bucketsPerRoute = array_get($bucketsPerRouteConfig, $routeName); 92 | } 93 | 94 | $this->prometheusExporter->setHistogram( 95 | 'requests_latency_milliseconds', 96 | 'duration of requests', 97 | $duration, 98 | config('prometheus_exporter.namespace_http'), 99 | [ 100 | 'route', 101 | 'method', 102 | 'status_code' 103 | ], 104 | [ 105 | $routeName, 106 | $method, 107 | $status 108 | ], 109 | $bucketsPerRoute 110 | ); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/PrometheusExporter.php: -------------------------------------------------------------------------------- 1 | registry = $registry; 25 | } 26 | 27 | /** 28 | * Get metric family samples 29 | * 30 | * @return MetricFamilySamples[] 31 | */ 32 | public function getMetricFamilySamples() 33 | { 34 | return $this->registry->getMetricFamilySamples(); 35 | } 36 | 37 | /** 38 | * @inheritdoc 39 | */ 40 | public function incCounter($name, $help, $namespace = null, array $labelKeys = [], array $labelValues = []) 41 | { 42 | $namespace = $this->getNamespace($namespace); 43 | 44 | try { 45 | $counter = $this->registry->getCounter($namespace, $name); 46 | } catch (MetricNotFoundException $e) { 47 | $counter = $this->registry->registerCounter($namespace, $name, $help, $labelKeys); 48 | } 49 | 50 | $counter->inc($labelValues); 51 | 52 | $this->pushGateway($this->registry, 'inc'); 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function incByCounter( 59 | $name, 60 | $help, 61 | $value, 62 | $namespace = null, 63 | array $labelKeys = [], 64 | array $labelValues = [] 65 | ) { 66 | $namespace = $this->getNamespace($namespace); 67 | 68 | try { 69 | $counter = $this->registry->getCounter($namespace, $name); 70 | } catch (MetricNotFoundException $e) { 71 | $counter = $this->registry->registerCounter($namespace, $name, $help, $labelKeys); 72 | } 73 | 74 | $counter->incBy($value, $labelValues); 75 | 76 | $this->pushGateway($this->registry, 'incBy'); 77 | } 78 | 79 | /** 80 | * @inheritdoc 81 | */ 82 | public function setGauge($name, $help, $value, $namespace = null, array $labelKeys = [], array $labelValues = []) 83 | { 84 | $namespace = $this->getNamespace($namespace); 85 | 86 | try { 87 | $gauge = $this->registry->getGauge($namespace, $name); 88 | } catch (MetricNotFoundException $e) { 89 | $gauge = $this->registry->registerGauge($namespace, $name, $help, $labelKeys); 90 | } 91 | 92 | $gauge->set($value, $labelValues); 93 | 94 | $this->pushGateway($this->registry, 'gauge'); 95 | } 96 | 97 | /** 98 | * @inheritdoc 99 | */ 100 | public function incGauge($name, $help, $namespace = null, array $labelKeys = [], array $labelValues = []) 101 | { 102 | $namespace = $this->getNamespace($namespace); 103 | 104 | try { 105 | $gauge = $this->registry->getGauge($namespace, $name); 106 | } catch (MetricNotFoundException $e) { 107 | $gauge = $this->registry->registerGauge($namespace, $name, $help, $labelKeys); 108 | } 109 | 110 | $gauge->inc($labelValues); 111 | 112 | $this->pushGateway($this->registry, 'inc'); 113 | } 114 | 115 | /** 116 | * @inheritdoc 117 | */ 118 | public function incByGauge( 119 | $name, 120 | $help, 121 | $value, 122 | $namespace = null, 123 | array $labelKeys = [], 124 | array $labelValues = [] 125 | ) { 126 | $namespace = $this->getNamespace($namespace); 127 | 128 | try { 129 | $gauge = $this->registry->getGauge($namespace, $name); 130 | } catch (MetricNotFoundException $e) { 131 | $gauge = $this->registry->registerGauge($namespace, $name, $help, $labelKeys); 132 | } 133 | 134 | $gauge->incBy($value, $labelValues); 135 | 136 | $this->pushGateway($this->registry, 'incBy'); 137 | } 138 | 139 | /** 140 | * @inheritdoc 141 | */ 142 | public function setHistogram( 143 | $name, 144 | $help, 145 | $value, 146 | $namespace = null, 147 | array $labelKeys = [], 148 | array $labelValues = [], 149 | ?array $buckets = null 150 | ) { 151 | $namespace = $this->getNamespace($namespace); 152 | 153 | try { 154 | $histogram = $this->registry->getHistogram($namespace, $name); 155 | } catch (MetricNotFoundException $e) { 156 | $histogram = $this->registry->registerHistogram($namespace, $name, $help, $labelKeys, $buckets); 157 | } 158 | 159 | $histogram->observe($value, $labelValues); 160 | 161 | $this->pushGateway($this->registry, 'histogram'); 162 | } 163 | 164 | private function getNamespace(?string $namespace = null) : string 165 | { 166 | if (!$namespace) { 167 | $namespace = config('prometheus-exporter.namespace'); 168 | } 169 | 170 | return $namespace; 171 | } 172 | 173 | private function pushGateway(CollectorRegistry $registry, string $job, ?array $groupingKey = null) 174 | { 175 | if (config('prometheus-exporter.adapter') == 'push') { 176 | $pushGateway = new PushGateway(config('prometheus-exporter.push_gateway.address')); 177 | 178 | $pushGateway->push( 179 | $registry, 180 | $job, 181 | $groupingKey 182 | ); 183 | } 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/Provider/PrometheusExporterServiceProvider.php: -------------------------------------------------------------------------------- 1 | bootConfig(); 24 | $this->bootRoutes(); 25 | } 26 | 27 | private function bootConfig() 28 | { 29 | $source = realpath(__DIR__ . '/../Config/config.php'); 30 | 31 | if (class_exists('Illuminate\Foundation\Application', false)) { 32 | $this->publishes([ 33 | __DIR__ . '/../Config/config.php' => config_path('prometheus-exporter.php'), 34 | ], 'config'); 35 | } elseif (class_exists('Laravel\Lumen\Application', false)) { 36 | $this->app/** @scrutinizer ignore-call */->configure('prometheus-exporter'); 37 | } 38 | 39 | $this->mergeConfigFrom($source, 'prometheus-exporter'); 40 | } 41 | 42 | private function bootRoutes() 43 | { 44 | if (class_exists('Illuminate\Foundation\Application', false)) { 45 | $this->loadRoutesFrom(__DIR__ . '/../Routes/routes.php'); 46 | } 47 | } 48 | 49 | /** 50 | * Register the service provider. 51 | * 52 | * @throws \ErrorException 53 | */ 54 | public function register() 55 | { 56 | $this->mergeConfigFrom(__DIR__ . '/../Config/config.php', 'prometheus-exporter'); 57 | 58 | $this->registerAdapter(); 59 | $this->registerMiddlewareForRequestMetrics(); 60 | 61 | $this->app->bind( 62 | PrometheusExporterContract::class, 63 | PrometheusExporter::class, 64 | true 65 | ); 66 | } 67 | 68 | /** 69 | * @throws \ErrorException 70 | */ 71 | private function registerAdapter() 72 | { 73 | switch (config('prometheus-exporter.adapter')) { 74 | case 'apc': 75 | $this->app->bind(Adapter::class, APC::class); 76 | break; 77 | case 'redis': 78 | $this->app->bind(Adapter::class, function () { 79 | return new Redis(config('prometheus-exporter.redis')); 80 | }); 81 | break; 82 | case 'push': 83 | $this->app->bind(Adapter::class, APC::class); 84 | break; 85 | case 'inmemory': 86 | $this->app->bind(Adapter::class, InMemory::class); 87 | break; 88 | default: 89 | throw new \ErrorException('"prometheus-exporter.adapter" must be either apc or redis'); 90 | } 91 | } 92 | 93 | private function registerMiddlewareForRequestMetrics() 94 | { 95 | if (class_exists('Illuminate\Foundation\Application', false)) { 96 | /** @var Router $router */ 97 | $router = $this->app['router']; 98 | $router->aliasMiddleware('lpe.requestPerRoute', RequestPerRoute::class); 99 | } 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Routes/routes.php: -------------------------------------------------------------------------------- 1 | name('triadev.pe.metrics'); 7 | -------------------------------------------------------------------------------- /tests/Middleware/RequestPerRouteTest.php: -------------------------------------------------------------------------------- 1 | app['router']; 22 | 23 | $router->get('testing', function () { 24 | return 'valid'; 25 | })->middleware('lpe.requestPerRoute')->name('testing'); 26 | 27 | $router->get('requestPerRoute', function () { 28 | return 'valid'; 29 | })->middleware('lpe.requestPerRoute')->name('requestPerRoute'); 30 | 31 | $this->prometheusExporter = app(PrometheusExporterContract::class); 32 | } 33 | 34 | /** 35 | * @test 36 | */ 37 | public function it_counts_metrics_for_request_per_route_middleware() 38 | { 39 | $this->get('testing'); 40 | 41 | $metricResponse = $this->get('/triadev/pe/metrics'); 42 | 43 | $requestsTotal = null; 44 | if (preg_match('/app_requests_total{route="testing",method="GET",status_code="200"} (?[0-9]+)/', $metricResponse->getContent(), $matches)) { 45 | $requestsTotal = $matches['metric']; 46 | } 47 | 48 | $requestsLatencyTotal = null; 49 | if (preg_match('/app_requests_latency_milliseconds_count{route="testing",method="GET",status_code="200"} (?[0-9]+)/', $metricResponse->getContent(), $matches)) { 50 | $requestsLatencyTotal = $matches['metric']; 51 | } 52 | 53 | $this->assertEquals(1, $requestsTotal); 54 | $this->assertEquals(1, $requestsLatencyTotal); 55 | 56 | $this->assertTrue( 57 | (bool)preg_match( 58 | '/app_requests_latency_milliseconds_bucket{route="testing",method="GET",status_code="200",le="0.005"} (?[0-9]+)/', 59 | $metricResponse->getContent() 60 | ) 61 | ); 62 | 63 | $this->assertTrue( 64 | (bool)preg_match( 65 | '/app_requests_latency_milliseconds_bucket{route="testing",method="GET",status_code="200",le="10"} (?[0-9]+)/', 66 | $metricResponse->getContent() 67 | ) 68 | ); 69 | } 70 | 71 | /** 72 | * @test 73 | */ 74 | public function it_counts_metrics_for_request_per_route_middleware_with_configured_buckets() 75 | { 76 | $this->get('requestPerRoute'); 77 | 78 | $metricResponse = $this->get('/triadev/pe/metrics'); 79 | 80 | $requestsTotal = null; 81 | if (preg_match('/app_requests_total{route="requestPerRoute",method="GET",status_code="200"} (?[0-9]+)/', $metricResponse->getContent(), $matches)) { 82 | $requestsTotal = $matches['metric']; 83 | } 84 | 85 | $requestsLatencyTotal = null; 86 | if (preg_match('/app_requests_latency_milliseconds_count{route="requestPerRoute",method="GET",status_code="200"} (?[0-9]+)/', $metricResponse->getContent(), $matches)) { 87 | $requestsLatencyTotal = $matches['metric']; 88 | } 89 | 90 | $this->assertEquals(1, $requestsTotal); 91 | $this->assertEquals(1, $requestsLatencyTotal); 92 | 93 | $this->assertTrue( 94 | (bool)preg_match( 95 | '/app_requests_latency_milliseconds_bucket{route="testing",method="GET",status_code="200",le="10"} (?[0-9]+)/', 96 | $metricResponse->getContent() 97 | ) 98 | ); 99 | 100 | $this->assertTrue( 101 | (bool)preg_match( 102 | '/app_requests_latency_milliseconds_bucket{route="testing",method="GET",status_code="200",le="200"} (?[0-9]+)/', 103 | $metricResponse->getContent() 104 | ) 105 | ); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /tests/PrometheusExporterTest.php: -------------------------------------------------------------------------------- 1 | service = app(PrometheusExporterContract::class); 21 | } 22 | 23 | private function getMetricResponseCallableForLaravel() : \Closure 24 | { 25 | return function () { 26 | return $this->get('/triadev/pe/metrics'); 27 | }; 28 | } 29 | 30 | private function getMetricResponseCallableForLumen() : \Closure 31 | { 32 | return function () { 33 | \Route::get( 34 | 'test', 35 | \Triadev\PrometheusExporter\Controller\LumenController::class . '@metrics' 36 | ); 37 | 38 | return $this->get('test'); 39 | }; 40 | } 41 | 42 | private function getMetricValue(string $name, \Closure $metricCall) : ?int 43 | { 44 | $pattern = sprintf('/app_%s (?[0-9]+)/', $name); 45 | 46 | $content = $metricCall()->getContent(); 47 | 48 | if (preg_match($pattern, $content, $matches)) { 49 | return $matches['metric']; 50 | } 51 | 52 | return null; 53 | } 54 | 55 | /** 56 | * @test 57 | */ 58 | public function it_inc_a_counter() 59 | { 60 | $this->service->incCounter('phpunit_incCounter', ''); 61 | 62 | $metricBeforeLaravel = $this->getMetricValue('phpunit_incCounter', $this->getMetricResponseCallableForLaravel()); 63 | $metricBeforeLumen = $this->getMetricValue('phpunit_incCounter', $this->getMetricResponseCallableForLumen()); 64 | 65 | $this->service->incCounter('phpunit_incCounter', ''); 66 | 67 | $metricAfterLaravel = $this->getMetricValue('phpunit_incCounter', $this->getMetricResponseCallableForLaravel()); 68 | $metricAfterLumen = $this->getMetricValue('phpunit_incCounter', $this->getMetricResponseCallableForLumen()); 69 | 70 | $this->assertGreaterThan($metricBeforeLaravel, $metricAfterLaravel); 71 | $this->assertGreaterThan($metricBeforeLumen, $metricAfterLumen); 72 | } 73 | 74 | /** 75 | * @test 76 | */ 77 | public function it_inc_by_counter() 78 | { 79 | $this->service->incByCounter('phpunit_incByCounter', '', 10); 80 | 81 | $this->assertEquals(10, $this->getMetricValue('phpunit_incByCounter', $this->getMetricResponseCallableForLaravel())); 82 | $this->assertEquals(10, $this->getMetricValue('phpunit_incByCounter', $this->getMetricResponseCallableForLumen())); 83 | } 84 | 85 | /** 86 | * @test 87 | */ 88 | public function it_set_a_gauge() 89 | { 90 | $this->service->setGauge('phpunit_setGauge', '', 2); 91 | 92 | $this->assertEquals(2, $this->getMetricValue('phpunit_setGauge', $this->getMetricResponseCallableForLaravel())); 93 | $this->assertEquals(2, $this->getMetricValue('phpunit_setGauge', $this->getMetricResponseCallableForLumen())); 94 | } 95 | 96 | /** 97 | * @test 98 | */ 99 | public function it_inc_gauge() 100 | { 101 | $this->service->incGauge('phpunit_incGauge', ''); 102 | 103 | $this->assertEquals(1, $this->getMetricValue('phpunit_incGauge', $this->getMetricResponseCallableForLaravel())); 104 | $this->assertEquals(1, $this->getMetricValue('phpunit_incGauge', $this->getMetricResponseCallableForLumen())); 105 | 106 | $this->service->incGauge('phpunit_incGauge', ''); 107 | 108 | $this->assertEquals(2, $this->getMetricValue('phpunit_incGauge', $this->getMetricResponseCallableForLaravel())); 109 | $this->assertEquals(2, $this->getMetricValue('phpunit_incGauge', $this->getMetricResponseCallableForLumen())); 110 | } 111 | 112 | /** 113 | * @test 114 | */ 115 | public function it_inc_by_gauge() 116 | { 117 | $this->service->incByGauge('phpunit_incByGauge', '', 2); 118 | 119 | $this->assertEquals(2, $this->getMetricValue('phpunit_incByGauge', $this->getMetricResponseCallableForLaravel())); 120 | $this->assertEquals(2, $this->getMetricValue('phpunit_incByGauge', $this->getMetricResponseCallableForLumen())); 121 | } 122 | 123 | /** 124 | * @test 125 | */ 126 | public function it_set_a_histogram() 127 | { 128 | $this->service->setHistogram('phpunit_setHistogram', '', 1); 129 | $this->service->setHistogram('phpunit_setHistogram', '', 2); 130 | 131 | $this->assertEquals(3, $this->getMetricValue('phpunit_setHistogram_sum', $this->getMetricResponseCallableForLaravel())); 132 | $this->assertEquals(3, $this->getMetricValue('phpunit_setHistogram_sum', $this->getMetricResponseCallableForLumen())); 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /tests/Provider/ApcAdapterTest.php: -------------------------------------------------------------------------------- 1 | set('prometheus-exporter.adapter', 'apc'); 23 | 24 | return [ 25 | PrometheusExporterServiceProvider::class 26 | ]; 27 | } 28 | 29 | /** 30 | * @test 31 | */ 32 | public function it_checks_the_concrete_implementation_of_prometheus_adapter() 33 | { 34 | $this->assertEquals('APC', class_basename(app(Adapter::class))); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Provider/InMemoryAdapterTest.php: -------------------------------------------------------------------------------- 1 | set('prometheus-exporter.adapter', 'inmemory'); 23 | 24 | return [ 25 | PrometheusExporterServiceProvider::class 26 | ]; 27 | } 28 | 29 | /** 30 | * @test 31 | */ 32 | public function it_checks_the_concrete_implementation_of_prometheus_adapter() 33 | { 34 | $this->assertEquals('InMemory', class_basename(app(Adapter::class))); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Provider/PushAdapterTest.php: -------------------------------------------------------------------------------- 1 | set('prometheus-exporter.adapter', 'push'); 23 | 24 | return [ 25 | PrometheusExporterServiceProvider::class 26 | ]; 27 | } 28 | 29 | /** 30 | * @test 31 | */ 32 | public function it_checks_the_concrete_implementation_of_prometheus_adapter() 33 | { 34 | $this->assertEquals('APC', class_basename(app(Adapter::class))); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/Provider/RedisAdapterTest.php: -------------------------------------------------------------------------------- 1 | set('prometheus-exporter.adapter', 'redis'); 23 | 24 | return [ 25 | PrometheusExporterServiceProvider::class 26 | ]; 27 | } 28 | 29 | /** 30 | * @test 31 | */ 32 | public function it_checks_the_concrete_implementation_of_prometheus_adapter() 33 | { 34 | $this->assertEquals('Redis', class_basename(app(Adapter::class))); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | set('prometheus-exporter.adapter', 'apc'); 38 | 39 | $app['config']->set('prometheus-exporter.buckets_per_route', [ 40 | 'requestPerRoute' => [10, 20, 50, 100, 200] 41 | ]); 42 | } 43 | 44 | /** 45 | * Get package providers. At a minimum this is the package being tested, but also 46 | * would include packages upon which our package depends, e.g. Cartalyst/Sentry 47 | * In a normal app environment these would be added to the 'providers' array in 48 | * the config/app.php file. 49 | * 50 | * @param \Illuminate\Foundation\Application $app 51 | * 52 | * @return array 53 | */ 54 | protected function getPackageProviders($app) 55 | { 56 | return [ 57 | PrometheusExporterServiceProvider::class 58 | ]; 59 | } 60 | } 61 | --------------------------------------------------------------------------------