├── .github └── workflows │ └── php.yml ├── .gitignore ├── .php_cs ├── .scrutinizer.yml ├── LICENSE ├── README.md ├── bin ├── dump_reference.php └── test_console.php ├── composer.json ├── doc ├── examples │ ├── 01_collect.md │ ├── 02_store.md │ ├── 03_respond.md │ └── 04_intermediate_storage.md └── extending.md ├── phpunit.xml ├── src ├── Adapters │ ├── Doctrine │ │ ├── AbstractDoctrineStorage.php │ │ └── AtomicMutableWrapper.php │ └── Redis │ │ ├── AbstractRedisStorage.php │ │ ├── MetricDto.php │ │ ├── MetricWrapper.php │ │ ├── MutatorRedisConnectionInterface.php │ │ ├── RedisConnection.php │ │ └── RedisConnectionInterface.php ├── Collector │ ├── CollectorRegistry.php │ ├── MergingCollector.php │ ├── MetricCollectorInterface.php │ ├── SingleSourceCollector.php │ └── TaggingCollectorDecorator.php ├── Common │ ├── DefaultTagsMetric.php │ ├── Metric.php │ ├── MetricInterface.php │ ├── MetricSourceInterface.php │ └── Source │ │ ├── DefaultTaggingMetricSource.php │ │ ├── IterableMetricSource.php │ │ └── MergingMetricSource.php ├── MetricBundle │ ├── Command │ │ ├── DebugMetricsCommand.php │ │ └── MaterializeMetricsCommand.php │ ├── Controller │ │ └── HttpFoundationResponder.php │ ├── DependencyInjection │ │ ├── Compiler │ │ │ ├── RegisterCollectorsPass.php │ │ │ ├── RegisterReceiversPass.php │ │ │ └── RegisterResponseFactoriesPass.php │ │ ├── Configuration.php │ │ ├── DefinitionFactory │ │ │ ├── Collector.php │ │ │ ├── Responder.php │ │ │ ├── ResponseFactory.php │ │ │ ├── Source.php │ │ │ └── Storage.php │ │ └── LamodaMetricExtension.php │ ├── LamodaMetricBundle.php │ ├── Resources │ │ ├── config │ │ │ ├── response_factories.yml │ │ │ └── services.yml │ │ └── docs │ │ │ ├── commands.md │ │ │ ├── integration.md │ │ │ └── reference.md │ └── Routing │ │ └── MetricRouteLoader.php ├── Responder │ ├── PsrResponder.php │ ├── ResponseFactory │ │ ├── PrometheusResponseFactory.php │ │ └── TelegrafJsonResponseFactory.php │ └── ResponseFactoryInterface.php └── Storage │ ├── Exception │ └── ReceiverException.php │ ├── MaterializeHelper.php │ ├── MetricMutatorInterface.php │ ├── MetricReceiverInterface.php │ ├── MetricStorageInterface.php │ ├── MutableMetricInterface.php │ ├── StorageRegistry.php │ └── StoredMetricMutator.php └── tests ├── Adapters ├── Doctrine │ ├── AbstractDoctrineStorageTest.php │ └── AtomicMutableWrapperTest.php └── Redis │ ├── AbstractRedisStorageTest.php │ └── RedisConnectionTest.php ├── Builders └── TraversableMetricSourceBuilder.php ├── Collector ├── CollectorRegistryTest.php ├── MergingCollectorTest.php ├── SingleSourceCollectorTest.php └── TaggingCollectorDecoratorTest.php ├── Common ├── DefaultTagsMetricTest.php ├── MetricTest.php └── Source │ ├── DefaultTaggingMetricSourceTest.php │ ├── IterableSourceTest.php │ └── MergingMetricSourceTest.php ├── MetricBundle ├── AbstractMetricBundleTestClass.php ├── Controller │ └── HttpFoundationResponderTest.php ├── DependencyInjection │ ├── ConfigurationTest.php │ └── valid_config_samples │ │ ├── empty.yml │ │ ├── full.yml │ │ └── reference.yml ├── Fixtures │ ├── Entity │ │ └── Metric.php │ ├── Resources │ │ └── config │ │ │ └── doctrine │ │ │ └── Metric.orm.yml │ ├── Storage │ │ └── MetricStorage.php │ ├── TestIntegrationBundle.php │ ├── TestKernel.php │ ├── config.yml │ ├── kernel_setup.yml │ └── routing.yml ├── MetricRespondingTest.php └── StoredMetricMutatorTest.php ├── Responder ├── PsrResponderTest.php └── ResponseFactory │ ├── PrometheusResponseFactoryTest.php │ └── TelegrafResponseFactoryTest.php └── Storage ├── MaterializeHelperTest.php ├── StorageRegistryTest.php └── StoredMetricMutatorTest.php /.github/workflows/php.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | tags: 7 | pull_request: 8 | 9 | jobs: 10 | build: 11 | name: PHP ${{ matrix.php }} with ${{ matrix.packages }} 12 | runs-on: ${{ matrix.os }} 13 | continue-on-error: ${{ matrix.experimental }} 14 | strategy: 15 | fail-fast: false 16 | matrix: 17 | include: 18 | - { os: ubuntu-latest, php: 7.1, experimental: false} 19 | - { os: ubuntu-latest, php: 7.2, experimental: false} 20 | - { os: ubuntu-latest, php: 7.3, experimental: false} 21 | - { os: ubuntu-latest, php: 7.4, experimental: false} 22 | - { os: ubuntu-latest, php: 8.0, experimental: false} 23 | - { os: ubuntu-latest, php: 8.1, experimental: false } 24 | - { os: ubuntu-latest, php: 8.2, packages: symfony/symfony=6.*, experimental: true } 25 | - { os: ubuntu-latest, php: 7.1, packages: symfony/symfony=3.4.*, experimental: false} 26 | - { os: ubuntu-latest, php: 7.1, packages: symfony/symfony=4.0.*, experimental: false} 27 | - { os: ubuntu-latest, php: 8.0, packages: symfony/symfony=4.*, experimental: false} 28 | steps: 29 | - uses: actions/checkout@v2 30 | 31 | - name: Install PHP 32 | uses: shivammathur/setup-php@v2 33 | with: 34 | php-version: ${{ matrix.php }} 35 | extensions: json, mbstring 36 | tools: composer 37 | - name: Show PHP and composer version 38 | run: php -v && composer -V 39 | 40 | - name: Validate composer.json and composer.lock 41 | run: composer validate 42 | 43 | - name: Get Composer Cache Directory 44 | id: composer-cache 45 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 46 | 47 | - name: Cache Composer packages 48 | uses: actions/cache@v2 49 | with: 50 | path: ${{ steps.composer-cache.outputs.dir }} 51 | key: ${{ runner.os }}-php-${{ hashFiles('**/composer.json') }} 52 | restore-keys: | 53 | ${{ runner.os }}-php- 54 | 55 | - name: Install dependencies 56 | run: | 57 | echo ${PACKAGES} 58 | composer require --no-update ${PACKAGES} 59 | composer install --prefer-source 60 | env: 61 | PACKAGES: ${{ matrix.packages}} 62 | 63 | # Add a test script to composer.json, for instance: "test": "vendor/bin/phpunit" 64 | # Docs: https://getcomposer.org/doc/articles/scripts.md 65 | 66 | - name: Run test suite 67 | run: ./vendor/bin/phpunit 68 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dev 2 | composer.lock 3 | vendor/ 4 | 5 | # CI runtime 6 | build/ 7 | target/ 8 | .php_cs.cache 9 | .idea/ -------------------------------------------------------------------------------- /.php_cs: -------------------------------------------------------------------------------- 1 | exclude('vendor') 5 | ->exclude('build') 6 | ->in(__DIR__); 7 | 8 | return PhpCsFixer\Config::create() 9 | ->setRules([ 10 | '@Symfony' => true, 11 | 'concat_space' => ['spacing' => 'one'], 12 | 'phpdoc_align' => false, 13 | 'phpdoc_to_comment' => false, 14 | 'header_comment' => false, 15 | 'no_unused_imports' => true, 16 | 'ordered_imports' => [ 17 | 'sort_algorithm' => 'alpha', 18 | ], 19 | 'single_blank_line_before_namespace' => true, 20 | 'no_extra_consecutive_blank_lines' => [ 21 | 'break', 22 | 'continue', 23 | 'extra', 24 | 'return', 25 | 'throw', 26 | 'use', 27 | 'parenthesis_brace_block', 28 | 'square_brace_block', 29 | 'curly_brace_block' 30 | ], 31 | 'array_syntax' => ['syntax' => 'short'], 32 | 'concat_space' => ['spacing' => 'one'], 33 | 'cast_spaces' => ['space' => 'single'], 34 | 'binary_operator_spaces' => ['default' => 'single_space'], 35 | 'no_superfluous_elseif' => false, 36 | 'no_superfluous_phpdoc_tags' => false, 37 | 'yoda_style' => false, 38 | 'single_line_throw' => false, 39 | ]) 40 | ->setFinder($finder); 41 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | tests: 3 | override: 4 | - 5 | command: vendor/bin/phpunit --coverage-clover=build/clover.xml 6 | coverage: 7 | file: build/clover.xml 8 | format: php-clover 9 | 10 | filter: 11 | excluded_paths: 12 | - "./tests" 13 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017-2018 Lamoda 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 | # Lamoda metric responder 2 | 3 | [![Build Status](https://github.com/lamoda/php-metrics/workflows/Tests/badge.svg?branch=master)](https://github.com/lamoda/php-metrics/workflows/Tests/badge.svg?branch=master) 4 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/lamoda/php-metrics/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/lamoda/php-metrics/?branch=master) 5 | [![Code Coverage](https://scrutinizer-ci.com/g/lamoda/php-metrics/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/lamoda/php-metrics/?branch=master) 6 | [![Build Status](https://scrutinizer-ci.com/g/lamoda/php-metrics/badges/build.png?b=master)](https://scrutinizer-ci.com/g/lamoda/php-metrics/build-status/master) 7 | [![Latest Stable Version](https://poser.pugx.org/lamoda/metrics/v/stable)](https://packagist.org/packages/lamoda/metrics) 8 | 9 | ## Features 10 | 11 | * Metric responder with lazy sourcing 12 | * Multiple metric response formats 13 | * [Telegraf `JSON`](https://github.com/influxdata/telegraf/tree/master/plugins/inputs/httpjson) 14 | * [Prometheus exporter](https://prometheus.io/docs/instrumenting/writing_exporters/) 15 | * Symfony bundle [optional] 16 | * Doctrine and redis/predis integration out of the box 17 | 18 | ## Installation 19 | 20 | * install with composer 21 | 22 | ```sh 23 | composer require lamoda/metrics:^2.0 24 | ``` 25 | 26 | ## Usage examples 27 | 28 | * [Collecting metrics](doc/examples/01_collect.md) 29 | * [Storing metrics](doc/examples/02_store.md) 30 | * [Responding metrics](doc/examples/03_respond.md) 31 | * [Symfony integration](src/MetricBundle/Resources/docs/integration.md) 32 | 33 | ## Main terms 34 | 35 | * **Metric** is a named value, representing system running state, health check or cumulative measurement, optionally tagged 36 | 37 | * **Metric response** is a set of metrics collected for the fixed moment of time, formatted for single input format 38 | * **Metric responder** is a http endpoint used to render collected metrics into suitable web response format 39 | 40 | * **Metrics** can be **sourced** in the terms of dynamic spawning for collection generation 41 | * The general advice is to have metrics as lazy as possible, resolving to the fixed at the time of generation response 42 | * Usually **Source** is implemented as an instance of `\Traversable` object to embed into collection 43 | 44 | Metric are generally of two types: 45 | * **Precomputed** metrics are usually generated outside of responding process, like counters 46 | * **Runtime** metrics are generated on call, representing the current state of the system 47 | 48 | From the point of metric responding the difference between types is nominal since PHP has share nothing architecture and most stored values should 49 | be obtained from storage in any case (and thus waste some caller time), so 50 | in general **Precomputed** metrics are just very fast **Runtime** metrics 51 | 52 | But from the point of metric storing **Precomputed** metrics have more significant difference - some of them 53 | can be retrieved from metric storage for update 54 | 55 | * **Mutable** metric is the **Metric**, which can be updated with either with some delta or with absolute value according to 56 | business rules: 57 | * Counters 58 | * Accumulated total 59 | * Balance 60 | 61 | * **Precomputed** metrics MAY NOT be **Mutable** since precomputing can be part of 62 | the responder performance optimization process and there is no sense in adjusting such value as it is overwritten 63 | during metric computation 64 | 65 | * **Mutable Metric Storage** is the general storage interface which allows end user to mutate some metric by name and tags. 66 | It's depends on internal storage implementation, what happens if the metric is not found. If the storage allows 67 | dynamic metric generation - new metric would be stored silently with given value 68 | 69 | ## Supplementary terms 70 | 71 | * **Collector** is the generic class serving the metric **Source** to other parts of the library 72 | * **Responder** to render 73 | * **Storage** receiver to cache them 74 | * Debug utilities for metric profiling and inspection 75 | 76 | In general **Collector** could just keep preconfigured **Source** or retrieve data from other sources (API, DB, Cache) 77 | 78 | * **Materializing** is the process of resolving single metric **Collector** into precomputed metric **Source** 79 | and storing it in a resolved form for fast access to some **Storage** (Cache, DB, etc) 80 | 81 | * **Storage** is the metric storing driver. It is responsible for the following actions 82 | * Create new metric instance by its primary parts - name, value and tags 83 | * Find metric by name and tags 84 | * Work as a **Source** of metrics 85 | * Work as a receiver accepting **Source** to be materialized in it 86 | 87 | ## Extending 88 | 89 | See [extending chapter](doc/extending.md) 90 | 91 | ## Development 92 | 93 | ### Running tests 94 | ```yaml 95 | composer install 96 | vendor/bin/phpunit 97 | ``` 98 | -------------------------------------------------------------------------------- /bin/dump_reference.php: -------------------------------------------------------------------------------- 1 | dump(new Configuration()); 12 | -------------------------------------------------------------------------------- /bin/test_console.php: -------------------------------------------------------------------------------- 1 | run($input); 16 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lamoda/metrics", 3 | "license": "MIT", 4 | "description": "Library for handling and displaying custom project metrics", 5 | "type": "library", 6 | "keywords": ["metrics", "monitoring", "prometheus", "telegraf", "symfony-bundle"], 7 | "authors": [ 8 | { 9 | "name": "Lamoda developers", 10 | "homepage": "https://tech.lamoda.ru/" 11 | } 12 | ], 13 | "require": { 14 | "php": "~7.1 || ^8.0", 15 | "guzzlehttp/psr7": "~1.4 || ^2.0" 16 | }, 17 | "require-dev": { 18 | "ext-json": "*", 19 | "ext-redis": "*", 20 | "doctrine/common": "^2.4.1 || ^3.0", 21 | "doctrine/dbal": "^2.3", 22 | "doctrine/doctrine-bundle": "~1.5 || ^2.0", 23 | "doctrine/orm": "~2.4", 24 | "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0 || ^10.0", 25 | "predis/predis": "^1.1", 26 | "symfony/browser-kit": "~2.8 || ~3.0 || ~4.0 || ^5.0 || ^6.0", 27 | "symfony/config": "~2.8 || ~3.0 || ~4.0 || ^5.0 || ^6.0", 28 | "symfony/dependency-injection": "~2.8 || ~3.0 || ~4.0 || ^5.0 || ^6.0", 29 | "symfony/framework-bundle": "~2.8 || ~3.0 || ~4.0 || ^5.0 || ^6.0", 30 | "symfony/http-kernel": "~2.8 || ~3.0 || ~4.0 || ^5.0 || ^6.0", 31 | "symfony/routing": "~2.8 || ~3.0 || ~4.0 || ^5.0 || ^6.0", 32 | "symfony/stopwatch": "~2.8 || ~3.0 || ~4.0 || ^5.0 || ^6.0", 33 | "symfony/yaml": "~2.8 || ~3.0 || ~4.0 || ^5.0 || ^6.0", 34 | "symfony/dom-crawler": "~2.8 || ~3.0 || ~4.0 || ^5.0 || ^6.0", 35 | "symfony/monolog-bundle": "~2.0 || ~3.0", 36 | "masterminds/html5": "^2.6" 37 | }, 38 | "autoload": { 39 | "psr-4": { 40 | "Lamoda\\Metric\\Common\\": "./src/Common", 41 | "Lamoda\\Metric\\Responder\\": "./src/Responder", 42 | "Lamoda\\Metric\\Storage\\": "./src/Storage", 43 | "Lamoda\\Metric\\Adapters\\": "./src/Adapters", 44 | "Lamoda\\Metric\\MetricBundle\\": "./src/MetricBundle", 45 | "Lamoda\\Metric\\Collector\\": "./src/Collector" 46 | } 47 | }, 48 | "autoload-dev": { 49 | "psr-4": { 50 | "Lamoda\\Metric\\Common\\Tests\\": "./tests/Common", 51 | "Lamoda\\Metric\\Responder\\Tests\\": "./tests/Responder", 52 | "Lamoda\\Metric\\Adapters\\Tests\\": "./tests/Adapters", 53 | "Lamoda\\Metric\\MetricBundle\\Tests\\": "./tests/MetricBundle", 54 | "Lamoda\\Metric\\Storage\\Tests\\": "./tests/Storage", 55 | "Lamoda\\Metric\\Collector\\Tests\\": "./tests/Collector", 56 | "Lamoda\\Metric\\Tests\\Builders\\": "./tests/Builders" 57 | } 58 | }, 59 | "config": { 60 | "sort-packages": true 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /doc/examples/01_collect.md: -------------------------------------------------------------------------------- 1 | # Collecting metrics 2 | 3 | Collector is a service with a single purpose - form and return *MetricSource* 4 | In some cases collector can hold source directly, in other cases 5 | it can combine, merge, adopt other source in order to create final source. 6 | Also collector can create source by itself. 7 | 8 | So general collector sample would be: 9 | 10 | ```php 11 | 'value']); 14 | 15 | // You can construct your own source from within your collector implementation 16 | // But for simplicity of the example we just use precreated source 17 | $source = new \Lamoda\Metric\Common\Source\IterableMetricSource([$metric]); 18 | $collector = new \Lamoda\Metric\Collector\SingleSourceCollector($source); 19 | 20 | $collectedSource = $collector->collect(); 21 | ``` 22 | 23 | This collector is simple and just holds the preconfigured source to collect it on demand. 24 | More complex example is `MergingCollector` which will combine metrics from different sources into one. 25 | 26 | Utility example is `TaggingCollectorDecorator` which will add default tag values 27 | for each metric collected by delegate. 28 | 29 | Collector is a kind of source wrapper in order to stabilize source definition and configuration for further purposes. 30 | 31 | You can also use a *Storage* as hold source 32 | 33 | ## Helpers 34 | 35 | `CollectorRegistry` can be used to hold named collectors 36 | -------------------------------------------------------------------------------- /doc/examples/02_store.md: -------------------------------------------------------------------------------- 1 | # Storing and mutating metrics 2 | 3 | Storage is responsible for several operations: 4 | 5 | * Finding single metric by name and tags 6 | * Operating as a metric source (find all metrics) 7 | * Creating new metrics suitable for storage 8 | * Receiving metrics (in a for of source) to be stored in 9 | 10 | Also you can configure a `MetricMutator` with a single storage 11 | and use it as an userland entrypoint for metric management 12 | 13 | ## Implementations 14 | 15 | * Abstract doctrine implementation 16 | * You have to implement entity finding, creating and sourcing 17 | * You can update receiving operation i.e with truncating or re-setting existing metrics 18 | 19 | ## Samples 20 | 21 | Here is sample `ArrayStorage` implementation which stores metric into in-memory array 22 | ignoring tags on searching 23 | 24 | ```php 25 | metrics = []; 39 | foreach ($source->getMetrics() as $metric) { 40 | $this->metrics[$metric->getName()] = $metric; 41 | } 42 | } 43 | 44 | public function getMetrics(): \Traversable { 45 | return new ArrayIterator($this->metrics); 46 | } 47 | 48 | public function getIterator() { 49 | return $this->getMetrics(); 50 | } 51 | 52 | public function findMetric(string $name, array $tags = []): ?MutableMetricInterface { 53 | return $this->metrics[$name] ?? null; 54 | } 55 | 56 | public function createMetric(string $name, float $value, array $tags = []): MutableMetricInterface { 57 | return $this->metrics[$name] = new Metric($name, $value, $tags); 58 | } 59 | } 60 | ``` 61 | 62 | Having this storage you can create a generic mutator on top of it 63 | ```php 64 | createMetric('known_metric', 0.0); 73 | 74 | // Here existent metric will be mutated and updated directly in the storage 75 | $mutator->adjustMetricValue(1.0, 'known_metric', ['tags' => 'are ignored for that storage']); 76 | 77 | // Here new metric would be created and put into the storage 78 | $mutator->setMetricValue(241.0, 'unknown_metric', ['tags' => 'are still ignored']); 79 | ``` 80 | 81 | ## Materializing metrics 82 | 83 | As shown in above example, receiving metrics is a process of batch importing metrics into storage, 84 | i.e persisting to database, caching, etc. 85 | 86 | Receiving could perform additional preparations on the storage, like wiping already stored metrics 87 | or setting them to pre-defined value (i.e zero). 88 | 89 | Materializing is two-step receiving metrics 90 | 91 | 1. resolve every collected metric to get static values 92 | 2. put resolved metrics into the storage 93 | 94 | Generally, storage itself would perform metric resolution, but it stands better to do this process outside 95 | of storage `receive` method execution in order to find resolution problems 96 | as early as possible (i.e not during locking transaction) 97 | 98 | ## General tips 99 | 100 | * Avoid using the storage you receive in as a source of mutable metrics in order to avoid locking issues. 101 | 102 | ## Helpers 103 | 104 | `StorageRegistry` can be used to hold named storages. 105 | 106 | If you are using complex collector and storage configurations, you probably want to use `StorageRegistry` 107 | and `CollectorRegistry` to manage named services. While using named services you can utilize `MaterializerHelper` 108 | in order to perform materializing. 109 | -------------------------------------------------------------------------------- /doc/examples/03_respond.md: -------------------------------------------------------------------------------- 1 | # Storageless responding 2 | 3 | The general idea of responding is to combine metric collector 4 | and metric response formatter in order to create proper response 5 | for metrics collected with collector 6 | 7 | So the general example code is: 8 | 9 | ```php 10 | 'value']); 21 | // Source is iterable lazy metric source 22 | $source = new IterableMetricSource([$metric]); 23 | // You can use your own collector here 24 | $collector = new SingleSourceCollector($source); 25 | $formatter = new TelegrafJsonResponseFactory(); 26 | 27 | $responder = new PsrResponder($collector, $formatter, ['prefix' => 'my_metric_']); 28 | // We have PSR-7 Response here 29 | $response = $responder->createResponse(); 30 | ``` 31 | 32 | This example illustrates the common approach to the metric responding which 33 | performs synchronous metric computation while formatting metric response 34 | -------------------------------------------------------------------------------- /doc/examples/04_intermediate_storage.md: -------------------------------------------------------------------------------- 1 | # Using intermediate storage for responding 2 | 3 | Here is more complex example. You can use intermediate storage as 4 | the source to collect the metrics from. General steps are: 5 | 6 | 1. Configure initial sources 7 | 2. Create collector on top of them 8 | 3. Materialize collector to the storage 9 | 4. Create new collector with storage as source of metrics 10 | 5. Create responder using this collector 11 | 12 | Repeat steps 1-3 in order to update metrics being responded. 13 | 14 | ```php 15 | 'value']); 27 | // 1. Configure initial storage 28 | $source = new IterableMetricSource([$metric]); 29 | // 2. Create collector on top of it 30 | $collector = new SingleSourceCollector($source); 31 | // See storage examples 32 | /** @var MetricStorageInterface$storage */ 33 | $storage = new ArrayStorage(); 34 | // 3. Materialize collector to the storage 35 | $storage->receive($collector->collect()); 36 | // At this moment you can persist or cache your collected metrics 37 | // if the storage driver supports restoring process 38 | 39 | // 4. Create new collector with storage as source of metrics 40 | $responderCollector = new SingleSourceCollector($storage); 41 | 42 | // 5. Create responder using this collector 43 | $formatter = new TelegrafJsonResponseFactory(); 44 | $responder = new PsrResponder($responderCollector, $formatter, ['prefix' => 'my_metric_']); 45 | $response = $responder->createResponse(); 46 | 47 | -------------------------------------------------------------------------------- /doc/extending.md: -------------------------------------------------------------------------------- 1 | # Extending 2 | ## Creating own response factory 3 | 4 | Implement `ResponseFactoryInterface` which consumes the metric group source and produces PSR-7 Response 5 | 6 | ## Sourcing 7 | 8 | These could provide any metrics, i.e FS stats, exec result, API calls, etc, 9 | represented with lazily wrapped `MetricInterface` implementation 10 | 11 | # Current extensions 12 | 13 | ## Storage 14 | 15 | * `AbstractDoctrineStorage` provides metrics stored with Doctrine 2 ORM powered with DBAL. Atomic adjustments allowed 16 | 17 | ## Response formats 18 | 19 | Common options: 20 | * `prefix` option allows you to add the prefix to every metric name in every group 21 | 22 | Built-in formats 23 | * `TelegrafJsonResponseFactory` (`telegraf_json`) creates telegraf JSON compatible output with the following options 24 | * `propagate_tags` allows you to display metric tags on the group level. In case of different tag values the one rendered later will win 25 | * `group_by_tags` allows you to create nested groups according to tag value. Usually you want also propagate these tags 26 | * `PrometheusResponseFactory` (`prometheus`) creates prometheus compatible output 27 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | ./tests/ 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | ./src/ 18 | 19 | 20 | ./build/ 21 | ./vendor/ 22 | ./tests/ 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Adapters/Doctrine/AbstractDoctrineStorage.php: -------------------------------------------------------------------------------- 1 | entityManager = $entityManager; 19 | } 20 | 21 | final public function getIterator(): \Traversable 22 | { 23 | return $this->getMetrics(); 24 | } 25 | 26 | /** {@inheritdoc} */ 27 | final public function receive(MetricSourceInterface $source): void 28 | { 29 | $this->entityManager->beginTransaction(); 30 | try { 31 | $this->doReceive($source); 32 | $this->entityManager->flush(); 33 | $this->entityManager->commit(); 34 | } catch (\Exception $exception) { 35 | $this->entityManager->rollback(); 36 | throw ReceiverException::becauseOfStorageFailure($exception); 37 | } 38 | } 39 | 40 | /** {@inheritdoc} */ 41 | final public function findMetric(string $name, array $tags = []): ?MutableMetricInterface 42 | { 43 | $metric = $this->doFindMetric($name, $tags); 44 | if (!$metric) { 45 | return null; 46 | } 47 | 48 | return new AtomicMutableWrapper($this->entityManager, $metric); 49 | } 50 | 51 | /** {@inheritdoc} */ 52 | final public function createMetric(string $name, float $value, array $tags = []): MutableMetricInterface 53 | { 54 | $metric = $this->doCreateMetric($name, $value, $tags); 55 | $this->entityManager->persist($metric); 56 | 57 | return new AtomicMutableWrapper($this->entityManager, $metric); 58 | } 59 | 60 | abstract protected function doFindMetric(string $name, array $tags = []): ?MutableMetricInterface; 61 | 62 | abstract protected function doCreateMetric(string $name, float $value, array $tags = []): MutableMetricInterface; 63 | 64 | /** 65 | * @param MetricSourceInterface $source 66 | */ 67 | protected function doReceive(MetricSourceInterface $source): void 68 | { 69 | foreach ($source->getMetrics() as $metric) { 70 | $tags = $metric->getTags(); 71 | $name = $metric->getName(); 72 | $value = $metric->resolve(); 73 | $resolved = $this->findMetric($name, $tags); 74 | if (!$resolved) { 75 | $resolved = $this->createMetric($name, 0, $tags); 76 | } 77 | $resolved->setValue($value); 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/Adapters/Doctrine/AtomicMutableWrapper.php: -------------------------------------------------------------------------------- 1 | manager = $manager; 20 | $this->metric = $metric; 21 | } 22 | 23 | /** {@inheritdoc} */ 24 | public function adjust(float $delta): void 25 | { 26 | $this->executeWithLock( 27 | function () use ($delta) { 28 | $this->metric->adjust($delta); 29 | } 30 | ); 31 | } 32 | 33 | /** {@inheritdoc} */ 34 | public function setValue(float $value): void 35 | { 36 | $this->executeWithLock( 37 | function () use ($value) { 38 | $this->metric->setValue($value); 39 | } 40 | ); 41 | } 42 | 43 | /** {@inheritdoc} */ 44 | public function getName(): string 45 | { 46 | return $this->metric->getName(); 47 | } 48 | 49 | /** {@inheritdoc} */ 50 | public function resolve(): float 51 | { 52 | return $this->metric->resolve(); 53 | } 54 | 55 | /** {@inheritdoc} */ 56 | public function getTags(): array 57 | { 58 | return $this->metric->getTags(); 59 | } 60 | 61 | private function executeWithLock(callable $fn): void 62 | { 63 | $this->manager->transactional( 64 | function () use ($fn) { 65 | $this->manager->lock($this->metric, LockMode::PESSIMISTIC_WRITE); 66 | 67 | $this->manager->refresh($this->metric); 68 | 69 | $fn(); 70 | 71 | $this->manager->flush(); 72 | } 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Adapters/Redis/AbstractRedisStorage.php: -------------------------------------------------------------------------------- 1 | redisConnection = $redisConnection; 19 | } 20 | 21 | /** {@inheritdoc} */ 22 | final public function getIterator(): \Traversable 23 | { 24 | return $this->getMetrics(); 25 | } 26 | 27 | /** {@inheritdoc} */ 28 | final public function receive(MetricSourceInterface $source): void 29 | { 30 | $metrics = []; 31 | foreach ($source->getMetrics() as $metric) { 32 | $metrics[] = new MetricDto( 33 | $metric->getName(), 34 | $metric->resolve(), 35 | $metric->getTags() 36 | ); 37 | } 38 | $this->redisConnection->setMetrics($metrics); 39 | } 40 | 41 | /** {@inheritdoc} */ 42 | final public function getMetrics(): \Traversable 43 | { 44 | $metricsData = $this->redisConnection->getAllMetrics(); 45 | foreach ($metricsData as $metricDto) { 46 | yield new MetricWrapper( 47 | $this->redisConnection, 48 | $this->doCreateMetric($metricDto->name, $metricDto->value, $metricDto->tags) 49 | ); 50 | } 51 | } 52 | 53 | /** {@inheritdoc} */ 54 | final public function findMetric(string $name, array $tags = []): ?MutableMetricInterface 55 | { 56 | $value = $this->redisConnection->getMetricValue($name, $tags); 57 | if ($value === null) { 58 | return null; 59 | } 60 | 61 | return new MetricWrapper( 62 | $this->redisConnection, 63 | $this->doCreateMetric($name, $value, $tags) 64 | ); 65 | } 66 | 67 | /** {@inheritdoc} */ 68 | final public function createMetric(string $name, float $value, array $tags = []): MutableMetricInterface 69 | { 70 | $metric = new MetricWrapper( 71 | $this->redisConnection, 72 | $this->doCreateMetric($name, 0, $tags) 73 | ); 74 | $metric->setValue($value); 75 | 76 | return $metric; 77 | } 78 | 79 | abstract protected function doCreateMetric(string $name, float $value, array $tags = []): MutableMetricInterface; 80 | 81 | /** {@inheritdoc} */ 82 | final public function setMetricValue(string $name, float $value, array $tags = []): void 83 | { 84 | $this->redisConnection->setMetrics([new MetricDto($name, $value, $tags)]); 85 | } 86 | 87 | /** {@inheritdoc} */ 88 | final public function adjustMetricValue(string $name, float $value, array $tags = []): float 89 | { 90 | return $this->redisConnection->adjustMetric($name, $value, $tags); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Adapters/Redis/MetricDto.php: -------------------------------------------------------------------------------- 1 | name = $name; 20 | $this->value = $value; 21 | $this->tags = $tags; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Adapters/Redis/MetricWrapper.php: -------------------------------------------------------------------------------- 1 | redisConnection = $redisConnection; 20 | $this->metric = $metric; 21 | } 22 | 23 | /** {@inheritdoc} */ 24 | public function adjust(float $delta): void 25 | { 26 | $value = $this->redisConnection->adjustMetric($this->getName(), $delta, $this->getTags()); 27 | $this->metric->setValue($value); 28 | } 29 | 30 | /** {@inheritdoc} */ 31 | public function setValue(float $value): void 32 | { 33 | $this->redisConnection->setMetrics([new MetricDto($this->getName(), $value, $this->getTags())]); 34 | $this->metric->setValue($value); 35 | } 36 | 37 | /** {@inheritdoc} */ 38 | public function getName(): string 39 | { 40 | return $this->metric->getName(); 41 | } 42 | 43 | /** {@inheritdoc} */ 44 | public function resolve(): float 45 | { 46 | return $this->metric->resolve(); 47 | } 48 | 49 | /** {@inheritdoc} */ 50 | public function getTags(): array 51 | { 52 | return $this->metric->getTags(); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Adapters/Redis/MutatorRedisConnectionInterface.php: -------------------------------------------------------------------------------- 1 | client = $client; 24 | $this->metricsKey = $metricsKey; 25 | } 26 | 27 | /** {@inheritdoc} */ 28 | public function getAllMetrics(): array 29 | { 30 | $rawMetricsData = $this->client->hgetall($this->metricsKey); 31 | $metrics = []; 32 | foreach ($rawMetricsData as $rawMetricData => $value) { 33 | $metricData = json_decode($rawMetricData, true); 34 | $metrics[] = new MetricDto( 35 | $metricData['name'], 36 | (float) $value, 37 | $this->convertTagsFromStorage($metricData['tags']) 38 | ); 39 | } 40 | 41 | return $metrics; 42 | } 43 | 44 | /** {@inheritdoc} */ 45 | public function adjustMetric(string $key, float $delta, array $tags): float 46 | { 47 | return (float) $this->client->hincrbyfloat($this->metricsKey, $this->buildField($key, $tags), $delta); 48 | } 49 | 50 | /** {@inheritdoc} */ 51 | public function setMetrics(array $metricsData): void 52 | { 53 | $fields = []; 54 | foreach ($metricsData as $metricDto) { 55 | $field = $this->buildField($metricDto->name, $metricDto->tags); 56 | $fields[$field] = $metricDto->value; 57 | } 58 | $this->client->hmset($this->metricsKey, $fields); 59 | } 60 | 61 | /** {@inheritdoc} */ 62 | public function getMetricValue(string $key, array $tags): ?float 63 | { 64 | $value = $this->client->hget($this->metricsKey, $this->buildField($key, $tags)); 65 | if ($value === false) { 66 | return null; 67 | } 68 | 69 | return (float) $value; 70 | } 71 | 72 | private function buildField(string $name, array $tags) 73 | { 74 | return json_encode([ 75 | 'name' => $name, 76 | 'tags' => $this->convertTagsForStorage($tags), 77 | ]); 78 | } 79 | 80 | private function convertTagsForStorage(array $tags): string 81 | { 82 | return json_encode($this->normalizeTags($tags)); 83 | } 84 | 85 | private function convertTagsFromStorage(string $tags): array 86 | { 87 | return json_decode($tags, true); 88 | } 89 | 90 | private function normalizeTags(array $tags): array 91 | { 92 | ksort($tags); 93 | 94 | return $tags; 95 | } 96 | 97 | } 98 | -------------------------------------------------------------------------------- /src/Adapters/Redis/RedisConnectionInterface.php: -------------------------------------------------------------------------------- 1 | collectors[$name] = $collector; 22 | } 23 | 24 | /** 25 | * Fetch collector from registry. 26 | * 27 | * @param string $name 28 | * 29 | * @return MetricCollectorInterface 30 | * 31 | * @throws \OutOfBoundsException 32 | */ 33 | public function getCollector(string $name): MetricCollectorInterface 34 | { 35 | if (!array_key_exists($name, $this->collectors)) { 36 | throw new \OutOfBoundsException('Unknown collector in registry: ' . $name); 37 | } 38 | 39 | return $this->collectors[$name]; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Collector/MergingCollector.php: -------------------------------------------------------------------------------- 1 | collectors = $collectors; 16 | } 17 | 18 | /** {@inheritdoc} */ 19 | public function collect(): MetricSourceInterface 20 | { 21 | $sources = []; 22 | foreach ($this->collectors as $collector) { 23 | $sources[] = $collector->collect(); 24 | } 25 | 26 | return new MergingMetricSource(...$sources); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Collector/MetricCollectorInterface.php: -------------------------------------------------------------------------------- 1 | source = $source; 18 | } 19 | 20 | /** {@inheritdoc} */ 21 | public function collect(): MetricSourceInterface 22 | { 23 | return $this->source; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/Collector/TaggingCollectorDecorator.php: -------------------------------------------------------------------------------- 1 | collector = $collector; 22 | $this->tags = $tags; 23 | } 24 | 25 | /** {@inheritdoc} */ 26 | public function collect(): MetricSourceInterface 27 | { 28 | return new DefaultTaggingMetricSource($this->collector->collect(), $this->tags); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Common/DefaultTagsMetric.php: -------------------------------------------------------------------------------- 1 | metric = $metric; 23 | $this->tags = $tags; 24 | } 25 | 26 | /** {@inheritdoc} */ 27 | public function getName(): string 28 | { 29 | return $this->metric->getName(); 30 | } 31 | 32 | /** {@inheritdoc} */ 33 | public function resolve(): float 34 | { 35 | return $this->metric->resolve(); 36 | } 37 | 38 | /** {@inheritdoc} */ 39 | public function getTags(): array 40 | { 41 | return array_replace($this->tags, $this->metric->getTags()); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Common/Metric.php: -------------------------------------------------------------------------------- 1 | name = $name; 26 | $this->value = $value; 27 | $this->tags = $tags; 28 | } 29 | 30 | /** {@inheritdoc} */ 31 | public function getName(): string 32 | { 33 | return $this->name; 34 | } 35 | 36 | /** {@inheritdoc} */ 37 | public function resolve(): float 38 | { 39 | return $this->value; 40 | } 41 | 42 | /** {@inheritdoc} */ 43 | public function getTags(): array 44 | { 45 | return $this->tags; 46 | } 47 | 48 | /** {@inheritdoc} */ 49 | public function adjust(float $delta): void 50 | { 51 | $this->value += $delta; 52 | } 53 | 54 | /** {@inheritdoc} */ 55 | public function setValue(float $value): void 56 | { 57 | $this->value = $value; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/Common/MetricInterface.php: -------------------------------------------------------------------------------- 1 | source = $source; 24 | $this->tags = $tags; 25 | } 26 | 27 | /** {@inheritdoc} */ 28 | public function getIterator(): \Traversable 29 | { 30 | return $this->getMetrics(); 31 | } 32 | 33 | /** {@inheritdoc} */ 34 | public function getMetrics(): \Traversable 35 | { 36 | foreach ($this->source as $metric) { 37 | yield new DefaultTagsMetric($metric, $this->tags); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/Common/Source/IterableMetricSource.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 23 | } 24 | 25 | /** {@inheritdoc} */ 26 | public function getMetrics(): \Traversable 27 | { 28 | return $this->metrics; 29 | } 30 | 31 | /** {@inheritdoc} */ 32 | public function getIterator(): \Traversable 33 | { 34 | return $this->getMetrics(); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Common/Source/MergingMetricSource.php: -------------------------------------------------------------------------------- 1 | sources = $sources; 15 | } 16 | 17 | /** {@inheritdoc} */ 18 | public function getIterator(): \Traversable 19 | { 20 | return $this->getMetrics(); 21 | } 22 | 23 | /** {@inheritdoc} */ 24 | public function getMetrics(): \Traversable 25 | { 26 | foreach ($this->sources as $source) { 27 | yield from $source; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/MetricBundle/Command/DebugMetricsCommand.php: -------------------------------------------------------------------------------- 1 | registry = $collectorRegistry; 25 | } 26 | 27 | protected function configure() 28 | { 29 | $this->addArgument('collector', InputArgument::REQUIRED, 'Collector name from configuration'); 30 | } 31 | 32 | protected function execute(InputInterface $input, OutputInterface $output) 33 | { 34 | $collector = $input->getArgument('collector'); 35 | $io = new SymfonyStyle($input, $output); 36 | 37 | $stopwatch = new Stopwatch(true); 38 | 39 | $source = $this->registry->getCollector($collector); 40 | 41 | $table = new Table($io); 42 | $table->setHeaders(['name', 'value', 'tags', 'resolution time (ms)', 'resolution memory (Mb)']); 43 | 44 | foreach ($source->collect()->getMetrics() as $metric) { 45 | $stopwatch->start($metric->getName()); 46 | 47 | $profile = $stopwatch->stop($metric->getName()); 48 | $table->addRow( 49 | [ 50 | $metric->getName(), 51 | $metric->resolve(), 52 | $this->formatTags($metric->getTags()), 53 | $profile->getDuration(), 54 | ($profile->getMemory() / 1024 / 1024), 55 | ] 56 | ); 57 | } 58 | $table->render(); 59 | } 60 | 61 | private function formatTags(array $tags): string 62 | { 63 | $parts = array_map( 64 | function (string $val, string $key) { 65 | return "$key:$val"; 66 | }, 67 | array_values($tags), 68 | array_keys($tags) 69 | ); 70 | 71 | return implode(', ', $parts); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/MetricBundle/Command/MaterializeMetricsCommand.php: -------------------------------------------------------------------------------- 1 | helper = $helper; 26 | } 27 | 28 | protected function configure(): void 29 | { 30 | $this->addArgument('collector', InputArgument::REQUIRED, 'Collector name from configuration'); 31 | $this->addArgument('storage', InputArgument::REQUIRED, 'Storage name from configuration'); 32 | } 33 | 34 | protected function execute(InputInterface $input, OutputInterface $output): ?int 35 | { 36 | $io = new SymfonyStyle($input, $output); 37 | 38 | $collector = $input->getArgument('collector'); 39 | $storage = $input->getArgument('storage'); 40 | 41 | try { 42 | $this->helper->materialize($collector, $storage); 43 | 44 | return 0; 45 | } catch (\OutOfBoundsException $exception) { 46 | $io->error('Invalid argument supplied'); 47 | } catch (ReceiverException $exception) { 48 | $io->error('Failed to receive metrics in storage: ' . $exception->getMessage()); 49 | } 50 | 51 | return -1; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/MetricBundle/Controller/HttpFoundationResponder.php: -------------------------------------------------------------------------------- 1 | psrResponder = $psrResponder; 22 | } 23 | 24 | /** 25 | * Create HTTP-Kernel response for collected. 26 | * 27 | * @return Response 28 | * 29 | * @throws HttpException 30 | */ 31 | public function createResponse(): Response 32 | { 33 | try { 34 | $response = $this->psrResponder->createResponse(); 35 | 36 | $symfonyResponse = new Response( 37 | (string) $response->getBody(), 38 | $response->getStatusCode(), 39 | $response->getHeaders() 40 | ); 41 | } catch (\Exception $exception) { 42 | throw new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, '', $exception); 43 | } 44 | 45 | $symfonyResponse->setPrivate(); 46 | 47 | return $symfonyResponse; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/MetricBundle/DependencyInjection/Compiler/RegisterCollectorsPass.php: -------------------------------------------------------------------------------- 1 | getDefinition(Collector::REGISTRY_ID); 16 | 17 | $services = $container->findTaggedServiceIds(Collector::TAG); 18 | foreach ($services as $id => $tags) { 19 | foreach ($tags as $attributes) { 20 | if (!isset($attributes[Collector::ALIAS_ATTRIBUTE])) { 21 | throw new \InvalidArgumentException( 22 | sprintf( 23 | 'Missing "%s" attribute for "%s" tag "%s"', 24 | Collector::ALIAS_ATTRIBUTE, 25 | $id, 26 | Collector::TAG 27 | ) 28 | ); 29 | } 30 | 31 | $registry->addMethodCall('register', [$attributes[Collector::ALIAS_ATTRIBUTE], new Reference($id)]); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/MetricBundle/DependencyInjection/Compiler/RegisterReceiversPass.php: -------------------------------------------------------------------------------- 1 | getDefinition(Storage::REGISTRY_ID); 16 | 17 | $services = $container->findTaggedServiceIds(Storage::TAG); 18 | foreach ($services as $id => $tags) { 19 | foreach ($tags as $attributes) { 20 | if (!isset($attributes[Storage::ALIAS_ATTRIBUTE])) { 21 | throw new \InvalidArgumentException( 22 | sprintf( 23 | 'Missing "%s" attribute for "%s" tag "%s"', 24 | Storage::ALIAS_ATTRIBUTE, 25 | $id, 26 | Storage::TAG 27 | ) 28 | ); 29 | } 30 | 31 | $registry->addMethodCall('register', [$attributes[Storage::ALIAS_ATTRIBUTE], new Reference($id)]); 32 | } 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/MetricBundle/DependencyInjection/Compiler/RegisterResponseFactoriesPass.php: -------------------------------------------------------------------------------- 1 | findTaggedServiceIds(ResponseFactory::TAG); 15 | foreach ($services as $id => $tags) { 16 | foreach ($tags as $attributes) { 17 | if (!isset($attributes[ResponseFactory::ALIAS_ATTRIBUTE])) { 18 | throw new \InvalidArgumentException( 19 | sprintf( 20 | 'Missing "%s" attribute for "%s" tag "%s"', 21 | ResponseFactory::ALIAS_ATTRIBUTE, 22 | $id, 23 | ResponseFactory::TAG 24 | ) 25 | ); 26 | } 27 | 28 | $container->setAlias(ResponseFactory::createId($attributes[ResponseFactory::ALIAS_ATTRIBUTE]), $id); 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/MetricBundle/DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | getRootNode(); 25 | } else { 26 | // BC layer for symfony/config 4.1 and older 27 | $root = $builder->root('lamoda_metrics'); 28 | } 29 | 30 | $root->addDefaultsIfNotSet(); 31 | 32 | $sources = $root->children()->arrayNode('sources'); 33 | $sources->useAttributeAsKey('name', false); 34 | $this->createSources($sources->prototype('array')); 35 | 36 | $responseFactories = $root->children()->arrayNode('response_factories'); 37 | $responseFactories->useAttributeAsKey('name', false); 38 | $this->createResponseFactory($responseFactories->prototype('array')); 39 | 40 | $responders = $root->children()->arrayNode('responders'); 41 | $responders->useAttributeAsKey('name', false); 42 | $this->createResponder($responders->prototype('array')); 43 | 44 | $storages = $root->children()->arrayNode('storages'); 45 | $storages->useAttributeAsKey('name', false); 46 | $this->createStorage($storages->prototype('array')); 47 | 48 | $collectors = $root->children()->arrayNode('collectors'); 49 | $collectors->useAttributeAsKey('name', false); 50 | $this->createCollector($collectors->prototype('array')); 51 | 52 | return $builder; 53 | } 54 | 55 | private function createSources(ArrayNodeDefinition $source): void 56 | { 57 | $source->info( 58 | 'Sources also can be configured as services via `' . DefinitionFactory\Source::TAG . '` tag with `' . DefinitionFactory\Source::ALIAS_ATTRIBUTE . '` attribute' 59 | ); 60 | $source->canBeDisabled(); 61 | $source->children() 62 | ->enumNode('type') 63 | ->cannotBeEmpty() 64 | ->defaultValue('service') 65 | ->values(Source::METRIC_SOURCE_TYPES) 66 | ->info('Type of the source'); 67 | 68 | $source->children() 69 | ->scalarNode('id') 70 | ->defaultNull() 71 | ->info('Source service identifier [service]'); 72 | 73 | $source->children() 74 | ->scalarNode('entity') 75 | ->defaultValue(MetricInterface::class) 76 | ->info('Entity class [doctrine]'); 77 | 78 | $source->children() 79 | ->arrayNode('metrics') 80 | ->info('Metric services [composite]') 81 | ->defaultValue([]) 82 | ->prototype('scalar'); 83 | 84 | $source->children() 85 | ->scalarNode('storage') 86 | ->info('Storage name [storage]') 87 | ->defaultNull(); 88 | } 89 | 90 | private function createResponseFactory(ArrayNodeDefinition $responseFactory): void 91 | { 92 | $responseFactory->info( 93 | 'Response factories also can be configured as services via `' . DefinitionFactory\ResponseFactory::TAG . '` tag with `' . DefinitionFactory\ResponseFactory::ALIAS_ATTRIBUTE . '` attribute' 94 | ); 95 | $responseFactory->canBeDisabled(); 96 | $responseFactory->beforeNormalization()->ifString()->then( 97 | function (string $v) { 98 | return ['type' => 'service', 'id' => $v]; 99 | } 100 | ); 101 | $responseFactory->children() 102 | ->enumNode('type') 103 | ->cannotBeEmpty() 104 | ->defaultValue('service') 105 | ->values(ResponseFactory::TYPES) 106 | ->info('Type of the factory'); 107 | 108 | $responseFactory->children() 109 | ->scalarNode('id') 110 | ->defaultNull() 111 | ->info('Response factory service identifier [service]'); 112 | } 113 | 114 | private function createCollector(ArrayNodeDefinition $collector): void 115 | { 116 | $collector->info( 117 | 'Collectors also can be configured as services via `' . DefinitionFactory\Collector::TAG . '` tag with `' . DefinitionFactory\Collector::ALIAS_ATTRIBUTE . '` attribute' 118 | ); 119 | $collector->beforeNormalization()->ifString()->then( 120 | function (string $v) { 121 | return ['type' => Collector::COLLECTOR_TYPE_SERVICE, 'id' => $v]; 122 | } 123 | ); 124 | $collector->canBeDisabled(); 125 | $collector->children()->scalarNode('id') 126 | ->info('Collector service ID') 127 | ->defaultNull() 128 | ->example(MetricCollectorInterface::class); 129 | 130 | $collector->children() 131 | ->enumNode('type') 132 | ->cannotBeEmpty() 133 | ->defaultValue('service') 134 | ->values(Collector::TYPES) 135 | ->info('Type of the collector'); 136 | 137 | $collector->children()->arrayNode('collectors') 138 | ->prototype('scalar') 139 | ->info('Nested collectors') 140 | ->defaultValue([]); 141 | 142 | $collector->children()->arrayNode('sources') 143 | ->prototype('scalar') 144 | ->info('Metrics source names for responder controller') 145 | ->defaultValue([]); 146 | 147 | $collector->children()->arrayNode('metric_services') 148 | ->prototype('scalar') 149 | ->info('Append single metrics from services') 150 | ->defaultValue([]); 151 | 152 | $collector->children() 153 | ->arrayNode('default_tags') 154 | ->defaultValue([]) 155 | ->info('Default tag values for metrics from this collector') 156 | ->prototype('scalar') 157 | ->cannotBeEmpty(); 158 | } 159 | 160 | private function createStorage(ArrayNodeDefinition $storage): void 161 | { 162 | $storage->info( 163 | 'Storages also can be configured as services via `' . DefinitionFactory\Storage::TAG . '` tag with `' . DefinitionFactory\Storage::ALIAS_ATTRIBUTE . '` attribute' 164 | ); 165 | $storage->beforeNormalization()->ifString()->then( 166 | function (string $v) { 167 | return ['type' => 'service', 'id' => $v]; 168 | } 169 | ); 170 | $storage->canBeDisabled(); 171 | $storage->children()->scalarNode('id') 172 | ->cannotBeEmpty() 173 | ->info('Storage service ID [service]') 174 | ->example(MetricStorageInterface::class); 175 | $storage->children() 176 | ->enumNode('type') 177 | ->cannotBeEmpty() 178 | ->defaultValue('service') 179 | ->values(Storage::TYPES) 180 | ->info('Type of the storage'); 181 | $storage->children() 182 | ->booleanNode('mutator') 183 | ->defaultFalse() 184 | ->info('Configure storage as default metric mutator'); 185 | } 186 | 187 | private function createResponder(ArrayNodeDefinition $responder): void 188 | { 189 | $responder->canBeDisabled(); 190 | $responder->children()->scalarNode('path') 191 | ->cannotBeEmpty() 192 | ->info('Responder route path. Defaults to /$name') 193 | ->defaultNull() 194 | ->example('/prometheus'); 195 | 196 | $options = $responder->children()->arrayNode('format_options'); 197 | $options->info('Formatter options'); 198 | $options->ignoreExtraKeys(false); 199 | $options->children()->scalarNode('prefix') 200 | ->info('Metrics prefix for responder') 201 | ->defaultValue('') 202 | ->example('project_name_'); 203 | $options->children()->arrayNode('propagate_tags') 204 | ->info('Propagate tags to group [telegraf_json]') 205 | ->prototype('scalar') 206 | ->defaultValue([]) 207 | ->example('type'); 208 | $options->children()->arrayNode('group_by_tags') 209 | ->info('Arrange metrics to groups according to tag value. Tag name goes to group name [telegraf_json]') 210 | ->prototype('scalar') 211 | ->defaultValue([]) 212 | ->example(['tag_1']); 213 | 214 | $responder->children()->scalarNode('response_factory') 215 | ->cannotBeEmpty() 216 | ->info('Response factory alias') 217 | ->defaultNull() 218 | ->example('prometheus'); 219 | 220 | $responder->children()->scalarNode('collector') 221 | ->info('Collector alias') 222 | ->isRequired() 223 | ->cannotBeEmpty(); 224 | } 225 | } 226 | -------------------------------------------------------------------------------- /src/MetricBundle/DependencyInjection/DefinitionFactory/Collector.php: -------------------------------------------------------------------------------- 1 | getDefinition(self::REGISTRY_ID)->addMethodCall( 51 | 'register', 52 | [$name, self::createReference($name)] 53 | ); 54 | $container->setAlias(self::createId($name), $config['id']); 55 | break; 56 | } 57 | 58 | if (!empty($config['default_tags'])) { 59 | self::decorateWithDefaultTags($container, $name, $config); 60 | } 61 | 62 | self::addToRegistry($container, $name); 63 | } 64 | 65 | public static function createReference(string $name): Reference 66 | { 67 | return new Reference(self::createId($name)); 68 | } 69 | 70 | private static function registerMerging(ContainerBuilder $container, string $name, array $config) 71 | { 72 | $definition = $container->register(self::createId($name), MergingCollector::class); 73 | $collectorNames = $config['collectors']; 74 | $refs = array_map([self::class, 'createReference'], $collectorNames); 75 | 76 | $definition->setArguments([$refs, $config['default_tags']]); 77 | } 78 | 79 | private static function registerPreconfigured(ContainerBuilder $container, string $name, array $config) 80 | { 81 | $definition = $container->register(self::createId($name), SingleSourceCollector::class); 82 | $sources = []; 83 | foreach ($config['sources'] as $sourceAlias) { 84 | $sources[] = Source::createReference($sourceAlias); 85 | } 86 | 87 | if (!empty($config['metrics'])) { 88 | $metricServices = []; 89 | foreach ($config['metrics'] as $metricService) { 90 | $metricServices[] = new Reference($metricService); 91 | } 92 | $sources[] = new Definition(IterableMetricSource::class, [$metricServices]); 93 | } 94 | 95 | $definition->setArguments( 96 | [new Definition(MergingMetricSource::class, $sources)] 97 | ); 98 | } 99 | 100 | private static function decorateWithDefaultTags(ContainerBuilder $container, string $name, array $config): void 101 | { 102 | $decoratorId = self::createId($name) . '.default_tags'; 103 | 104 | $container->register($decoratorId, TaggingCollectorDecorator::class) 105 | ->setDecoratedService(self::createId($name)) 106 | ->setArguments([new Reference($decoratorId . '.inner'), $config['default_tags']]); 107 | } 108 | 109 | /** 110 | * @param ContainerBuilder $container 111 | * @param string $name 112 | */ 113 | private static function addToRegistry(ContainerBuilder $container, string $name): void 114 | { 115 | $registry = $container->getDefinition(Collector::REGISTRY_ID); 116 | $registry->addMethodCall('register', [$name, self::createReference($name)]); 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /src/MetricBundle/DependencyInjection/DefinitionFactory/Responder.php: -------------------------------------------------------------------------------- 1 | setAlias(self::createId($name), $config['id']); 39 | break; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/MetricBundle/DependencyInjection/DefinitionFactory/Source.php: -------------------------------------------------------------------------------- 1 | Storage::createId($config['storage'])] 42 | ); 43 | break; 44 | default: 45 | throw new \InvalidArgumentException('Invalid metric source type: ' . $config['type']); 46 | } 47 | } 48 | 49 | public static function createId(string $name): string 50 | { 51 | return self::ID_PREFIX . $name; 52 | } 53 | 54 | public static function createReference(string $name): Reference 55 | { 56 | return new Reference(self::createId($name)); 57 | } 58 | 59 | private static function createServiceMetricDefinition(ContainerBuilder $container, string $name, array $config) 60 | { 61 | if (!array_key_exists('id', $config) || !is_string($config['id'])) { 62 | throw new \InvalidArgumentException('`id` key should be configured for metric source'); 63 | } 64 | 65 | $container->setAlias(self::createId($name), $config['id']); 66 | } 67 | 68 | private static function createCompositeMetricDefinition(ContainerBuilder $container, string $name, array $config) 69 | { 70 | if (!array_key_exists('metrics', $config) || !is_array($config['metrics'])) { 71 | throw new \InvalidArgumentException('`metrics` key should be configured for composite source'); 72 | } 73 | 74 | $metrics = []; 75 | 76 | foreach ($config['metrics'] as $ref) { 77 | $metrics[] = new Reference($ref); 78 | } 79 | 80 | $definition = new Definition(IterableMetricSource::class, [new Definition(\ArrayIterator::class, [$metrics])]); 81 | 82 | $container->setDefinition(self::createId($name), $definition); 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/MetricBundle/DependencyInjection/DefinitionFactory/Storage.php: -------------------------------------------------------------------------------- 1 | setAlias(self::createId($name), $config['id']); 42 | break; 43 | } 44 | 45 | if ($config['mutator'] ?? false) { 46 | self::registerAsMutator($container, $name); 47 | } 48 | 49 | self::addToRegistry($container, $name); 50 | } 51 | 52 | /** 53 | * @param ContainerBuilder $container 54 | * @param string $name 55 | */ 56 | private static function addToRegistry(ContainerBuilder $container, string $name): void 57 | { 58 | $container->getDefinition(self::REGISTRY_ID)->addMethodCall('register', [$name, self::createReference($name)]); 59 | } 60 | 61 | /** 62 | * @param ContainerBuilder $container 63 | * @param string $name 64 | */ 65 | private static function registerAsMutator(ContainerBuilder $container, string $name): void 66 | { 67 | $container->setAlias(self::MUTATOR_STORAGE_ID, self::createId($name)); 68 | $container->getDefinition(self::MUTATOR_ID)->setArguments([self::createReference($name)]); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/MetricBundle/DependencyInjection/LamodaMetricExtension.php: -------------------------------------------------------------------------------- 1 | load('response_factories.yml'); 31 | $loader->load('services.yml'); 32 | 33 | $this->processFactories($container, $mergedConfig['response_factories'] ?? []); 34 | $this->processSources($container, $mergedConfig['sources'] ?? []); 35 | $this->processCollectors($container, $mergedConfig['collectors'] ?? []); 36 | $this->processResponders($container, $mergedConfig['responders'] ?? []); 37 | $this->processStorages($container, $mergedConfig['storages'] ?? []); 38 | } 39 | 40 | private function processFactories(ContainerBuilder $container, array $config): void 41 | { 42 | foreach ($config as $name => $factoryConfig) { 43 | ResponseFactory::register($container, $name, $factoryConfig); 44 | } 45 | } 46 | 47 | private function processCollectors(ContainerBuilder $container, array $config): void 48 | { 49 | foreach ($config as $name => $collectorConfig) { 50 | if (!$collectorConfig['enabled']) { 51 | continue; 52 | } 53 | 54 | Collector::register($container, $name, $collectorConfig); 55 | } 56 | } 57 | 58 | private function processStorages(ContainerBuilder $container, array $config): void 59 | { 60 | foreach ($config as $name => $storageConfig) { 61 | if (!$storageConfig['enabled']) { 62 | continue; 63 | } 64 | 65 | Storage::register($container, $name, $storageConfig); 66 | } 67 | } 68 | 69 | private function processSources(ContainerBuilder $container, array $sources): void 70 | { 71 | foreach ($sources as $name => $sourceConfig) { 72 | if (!$sourceConfig['enabled']) { 73 | continue; 74 | } 75 | 76 | Source::register($container, $name, $sourceConfig); 77 | } 78 | } 79 | 80 | private function processResponders(ContainerBuilder $container, array $config): void 81 | { 82 | $routerLoader = $container->getDefinition('lamoda_metrics.route_loader'); 83 | $separator = ':'; 84 | if (Kernel::VERSION_ID >= 40100) { 85 | $separator = '::'; 86 | } 87 | 88 | foreach ($config as $name => $responderConfig) { 89 | if (!$responderConfig['enabled']) { 90 | continue; 91 | } 92 | 93 | $controllerId = Responder::createId($name); 94 | 95 | $psrController = new Definition(PsrResponder::class); 96 | $psrController->setPublic(false); 97 | $psrController->setArguments( 98 | [ 99 | Collector::createReference($responderConfig['collector']), 100 | ResponseFactory::createReference($responderConfig['response_factory'] ?? $name), 101 | $responderConfig['format_options'] ?? [], 102 | ] 103 | ); 104 | 105 | $controller = $container->register($controllerId, HttpFoundationResponder::class); 106 | $controller->setPublic(true); 107 | $controller->setArguments([$psrController]); 108 | 109 | $path = $responderConfig['path'] ?? '/' . $name; 110 | 111 | $routerLoader->addMethodCall('registerController', [$name, $path, $controllerId . $separator . 'createResponse']); 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/MetricBundle/LamodaMetricBundle.php: -------------------------------------------------------------------------------- 1 | addCompilerPass(new RegisterReceiversPass()); 26 | $container->addCompilerPass(new RegisterCollectorsPass()); 27 | $container->addCompilerPass(new RegisterResponseFactoriesPass()); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/MetricBundle/Resources/config/response_factories.yml: -------------------------------------------------------------------------------- 1 | services: 2 | lamoda_metrics.response_factory.builtin.telegraf_httpjson: 3 | class: Lamoda\Metric\Responder\ResponseFactory\TelegrafJsonResponseFactory 4 | tags: 5 | - { name: lamoda_metrics.response_factory, alias: telegraf_json } 6 | 7 | lamoda_metrics.response_factory.builtin.prometheus: 8 | class: Lamoda\Metric\Responder\ResponseFactory\PrometheusResponseFactory 9 | tags: 10 | - { name: lamoda_metrics.response_factory, alias: prometheus } 11 | -------------------------------------------------------------------------------- /src/MetricBundle/Resources/config/services.yml: -------------------------------------------------------------------------------- 1 | services: 2 | lamoda_metrics.metric_mutator: 3 | class: Lamoda\Metric\Storage\StoredMetricMutator 4 | arguments: [] 5 | public: false 6 | 7 | Lamoda\Metric\Storage\MetricMutatorInterface: 8 | alias: lamoda_metrics.metric_mutator 9 | 10 | lamoda_metrics.route_loader: 11 | class: Lamoda\Metric\MetricBundle\Routing\MetricRouteLoader 12 | public: false 13 | tags: 14 | - { name: routing.loader } 15 | 16 | lamoda_metrics.storage_registry: 17 | class: Lamoda\Metric\Storage\StorageRegistry 18 | public: false 19 | 20 | lamoda_metrics.collector_registry: 21 | class: Lamoda\Metric\Collector\CollectorRegistry 22 | public: false 23 | 24 | lamoda_metrics.dump_command: 25 | class: Lamoda\Metric\MetricBundle\Command\DebugMetricsCommand 26 | arguments: 27 | - "@lamoda_metrics.collector_registry" 28 | public: true 29 | tags: 30 | - {name: console.command, command: 'metrics:debug'} 31 | 32 | lamoda_metrics.materialize_helper: 33 | class: Lamoda\Metric\Storage\MaterializeHelper 34 | public: false 35 | arguments: 36 | - "@lamoda_metrics.collector_registry" 37 | - "@lamoda_metrics.storage_registry" 38 | 39 | lamoda_metrics.materialize_command: 40 | class: Lamoda\Metric\MetricBundle\Command\MaterializeMetricsCommand 41 | arguments: 42 | - '@lamoda_metrics.materialize_helper' 43 | public: true 44 | tags: 45 | - {name: console.command, command: 'metrics:materialize'} 46 | -------------------------------------------------------------------------------- /src/MetricBundle/Resources/docs/commands.md: -------------------------------------------------------------------------------- 1 | # Bundle commands 2 | -------------------------------------------------------------------------------- /src/MetricBundle/Resources/docs/integration.md: -------------------------------------------------------------------------------- 1 | # Symfony integration 2 | 3 | ## Installation 4 | 5 | Configure kernel class 6 | 7 | ```php 8 | public function registerBundles() 9 | { 10 | return [ 11 | new FrameworkBundle(), 12 | // <...> More bundles 13 | new LamodaMetricBundle(), 14 | ]; 15 | } 16 | 17 | ``` 18 | 19 | In general you do not have to use `FrameworkBundle`, but then you'll have to deal 20 | with routing of metric responders by yourself. 21 | 22 | Configure some metrics: 23 | ```yaml 24 | services: 25 | custom_tagged_metric: 26 | class: Lamoda\Metric\Common\Metric 27 | arguments: 28 | - "tagged_metric" 29 | - 2.0 30 | - [{ name: lamoda_telegraf_metric, group: heartbeat }] 31 | 32 | custom_metric: 33 | class: Lamoda\Metric\Common\Metric 34 | arguments: 35 | - "custom_metric" 36 | - 1.0 37 | 38 | custom_metric_for_composite: 39 | class: Lamoda\Metric\Common\Metric 40 | arguments: 41 | - "custom_metric_for_composite" 42 | - 2.2 43 | ``` 44 | 45 | Configure bundle (sample from tests): 46 | 47 | ```yaml 48 | lamoda_metrics: 49 | sources: 50 | doctrine_entity_source: 51 | type: storage 52 | storage: doctrine 53 | composite_source: 54 | type: composite 55 | metrics: 56 | - custom_metric 57 | - custom_metric_for_composite 58 | 59 | collectors: 60 | raw_sources: 61 | type: sources 62 | sources: 63 | - composite_source 64 | metric_services: 65 | - custom_tagged_metric 66 | default_tags: {collector: raw} 67 | 68 | doctrine: 69 | type: sources 70 | sources: 71 | - doctrine_entity_source 72 | default_tags: {collector: doctrine} 73 | 74 | storages: 75 | doctrine: 76 | type: service 77 | mutator: true 78 | id: test.doctrine_metric_storage 79 | 80 | responders: 81 | telegraf_json: 82 | enabled: true 83 | collector: raw_sources 84 | format_options: 85 | group_by_tags: 86 | - type 87 | propagate_tags: 88 | - type 89 | 90 | custom_telegraf: 91 | enabled: true 92 | collector: raw_sources 93 | response_factory: telegraf_json 94 | format_options: 95 | group_by_tags: [] 96 | propagate_tags: 97 | - type 98 | path: /custom_telegraf 99 | 100 | prometheus: 101 | enabled: true 102 | collector: raw_sources 103 | format_options: 104 | prefix: metrics_ 105 | path: /prometheus 106 | 107 | ``` 108 | 109 | Configure routing 110 | ```yaml 111 | _lamoda_metrics: 112 | resource: . 113 | type: lamoda_metrics 114 | prefix: /metrics/ 115 | ``` 116 | 117 | ### Configuration 118 | 119 | See [configuration reference](reference.md) 120 | -------------------------------------------------------------------------------- /src/MetricBundle/Resources/docs/reference.md: -------------------------------------------------------------------------------- 1 | # Configuration reference 2 | 3 | ```yaml 4 | lamoda_metrics: 5 | sources: 6 | 7 | # Prototype: Sources also can be configured as services via `lamoda_metrics.source` tag with `alias` attribute 8 | name: 9 | enabled: true 10 | 11 | # Type of the source 12 | type: service # One of "service"; "composite"; "storage" 13 | 14 | # Source service identifier [service] 15 | id: null 16 | 17 | # Entity class [doctrine] 18 | entity: Lamoda\Metric\Common\MetricInterface 19 | 20 | # Metric services [composite] 21 | metrics: [] 22 | 23 | # Storage name [storage] 24 | storage: null 25 | response_factories: 26 | 27 | # Prototype: Response factories also can be configured as services via `lamoda_metrics.response_factory` tag with `alias` attribute 28 | name: 29 | enabled: true 30 | 31 | # Type of the factory 32 | type: service # One of "service" 33 | 34 | # Response factory service identifier [service] 35 | id: null 36 | responders: 37 | 38 | # Prototype 39 | name: 40 | enabled: true 41 | 42 | # Responder route path. Defaults to /$name 43 | path: null # Example: /prometheus 44 | 45 | # Formatter options 46 | format_options: 47 | 48 | # Metrics prefix for responder 49 | prefix: '' # Example: project_name_ 50 | 51 | # Propagate tags to group [telegraf_json] 52 | propagate_tags: [] 53 | 54 | # Arrange metrics to groups according to tag value. Tag name goes to group name [telegraf_json] 55 | group_by_tags: [] 56 | 57 | # Response factory alias 58 | response_factory: null # Example: prometheus 59 | 60 | # Collector alias 61 | collector: ~ # Required 62 | storages: 63 | 64 | # Prototype: Storages also can be configured as services via `lamoda_metrics.storage` tag with `alias` attribute 65 | name: 66 | enabled: true 67 | 68 | # Storage service ID [service] 69 | id: ~ # Example: Lamoda\Metric\Storage\MetricStorageInterface 70 | 71 | # Type of the storage 72 | type: service # One of "service" 73 | 74 | # Configure storage as default metric mutator 75 | mutator: false 76 | collectors: 77 | 78 | # Prototype: Collectors also can be configured as services via `lamoda_metrics.collector` tag with `alias` attribute 79 | name: 80 | enabled: true 81 | 82 | # Collector service ID 83 | id: null # Example: Lamoda\Metric\Collector\MetricCollectorInterface 84 | 85 | # Type of the collector 86 | type: service # One of "service"; "sources"; "merge" 87 | collectors: [] 88 | sources: [] 89 | metric_services: [] 90 | 91 | # Default tag values for metrics from this collector 92 | default_tags: [] 93 | 94 | ``` 95 | -------------------------------------------------------------------------------- /src/MetricBundle/Routing/MetricRouteLoader.php: -------------------------------------------------------------------------------- 1 | controllers)) { 29 | throw new \LogicException('Cannot register metric controller on the same path twice'); 30 | } 31 | 32 | $this->controllers[$name] = [$path, $serviceId, $method]; 33 | } 34 | 35 | /** {@inheritdoc} */ 36 | public function load($resource, $type = null): RouteCollection 37 | { 38 | if ($this->loaded) { 39 | throw new \LogicException('Lamoda metrics routes have been already loaded'); 40 | } 41 | 42 | $collection = new RouteCollection(); 43 | 44 | foreach ($this->controllers as $name => list($path, $controller)) { 45 | $collection->add($controller, new Route($path, ['_controller' => $controller])); 46 | } 47 | 48 | $this->loaded = true; 49 | 50 | return $collection; 51 | } 52 | 53 | /** {@inheritdoc} */ 54 | public function supports($resource, $type = null): bool 55 | { 56 | return 'lamoda_metrics' === $type; 57 | } 58 | 59 | /** {@inheritdoc} */ 60 | public function getResolver() 61 | { 62 | } 63 | 64 | /** {@inheritdoc} */ 65 | public function setResolver(LoaderResolverInterface $resolver) 66 | { 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/Responder/PsrResponder.php: -------------------------------------------------------------------------------- 1 | collector = $collector; 29 | $this->factory = $factory; 30 | $this->options = $options; 31 | } 32 | 33 | /** 34 | * Create PSR-7 HTTP response for collected metrics. 35 | * 36 | * @return ResponseInterface 37 | */ 38 | public function createResponse(): ResponseInterface 39 | { 40 | return $this->factory->create($this->collector->collect(), $this->options); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Responder/ResponseFactory/PrometheusResponseFactory.php: -------------------------------------------------------------------------------- 1 | getMetrics() as $metric) { 33 | $data[] = [ 34 | 'name' => ($options['prefix'] ?? '') . $metric->getName(), 35 | 'value' => $metric->resolve(), 36 | 'tags' => $metric->getTags(), 37 | ]; 38 | } 39 | 40 | return new Response( 41 | 200, 42 | ['Content-Type' => self::CONTENT_TYPE], 43 | $this->getContent($data) 44 | ); 45 | } 46 | 47 | /** 48 | * Get response content. 49 | * 50 | * @param array[] $data 51 | * 52 | * @return string 53 | */ 54 | private function getContent(array $data): string 55 | { 56 | $lines = []; 57 | foreach ($data as $line) { 58 | $lines[] = $this->getLine($line['name'], $line['tags'], $line['value']); 59 | } 60 | $lines = array_filter($lines); 61 | 62 | return implode(self::GLUE_LINES, $lines) . PHP_EOL; 63 | } 64 | 65 | /** 66 | * Get single line of Prometheus output. 67 | * 68 | * @param string $name 69 | * @param string[] $tags 70 | * @param float $value 71 | * 72 | * @return string 73 | */ 74 | private function getLine(string $name, array $tags, float $value): string 75 | { 76 | return sprintf(self::FORMAT_LINE, $name, $this->formatLabels($tags), $value); 77 | } 78 | 79 | /** 80 | * Get tags string. 81 | * 82 | * @param array $labels 83 | * 84 | * @return string 85 | */ 86 | private function formatLabels(array $labels): string 87 | { 88 | if ($labels === []) { 89 | return ''; 90 | } 91 | 92 | $tagsString = []; 93 | foreach ($labels as $name => $value) { 94 | $name = $this->formatLabelName($name); 95 | $value = $this->formatLabelValue($value); 96 | $tagsString[] = sprintf(self::FORMAT_LABEL, $name, $value); 97 | } 98 | 99 | return sprintf(self::LABELS_ENCLOSURE, implode(self::GLUE_LABELS, $tagsString)); 100 | } 101 | 102 | /** 103 | * Add slashes to values. 104 | * 105 | * @param $value 106 | * 107 | * @return mixed 108 | */ 109 | private function formatLabelValue(string $value): string 110 | { 111 | return addcslashes($value, "\n\"\\"); 112 | } 113 | 114 | /** 115 | * Remove unsupported symbols from. 116 | * 117 | * @param string $name 118 | * 119 | * @return string 120 | */ 121 | private function formatLabelName(string $name): string 122 | { 123 | // Only letters, digits and slashes. 124 | return preg_replace(self::PATTERN_FILTER_LABEL_NAME, '', $name); 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/Responder/ResponseFactory/TelegrafJsonResponseFactory.php: -------------------------------------------------------------------------------- 1 | arrangeGroups($source, $options); 20 | foreach ($groups as $name => $group) { 21 | $normalizedGroups[$name] = $this->formatGroup($group, $options); 22 | } 23 | 24 | $normalizedGroups = array_values(array_filter($normalizedGroups)); 25 | 26 | if (array_keys($normalizedGroups) === [0]) { 27 | $normalizedGroups = $normalizedGroups[0]; 28 | } 29 | 30 | return new Response( 31 | 200, 32 | ['Content-Type' => self::CONTENT_TYPE], 33 | json_encode($normalizedGroups) 34 | ); 35 | } 36 | 37 | private function formatGroup(array $source, array $options): array 38 | { 39 | $result = []; 40 | $prefix = $options['prefix'] ?? ''; 41 | 42 | foreach ($source as $metric) { 43 | $result[$prefix . $metric->getName()] = $metric->resolve(); 44 | } 45 | 46 | if (!empty($options['propagate_tags'])) { 47 | $this->propagateTags($result, $source, $options['propagate_tags']); 48 | } 49 | 50 | return $result; 51 | } 52 | 53 | /** 54 | * @param array $result 55 | * @param MetricInterface[] $source 56 | * @param string[] $propagatedTags 57 | */ 58 | private function propagateTags(array &$result, array $source, array $propagatedTags): void 59 | { 60 | foreach ($source as $metric) { 61 | foreach ($metric->getTags() as $tag => $value) { 62 | if (\in_array($tag, $propagatedTags, true)) { 63 | $result[$tag] = (string) $value; 64 | } 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * @param MetricSourceInterface $source 71 | * @param array $options 72 | * 73 | * @return MetricInterface[][] 74 | */ 75 | private function arrangeGroups(MetricSourceInterface $source, array $options): array 76 | { 77 | $groups = []; 78 | if (empty($options['group_by_tags'])) { 79 | $groups[0] = iterator_to_array($source->getMetrics(), false); 80 | 81 | return $groups; 82 | } 83 | 84 | $tags = (array) $options['group_by_tags']; 85 | foreach ($source->getMetrics() as $metric) { 86 | $vector = []; 87 | foreach ($tags as $tag) { 88 | $vector[] = $metric->getTags()[$tag] ?? '__'; 89 | } 90 | 91 | $groups[$this->createTagVector(...$vector)][] = $metric; 92 | } 93 | 94 | return array_values($groups); 95 | } 96 | 97 | private function createTagVector(string ...$values) 98 | { 99 | return implode(';', $values); 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/Responder/ResponseFactoryInterface.php: -------------------------------------------------------------------------------- 1 | getCode(), $e 11 | ); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Storage/MaterializeHelper.php: -------------------------------------------------------------------------------- 1 | collectorRegistry = $collectorRegistry; 25 | $this->storageRegistry = $storageRegistry; 26 | } 27 | 28 | /** 29 | * Receives metrics from named collector to named storage. 30 | * 31 | * @param string $collectorName 32 | * @param string $storageName 33 | * 34 | * @throws ReceiverException 35 | * @throws \OutOfBoundsException 36 | */ 37 | public function materialize(string $collectorName, string $storageName): void 38 | { 39 | $collector = $this->collectorRegistry->getCollector($collectorName); 40 | $storage = $this->storageRegistry->getStorage($storageName); 41 | 42 | $storage->receive($this->materializeSource($collector->collect())); 43 | } 44 | 45 | private function materializeSource(MetricSourceInterface $source): MetricSourceInterface 46 | { 47 | $metrics = []; 48 | foreach ($source->getMetrics() as $metric) { 49 | $metrics[] = new Metric($metric->getName(), $metric->resolve(), $metric->getTags()); 50 | } 51 | 52 | return new IterableMetricSource($metrics); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Storage/MetricMutatorInterface.php: -------------------------------------------------------------------------------- 1 | storages[$name] = $storage; 16 | } 17 | 18 | /** 19 | * @param string $name 20 | * 21 | * @return MetricStorageInterface 22 | * 23 | * @throws \OutOfBoundsException 24 | */ 25 | public function getStorage(string $name): MetricStorageInterface 26 | { 27 | if (!array_key_exists($name, $this->storages)) { 28 | throw new \OutOfBoundsException('Unknown storage in registry: ' . $name); 29 | } 30 | 31 | return $this->storages[$name]; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Storage/StoredMetricMutator.php: -------------------------------------------------------------------------------- 1 | storage = $storage; 13 | } 14 | 15 | /** {@inheritdoc} */ 16 | public function adjustMetricValue(float $delta, string $name, array $tags = []): void 17 | { 18 | $this->findOrCreateMetric($name, $tags)->adjust($delta); 19 | } 20 | 21 | /** {@inheritdoc} */ 22 | public function setMetricValue(float $value, string $name, array $tags = []): void 23 | { 24 | $this->findOrCreateMetric($name, $tags)->setValue($value); 25 | } 26 | 27 | private function findOrCreateMetric(string $name, array $tags): MutableMetricInterface 28 | { 29 | return $this->storage->findMetric($name, $tags) ?: $this->storage->createMetric($name, 0, $tags); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Adapters/Doctrine/AbstractDoctrineStorageTest.php: -------------------------------------------------------------------------------- 1 | 'v1']); 20 | $unknownSourceMetric = new Metric('unknown_metric', 241.0, ['tag' => 'v1']); 21 | $createdMetric = new Metric($unknownSourceMetric->getName(), 0, $unknownSourceMetric->getTags()); 22 | 23 | $metrics = new \ArrayIterator( 24 | [ 25 | $knownSourceMetric, 26 | $unknownSourceMetric, 27 | ] 28 | ); 29 | 30 | $source = $this->createMock(MetricSourceInterface::class); 31 | $source->method('getMetrics')->willReturn($metrics); 32 | 33 | $em = $this->createMock(EntityManagerInterface::class); 34 | $em->expects($this->once())->method('beginTransaction'); 35 | $em->expects($this->once())->method('flush'); 36 | $em->expects($this->once())->method('commit'); 37 | $em->expects($this->never())->method('rollback'); 38 | $em->expects($this->once())->method('persist')->with( 39 | $this->callback( 40 | function ($metric) use ($unknownSourceMetric, $createdMetric) { 41 | self::assertNotSame($metric, $unknownSourceMetric); 42 | self::assertEquals($metric, $createdMetric); 43 | 44 | return true; 45 | } 46 | ) 47 | ); 48 | 49 | $storage = $this->getMockBuilder(AbstractDoctrineStorage::class) 50 | ->setConstructorArgs([$em]) 51 | ->getMock(); 52 | 53 | $storage->expects($this->exactly(2))->method('doFindMetric') 54 | ->willReturnMap( 55 | [ 56 | [$knownSourceMetric->getName(), $knownSourceMetric->getTags(), clone $knownSourceMetric], 57 | [$unknownSourceMetric->getName(), $unknownSourceMetric->getTags(), null], 58 | ] 59 | ); 60 | 61 | $storage->expects($this->once())->method('doCreateMetric') 62 | ->with($unknownSourceMetric->getName(), 0, $unknownSourceMetric->getTags()) 63 | ->willReturn($createdMetric); 64 | 65 | $storage->receive($source); 66 | } 67 | 68 | public function testExceptionCallsRollback(): void 69 | { 70 | $source = $this->createMock(MetricSourceInterface::class); 71 | $source->method('getMetrics')->willReturn(new \ArrayIterator([new Metric('test', 0)])); 72 | 73 | $em = $this->createMock(EntityManagerInterface::class); 74 | $em->expects($this->once())->method('beginTransaction'); 75 | $em->expects($this->never())->method('flush'); 76 | $em->expects($this->never())->method('commit'); 77 | $em->expects($this->once())->method('rollback'); 78 | 79 | $storage = $this->getMockBuilder(AbstractDoctrineStorage::class) 80 | ->setConstructorArgs([$em]) 81 | ->getMock(); 82 | 83 | $storage->expects($this->once())->method('doFindMetric')->willThrowException(new \RuntimeException()); 84 | 85 | $this->expectException(\RuntimeException::class); 86 | $storage->receive($source); 87 | } 88 | 89 | public function testIteratorIsGetMetricsProxy(): void 90 | { 91 | $storage = $this->createMock(AbstractDoctrineStorage::class); 92 | 93 | $expected = new \ArrayIterator([]); 94 | 95 | $storage->expects($this->once())->method('getMetrics')->willReturn($expected); 96 | self::assertSame($expected, $storage->getIterator()); 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Adapters/Doctrine/AtomicMutableWrapperTest.php: -------------------------------------------------------------------------------- 1 | createMock(EntityManagerInterface::class); 18 | $metric = $this->createMock(MutableMetricInterface::class); 19 | $value = 241.0; 20 | $metric->expects($this->once())->method('resolve')->willReturn($value); 21 | 22 | $name = 'test_metric'; 23 | $metric->expects($this->once())->method('getName')->willReturn($name); 24 | 25 | $tags = ['tag' => 'v1']; 26 | $metric->expects($this->once())->method('getTags')->willReturn($tags); 27 | 28 | $wrapper = new AtomicMutableWrapper($em, $metric); 29 | self::assertSame($value, $wrapper->resolve()); 30 | self::assertSame($name, $wrapper->getName()); 31 | self::assertSame($tags, $wrapper->getTags()); 32 | } 33 | 34 | public function testSettingIsAtomicOperation(): void 35 | { 36 | $em = $this->createMock(EntityManagerInterface::class); 37 | $metric = $this->createMock(MutableMetricInterface::class); 38 | 39 | $em->expects($this->once())->method('transactional')->willReturnCallback( 40 | function (callable $callback) { 41 | return $callback(); 42 | } 43 | ); 44 | $em->expects($this->once())->method('lock')->with($metric); 45 | $em->expects($this->once())->method('refresh')->with($metric); 46 | $em->expects($this->once())->method('flush'); 47 | 48 | $metric->expects($this->once())->method('setValue')->with(10); 49 | 50 | $wrapper = new AtomicMutableWrapper($em, $metric); 51 | $wrapper->setValue(10); 52 | } 53 | 54 | public function testAdjustingIsAtomicOperation(): void 55 | { 56 | $em = $this->createMock(EntityManagerInterface::class); 57 | $metric = $this->createMock(MutableMetricInterface::class); 58 | 59 | $em->expects($this->once())->method('transactional')->willReturnCallback( 60 | function (callable $callback) { 61 | return $callback(); 62 | } 63 | ); 64 | $em->expects($this->once())->method('lock')->with($metric); 65 | $em->expects($this->once())->method('refresh')->with($metric); 66 | $em->expects($this->once())->method('flush'); 67 | 68 | $metric->expects($this->once())->method('adjust')->with(10); 69 | 70 | $wrapper = new AtomicMutableWrapper($em, $metric); 71 | $wrapper->adjust(10); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /tests/Adapters/Redis/AbstractRedisStorageTest.php: -------------------------------------------------------------------------------- 1 | redisConnection = $this->createMock(RedisConnectionInterface::class); 28 | $this->redisStorage = $this->createRedisStorage(); 29 | } 30 | 31 | public function testReceive(): void 32 | { 33 | $metrics = [ 34 | $this->createMetric('test1', 17, ['source' => 'fast', 'path' => 'inner']), 35 | $this->createMetric('test2', 4, ['margin' => 'left']), 36 | ]; 37 | 38 | $source = $this->createMock(MetricSourceInterface::class); 39 | $source 40 | ->expects($this->once()) 41 | ->method('getMetrics') 42 | ->willReturn(new \ArrayIterator($metrics)); 43 | 44 | $expectedDto = [ 45 | new MetricDto('test1', 17, ['source' => 'fast', 'path' => 'inner']), 46 | new MetricDto('test2', 4, ['margin' => 'left']), 47 | ]; 48 | $this->redisConnection 49 | ->expects($this->once()) 50 | ->method('setMetrics') 51 | ->with($expectedDto); 52 | 53 | $this->redisStorage->receive($source); 54 | } 55 | 56 | public function testGetMetrics(): void 57 | { 58 | $this->redisConnection 59 | ->expects($this->once()) 60 | ->method('getAllMetrics') 61 | ->willReturn([ 62 | new MetricDto('test1', 17, ['source' => 'fast', 'path' => 'inner']), 63 | new MetricDto('test2', 4, ['margin' => 'left']), 64 | ]); 65 | 66 | $expected = [ 67 | new MetricWrapper( 68 | $this->redisConnection, 69 | new Metric('test1', 17, ['source' => 'fast', 'path' => 'inner']) 70 | ), 71 | new MetricWrapper( 72 | $this->redisConnection, 73 | new Metric('test2', 4, ['margin' => 'left']) 74 | ), 75 | ]; 76 | 77 | $actual = $this->redisStorage->getMetrics(); 78 | 79 | self::assertEquals($expected, iterator_to_array($actual)); 80 | } 81 | 82 | public function testFindMetric(): void 83 | { 84 | $this->redisConnection 85 | ->expects($this->once()) 86 | ->method('getMetricValue') 87 | ->with('test1', ['source' => 'fast', 'path' => 'inner']) 88 | ->willReturn(4.0); 89 | 90 | $expected = new MetricWrapper( 91 | $this->redisConnection, 92 | new Metric('test1', 4, ['source' => 'fast', 'path' => 'inner']) 93 | ); 94 | 95 | $actual = $this->redisStorage->findMetric('test1', ['source' => 'fast', 'path' => 'inner']); 96 | 97 | self::assertEquals($expected, $actual); 98 | } 99 | 100 | public function testFindMetricWhenMetricIsNotFound(): void 101 | { 102 | $this->redisConnection 103 | ->expects($this->once()) 104 | ->method('getMetricValue') 105 | ->with('test1', ['source' => 'fast', 'path' => 'inner']) 106 | ->willReturn(null); 107 | 108 | $actual = $this->redisStorage->findMetric('test1', ['source' => 'fast', 'path' => 'inner']); 109 | 110 | self::assertNull($actual); 111 | } 112 | 113 | public function testCreateMetric(): void 114 | { 115 | $expectedDto = new MetricDto('test1', 17, ['source' => 'fast', 'path' => 'inner']); 116 | $this->redisConnection 117 | ->expects($this->once()) 118 | ->method('setMetrics') 119 | ->with([$expectedDto]); 120 | 121 | $expected = new MetricWrapper( 122 | $this->redisConnection, 123 | new Metric('test1', 17, ['source' => 'fast', 'path' => 'inner']) 124 | ); 125 | 126 | $actual = $this->redisStorage->createMetric('test1', 17, ['source' => 'fast', 'path' => 'inner']); 127 | 128 | self::assertEquals($expected, $actual); 129 | } 130 | 131 | public function testSetMetricValue(): void 132 | { 133 | $expectedDto = new MetricDto('test1', 17, ['source' => 'fast', 'path' => 'inner']); 134 | $this->redisConnection 135 | ->expects($this->once()) 136 | ->method('setMetrics') 137 | ->with([$expectedDto]); 138 | 139 | $this->redisStorage->setMetricValue('test1', 17, ['source' => 'fast', 'path' => 'inner']); 140 | } 141 | 142 | public function testAdjustMetricValue(): void 143 | { 144 | $this->redisConnection 145 | ->expects($this->once()) 146 | ->method('adjustMetric') 147 | ->with('test1', 17, ['source' => 'fast', 'path' => 'inner']); 148 | 149 | $this->redisStorage->adjustMetricValue('test1', 17, ['source' => 'fast', 'path' => 'inner']); 150 | } 151 | 152 | private function createMetric(string $name, float $value, array $tags) 153 | { 154 | $metric = $this->createMock(MetricInterface::class); 155 | $metric 156 | ->method('getName') 157 | ->willReturn($name); 158 | $metric 159 | ->method('resolve') 160 | ->willReturn($value); 161 | $metric 162 | ->method('getTags') 163 | ->willReturn($tags); 164 | 165 | return $metric; 166 | } 167 | 168 | private function createRedisStorage(): AbstractRedisStorage 169 | { 170 | return new class($this->redisConnection) extends AbstractRedisStorage 171 | { 172 | protected function doCreateMetric(string $name, float $value, array $tags = []): MutableMetricInterface 173 | { 174 | return new Metric($name, $value, $tags); 175 | } 176 | }; 177 | } 178 | } 179 | -------------------------------------------------------------------------------- /tests/Adapters/Redis/RedisConnectionTest.php: -------------------------------------------------------------------------------- 1 | redis = $this->createMock(\Redis::class); 23 | $this->redisConnection = new RedisConnection($this->redis, self::METRICS_KEY); 24 | } 25 | 26 | public function testReceivingAllMetrics(): void 27 | { 28 | $this->redis 29 | ->expects($this->once()) 30 | ->method('hgetall') 31 | ->with(self::METRICS_KEY) 32 | ->willReturn([ 33 | '{"name":"test1","tags":"{\"status\":15,\"port\":1}"}' => '17', 34 | '{"name":"test2","tags":"{\"severity\":\"high\"}"}' => '2', 35 | ]); 36 | 37 | $expected = [ 38 | new MetricDto('test1', 17, ['status' => 15, 'port' => 1]), 39 | new MetricDto('test2', 2, ['severity' => 'high']), 40 | ]; 41 | $actual = $this->redisConnection->getAllMetrics(); 42 | 43 | self::assertEquals($expected, $actual); 44 | } 45 | 46 | public function testAdjustMetric(): void 47 | { 48 | $value = 15; 49 | $expectedField = '{"name":"test","tags":"{\"severity\":\"high\"}"}'; 50 | $this->redis 51 | ->expects($this->once()) 52 | ->method('hincrbyfloat') 53 | ->with(self::METRICS_KEY, $expectedField, $value) 54 | ->willReturn(17.0); 55 | 56 | $actual = $this->redisConnection->adjustMetric('test', $value, ['severity' => 'high']); 57 | self::assertEquals(17, $actual); 58 | } 59 | 60 | public function testSetMetrics(): void 61 | { 62 | $fields = [ 63 | '{"name":"test1","tags":"{\"port\":1,\"status\":15}"}' => 17, 64 | '{"name":"test2","tags":"{\"severity\":\"high\"}"}' => 2, 65 | ]; 66 | $this->redis 67 | ->expects($this->once()) 68 | ->method('hmset') 69 | ->with(self::METRICS_KEY, $fields) 70 | ->willReturn(false); 71 | $metrics = [ 72 | new MetricDto('test1', 17, ['status' => 15, 'port' => 1]), 73 | new MetricDto('test2', 2, ['severity' => 'high']), 74 | ]; 75 | 76 | $this->redisConnection->setMetrics($metrics); 77 | } 78 | 79 | public function testGetMetricValue(): void 80 | { 81 | $expectedField = '{"name":"test","tags":"{\"severity\":\"high\"}"}'; 82 | $this->redis 83 | ->expects($this->once()) 84 | ->method('hget') 85 | ->with(self::METRICS_KEY, $expectedField) 86 | ->willReturn('17'); 87 | 88 | $actual = $this->redisConnection->getMetricValue('test', ['severity' => 'high']); 89 | self::assertEquals(17, $actual); 90 | } 91 | 92 | public function testFailedGetMetricValue(): void 93 | { 94 | $expectedField = '{"name":"test","tags":"{\"severity\":\"high\"}"}'; 95 | $this->redis 96 | ->expects($this->once()) 97 | ->method('hget') 98 | ->with(self::METRICS_KEY, $expectedField) 99 | ->willReturn(false); 100 | 101 | $actual = $this->redisConnection->getMetricValue('test', ['severity' => 'high']); 102 | self::assertEquals(0, $actual); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/Builders/TraversableMetricSourceBuilder.php: -------------------------------------------------------------------------------- 1 | metrics = $metrics; 20 | } 21 | 22 | public function getMetrics(): \Traversable 23 | { 24 | return new \ArrayIterator($this->metrics); 25 | } 26 | 27 | public function getIterator(): \ArrayIterator 28 | { 29 | return new \ArrayIterator($this->metrics); 30 | } 31 | }; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /tests/Collector/CollectorRegistryTest.php: -------------------------------------------------------------------------------- 1 | createMock(MetricCollectorInterface::class); 17 | 18 | $registry = new CollectorRegistry(); 19 | $registry->register('test', $mock); 20 | 21 | self::assertSame($mock, $registry->getCollector('test')); 22 | } 23 | 24 | public function testRegistryThrowsExceptionsForUnknownCollector(): void 25 | { 26 | $registry = new CollectorRegistry(); 27 | 28 | $this->expectException(\OutOfBoundsException::class); 29 | $registry->getCollector('test'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Collector/MergingCollectorTest.php: -------------------------------------------------------------------------------- 1 | collect(); 32 | $metrics = $source->getMetrics(); 33 | $metrics = iterator_to_array($metrics, false); 34 | 35 | self::assertCount(4, $metrics); 36 | foreach ([$originalMetric11, $originalMetric12, $originalMetric21, $originalMetric22] as $originalMetric) { 37 | self::assertContains($originalMetric, $metrics); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/Collector/SingleSourceCollectorTest.php: -------------------------------------------------------------------------------- 1 | createMock(MetricSourceInterface::class); 17 | $collector = new SingleSourceCollector($source); 18 | self::assertSame($source, $collector->collect()); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Collector/TaggingCollectorDecoratorTest.php: -------------------------------------------------------------------------------- 1 | 'tag']); 19 | $metric2 = new Metric('test_2', 2.0, ['common' => 'tag']); 20 | $source = new IterableMetricSource([$metric1, $metric2]); 21 | $collector = $this->createMock(MetricCollectorInterface::class); 22 | $collector->expects($this->once())->method('collect')->willReturn($source); 23 | 24 | $decorator = new TaggingCollectorDecorator($collector, ['extra' => 'custom']); 25 | foreach ($decorator->collect()->getMetrics() as $metric) { 26 | self::assertArrayHasKey('extra', $metric->getTags()); 27 | self::assertSame('custom', $metric->getTags()['extra']); 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tests/Common/DefaultTagsMetricTest.php: -------------------------------------------------------------------------------- 1 | 'value']; 19 | $metric = $this->createMock(MetricInterface::class); 20 | $metric->expects($this->once())->method('getName')->willReturn($name); 21 | $metric->expects($this->once())->method('resolve')->willReturn($value); 22 | $metric->expects($this->once())->method('getTags')->willReturn($tags); 23 | 24 | $wrapper = new DefaultTagsMetric($metric); 25 | 26 | self::assertSame($name, $wrapper->getName()); 27 | self::assertSame($value, $wrapper->resolve()); 28 | self::assertSame($tags, $wrapper->getTags()); 29 | } 30 | 31 | public function testExtraTagsAdded(): void 32 | { 33 | $metric = $this->createMock(MetricInterface::class); 34 | $metric->expects($this->once())->method('getTags')->willReturn(['tag' => 'value']); 35 | 36 | $wrapper = new DefaultTagsMetric($metric, ['extra' => 'tag']); 37 | 38 | self::assertSame(['extra' => 'tag', 'tag' => 'value'], $wrapper->getTags()); 39 | } 40 | 41 | public function testExistingTagsAreNotOverwritten(): void 42 | { 43 | $metric = $this->createMock(MetricInterface::class); 44 | $metric->expects($this->once())->method('getTags')->willReturn(['tag' => 'value']); 45 | 46 | $wrapper = new DefaultTagsMetric($metric, ['tag' => 'new_value']); 47 | 48 | self::assertSame(['tag' => 'value'], $wrapper->getTags()); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /tests/Common/MetricTest.php: -------------------------------------------------------------------------------- 1 | 'value']; 16 | $value = 1.0; 17 | $name = 'test'; 18 | $metric = new Metric($name, $value, $tags); 19 | self::assertSame($name, $metric->getName()); 20 | self::assertSame($value, $metric->resolve()); 21 | self::assertSame($tags, $metric->getTags()); 22 | } 23 | 24 | public function testMetricAdjusting(): void 25 | { 26 | $metric = new Metric('test', 1.0); 27 | $metric->adjust(2.0); 28 | self::assertSame(3.0, $metric->resolve()); 29 | } 30 | 31 | public function testMetricSetting(): void 32 | { 33 | $metric = new Metric('test', 1.0); 34 | $metric->setValue(2.0); 35 | self::assertSame(2.0, $metric->resolve()); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /tests/Common/Source/DefaultTaggingMetricSourceTest.php: -------------------------------------------------------------------------------- 1 | 'value']); 19 | $m2 = new Metric('test_2', 2.0, ['common' => 'value']); 20 | $inner = TraversableMetricSourceBuilder::build([$m1, $m2]); 21 | 22 | $source = new DefaultTaggingMetricSource($inner); 23 | $metrics = iterator_to_array($source); 24 | self::assertCount(2, $metrics); 25 | $expected = [ 26 | 'test_1' => 1.0, 27 | 'test_2' => 2.0, 28 | ]; 29 | $actual = []; 30 | foreach ($metrics as $metric) { 31 | $actual[$metric->getName()] = $metric->resolve(); 32 | } 33 | self::assertSame($expected, $actual); 34 | } 35 | 36 | public function testSourceAddsExtraTags(): void 37 | { 38 | $m1 = new Metric('test_1', 1.0); 39 | $m2 = new Metric('test_2', 2.0); 40 | $inner = TraversableMetricSourceBuilder::build([$m1, $m2]); 41 | 42 | $source = new DefaultTaggingMetricSource($inner, ['extra' => 'value']); 43 | /** @var MetricInterface[] $metrics */ 44 | $metrics = iterator_to_array($source); 45 | foreach ($metrics as $metric) { 46 | self::assertArrayHasKey('extra', $metric->getTags()); 47 | self::assertSame('value', $metric->getTags()['extra']); 48 | } 49 | } 50 | 51 | public function testSourceDoesNotOverrideTags(): void 52 | { 53 | $m1 = new Metric('test_1', 1.0, ['tag' => 'value']); 54 | $inner = TraversableMetricSourceBuilder::build([$m1]); 55 | 56 | $source = new DefaultTaggingMetricSource($inner, ['tag' => 'new_value']); 57 | foreach ($source as $metric) { 58 | /** @var MetricInterface $metric */ 59 | self::assertArrayHasKey('tag', $metric->getTags()); 60 | self::assertSame('value', $metric->getTags()['tag']); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Common/Source/IterableSourceTest.php: -------------------------------------------------------------------------------- 1 | getMetrics(); 23 | self::assertSame($metrics, $source->getIterator()); 24 | self::assertSame($array, iterator_to_array($metrics)); 25 | } 26 | 27 | public function testIteratingTraversable(): void 28 | { 29 | $m1 = new Metric('test_1', 1.0); 30 | $m2 = new Metric('test_2', 2.0); 31 | $array = [$m1, $m2]; 32 | 33 | $source = new IterableMetricSource(new \ArrayIterator($array)); 34 | 35 | $metrics = $source->getMetrics(); 36 | self::assertSame($metrics, $source->getIterator()); 37 | self::assertSame($array, iterator_to_array($metrics)); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Common/Source/MergingMetricSourceTest.php: -------------------------------------------------------------------------------- 1 | getMetrics(); 26 | self::assertEquals($metrics, $source->getIterator()); 27 | $metrics = iterator_to_array($metrics, false); 28 | self::assertCount(4, $metrics); 29 | self::assertContains($m1, $metrics); 30 | self::assertContains($m2, $metrics); 31 | self::assertContains($m3, $metrics); 32 | self::assertContains($m4, $metrics); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/MetricBundle/AbstractMetricBundleTestClass.php: -------------------------------------------------------------------------------- 1 | getContainer()->get('doctrine')->getManager(); 32 | } 33 | 34 | return static::$em; 35 | } 36 | 37 | protected static function createKernel(array $options = []): KernelInterface 38 | { 39 | $kernel = parent::createKernel($options); 40 | $fs = new Filesystem(); 41 | $fs->remove($kernel->getCacheDir()); 42 | $fs->remove($kernel->getLogDir()); 43 | 44 | return $kernel; 45 | } 46 | 47 | protected static function getContainer(): ContainerInterface 48 | { 49 | return static::$client->getContainer(); 50 | } 51 | 52 | /** 53 | * @throws \Doctrine\ORM\Tools\ToolsException 54 | */ 55 | private static function mockDoctrine() 56 | { 57 | $entityManager = static::getEntityManager(); 58 | $tool = new SchemaTool($entityManager); 59 | $tool->dropDatabase(); 60 | $tool->createSchema($entityManager->getMetadataFactory()->getAllMetadata()); 61 | self::validateEntityManager(); 62 | } 63 | 64 | private static function validateEntityManager() 65 | { 66 | $validator = new SchemaValidator(static::getEntityManager()); 67 | $errors = $validator->validateMapping(); 68 | static::assertCount( 69 | 0, 70 | $errors, 71 | implode( 72 | "\n\n", 73 | array_map( 74 | function ($l) { 75 | return implode("\n\n", $l); 76 | }, 77 | $errors 78 | ) 79 | ) 80 | ); 81 | } 82 | 83 | protected function tearDown(): void 84 | { 85 | $fs = new Filesystem(); 86 | static::$em->close(); 87 | $cacheDir = static::$kernel->getCacheDir(); 88 | $logDir = static::$kernel->getLogDir(); 89 | 90 | parent::tearDown(); 91 | $fs->remove($cacheDir); 92 | $fs->remove($logDir); 93 | } 94 | 95 | /** 96 | * @throws \Doctrine\ORM\Tools\ToolsException 97 | */ 98 | protected function setUp(): void 99 | { 100 | static::$client = static::createClient(); 101 | self::mockDoctrine(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/MetricBundle/Controller/HttpFoundationResponderTest.php: -------------------------------------------------------------------------------- 1 | createMock(MetricCollectorInterface::class); 37 | $collector->method('collect')->willReturn($source); 38 | 39 | $controller = new HttpFoundationResponder(new PsrResponder($collector, new TelegrafJsonResponseFactory())); 40 | 41 | $response = $controller->createResponse(); 42 | 43 | $this->assertTrue($response->isSuccessful()); 44 | $this->assertSame('application/json', $response->headers->get('Content-Type')); 45 | $this->assertJsonStringEqualsJsonString($expected, $response->getContent()); 46 | } 47 | 48 | public function testControllerCorrectHandleException(): void 49 | { 50 | $exception = new Exception('Test exception'); 51 | $expectedExceptionObject = new HttpException(Response::HTTP_INTERNAL_SERVER_ERROR, '', $exception); 52 | 53 | $collector = $this->createMock(MetricCollectorInterface::class); 54 | $collector->method('collect')->willThrowException($exception); 55 | 56 | $controller = new HttpFoundationResponder(new PsrResponder($collector, new TelegrafJsonResponseFactory())); 57 | 58 | $this->expectExceptionObject($expectedExceptionObject); 59 | 60 | $controller->createResponse(); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/MetricBundle/DependencyInjection/ConfigurationTest.php: -------------------------------------------------------------------------------- 1 | getConfigTreeBuilder()->buildTree(); 39 | 40 | $data = Yaml::parse(file_get_contents($fname)); 41 | 42 | $config = $tree->finalize($data); 43 | 44 | self::assertArrayHasKey('sources', $config); 45 | self::assertArrayHasKey('storages', $config); 46 | self::assertArrayHasKey('collectors', $config); 47 | self::assertArrayHasKey('responders', $config); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/MetricBundle/DependencyInjection/valid_config_samples/empty.yml: -------------------------------------------------------------------------------- 1 | lamoda_metrics: 2 | -------------------------------------------------------------------------------- /tests/MetricBundle/DependencyInjection/valid_config_samples/full.yml: -------------------------------------------------------------------------------- 1 | lamoda_metrics: 2 | metrics: 3 | sources: 4 | my_custom_metric_entity: 5 | type: doctrine 6 | entity: Lamoda\MetricResponder\Entity\Metric 7 | composite: 8 | type: composite 9 | metrics: 10 | - custom_metric 11 | 12 | groups: 13 | sources: 14 | doctrine_entity_source: 15 | type: doctrine 16 | entity: Lamoda\MetricResponder\Entity\MetricGroup 17 | custom: 18 | my_custom_group: 19 | tags: {type: custom} 20 | metric_sources: 21 | - my_custom_metric_entity 22 | - composite 23 | metric_services: 24 | - custom_metric 25 | heartbeat: 26 | tags: {type: heartbeat} 27 | 28 | responders: 29 | telegraf: 30 | groups: 31 | - my_custom_group 32 | sources: 33 | - doctrine_entity_source 34 | custom_telegraf: 35 | formatter: lamoda_metrics.formatter.telegraf 36 | path: /custom_telegraf 37 | groups: 38 | - my_custom_group 39 | sources: 40 | - doctrine_entity_source 41 | -------------------------------------------------------------------------------- /tests/MetricBundle/DependencyInjection/valid_config_samples/reference.yml: -------------------------------------------------------------------------------- 1 | lamoda_metrics: 2 | metrics: 3 | sources: 4 | 5 | # Prototype 6 | name: 7 | 8 | # Type of the source 9 | type: service # One of "doctrine"; "service"; "composite" 10 | 11 | # Source service identifier 12 | id: null 13 | 14 | # Entity class 15 | entity: Lamoda\MetricResponder\MetricInterface 16 | 17 | # Metric services 18 | metrics: [] 19 | groups: 20 | sources: 21 | 22 | # Prototype 23 | name: 24 | 25 | # Type of the source 26 | type: service # One of "doctrine"; "service"; "merging" 27 | 28 | # Service identifier 29 | id: null 30 | 31 | # Entity class 32 | entity: Lamoda\MetricResponder\MetricGroupInterface 33 | 34 | # Group services 35 | groups: [] 36 | custom: 37 | 38 | # Prototype 39 | name: 40 | 41 | # Group tags 42 | tags: [] 43 | 44 | # Metric source names or service ids 45 | metric_sources: [] 46 | 47 | # Additional metric services for this group (also populated with tag) 48 | metric_services: [] 49 | responders: 50 | telegraf: 51 | enabled: false 52 | 53 | # Responder route path 54 | path: /telegraf 55 | 56 | # ResponseFactory service ID 57 | formatter: lamoda_metrics.formatter.telegraf 58 | sources: [] 59 | groups: [] 60 | -------------------------------------------------------------------------------- /tests/MetricBundle/Fixtures/Entity/Metric.php: -------------------------------------------------------------------------------- 1 | name = $key; 21 | $this->value = $value; 22 | $this->tags = $tags; 23 | ksort($this->tags); 24 | } 25 | 26 | public function getId(): string 27 | { 28 | return $this->id; 29 | } 30 | 31 | /** {@inheritdoc} */ 32 | public function getName(): string 33 | { 34 | return $this->name; 35 | } 36 | 37 | /** {@inheritdoc} */ 38 | public function resolve(): float 39 | { 40 | return $this->value; 41 | } 42 | 43 | /** {@inheritdoc} */ 44 | public function adjust(float $delta): void 45 | { 46 | $this->value += $delta; 47 | } 48 | 49 | /** {@inheritdoc} */ 50 | public function setValue(float $value): void 51 | { 52 | $this->value = $value; 53 | } 54 | 55 | /** {@internal} */ 56 | public function getTags(): array 57 | { 58 | return $this->tags; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /tests/MetricBundle/Fixtures/Resources/config/doctrine/Metric.orm.yml: -------------------------------------------------------------------------------- 1 | Lamoda\Metric\MetricBundle\Tests\Fixtures\Entity\Metric: 2 | type: entity 3 | id: 4 | id: 5 | type: guid 6 | generator: 7 | strategy: UUID 8 | uniqueConstraints: 9 | metric_key_tags_unique: 10 | columns: 11 | - name 12 | - tags 13 | fields: 14 | name: { type: string } 15 | value: { type: float } 16 | tags: { type: array } 17 | -------------------------------------------------------------------------------- /tests/MetricBundle/Fixtures/Storage/MetricStorage.php: -------------------------------------------------------------------------------- 1 | createMetricQueryBuilder('metric') 19 | ->andWhere('metric.name = :name') 20 | ->setParameter('name', $name) 21 | ->andWhere('metric.tags = :tags') 22 | ->setParameter('tags', serialize($tags))->setMaxResults(1)->getQuery()->getOneOrNullResult(); 23 | } 24 | 25 | /** {@inheritdoc} */ 26 | public function getMetrics(): \Traversable 27 | { 28 | foreach ($this->createMetricQueryBuilder('metrics')->getQuery()->iterate() as $row) { 29 | yield $row[0]; 30 | } 31 | } 32 | 33 | protected function doCreateMetric(string $name, float $value, array $tags = []): MutableMetricInterface 34 | { 35 | return new Metric($name, $value, $tags); 36 | } 37 | 38 | private function createMetricQueryBuilder(string $alias): QueryBuilder 39 | { 40 | return $this->entityManager->createQueryBuilder()->select($alias)->from(Metric::class, $alias); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /tests/MetricBundle/Fixtures/TestIntegrationBundle.php: -------------------------------------------------------------------------------- 1 | load(__DIR__ . '/config.yml'); 28 | } 29 | 30 | public function getRootDir(): string 31 | { 32 | return __DIR__; 33 | } 34 | 35 | public function getProjectDir(): string 36 | { 37 | return __DIR__; 38 | } 39 | 40 | public function getCacheDir(): string 41 | { 42 | return __DIR__ . '/../../../build/cache'; 43 | } 44 | 45 | public function getLogDir(): string 46 | { 47 | return __DIR__ . '/../../../build/logs'; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tests/MetricBundle/Fixtures/config.yml: -------------------------------------------------------------------------------- 1 | imports: 2 | - { resource: kernel_setup.yml } 3 | 4 | services: 5 | custom_tagged_metric: 6 | class: Lamoda\Metric\Common\Metric 7 | arguments: 8 | - "tagged_metric" 9 | - 2.0 10 | - [{ name: lamoda_telegraf_metric, group: heartbeat }] 11 | 12 | custom_metric: 13 | class: Lamoda\Metric\Common\Metric 14 | arguments: 15 | - "custom_metric" 16 | - 1.0 17 | 18 | custom_metric_for_composite: 19 | class: Lamoda\Metric\Common\Metric 20 | arguments: 21 | - "custom_metric_for_composite" 22 | - 2.2 23 | 24 | # Make existing service public: 25 | test.Lamoda\Metric\Storage\MetricMutatorInterface: 26 | alias: Lamoda\Metric\Storage\MetricMutatorInterface 27 | public: true 28 | 29 | test.doctrine_metric_storage: 30 | class: Lamoda\Metric\MetricBundle\Tests\Fixtures\Storage\MetricStorage 31 | public: true 32 | arguments: 33 | - "@doctrine.orm.entity_manager" 34 | 35 | lamoda_metrics: 36 | sources: 37 | doctrine_entity_source: 38 | type: storage 39 | storage: doctrine 40 | composite_source: 41 | type: composite 42 | metrics: 43 | - custom_metric 44 | - custom_metric_for_composite 45 | 46 | collectors: 47 | raw_sources: 48 | type: sources 49 | sources: 50 | - composite_source 51 | metric_services: 52 | - custom_tagged_metric 53 | default_tags: {collector: raw} 54 | 55 | doctrine: 56 | type: sources 57 | sources: 58 | - doctrine_entity_source 59 | default_tags: {collector: doctrine} 60 | 61 | all: 62 | type: merge 63 | collectors: 64 | - doctrine 65 | - raw_sources 66 | 67 | storages: 68 | doctrine: 69 | type: service 70 | mutator: true 71 | id: test.doctrine_metric_storage 72 | 73 | responders: 74 | telegraf_json: 75 | enabled: true 76 | collector: raw_sources 77 | format_options: 78 | group_by_tags: 79 | - type 80 | propagate_tags: 81 | - type 82 | 83 | custom_telegraf: 84 | enabled: true 85 | collector: raw_sources 86 | response_factory: telegraf_json 87 | format_options: 88 | group_by_tags: [] 89 | propagate_tags: 90 | - type 91 | path: /custom_telegraf 92 | 93 | prometheus: 94 | enabled: true 95 | collector: raw_sources 96 | format_options: 97 | prefix: metrics_ 98 | path: /prometheus 99 | -------------------------------------------------------------------------------- /tests/MetricBundle/Fixtures/kernel_setup.yml: -------------------------------------------------------------------------------- 1 | parameters: 2 | secret: test 3 | 4 | framework: 5 | secret: '%secret%' 6 | test: true 7 | router: 8 | resource: '%kernel.project_dir%/routing.yml' 9 | 10 | doctrine: 11 | dbal: 12 | driver: pdo_sqlite 13 | # memory: true 14 | path: "%kernel.cache_dir%/test.db" 15 | orm: 16 | auto_mapping: true 17 | resolve_target_entities: 18 | Lamoda\MetricResponder\MetricInterface: Lamoda\MetricBundle\Tests\Fixtures\Entity\Metric 19 | Lamoda\MetricResponder\MetricGroupInterface: Lamoda\MetricBundle\Tests\Fixtures\Entity\MetricGroup 20 | naming_strategy: doctrine.orm.naming_strategy.underscore 21 | -------------------------------------------------------------------------------- /tests/MetricBundle/Fixtures/routing.yml: -------------------------------------------------------------------------------- 1 | _lamoda_metrics: 2 | resource: . 3 | type: lamoda_metrics 4 | prefix: /metrics/ 5 | -------------------------------------------------------------------------------- /tests/MetricBundle/MetricRespondingTest.php: -------------------------------------------------------------------------------- 1 | ['/metrics/telegraf_json'], 27 | 'custom' => ['/metrics/custom_telegraf'], 28 | ]; 29 | } 30 | 31 | /** 32 | * @param string $path 33 | * 34 | * @dataProvider getTelegrafTestRoutes 35 | */ 36 | public function testTelegrafMetricsReturned(string $path): void 37 | { 38 | $container = static::getContainer(); 39 | $this->persistMetrics($container); 40 | static::$client->request('GET', $path); 41 | $response = static::$client->getResponse(); 42 | self::assertNotNull($response); 43 | self::assertTrue($response->isSuccessful()); 44 | self::assertFalse($response->isCacheable()); 45 | 46 | self::assertJsonStringEqualsJsonString( 47 | <<<'JSON' 48 | { 49 | "custom_metric": 1, 50 | "custom_metric_for_composite": 2.2 51 | } 52 | JSON 53 | , 54 | $response->getContent() 55 | ); 56 | } 57 | 58 | public function testPrometheusMetricsReturned(): void 59 | { 60 | $container = static::getContainer(); 61 | $this->persistMetrics($container); 62 | static::$client->request('GET', '/metrics/prometheus'); 63 | $response = static::$client->getResponse(); 64 | self::assertNotNull($response); 65 | self::assertTrue($response->isSuccessful()); 66 | self::assertFalse($response->isCacheable()); 67 | 68 | self::assertSame( 69 | <<<'PROMETHEUS' 70 | metrics_custom_metric{collector="raw"} 1 71 | metrics_custom_metric_for_composite{collector="raw"} 2.2 72 | 73 | PROMETHEUS 74 | , 75 | $response->getContent() 76 | ); 77 | } 78 | 79 | /** 80 | * @param $container 81 | * 82 | * @return EntityManagerInterface 83 | */ 84 | private function persistMetrics($container): EntityManagerInterface 85 | { 86 | /** @var EntityManagerInterface $doctrine */ 87 | $doctrine = $container->get('doctrine.orm.entity_manager'); 88 | 89 | $m1 = new Metric('test_1', 241.0, ['own_tag' => 'm1']); 90 | $m2 = new Metric('test_2', 12.3, ['own_tag' => 'm2']); 91 | $m3 = new Metric('test_3', 17.0, ['own_tag' => 'm3']); 92 | $m4 = new Metric('test_4', 5.5, ['own_tag' => 'm4']); 93 | $doctrine->persist($m1); 94 | $doctrine->persist($m3); 95 | $doctrine->persist($m4); 96 | $doctrine->persist($m2); 97 | 98 | $doctrine->flush(); 99 | $doctrine->clear(); 100 | 101 | return $doctrine; 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /tests/MetricBundle/StoredMetricMutatorTest.php: -------------------------------------------------------------------------------- 1 | get('test.' . MetricMutatorInterface::class); 27 | /** @var MetricStorageInterface $storage */ 28 | $storage = self::getContainer()->get('test.doctrine_metric_storage'); 29 | 30 | $adjuster->adjustMetricValue(10, 'test_1', ['tag' => 'value1']); 31 | $adjuster->adjustMetricValue(20, 'test_1', ['tag' => 'value2']); 32 | 33 | $metric1 = $storage->findMetric('test_1', ['tag' => 'value1']); 34 | $metric2 = $storage->findMetric('test_1', ['tag' => 'value2']); 35 | self::assertNotNull($metric1); 36 | self::assertNotNull($metric2); 37 | 38 | self::assertNotSame($metric1, $metric2); 39 | self::assertEquals(10, $metric1->resolve()); 40 | self::assertEquals(20, $metric2->resolve()); 41 | 42 | $adjuster->adjustMetricValue(30, 'test_1', ['tag' => 'value1']); 43 | $metric3 = $storage->findMetric('test_1', ['tag' => 'value1']); 44 | self::assertNotNull($metric3); 45 | self::assertEquals($metric1, $metric3); 46 | self::assertEquals(40, $metric3->resolve()); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /tests/Responder/PsrResponderTest.php: -------------------------------------------------------------------------------- 1 | 'metric_']; 20 | 21 | $source = $this->createMock(MetricSourceInterface::class); 22 | $collector = $this->createMock(MetricCollectorInterface::class); 23 | $collector->expects($this->once())->method('collect')->willReturn($source); 24 | 25 | $factory = $this->createMock(ResponseFactoryInterface::class); 26 | $response = $this->createMock(ResponseInterface::class); 27 | $factory->expects($this->once())->method('create') 28 | ->with($source, $options) 29 | ->willReturn($response); 30 | 31 | $responder = new PsrResponder($collector, $factory, $options); 32 | self::assertSame($response, $responder->createResponse()); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /tests/Responder/ResponseFactory/PrometheusResponseFactoryTest.php: -------------------------------------------------------------------------------- 1 | 'ru']), 20 | new Metric('metrics_errors', 0.0, ['country' => 'ru']), 21 | new Metric('untagged_metric', 5.0), 22 | ] 23 | ); 24 | 25 | $response = (new PrometheusResponseFactory())->create($source); 26 | $data = (string) $response->getBody(); 27 | $this->assertStringContainsString('text/plain', $response->getHeaderLine('Content-Type')); 28 | $this->assertSame( 29 | << 'ru']), 20 | new Metric('metrics_errors', 0.0, ['country' => 'ru']), 21 | new Metric('untagged_metric', 5.0), 22 | ] 23 | ); 24 | 25 | $factory = new TelegrafJsonResponseFactory(); 26 | $response = $factory->create( 27 | $source, 28 | [ 29 | 'propagate_tags' => ['country'], 30 | 'group_by_tags' => ['country'], 31 | ] 32 | ); 33 | self::assertStringContainsString('application/json', $response->getHeaderLine('Content-Type')); 34 | 35 | $data = (string) $response->getBody(); 36 | $this->assertJsonStringEqualsJsonString( 37 | json_encode( 38 | [ 39 | [ 40 | 'metrics_orders' => 200.0, 41 | 'metrics_errors' => 0.0, 42 | 'country' => 'ru', 43 | ], 44 | [ 45 | 'untagged_metric' => 5.0, 46 | ], 47 | ] 48 | ), 49 | $data 50 | ); 51 | } 52 | 53 | public function testSingleGroupIsNotFormattedAsSingleElementArray(): void 54 | { 55 | $source = new IterableMetricSource( 56 | [ 57 | new Metric('untagged_metric', 5.0), 58 | ] 59 | ); 60 | 61 | $factory = new TelegrafJsonResponseFactory(); 62 | $response = $factory->create($source); 63 | 64 | $this->assertJsonStringEqualsJsonString( 65 | json_encode(['untagged_metric' => 5.0]), 66 | (string) $response->getBody() 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /tests/Storage/MaterializeHelperTest.php: -------------------------------------------------------------------------------- 1 | createMock(MetricCollectorInterface::class); 27 | $collectorRegistry->register('collector', $collector); 28 | 29 | $storage = $this->createMock(MetricStorageInterface::class); 30 | $storageRegistry->register('storage', $storage); 31 | 32 | $m1 = $this->createMock(MetricInterface::class); 33 | $m2 = $this->createMock(MetricInterface::class); 34 | $source = $this->createMock(MetricSourceInterface::class); 35 | $source->expects($this->once())->method('getMetrics')->willReturn(new \ArrayIterator([$m1, $m2])); 36 | 37 | $collector->expects($this->once())->method('collect')->willReturn($source); 38 | $storage->expects($this->once())->method('receive')->with( 39 | $this->callback( 40 | function (MetricSourceInterface $metricSource) use ($m1, $m2) { 41 | $actual = []; 42 | foreach ($metricSource->getMetrics() as $metric) { 43 | $actual[$metric->getName()] = ['value' => $metric->resolve(), 'tags' => $metric->getTags()]; 44 | } 45 | $expected = [ 46 | $m1->getName() => ['value' => $m1->resolve(), 'tags' => $m1->getTags()], 47 | $m2->getName() => ['value' => $m2->resolve(), 'tags' => $m2->getTags()], 48 | ]; 49 | $this->assertEquals($expected, $actual); 50 | 51 | return true; 52 | } 53 | ) 54 | ); 55 | 56 | $helper->materialize('collector', 'storage'); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tests/Storage/StorageRegistryTest.php: -------------------------------------------------------------------------------- 1 | createMock(MetricStorageInterface::class); 17 | 18 | $registry = new StorageRegistry(); 19 | $registry->register('test', $mock); 20 | 21 | self::assertSame($mock, $registry->getStorage('test')); 22 | } 23 | 24 | public function testRegistryThrowsExceptionsForUnknownCollector(): void 25 | { 26 | $registry = new StorageRegistry(); 27 | 28 | $this->expectException(\OutOfBoundsException::class); 29 | $registry->getStorage('test'); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tests/Storage/StoredMetricMutatorTest.php: -------------------------------------------------------------------------------- 1 | 'value']; 16 | private const NAME = 'test'; 17 | 18 | public function testMutatorMutatorAdjustsFoundMetric(): void 19 | { 20 | $metric = $this->createMock(MutableMetricInterface::class); 21 | 22 | $storage = $this->createMock(MetricStorageInterface::class); 23 | $storage->expects($this->once())->method('findMetric') 24 | ->with(self::NAME, self::TAG) 25 | ->willReturn($metric); 26 | 27 | $metric->expects($this->once())->method('adjust')->with(5.0); 28 | 29 | $mutator = new StoredMetricMutator($storage); 30 | $mutator->adjustMetricValue(5.0, self::NAME, self::TAG); 31 | } 32 | 33 | public function testMutatorMutatorAdjustsUnknownCreatedMetric(): void 34 | { 35 | $metric = $this->createMock(MutableMetricInterface::class); 36 | 37 | $storage = $this->createMock(MetricStorageInterface::class); 38 | $storage->expects($this->once())->method('findMetric') 39 | ->with(self::NAME, self::TAG) 40 | ->willReturn(null); 41 | $storage->expects($this->once())->method('createMetric') 42 | ->with(self::NAME, 0, self::TAG) 43 | ->willReturn($metric); 44 | 45 | $metric->expects($this->once())->method('adjust')->with(5.0); 46 | 47 | $mutator = new StoredMetricMutator($storage); 48 | $mutator->adjustMetricValue(5.0, self::NAME, self::TAG); 49 | } 50 | 51 | public function testMutatorMutatorUpdatesFoundMetric(): void 52 | { 53 | $metric = $this->createMock(MutableMetricInterface::class); 54 | 55 | $storage = $this->createMock(MetricStorageInterface::class); 56 | $storage->expects($this->once())->method('findMetric') 57 | ->with(self::NAME, self::TAG) 58 | ->willReturn($metric); 59 | 60 | $metric->expects($this->once())->method('setValue')->with(5.0); 61 | 62 | $mutator = new StoredMetricMutator($storage); 63 | $mutator->setMetricValue(5.0, self::NAME, self::TAG); 64 | } 65 | 66 | public function testMutatorMutatorUpdatesUnknownCreatedMetric(): void 67 | { 68 | $metric = $this->createMock(MutableMetricInterface::class); 69 | 70 | $storage = $this->createMock(MetricStorageInterface::class); 71 | $storage->expects($this->once())->method('findMetric') 72 | ->with(self::NAME, self::TAG) 73 | ->willReturn(null); 74 | $storage->expects($this->once())->method('createMetric') 75 | ->with(self::NAME, 0, self::TAG) 76 | ->willReturn($metric); 77 | 78 | $metric->expects($this->once())->method('setValue')->with(5.0); 79 | 80 | $mutator = new StoredMetricMutator($storage); 81 | $mutator->setMetricValue(5.0, self::NAME, self::TAG); 82 | } 83 | } 84 | --------------------------------------------------------------------------------