├── .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 | [](https://github.com/lamoda/php-metrics/workflows/Tests/badge.svg?branch=master)
4 | [](https://scrutinizer-ci.com/g/lamoda/php-metrics/?branch=master)
5 | [](https://scrutinizer-ci.com/g/lamoda/php-metrics/?branch=master)
6 | [](https://scrutinizer-ci.com/g/lamoda/php-metrics/build-status/master)
7 | [](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 |
--------------------------------------------------------------------------------