diff --git a/.git_hooks/post-checkout/01-install-dependencies.sh b/.git_hooks/post-checkout/01-install-dependencies.sh new file mode 120000 index 0000000..43a65ea --- /dev/null +++ b/.git_hooks/post-checkout/01-install-dependencies.sh @@ -0,0 +1 @@ +../scripts/install-dependencies.sh \ No newline at end of file diff --git a/.git_hooks/post-merge/01-install-dependencies.sh b/.git_hooks/post-merge/01-install-dependencies.sh new file mode 120000 index 0000000..43a65ea --- /dev/null +++ b/.git_hooks/post-merge/01-install-dependencies.sh @@ -0,0 +1 @@ +../scripts/install-dependencies.sh \ No newline at end of file diff --git a/.git_hooks/post-merge/install-dependecies.sh b/.git_hooks/post-merge/install-dependecies.sh deleted file mode 100755 index 3ce9a43..0000000 --- a/.git_hooks/post-merge/install-dependecies.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -ESC_SEQ="\x1b[" -COL_RESET=$ESC_SEQ"39;49;00m" -COL_RED=$ESC_SEQ"0;31m" -COL_GREEN=$ESC_SEQ"0;32m" -COL_YELLOW=$ESC_SEQ"0;33m" - -changed_files="$(git diff-tree -r --name-only --no-commit-id HEAD@{1} HEAD)" - -check_run() { - echo "$changed_files" | grep --quiet "$1" && echo " * changes detected in $1" && echo " * running $2" && eval "$2" -} - -check_run composer.lock "composer install" -check_run package-lock.json "npm install" -exit 0 diff --git a/.git_hooks/pre-commit/01-lint-php.sh b/.git_hooks/pre-commit/01-lint-php.sh new file mode 120000 index 0000000..5833040 --- /dev/null +++ b/.git_hooks/pre-commit/01-lint-php.sh @@ -0,0 +1 @@ +../scripts/lint-php.sh \ No newline at end of file diff --git a/.git_hooks/pre-commit/02-php-cs-fixer.sh b/.git_hooks/pre-commit/02-php-cs-fixer.sh new file mode 120000 index 0000000..66c2032 --- /dev/null +++ b/.git_hooks/pre-commit/02-php-cs-fixer.sh @@ -0,0 +1 @@ +../scripts/php-cs-fixer.sh \ No newline at end of file diff --git a/.git_hooks/pre-push/01-composer-validate.sh b/.git_hooks/pre-push/01-composer-validate.sh new file mode 120000 index 0000000..61e981f --- /dev/null +++ b/.git_hooks/pre-push/01-composer-validate.sh @@ -0,0 +1 @@ +../scripts/composer-validate.sh \ No newline at end of file diff --git a/.git_hooks/pre-push/02-phpstan.sh b/.git_hooks/pre-push/02-phpstan.sh new file mode 120000 index 0000000..05c8a9b --- /dev/null +++ b/.git_hooks/pre-push/02-phpstan.sh @@ -0,0 +1 @@ +../scripts/phpstan.sh \ No newline at end of file diff --git a/.git_hooks/pre-push/03-test-code.sh b/.git_hooks/pre-push/03-test-code.sh new file mode 120000 index 0000000..b33ab89 --- /dev/null +++ b/.git_hooks/pre-push/03-test-code.sh @@ -0,0 +1 @@ +../scripts/test-code.sh \ No newline at end of file diff --git a/.git_hooks/pre-push/var-dump-checker.sh b/.git_hooks/pre-push/var-dump-checker.sh deleted file mode 100755 index 167b6b4..0000000 --- a/.git_hooks/pre-push/var-dump-checker.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/bash - -ESC_SEQ="\x1b[" -COL_RESET=$ESC_SEQ"39;49;00m" -COL_RED=$ESC_SEQ"0;31m" -COL_GREEN=$ESC_SEQ"0;32m" -COL_YELLOW=$ESC_SEQ"0;33m" - -echo -printf "$COL_YELLOW%s$COL_RESET\n" "Running pre-push hook: \"var-dump-checker\"" - -./vendor/bin/var-dump-check --laravel --exclude bootstrap --exclude node_modules --exclude vendor . - -# If the grep command has no hits - echo a warning and exit with non-zero status. -if [ $? == 1 ]; then - printf "$COL_RED%s$COL_RESET\r\n\r\n" "Some var_dump usage found. Please fix your code" - exit 1 -fi - -echo "Okay" -exit 0 diff --git a/.git_hooks/pre-push/composer-validate.sh b/.git_hooks/scripts/composer-validate.sh similarity index 91% rename from .git_hooks/pre-push/composer-validate.sh rename to .git_hooks/scripts/composer-validate.sh index d6df93e..543451a 100755 --- a/.git_hooks/pre-push/composer-validate.sh +++ b/.git_hooks/scripts/composer-validate.sh @@ -1,6 +1,6 @@ #!/bin/bash -# validate composer +# Validate composer.json before commit ESC_SEQ="\x1b[" COL_RESET=$ESC_SEQ"39;49;00m" @@ -8,7 +8,7 @@ COL_RED=$ESC_SEQ"0;31m" COL_GREEN=$ESC_SEQ"0;32m" COL_YELLOW=$ESC_SEQ"0;33m" -echo +echo printf "$COL_YELLOW%s$COL_RESET\n" "Running pre-push hook: \"composer-validate\"" VALID=$(composer validate --strict --no-check-publish --no-check-all | grep "is valid") diff --git a/.git_hooks/post-checkout/install-dependecies.sh b/.git_hooks/scripts/install-dependencies.sh similarity index 53% rename from .git_hooks/post-checkout/install-dependecies.sh rename to .git_hooks/scripts/install-dependencies.sh index 3ce9a43..3b308e5 100755 --- a/.git_hooks/post-checkout/install-dependecies.sh +++ b/.git_hooks/scripts/install-dependencies.sh @@ -1,5 +1,7 @@ #!/bin/bash +# - 'composer update' if changed composer.json + ESC_SEQ="\x1b[" COL_RESET=$ESC_SEQ"39;49;00m" COL_RED=$ESC_SEQ"0;31m" @@ -9,9 +11,8 @@ COL_YELLOW=$ESC_SEQ"0;33m" changed_files="$(git diff-tree -r --name-only --no-commit-id HEAD@{1} HEAD)" check_run() { - echo "$changed_files" | grep --quiet "$1" && echo " * changes detected in $1" && echo " * running $2" && eval "$2" + echo "$changed_files" | grep -q "$1" && echo " * changes detected in $1" && echo " * running $2" && eval "$2" } - -check_run composer.lock "composer install" -check_run package-lock.json "npm install" + +check_run composer.json "composer update" exit 0 diff --git a/.git_hooks/pre-commit/lint-php.sh b/.git_hooks/scripts/lint-php.sh similarity index 94% rename from .git_hooks/pre-commit/lint-php.sh rename to .git_hooks/scripts/lint-php.sh index 1c027a8..2850447 100755 --- a/.git_hooks/pre-commit/lint-php.sh +++ b/.git_hooks/scripts/lint-php.sh @@ -1,5 +1,7 @@ #!/bin/bash +# Lint all added php-files via 'php -l' + ROOT_DIR="$(pwd)/" LIST=$(git diff-index --cached --name-only --diff-filter=ACMR HEAD) ERRORS_BUFFER="" @@ -31,13 +33,13 @@ do fi done if [ "$ERRORS_BUFFER" != "" ]; then - echo + echo echo "These errors were found in try-to-commit files: " echo -e $ERRORS_BUFFER - echo + echo printf "$COL_RED%s$COL_RESET\r\n\r\n" "Can't commit, fix errors first." exit 1 else echo "Okay" exit 0 -fi \ No newline at end of file +fi diff --git a/.git_hooks/pre-commit/php-cs-fixer.sh b/.git_hooks/scripts/php-cs-fixer.sh similarity index 97% rename from .git_hooks/pre-commit/php-cs-fixer.sh rename to .git_hooks/scripts/php-cs-fixer.sh index c45bc51..3fe095a 100755 --- a/.git_hooks/pre-commit/php-cs-fixer.sh +++ b/.git_hooks/scripts/php-cs-fixer.sh @@ -1,5 +1,7 @@ #!/bin/bash +# Check code style via '.php-cs-fixer.php' + EXECUTABLE_NAME=php-cs-fixer EXECUTABLE_COMMAND=fix CONFIG_FILE=.php-cs-fixer.php diff --git a/.git_hooks/scripts/phpstan.sh b/.git_hooks/scripts/phpstan.sh new file mode 100755 index 0000000..7c15b55 --- /dev/null +++ b/.git_hooks/scripts/phpstan.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Run composer phpstan + +ESC_SEQ="\x1b[" +COL_RESET=$ESC_SEQ"39;49;00m" +COL_RED=$ESC_SEQ"0;31m" +COL_GREEN=$ESC_SEQ"0;32m" +COL_YELLOW=$ESC_SEQ"0;33m" + +echo +printf "$COL_YELLOW%s$COL_RESET\n" "Running pre-push hook: \"phpstan\"" + +if composer phpstan; then + echo "Okay" + exit 0 +else + printf "$COL_RED%s$COL_RESET\r\n" "phpstan analysis failed." + exit 1 +fi diff --git a/.git_hooks/scripts/test-code.sh b/.git_hooks/scripts/test-code.sh new file mode 100755 index 0000000..faa22a1 --- /dev/null +++ b/.git_hooks/scripts/test-code.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# Run composer test + +ESC_SEQ="\x1b[" +COL_RESET=$ESC_SEQ"39;49;00m" +COL_RED=$ESC_SEQ"0;31m" +COL_GREEN=$ESC_SEQ"0;32m" +COL_YELLOW=$ESC_SEQ"0;33m" + +echo +printf "$COL_YELLOW%s$COL_RESET\n" "Running pre-push hook: \"test-code\"" + +if composer test; then + echo "Okay" + exit 0 +else + printf "$COL_RED%s$COL_RESET\r\n" "Tests failed." + exit 1 +fi diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index ad39967..8169284 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -42,8 +42,6 @@ If the project maintainer has any additional requirements, you will find them li - **Add tests!** - Your patch won't be accepted if it doesn't have tests. -- **Use hooks!** - You can make use of .git hooks if you install them using `npm i` - - **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date. - **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index d837ebe..8cc5b7e 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -14,13 +14,6 @@ jobs: matrix: php: [8.1, 8.2, 8.3] laravel: [9.*, 10.*, 11.*] - include: - - laravel: 9.* - testbench: 7.* - - laravel: 10.* - testbench: 8.* - - laravel: 11.* - testbench: 9.* exclude: - laravel: 11.* php: 8.1 @@ -47,5 +40,12 @@ jobs: run: | composer require "laravel/framework:${{ matrix.laravel }}" --no-interaction --no-update composer update --prefer-stable --prefer-dist --no-interaction + + - name: Composer Validate + run: ./.git_hooks/scripts/composer-validate.sh + - name: Execute tests - run: vendor/bin/phpunit + run: composer test-ci + + - name: Execute phpstan + run: ./.git_hooks/scripts/phpstan.sh diff --git a/.gitignore b/.gitignore index 416b354..daa725d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,85 @@ -.idea -vendor -composer.lock -studio.json -.phpunit.* +# Project # +######################## +.php_cs.cache .php-cs-fixer.cache +.huskyrc +clients/* +!clients/.gitkeep +storage/ensi +generated +studio.json +build +/node_modules +/vendor +.phpunit.result.cache +composer.lock +composer.local.json +Homestead.json +Homestead.yaml +npm-debug.log +yarn-error.log + +# IDEs # +################### +*.sublime-project +*.sublime-workspace +/.idea +/.vscode +*.komodoproject +.vscode + +# Static content # +################### +*.csv +*.pdf +*.doc +*.docx +*.xls +*.xlsx +*.xml +!phpunit.xml +!psalm.xml +*.yml +*.txt +*.wav +*.mp3 +*.avi + +# Compiled source # +################### +*.com +*.class +*.dll +*.exe +*.o +*.so +*.box + +# Packages # +############ +# it's better to unpack these files and commit the raw source +# git has its own built in compression methods +*.7z +*.dmg +*.gz +*.tgz +*.iso +*.jar +*.rar +*.tar +*.zip +*.phar + +# OS generated files # +###################### +.DS_Store +.DS_Store? +.nfs* +._* +.Spotlight-V100 +.Trashes +.vagrant +ehthumbs.db +Thumbs.db +sftp-config.json +auth.json \ No newline at end of file diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php index 9a110fe..b9ca63e 100644 --- a/.php-cs-fixer.php +++ b/.php-cs-fixer.php @@ -13,12 +13,15 @@ return (new PhpCsFixer\Config()) ->setRules([ '@PSR2' => true, + '@PSR12' => true, 'array_syntax' => ['syntax' => 'short'], 'ordered_imports' => ['sort_algorithm' => 'alpha'], 'no_unused_imports' => true, 'trailing_comma_in_multiline' => true, 'phpdoc_scalar' => true, 'unary_operator_spaces' => true, + 'binary_operator_spaces' => true, + 'concat_space' => ['spacing' => 'one'], 'blank_line_before_statement' => [ 'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'], ], @@ -36,6 +39,5 @@ 'single_trait_insert_per_statement' => true, 'no_whitespace_in_blank_line' => true, 'method_chaining_indentation' => true, - ]) ->setFinder($finder); diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index 48d6309..0000000 --- a/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -FROM php:8.1-alpine3.16 - -RUN apk add --virtual .build-deps --no-cache --update autoconf file g++ gcc libc-dev make pkgconf re2c zlib-dev bash git && \ - apk add --repository http://dl-cdn.alpinelinux.org/alpine/edge/community/ --allow-untrusted gnu-libiconv && \ - pecl install apcu redis xdebug && \ - docker-php-ext-install pcntl && \ - docker-php-ext-enable apcu redis xdebug && \ - apk del -f .build-deps && \ - pecl clear cache - -ENV LD_PRELOAD /usr/lib/preloadable_libiconv.so php - -COPY --from=composer:2.2 /usr/bin/composer /usr/bin/composer - -WORKDIR /var/www \ No newline at end of file diff --git a/LICENSE.md b/LICENSE.md index 1daddaa..2ed94d2 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,21 +1,75 @@ -MIT License - -Copyright (c) 2022 Ensi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Открытая лицензия на право использования программы для ЭВМ Greensight Ecom Platform (GEP) + +1. Преамбула + +1.1. Общество с ограниченной ответственностью «ГринСайт», в лице генерального директора Волкова Егора Владимировича, действующего на основании Устава, публикует условия публичной оферты о предоставлении открытой лицензии на право использования программы для ЭВМ Greensight Ecom Platform (GEP) (далее — Ensi) в соответствии с условиями ст. 1286.1 Гражданского кодекса РФ (далее — Оферта). + +1.2. Правообладателем Ensi является ООО «ГринСайт» (далее — Правообладатель), в соответствии со свидетельством о государственной регистрации программы для ЭВМ № 2 020 663 096 от 22.10.2020 г. + +1.3. В соответствии с пунктом 2 статьи 437 Гражданского кодекса РФ в случае принятия изложенных в Оферте условий Правообладателя, юридическое или физическое лицо, производящее акцепт Оферты, становится лицензиатом (в соответствии с пунктом 3 статьи 438 ГК РФ акцепт оферты равносилен заключению договора на условиях, изложенных в оферте), а Правообладатель и лицензиат совместно — сторонами лицензионного договора. + +1.4. Вся документация, функциональные задания, сервисы и исходные коды Ensi размещены в сети Интернет по адресам: https://ensi.tech/ https://gitlab.com/greensight/ensi (далее — Сайт платформы). + +1.5. Правообладатель является участником проекта «Сколково» и предоставляет права использования Ensi в рамках коммерциализации результатов своих исследований и разработок по направлению «стратегические компьютерные технологии и программное обеспечение». + +2. Порядок акцепта оферты + +2.1. Лицензионный договор, заключаемый на основании акцептирования лицензиатом Оферты (далее — Лицензионный договор), является договором присоединения, к которому лицензиат присоединяется без каких-либо исключений и/или оговорок. + +2.2. Акцепт Оферты происходит в момент скачивания материалов Ensi с Сайта платформы. + +2.3. Срок акцепта Оферты не ограничен. + +3. Перечень прав использования Ensi + +3.1. При соблюдении лицензиатом требований раздела 4 Оферты, предоставляемое право использования ENSI включает в себя: + +3.1.1. Право использования Ensi на технических средствах лицензиата в соответствии с назначением Ensi, в том числе, все права использования, предусмотренные ст. 1280 Гражданского кодекса РФ; + +3.1.2. Право на воспроизведение Ensi, не ограниченное правом его инсталляции и запуска; + +3.1.3. Право на модификацию, адаптацию, внесение изменений и создание производных произведений (сложных произведений) с Ensi. + +3.2. Лицензиату предоставляется право передачи третьим лицам прав, указанных в п. 3.1 Оферты (право сублицензирования). + +3.3. Действие Лицензионного договора — территория всего мира. + +3.4. Право использования Ensi предоставляется лицензиату на весь срок действия исключительных прав Правообладателя. + +3.5. Право использования Ensi предоставляется безвозмездно. Лицензиат вправе использовать Ensi для создания производных произведений (сложных произведений) и их коммерческого применения, с учетом ограничений раздела 4 Оферты. + +4. Обязанности лицензиата + +4.1. Лицензиату предоставляются права указанные в разделе 3 Оферты при соблюдении им следующих условий: + +4.1.1. Наличия письменного указания на авторство Правообладателя и ссылки на Сайт платформы при реализации третьим лицам Ensi (в коммерческих или некоммерческих целях), а также в любых созданных производных от Ensi произведениях. + +4.1.2. Сохранения неизменным следующих частей кода Ensi: +- в файле src/pages/_app.tsx строка — <meta name="generator" content="Ensi Platform" /> +- в файле next.config.js строка — return [{ source: '/(.*)', headers: [{ key: 'X-Ensi-Platform', value: '1' }] }]; + +Удаление данных частей кода будет является существенным нарушением условий Оферты. + +4.1.3. Использования Ensi в законных целях, а именно: не нарушающих законодательство Российской Федерации, норм международных договоров Российской Федерации, общепризнанных принципов и норм международного права. Не допускается использование Ensi в проектах, противоречащих принципам гуманности и морали, в распространении материалов и информации запрещенных в Российской Федерации. + +4.2. При нарушении лицензиатом условий п. 4.1 Оферты, Правообладатель вправе в одностороннем порядке расторгнуть Лицензионный договор и потребовать мер защиты исключительных прав, включая положения ст.ст. 1252, 1301 Гражданского кодекса РФ. + +4.3. Лицензиат дает Правообладателю согласие на указание своего фирменного наименования и логотипа на сайте Платформы. Правообладатель вправе использовать фирменное наименование и логотип Лицензиата в своих маркетинговых целях без дополнительного согласования с Лицензиатом. + +5. Ограничение ответственности + +5.1. Права использования Ensi предоставляются на условии «как есть» («as is») без какого-либо вида гарантий. Правообладатель не имеет обязательств перед лицензиатом по поддержанию функционирования Ensi в случае сбоя в работе, обеспечению отказоустойчивости и иных параметров, позволяющих использовать Ensi. Правообладатель не несет ответственности за любые убытки, упущенную выгоду, связанную с повреждением имущества, неполученным доходом, прерыванием коммерческой или производственной деятельности, возникшие вследствие использования Ensi лицензиатом. + +6. Заключительные положения + +6.1. Оферта вступает в силу с даты ее размещения на Сайте платформы и действует до момента прекращения исключительных прав на Ensi у Правообладателя. + +6.2. Переход исключительного права на Ensi к новому правообладателю не будет являться основанием для изменения или расторжения Лицензионного договора. + +6.3. К отношениям между Правообладателем и лицензиатом применяется право Российской Федерации. + +6.4. Реквизиты Правообладателя: +Общество с ограниченной ответственностью «ГринСайт» +ОГРН 11 087 746 328 812 +ИНН 7 735 538 694 +КПП 773 501 001 \ No newline at end of file diff --git a/README.md b/README.md index 3bd63e5..6c79c19 100644 --- a/README.md +++ b/README.md @@ -1,24 +1,37 @@ # Prometheus client for laravel -[![tests](https://github.com/ensi-platform/laravel-prometheus/actions/workflows/tests.yml/badge.svg)](https://github.com/ensi-platform/laravel-prometheus/actions/workflows/tests.yml) +[![Latest Version on Packagist](https://img.shields.io/packagist/v/ensi/laravel-prometheus.svg?style=flat-square)](https://packagist.org/packages/ensi/laravel-prometheus) +[![Tests](https://github.com/ensi-platform/laravel-prometheus/actions/workflows/run-tests.yml/badge.svg?branch=master)](https://github.com/ensi-platform/laravel-prometheus/actions/workflows/run-tests.yml) +[![Total Downloads](https://img.shields.io/packagist/dt/ensi/laravel-prometheus.svg?style=flat-square)](https://packagist.org/packages/ensi/laravel-prometheus) -Адаптер для [promphp/prometheus_client_php](https://github.com/PromPHP/prometheus_client_php) +Adapter for [promphp/prometheus_client_php](https://github.com/PromPHP/prometheus_client_php) ## Installation -Добавьте пакет в приложение +You can install the package via composer: + ```bash composer require ensi/laravel-prometheus ``` -Скопируйте конфигурацию для дальнейшей настройки +Publish the config with: + ```bash -php artisan vendor:publish --tag=prometheus-config +php artisan vendor:publish --provider="Ensi\LaravelPrometheus\PrometheusServiceProvider" ``` -## Usage +## Version Compatibility + +| Laravel Prometheus | Laravel | PHP | +|--------------------|----------------------------|------| +| ^1.0.0 | ^9.x | ^8.1 | +| ^1.0.4 | ^9.x \|\| ^10.x | ^8.1 | +| ^1.0.9 | ^9.x \|\| ^10.x \|\| ^11.x | ^8.1 | + +## Basic Usage + +Before you wind up the metric counters, you need to register them. The best thing to do is to use the boot() method from the application service provider. -Перед тем как накручивать счётчики метрик, их надо зарегистрировать. Лучше всего это делать в методе boot() в AppServiceProvider. ```php # app/Providers/AppServiceProvider.php public function boot() { @@ -26,7 +39,7 @@ public function boot() { Prometheus::summary('http_requests_duration_seconds', 60, [0.5, 0.95, 0.99]); } ``` -Обновить значение счётчика так же просто +Updating the counter value is just as easy ```php # app/Http/Middleware/Telemetry.php public function handle($request, Closure $next) @@ -44,7 +57,7 @@ public function handle($request, Closure $next) ## Configuration -Структура файла конфигурации +The structure of the configuration file ```php # config/prometheus.php @@ -76,34 +89,32 @@ return [ **Bag** -Вы можете захотеть иметь несколько наборов метрик, например один набор с техническими метриками, вроде количества http запросов или непойманных исключений, -и второй для бизнес-значений вроде количества заказов или показов определённой страницы. -Для этого вводится понятие bag. -Вы можете настроить несколько бэгов, указав для каждого своё хранилище данных, отдельный эндпоинт для сбора метрик и т.д. +You may want to have several sets of metrics, for example, one set with technical metrics, such as the number of http requests or unexpected exceptions, and a second set for business values, such as the number of orders or impressions of a particular page. +To do this, the concept of bag is introduced. +You can configure several bugs by specifying your own data warehouse for each, a separate endpoint for collecting metrics, etc. **Storage type** -Вы можете использовать все хранилища (Adapters) из пакета promphp/prometheus_client_php. Кроме того вы можете указать имя -redis connection'a из файла `config/databases.php`. +You can use all the storage (Adapters) from the promphp/prometheus_client_php package. In addition, you can specify the name of the redis connection from the file `config/databases.php`. -Варианты настройки хранилища. -Хранить метрики в памяти процесса. +Storage configuration options. +Store metrics in the process memory. ```php 'memory' => true ``` -Использовать APCu +Use apcupsd ```php 'apcu' => [ 'prefix' => 'metrics' ] ``` -или альтернативный адаптер APCuNG +or an alternative APCuNG adapter ```php 'apcu-ng' => [ 'prefix' => 'metrics' ] ``` -Redis адаптер, который сам создаст phpredis соединение +A Redis adapter that will create a phpredis connection by itself ```php 'redis' => [ 'host' => '127.0.0.1', @@ -116,8 +127,7 @@ Redis адаптер, который сам создаст phpredis соедин 'bag' => 'my-metrics-bag' ] ``` -Laravel Redis соединение из `config/databases.php`. Под капотом будет создан тот же Redis адаптер, -но он возьмёт нативный объект соединения phpredis из RedisManager'a ларавеля. +Laravel Redis connection from `config/databases.php`. The same Redis adapter will be created under the hood, but it will take the native phpredis connection object from laravel's Redismanager. ```php 'connection' => [ 'connection' => 'default', @@ -125,7 +135,8 @@ Laravel Redis соединение из `config/databases.php`. Под капо ] ``` ## Advanced Usage -Выбрать другой bag для создания и обновления в нём метрик можно через метод `bag($bagName)`. + +You can select another bag to create and update metrics in it using the `bag($bagName)` method. ```php # app/Providers/AppServiceProvider.php public function boot() { @@ -142,11 +153,9 @@ public function execute(Order $order) { ### Label Middlewares -Вы можете добавить лейбл ко всем метрикам bag'a указав в его конфигурации т.н. Label middleware. Label middleware -срабатывает в момент определения метрики и в момент обновления её счётчика, добавляя в первом случае на название лейбла, -а во втором значение. +You can add a label to all bagmetrics by specifying the so-called Label middleware in its configuration. Label middleware is triggered at the moment the metric is determined and at the moment its counter is updated, adding in the first case to the label name, and in the second case the value. -Например у намс есть TenantLabelProvider +For example, we have a TenantLabelProvider ```php class TenantLabelMiddleware implements LabelMiddleware { @@ -161,7 +170,7 @@ class TenantLabelMiddleware implements LabelMiddleware } } ``` -Регистрируем его в конфигурации bag'a. +We register it in the bag configuration. ```php # config/prometheus.php return [ @@ -176,23 +185,22 @@ return [ ], ]; ``` -Далее как обычно работаем с метриками. +Then, as usual, we work with metrics. ```php Prometheus::counter('http_requests_count')->labels(['endpoint', 'code']); // ... Prometheus::update('http_requests_count', 1, [Route::current()?->uri, $response->status()]); ``` -В результате метрика будет иметь не два, а три лейбла +As a result, the metric will have not two, but three labels ``` app_http_requests_count{endpoint="catalog/products",code="200",tenant="JBZ-987-H6"} 987 ``` ### On demand metrics -Иногда метрики не привязаны к событиям приложения. Обычно это метрики типа gauge, которые нет смысла обновлять на каждом входящем запросе, -т.к. прометеус всё-равно заберёт только последнее установленное значение. -Такие метрики можно рассчитывать в момент сбора метрик прометеусом. -Для этого вам нужно создать т.н. on demand метрику. Это класс, в котором вы регистрируете метрики и устанавливаете в них значения. +Sometimes metrics are not linked to application events. Usually these are metrics of the gauge type, which it makes no sense to update on each incoming request, because prometheus will still take only the last set value. +Such metrics can be calculated at the time of collection of metrics by prometheus. +To do this, you need to create a so-called on demand metric. This is the class in which you register metrics and set values in them. ```php class QueueLengthOnDemandMetric extends OnDemandMetric { public function register(MetricsBag $metricsBag): void @@ -206,7 +214,21 @@ class QueueLengthOnDemandMetric extends OnDemandMetric { } } ``` -Обновление таких метрик происходит в момент обращения прометеуса к эндпоинту получения метрик. +The update of such metrics occurs at the moment prometheus addresses the endpoint of obtaining metrics. + +## Contributing + +Please see [CONTRIBUTING](.github/CONTRIBUTING.md) for details. + +### Testing + +1. composer install +2. composer test + +## Security Vulnerabilities + +Please review [our security policy](.github/SECURITY.md) on how to report security vulnerabilities. ## License -Laravel Prometheus is open-sourced software licensed under the [MIT license](LICENSE.md). \ No newline at end of file + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. diff --git a/composer.json b/composer.json index 39da18e..1874aa4 100644 --- a/composer.json +++ b/composer.json @@ -1,8 +1,24 @@ { "name": "ensi/laravel-prometheus", - "description": "", + "description": "laravel prometheus", "type": "library", "license": "MIT", + "require": { + "php": "^8.1", + "ext-redis": "*", + "laravel/framework": "^9.0 || ^10.0 || ^11.0", + "promphp/prometheus_client_php": "^2.6" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "nunomaduro/collision": "^6.0 || ^7.0 || ^8.1", + "pestphp/pest": "^1.22 || ^2.0", + "pestphp/pest-plugin-laravel": "^1.1 || ^2.0", + "phpstan/extension-installer": "^1.3", + "phpstan/phpstan": "^1.11", + "spaze/phpstan-disallowed-calls": "^2.15", + "orchestra/testbench": "^7.0 || ^8.0 || ^9.0" + }, "autoload": { "psr-4": { "Ensi\\LaravelPrometheus\\": "src/" @@ -13,18 +29,12 @@ "Ensi\\LaravelPrometheus\\Tests\\": "tests/" } }, - "minimum-stability": "stable", - "require": { - "php": ">=8.1", - "ext-redis": "*", - "promphp/prometheus_client_php": "^2.6", - "laravel/framework": "^9.0 || ^10.0 || ^11.0" - }, - "require-dev": { - "friendsofphp/php-cs-fixer": "^3.2", - "orchestra/testbench": "^6.0||^7.0||^8.0||^9.0", - "phpunit/phpunit": "^9.0||^10.0||^11.0", - "php-parallel-lint/php-var-dump-check": "^0.5.0" + "scripts": { + "cs": "php-cs-fixer fix --config .php-cs-fixer.php", + "phpstan": "phpstan analyse", + "test": "./vendor/bin/pest --parallel --no-coverage", + "test-ci": "./vendor/bin/pest --no-coverage", + "test-coverage": "XDEBUG_MODE=coverage ./vendor/bin/pest --parallel --coverage" }, "extra": { "laravel": { @@ -33,8 +43,11 @@ ] } }, - "scripts": { - "cs": "php-cs-fixer fix --config .php-cs-fixer.php", - "test": "./vendor/bin/phpunit" + "config": { + "sort-packages": true, + "allow-plugins": { + "pestphp/pest-plugin": true, + "phpstan/extension-installer": true + } } } diff --git a/phpstan-package.neon b/phpstan-package.neon new file mode 100644 index 0000000..e69de29 diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..a2205ac --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,61 @@ +includes: + - ./vendor/spaze/phpstan-disallowed-calls/disallowed-dangerous-calls.neon + - ./phpstan-package.neon + +parameters: + paths: + - src + + scanFiles: + + # Pest handles loading custom helpers only when running tests + # @see https://pestphp.com/docs/helpers#usage + - tests/Pest.php + + # The level 9 is the highest level + level: 5 + + ignoreErrors: + - '#PHPDoc tag @var#' + + - '#Unsafe usage of new static\(\)\.#' + + # Pest implicitly binds $this to the current test case + # @see https://pestphp.com/docs/underlying-test-case + - + message: '#^Undefined variable: \$this$#' + path: '*Test.php' + + # Pest custom expectations are dynamic and not conducive static analysis + # @see https://pestphp.com/docs/expectations#custom-expectations + - + message: '#Call to an undefined method Pest\\Expectation|Pest\\Support\\Extendable::#' + path: '*Test.php' + + # Pest allow pass any array for TestCall::with + - + message: '#Parameter \#\d ...\$data of method Pest\\PendingCalls\\TestCall::with(.*) array(.*)given#' + path: '*Test.php' + + # Ignore custom method for Faker\Generator + - + message: '#Call to an undefined method Faker\\Generator|Ensi\\TestFactories\\FakerProvider::#' + path: '*Factory.php' + + # Ignore transfer of UploadedFile in auto-generated lib + - + message: '#expects SplFileObject\|null, Illuminate\\Http\\UploadedFile given.#' + path: '*Action.php' + + excludePaths: + - ./*/*/FileToBeExcluded.php + + disallowedFunctionCalls: + - + function: 'dd()' + message: 'use some logger instead' + - + function: 'dump()' + message: 'use some logger instead' + + reportUnmatchedIgnoredErrors: false diff --git a/phpunit.xml b/phpunit.xml index 76e6ea6..c2a4f58 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,30 +1,20 @@ + colors="true" +> - + tests - - + - src + ./src - - src/Storage/Redis.php - + + + diff --git a/src/MetricsBag.php b/src/MetricsBag.php index 5301552..cb34e9d 100644 --- a/src/MetricsBag.php +++ b/src/MetricsBag.php @@ -3,7 +3,6 @@ namespace Ensi\LaravelPrometheus; use Ensi\LaravelPrometheus\LabelMiddlewares\LabelMiddleware; -use Ensi\LaravelPrometheus\Metrics\AbstractMetric; use Ensi\LaravelPrometheus\Metrics\Counter; use Ensi\LaravelPrometheus\Metrics\Gauge; use Ensi\LaravelPrometheus\Metrics\Histogram; @@ -26,9 +25,9 @@ class MetricsBag private ?CollectorRegistry $collectors = null; /** @var array */ private array $middlewares = []; - /** @var array */ + /** @var array */ private array $metrics = []; - /** @var array */ + /** @var OnDemandMetric[] */ private array $onDemandMetrics = []; public function __construct(private array $config) diff --git a/src/Prometheus.php b/src/Prometheus.php index 8f22718..d8f4660 100644 --- a/src/Prometheus.php +++ b/src/Prometheus.php @@ -14,11 +14,11 @@ * @method static void addMiddleware(string $labelProcessorClass, array $parameters = []) * @method static void addOnDemandMetric(string $onDemandMetricClass) * - * @method static Counter counter(string $name, array $labels = []) - * @method static Gauge gauge(string $name, array $labels = []) - * @method static Histogram histogram(string $name, array $buckets, array $labels = []) - * @method static Summary summary(string $name, int $maxAgeSeconds, array $quantiles, array $labels = []) - * @method static void update(string $name, $value, array $labelValues) + * @method static Counter counter(string $name) + * @method static Gauge gauge(string $name) + * @method static Histogram histogram(string $name, array $buckets) + * @method static Summary summary(string $name, int $maxAgeSeconds, array $quantiles) + * @method static void update(string $name, $value, array $labelValues = []) * * @method static void processOnDemandMetrics() * @method static string dumpTxt() diff --git a/src/PrometheusServiceProvider.php b/src/PrometheusServiceProvider.php index 640f9cb..d5967bc 100644 --- a/src/PrometheusServiceProvider.php +++ b/src/PrometheusServiceProvider.php @@ -8,19 +8,19 @@ class PrometheusServiceProvider extends ServiceProvider { - public function register() + public function register(): void { + $this->mergeConfigFrom(__DIR__ . '/../config/prometheus.php', 'prometheus'); + $this->app->singleton(PrometheusManager::class); $this->app->alias(PrometheusManager::class, 'prometheus'); - - $this->mergeConfigFrom(__DIR__.'/../config/prometheus.php', 'prometheus'); } - public function boot() + public function boot(): void { if ($this->app->runningInConsole()) { $this->publishes([ - __DIR__.'/../config/prometheus.php' => config_path('prometheus.php'), + __DIR__ . '/../config/prometheus.php' => config_path('prometheus.php'), ], 'prometheus-config'); } diff --git a/src/Storage/Redis.php b/src/Storage/Redis.php index 5198125..10a90ca 100644 --- a/src/Storage/Redis.php +++ b/src/Storage/Redis.php @@ -18,7 +18,7 @@ class Redis implements Adapter { - const PROMETHEUS_METRIC_KEYS_SUFFIX = '_METRIC_KEYS'; + public const PROMETHEUS_METRIC_KEYS_SUFFIX = '_METRIC_KEYS'; /** * @var mixed[] @@ -116,8 +116,8 @@ public function flushRedis(): void */ public function getGlobalPrefix(): string { + /** @var mixed $globalPrefix */ $globalPrefix = $this->redis->getOption(\Redis::OPT_PREFIX); - // @phpstan-ignore-next-line false positive, phpstan thinks getOptions returns int if (!is_string($globalPrefix)) { return ''; } @@ -477,11 +477,11 @@ public function removeMetric(string $type, array $data): bool { $metricKeyForDelete = $this->getGlobalPrefix() . $this->prefix; $metricKeyForDelete .= match ($data['type']) { - Summary::TYPE => $type . implode(':', [self::PROMETHEUS_METRIC_KEYS_SUFFIX, $data['name']]). ':*', + Summary::TYPE => $type . implode(':', [self::PROMETHEUS_METRIC_KEYS_SUFFIX, $data['name']]) . ':*', default => ':' . implode(':', [$type, $data['name']]), }; - $result = boolval($this->redis->eval( + $result = boolval($this->redis->eval( <<dumpTxt(); - $this->assertStringContainsString($needle, $bagValues); - } - - private function assertBagContainsMetric(MetricsBag $bag, string $metric, array $labels, $value): void - { - $labelItems = []; - foreach ($labels as $label => $labelValue) { - $labelItems[] = "{$label}=\"{$labelValue}\""; - } - - if ($labelItems) { - $labelsStr = join(",", $labelItems); - $metricLine = "{$metric}{{$labelsStr}} {$value}"; - } else { - $metricLine = "{$metric} {$value}"; - } - - $this->assertBagContainsString($bag, $metricLine); - } - - private function assertHistogramState(MetricsBag $bag, string $metric, array $labels, int $sum, int $count, array $buckets): void - { - $this->assertBagContainsMetric($bag, $metric . '_sum', $labels, $sum); - $this->assertBagContainsMetric($bag, $metric . '_count', $labels, $count); - - foreach ($buckets as $bucket => $value) { - $bucketLabels = $labels; - $bucketLabels['le'] = $bucket; - $this->assertBagContainsMetric($bag, $metric . '_bucket', $bucketLabels, $value); - } - } - - protected function assertSummaryState(MetricsBag $bag, string $metric, array $labels, float $sum, float $count, array $quantiles): void - { - $this->assertBagContainsMetric($bag, $metric . '_sum', $labels, $sum); - $this->assertBagContainsMetric($bag, $metric . '_count', $labels, $count); - - foreach ($quantiles as $quantile => $value) { - $quantileLabels = $labels; - $quantileLabels['quantile'] = $quantile; - - $this->assertBagContainsMetric($bag, $metric, $quantileLabels, $value); - } - } - - public function testMetricHelp() - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - ]); - - $bag->counter('my_counter')->help("Super metric")->update(); - - $this->assertBagContainsString($bag, "# HELP test_my_counter Super metric"); - } - - public static function labelsProvider(): array - { - return [ - [[]], - [['label' => 'value']], - [['label_1' => 'value-1', 'label_2' => 'value-2']], - ]; - } - - /** - * @dataProvider labelsProvider - */ - public function testMetricLabels(array $labels) - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - ]); - - $bag->counter('my_counter')->labels(array_keys($labels))->update(1, array_values($labels)); - $this->assertBagContainsMetric($bag, 'test_my_counter', $labels, 1); - } - - /** - * @dataProvider labelsProvider - */ - public function testGlobalMiddleware(array $labels) - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - 'label_middlewares' => [ - GlobalMiddleware::class, - ], - ]); - - $bag->counter('my_counter')->labels(array_keys($labels))->update(1, array_values($labels)); - - $labels = GlobalMiddleware::injectToMap($labels); - - $this->assertBagContainsMetric($bag, 'test_my_counter', $labels, 1); - } - - /** - * @dataProvider labelsProvider - */ - public function testLocalMiddleware(array $labels) - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - ]); - - $bag->counter('my_counter') - ->middleware(LocalMiddleware::class) - ->labels(array_keys($labels)) - ->update(1, array_values($labels)); - - $labels = LocalMiddleware::injectToMap($labels); - - $this->assertBagContainsMetric($bag, 'test_my_counter', $labels, 1); - } - - /** - * @dataProvider labelsProvider - */ - public function testBothMiddlewares(array $labels) - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - 'label_middlewares' => [ - GlobalMiddleware::class, - ], - ]); - - $bag->counter('my_counter') - ->middleware(LocalMiddleware::class) - ->labels(array_keys($labels)) - ->update(1, array_values($labels)); - - $labels = GlobalMiddleware::injectToMap($labels); - $labels = LocalMiddleware::injectToMap($labels); - - $this->assertBagContainsMetric($bag, 'test_my_counter', $labels, 1); - } - - public function testCounter() - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - ]); - - $bag->counter('my_counter')->labels(['my_label']); - - $bag->update('my_counter', 1, ['my-value']); - $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value"], 1); +uses(MetricsBagTestCase::class); - $bag->update('my_counter', 1, ['my-value']); - $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value"], 2); +test('test metric help', function () { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + ]); - $bag->update('my_counter', 2.5, ['my-value']); - $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value"], 4.5); - } - - public function testGauge() - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - ]); - - $bag->gauge('my_gauge')->labels(['my_label']); - - $bag->update('my_gauge', 10, ['my-value']); - $this->assertBagContainsMetric($bag, 'test_my_gauge', ["my_label" => "my-value"], 10); - - $bag->update('my_gauge', 5, ['my-value']); - $this->assertBagContainsMetric($bag, 'test_my_gauge', ["my_label" => "my-value"], 5); - } - - public function testHistogram() - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - ]); - - $bag->histogram('my_histogram', [2, 4])->labels(['my_label']); - - $bag->update('my_histogram', 3, ["my_label" => "my-value"]); - $this->assertHistogramState($bag, 'test_my_histogram', ["my_label" => "my-value"], 3, 1, [ - 2 => 0, - 4 => 1, - "+Inf" => 1, - ]); - - $bag->update('my_histogram', 5, ["my_label" => "my-value"]); - $this->assertHistogramState($bag, 'test_my_histogram', ["my_label" => "my-value"], 8, 2, [ - 2 => 0, - 4 => 1, - "+Inf" => 2, - ]); - - $bag->update('my_histogram', 1, ["my_label" => "my-value"]); - $this->assertHistogramState($bag, 'test_my_histogram', ["my_label" => "my-value"], 9, 3, [ - 2 => 1, - 4 => 2, - "+Inf" => 3, - ]); - - $bag->update('my_histogram', 50, ["my_label" => "my-value"]); - $this->assertHistogramState($bag, 'test_my_histogram', ["my_label" => "my-value"], 59, 4, [ - 2 => 1, - 4 => 2, - "+Inf" => 4, - ]); - } - - protected function getPercentile(array $values, float $level) - { - sort($values); - $idealIndex = (count($values) - 1) * $level; - $leftIndex = floor($idealIndex); - - return $values[$leftIndex]; - } - - public function testSummary() - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - ]); - $bag->summary('my_summary', 120, [0.5, 0.9])->labels(['my_label']); - - $values = []; - - for ($i = 0; $i < 10; $i++) { - $value = mt_rand(0, 100); - $values[] = $value; - $bag->update('my_summary', $value, ["my-value"]); - } - - $this->assertSummaryState($bag, 'test_my_summary', ['my_label' => 'my-value'], array_sum($values), 10, [ - "0.5" => $this->getPercentile($values, 0.5), - "0.9" => $this->getPercentile($values, 0.9), - ]); - } - - public function testNoCreateSameMetricTwice(): void - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - ]); - - $counter1 = $bag->counter('my_counter'); - $counter2 = $bag->counter('my_counter'); - - $this->assertSame($counter1, $counter2); - } + $bag->counter('my_counter')->help("Super metric")->update(); - public function testLabelMiddleware() - { - config([ - 'prometheus.app_name' => 'app-name', - ]); + $this->assertStringContainsString("# HELP test_my_counter Super metric", $bag->dumpTxt()); +})->with(MetricsBagTestCase::labelsDataset()); - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - 'label_middlewares' => [ - AppNameLabelMiddleware::class, - ], - ]); +test('test metric labels', function (array $labels) { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + ]); - $bag->counter('my_counter')->labels(['my_label']); + $bag->counter('my_counter')->labels(array_keys($labels))->update(1, array_values($labels)); + $this->assertBagContainsMetric($bag, 'test_my_counter', $labels, 1); +})->with(MetricsBagTestCase::labelsDataset()); - $bag->update('my_counter', 42, ['my-value']); - $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value", "app" => "app-name"], 42); +test('test global middleware', function (array $labels) { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + 'label_middlewares' => [ + GlobalMiddleware::class, + ], + ]); + + $bag->counter('my_counter')->labels(array_keys($labels))->update(1, array_values($labels)); + + $labels = GlobalMiddleware::injectToMap($labels); + + $this->assertBagContainsMetric($bag, 'test_my_counter', $labels, 1); +})->with(MetricsBagTestCase::labelsDataset()); + +test('test local middleware', function (array $labels) { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + ]); + + $bag->counter('my_counter') + ->middleware(LocalMiddleware::class) + ->labels(array_keys($labels)) + ->update(1, array_values($labels)); + + $labels = LocalMiddleware::injectToMap($labels); + + $this->assertBagContainsMetric($bag, 'test_my_counter', $labels, 1); +})->with(MetricsBagTestCase::labelsDataset()); + +test('test both middlewares', function (array $labels) { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + 'label_middlewares' => [ + GlobalMiddleware::class, + ], + ]); + + $bag->counter('my_counter') + ->middleware(LocalMiddleware::class) + ->labels(array_keys($labels)) + ->update(1, array_values($labels)); + + $labels = GlobalMiddleware::injectToMap($labels); + $labels = LocalMiddleware::injectToMap($labels); + + $this->assertBagContainsMetric($bag, 'test_my_counter', $labels, 1); +})->with(MetricsBagTestCase::labelsDataset()); + +test('test counter', function () { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + ]); + + $bag->counter('my_counter')->labels(['my_label']); + + $bag->update('my_counter', 1, ['my-value']); + $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value"], 1); + + $bag->update('my_counter', 1, ['my-value']); + $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value"], 2); + + $bag->update('my_counter', 2.5, ['my-value']); + $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value"], 4.5); +}); + +test('test gauge', function () { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + ]); + + $bag->gauge('my_gauge')->labels(['my_label']); + + $bag->update('my_gauge', 10, ['my-value']); + $this->assertBagContainsMetric($bag, 'test_my_gauge', ["my_label" => "my-value"], 10); + + $bag->update('my_gauge', 5, ['my-value']); + $this->assertBagContainsMetric($bag, 'test_my_gauge', ["my_label" => "my-value"], 5); +}); + +test('test histogram', function () { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + ]); + + $bag->histogram('my_histogram', [2, 4])->labels(['my_label']); + + $bag->update('my_histogram', 3, ["my_label" => "my-value"]); + $this->assertHistogramState($bag, 'test_my_histogram', ["my_label" => "my-value"], 3, 1, [ + 2 => 0, + 4 => 1, + "+Inf" => 1, + ]); + + $bag->update('my_histogram', 5, ["my_label" => "my-value"]); + $this->assertHistogramState($bag, 'test_my_histogram', ["my_label" => "my-value"], 8, 2, [ + 2 => 0, + 4 => 1, + "+Inf" => 2, + ]); + + $bag->update('my_histogram', 1, ["my_label" => "my-value"]); + $this->assertHistogramState($bag, 'test_my_histogram', ["my_label" => "my-value"], 9, 3, [ + 2 => 1, + 4 => 2, + "+Inf" => 3, + ]); + + $bag->update('my_histogram', 50, ["my_label" => "my-value"]); + $this->assertHistogramState($bag, 'test_my_histogram', ["my_label" => "my-value"], 59, 4, [ + 2 => 1, + 4 => 2, + "+Inf" => 4, + ]); +}); + +test('test summary', function () { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + ]); + $bag->summary('my_summary', 120, [0.5, 0.9])->labels(['my_label']); + + $values = []; + + for ($i = 0; $i < 10; $i++) { + $value = mt_rand(0, 100); + $values[] = $value; + $bag->update('my_summary', $value, ["my-value"]); } - public function testWipeStorage() - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - ]); - - $bag->counter('my_counter')->labels(['my_label']); - - $bag->update('my_counter', 42, ['my-value']); - $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value"], 42); - - $bag->wipe(); - $bag->update('my_counter', 5, ['my-value']); - $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value"], 5); - } - - public function testBagAuth() - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - 'basic_auth' => [ - 'login' => 'user', - 'password' => 'secret', - ], - ]); - - $this->assertTrue($bag->auth(Request::create("http://user:secret@localhost/metrics"))); - $this->assertFalse($bag->auth(Request::create("http://user:123456@localhost/metrics"))); - $this->assertFalse($bag->auth(Request::create("http://bot:secret@localhost/metrics"))); - $this->assertFalse($bag->auth(Request::create("http://localhost/metrics"))); - } - - public function testOnDemandMetrics() - { - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - 'on_demand_metrics' => [ - SomeOnDemandMetric::class, - ], - ]); - - $bag->processOnDemandMetrics(); - - $this->assertBagContainsMetric($bag, 'test_on_demand_counter', [], 1); - } - - public function testNoUpdatesWhenPrometheusDisabled(): void - { - config([ - 'prometheus' => [ - 'enabled' => false, - ], - ]); - - $bag = new MetricsBag([ - 'namespace' => 'test', - 'memory' => true, - ]); - - $bag->counter('my_counter')->labels(['my_label']); - - $bag->update('my_counter', 1, ['my-value']); - - $bagValues = $bag->dumpTxt(); - $this->assertStringNotContainsString('test_my_counter', $bagValues); - } -} + $this->assertSummaryState($bag, 'test_my_summary', ['my_label' => 'my-value'], array_sum($values), 10, [ + "0.5" => $this->getPercentile($values, 0.5), + "0.9" => $this->getPercentile($values, 0.9), + ]); +}); + +test('test no create same metric twice', function () { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + ]); + + $counter1 = $bag->counter('my_counter'); + $counter2 = $bag->counter('my_counter'); + + $this->assertSame($counter1, $counter2); +}); + +test('test label middleware', function () { + config([ + 'prometheus.app_name' => 'app-name', + ]); + + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + 'label_middlewares' => [ + AppNameLabelMiddleware::class, + ], + ]); + + $bag->counter('my_counter')->labels(['my_label']); + + $bag->update('my_counter', 42, ['my-value']); + $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value", "app" => "app-name"], 42); +}); + +test('test wipe storage', function () { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + ]); + + $bag->counter('my_counter')->labels(['my_label']); + + $bag->update('my_counter', 42, ['my-value']); + $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value"], 42); + + $bag->wipe(); + $bag->update('my_counter', 5, ['my-value']); + $this->assertBagContainsMetric($bag, 'test_my_counter', ["my_label" => "my-value"], 5); +}); + +test('test bag auth', function () { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + 'basic_auth' => [ + 'login' => 'user', + 'password' => 'secret', + ], + ]); + + $this->assertTrue($bag->auth(Request::create("http://user:secret@localhost/metrics"))); + $this->assertFalse($bag->auth(Request::create("http://user:123456@localhost/metrics"))); + $this->assertFalse($bag->auth(Request::create("http://bot:secret@localhost/metrics"))); + $this->assertFalse($bag->auth(Request::create("http://localhost/metrics"))); +}); + +test('test on demand metrics', function () { + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + 'on_demand_metrics' => [ + SomeOnDemandMetric::class, + ], + ]); + + $bag->processOnDemandMetrics(); + + $this->assertBagContainsMetric($bag, 'test_on_demand_counter', [], 1); +}); + +test('test no updates when prometheus disabled', function () { + config([ + 'prometheus' => [ + 'enabled' => false, + ], + ]); + + $bag = new MetricsBag([ + 'namespace' => 'test', + 'memory' => true, + ]); + + $bag->counter('my_counter')->labels(['my_label']); + + $bag->update('my_counter', 1, ['my-value']); + + $bagValues = $bag->dumpTxt(); + $this->assertStringNotContainsString('test_my_counter', $bagValues); +}); diff --git a/tests/MetricsControllerTest.php b/tests/MetricsControllerTest.php index 7592b20..63b4c2b 100644 --- a/tests/MetricsControllerTest.php +++ b/tests/MetricsControllerTest.php @@ -5,76 +5,76 @@ use Ensi\LaravelPrometheus\Controllers\MetricsController; use Ensi\LaravelPrometheus\PrometheusManager; use Illuminate\Http\Request; + +use function PHPUnit\Framework\assertEquals; +use function PHPUnit\Framework\assertStringContainsString; + use Symfony\Component\HttpKernel\Exception\HttpException; -class MetricsControllerTest extends TestCase -{ - public function testMetricsRoute() - { - config([ - 'prometheus.bags' => [ - 'default' => [ - 'namespace' => 'app', - 'route' => 'metrics', - 'memory' => true, - ], +uses(TestCase::class); + +test('test metrics route', function () { + config([ + 'prometheus.bags' => [ + 'default' => [ + 'namespace' => 'app', + 'route' => 'metrics', + 'memory' => true, ], - ]); + ], + ]); - /** @var PrometheusManager $manager */ - $manager = resolve(PrometheusManager::class); - $request = Request::create("http://localhost/metrics"); + /** @var PrometheusManager $manager */ + $manager = resolve(PrometheusManager::class); + $request = Request::create("http://localhost/metrics"); - $manager->counter('orders_count')->update(); + $manager->counter('orders_count')->update(); - $response = (new MetricsController())($request, $manager); - $this->assertStringContainsString('app_orders_count', $response->getContent()); - } + $response = (new MetricsController())($request, $manager); + assertStringContainsString('app_orders_count', $response->getContent()); +}); - public function testMetricsRouteWithAuth() - { - config([ - 'prometheus.bags' => [ - 'default' => [ - 'namespace' => 'app', - 'route' => 'metrics', - 'memory' => true, - 'basic_auth' => [ - 'login' => 'user', - 'password' => 'password', - ], +test('test metrics route with auth', function () { + config([ + 'prometheus.bags' => [ + 'default' => [ + 'namespace' => 'app', + 'route' => 'metrics', + 'memory' => true, + 'basic_auth' => [ + 'login' => 'user', + 'password' => 'password', ], ], - ]); + ], + ]); - /** @var PrometheusManager $manager */ - $manager = resolve(PrometheusManager::class); - $request = Request::create("http://user:password@localhost/metrics"); + /** @var PrometheusManager $manager */ + $manager = resolve(PrometheusManager::class); + $request = Request::create("http://user:password@localhost/metrics"); - $response = (new MetricsController())($request, $manager); - $this->assertEquals(200, $response->status()); - } + $response = (new MetricsController())($request, $manager); + assertEquals(200, $response->status()); +}); - public function testMetricsRouteAccessDenied() - { - config([ - 'prometheus.bags' => [ - 'default' => [ - 'namespace' => 'app', - 'route' => 'metrics', - 'memory' => true, - 'basic_auth' => [ - 'login' => 'user', - 'password' => 'password', - ], +test('test metrics route access denied', function () { + config([ + 'prometheus.bags' => [ + 'default' => [ + 'namespace' => 'app', + 'route' => 'metrics', + 'memory' => true, + 'basic_auth' => [ + 'login' => 'user', + 'password' => 'password', ], ], - ]); + ], + ]); - /** @var PrometheusManager $manager */ - $manager = resolve(PrometheusManager::class); - $request = Request::create("http://localhost/metrics"); + /** @var PrometheusManager $manager */ + $manager = resolve(PrometheusManager::class); + $request = Request::create("http://localhost/metrics"); - $this->assertThrows(fn () => (new MetricsController())($request, $manager), HttpException::class); - } -} + $this->assertThrows(fn () => (new MetricsController())($request, $manager), HttpException::class); +}); diff --git a/tests/Pest.php b/tests/Pest.php new file mode 100644 index 0000000..febc07e --- /dev/null +++ b/tests/Pest.php @@ -0,0 +1,46 @@ +in(__DIR__); + + +/* +|-------------------------------------------------------------------------- +| Expectations +|-------------------------------------------------------------------------- +| +| When you're writing tests, you often need to check that values meet certain conditions. The +| "expect()" function gives you access to a set of "expectations" methods that you can use +| to assert different things. Of course, you may extend the Expectation API at any time. +| +*/ + +//expect()->extend('toBeOne', function () { +// return $this->toBe(1); +//}); + +/* +|-------------------------------------------------------------------------- +| Functions +|-------------------------------------------------------------------------- +| +| While Pest is very powerful out-of-the-box, you may have some testing code specific to your +| project that you don't want to repeat in every file. Here you can also expose helpers as +| global functions to help you to reduce the number of lines of code in your test files. +| +*/ + +//function something() +//{ +// // .. +//} diff --git a/tests/PrometheusManagerTest.php b/tests/PrometheusManagerTest.php index 911e01e..7b6fda5 100644 --- a/tests/PrometheusManagerTest.php +++ b/tests/PrometheusManagerTest.php @@ -7,78 +7,75 @@ use InvalidArgumentException; use Mockery\MockInterface; -class PrometheusManagerTest extends TestCase -{ - public function testDefaultBagIsExists() - { - $this->assertInstanceOf(MetricsBag::class, resolve(PrometheusManager::class)->bag()); - } - - public function testGetBagByName() - { - config([ - 'prometheus.bags' => [ +use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertNotSame; +use function PHPUnit\Framework\assertSame; + +uses(TestCase::class); + +test('test default bag is exists', function () { + assertInstanceOf(MetricsBag::class, resolve(PrometheusManager::class)->bag()); +}); + +test('test get bag by name', function () { + config([ + 'prometheus.bags' => [ + 'first' => [1], + 'second' => [1], + ], + ]); + $manager = resolve(PrometheusManager::class); + assertInstanceOf(MetricsBag::class, $manager->bag('second')); +}); + +test('test undefined bag throws exception', function () { + $this->assertThrows(function () { + resolve(PrometheusManager::class)->bag('undefined'); + }, InvalidArgumentException::class); +}); + +test('test bag is not recreated at second call', function () { + /** @var PrometheusManager $manager */ + $manager = resolve(PrometheusManager::class); + + $firstBag = $manager->bag(); + $secondBag = $manager->bag(); + + assertSame($firstBag, $secondBag); +}); + +test('test set default bag', function () { + config([ + 'prometheus' => [ + 'default_bag' => 'first', + 'bags' => [ 'first' => [1], 'second' => [1], ], - ]); - $manager = resolve(PrometheusManager::class); - $this->assertInstanceOf(MetricsBag::class, $manager->bag('second')); - } - - public function testUndefinedBagThrowsException() - { - $this->assertThrows(function () { - resolve(PrometheusManager::class)->bag('undefined'); - }, InvalidArgumentException::class); - } - - public function testBagIsNotRecreatedAtSecondCall() - { - /** @var PrometheusManager $manager */ - $manager = resolve(PrometheusManager::class); - - $firstBag = $manager->bag(); - $secondBag = $manager->bag(); - - $this->assertSame($firstBag, $secondBag); - } - - public function testSetDefaultBag() - { - config([ - 'prometheus' => [ - 'default_bag' => 'first', - 'bags' => [ - 'first' => [1], - 'second' => [1], - ], - ], - ]); - /** @var PrometheusManager $manager */ - $manager = resolve(PrometheusManager::class); - - $firstBag = $manager->bag(); - $manager->setDefaultBag('second'); - $secondBag = $manager->bag(); - - $this->assertNotSame($firstBag, $secondBag); - } - - public function testInvokeDefaultBagsMethodOnCall() - { - /** @var PrometheusManager|MockInterface $manager */ - $manager = $this->partialMock(PrometheusManager::class) - ->shouldAllowMockingProtectedMethods(); - - $bag = $this->mock(MetricsBag::class); - $bag->expects('declareCounter') - ->withArgs(['example']); - - $manager->expects('createMetricsBag') - ->withArgs(['default']) - ->andReturn($bag); - - $manager->declareCounter('example'); - } -} + ], + ]); + /** @var PrometheusManager $manager */ + $manager = resolve(PrometheusManager::class); + + $firstBag = $manager->bag(); + $manager->setDefaultBag('second'); + $secondBag = $manager->bag(); + + assertNotSame($firstBag, $secondBag); +}); + +test('test invoke default bags method on call', function () { + /** @var PrometheusManager|MockInterface $manager */ + $manager = $this->partialMock(PrometheusManager::class) + ->shouldAllowMockingProtectedMethods(); + + $bag = $this->mock(MetricsBag::class); + $bag->expects('declareCounter') + ->withArgs(['example']); + + $manager->expects('createMetricsBag') + ->withArgs(['default']) + ->andReturn($bag); + + $manager->declareCounter('example'); +}); diff --git a/tests/ServiceProviderTest.php b/tests/ServiceProviderTest.php index d81d231..aedd360 100644 --- a/tests/ServiceProviderTest.php +++ b/tests/ServiceProviderTest.php @@ -5,15 +5,15 @@ use Ensi\LaravelPrometheus\PrometheusManager; use Illuminate\Support\Facades\Route; -class ServiceProviderTest extends TestCase -{ - public function testManagerIsRegistered() - { - self::assertInstanceOf(PrometheusManager::class, resolve(PrometheusManager::class)); - } +use function PHPUnit\Framework\assertInstanceOf; +use function PHPUnit\Framework\assertTrue; - public function testDefaultBagRouteIsRegistered() - { - $this->assertTrue(Route::has('prometheus.default')); - } -} +uses(TestCase::class); + +test('test manager is registered', function () { + assertInstanceOf(PrometheusManager::class, resolve(PrometheusManager::class)); +}); + +test('test default bag route is registered', function () { + assertTrue(Route::has('prometheus.default')); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index fa8117b..202470a 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,12 +2,15 @@ namespace Ensi\LaravelPrometheus\Tests; -class TestCase extends \Orchestra\Testbench\TestCase +use Ensi\LaravelPrometheus\PrometheusServiceProvider; +use Orchestra\Testbench\TestCase as Orchestra; + +class TestCase extends Orchestra { - protected function getPackageProviders($app) + protected function getPackageProviders($app): array { return [ - \Ensi\LaravelPrometheus\PrometheusServiceProvider::class, + PrometheusServiceProvider::class, ]; } } diff --git a/tests/TestCases/MetricsBagTestCase.php b/tests/TestCases/MetricsBagTestCase.php new file mode 100644 index 0000000..369715b --- /dev/null +++ b/tests/TestCases/MetricsBagTestCase.php @@ -0,0 +1,69 @@ + $labelValue) { + $labelItems[] = "{$label}=\"{$labelValue}\""; + } + + if ($labelItems) { + $labelsStr = join(",", $labelItems); + $metricLine = "{$metric}{{$labelsStr}} {$value}"; + } else { + $metricLine = "{$metric} {$value}"; + } + + $this->assertStringContainsString($metricLine, $bag->dumpTxt()); + } + + protected function assertHistogramState(MetricsBag $bag, string $metric, array $labels, int $sum, int $count, array $buckets): void + { + $this->assertBagContainsMetric($bag, $metric . '_sum', $labels, $sum); + $this->assertBagContainsMetric($bag, $metric . '_count', $labels, $count); + + foreach ($buckets as $bucket => $value) { + $bucketLabels = $labels; + $bucketLabels['le'] = $bucket; + $this->assertBagContainsMetric($bag, $metric . '_bucket', $bucketLabels, $value); + } + } + + protected function assertSummaryState(MetricsBag $bag, string $metric, array $labels, float $sum, float $count, array $quantiles): void + { + $this->assertBagContainsMetric($bag, $metric . '_sum', $labels, $sum); + $this->assertBagContainsMetric($bag, $metric . '_count', $labels, $count); + + foreach ($quantiles as $quantile => $value) { + $quantileLabels = $labels; + $quantileLabels['quantile'] = $quantile; + + $this->assertBagContainsMetric($bag, $metric, $quantileLabels, $value); + } + } + + protected function getPercentile(array $values, float $level) + { + sort($values); + $idealIndex = (count($values) - 1) * $level; + $leftIndex = floor($idealIndex); + + return $values[$leftIndex]; + } + + public static function labelsDataset(): array + { + return [ + [[]], + [['label' => 'value']], + [['label_1' => 'value-1', 'label_2' => 'value-2']], + ]; + } +}