├── .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 | [](https://coveralls.io/github/triadev/LaravelPrometheusExporter?branch=master)
6 | [](https://codecov.io/gh/triadev/LaravelPrometheusExporter)
7 | [](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 | [](https://packagist.org/packages/triadev/laravel-prometheus-exporter)
11 | [](http://isitmaintained.com/project/triadev/LaravelPrometheusExporter "Average time to resolve an issue")
12 | [](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 |
--------------------------------------------------------------------------------