diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..32edd2f --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +.git +docker \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..eaf6bc1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +.idea/ +vendor/ +composer.lock +__pycache__ +.DS_Store diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..550782c --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Comsave + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md index b16da07..2336eaf 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,81 @@ -# morty-counts +# PrometheusPushGatewayBundle -![](https://media.giphy.com/media/e6tJpLvjY8jXa/giphy.gif) +## Symfony Prometheus + PushGateway integration + +Send metrics to Prometheus. +High Availability Setup + +## Requirements + +1. `Redis` (the service & the PHP extension): to act as a buffer before push and avoid latency in the code, and as a store for the current counter values. + +2. `bin/console comsave:prometheus:push` cronjob: to push data periodically to Prometheus Pushgateway + +## Configuration + +In your `services.yml` add: + +```yaml +comsave_prometheus_pushgateway: + prometheus: + host: 'prometheus:9090' + username: 'admin' # optional + password: 'duuude' # optional + instance: 'moms_basement:6666' # your server host/name/etc + pushgateway: + host: 'pushgateway:9191' + username: 'admin2' # optional + password: 'duuude2' # optional + redis: 'redis:6379' + metrics: + api: # metric namespace + - name: 'orders' + type: 'counter' + help: 'counts the number of orders' + prefetch_group_label: 'user_id' # optional & only available for the counter; prefetch current value from prometheus by grouping sum() query by this label and populate the counter if it's missing in redis cache + labels: + - 'order_id' + - 'user_id' +``` + +Add the bundle to your Symfony kernel. +```php +new Comsave\PrometheusPushGatewayBundle\ComsavePrometheusPushGatewayBundle(), +``` + +## How does it work? + +### General Prometheus architecture overview + +![](https://camo.githubusercontent.com/78b3b29d22cea8eee673e34fd204818ea532c171/68747470733a2f2f63646e2e6a7364656c6976722e6e65742f67682f70726f6d6574686575732f70726f6d65746865757340633334323537643036396336333036383564613335626365663038343633326666643564363230392f646f63756d656e746174696f6e2f696d616765732f6172636869746563747572652e737667) + +### Single Node Prometheus + Pushgateway + +Single node is pretty straightforward. + +1. Use `PushGatewayClient` to create a metric. Metric is stored in `Redis`. +2. Use `PushGatewayClient` can be pushed manually or with a command. After push metrics stored in Redis are transported to the actual `PushGateway` service. +3. `Prometheus` periodically pulls in new metrics from `PushGateway`. + +![](./images/basic_prometheus_cluster_setup.png) + +### Multi-Node Prometheus + Pushgateway Cluster + +Multi-node set up works with the basics described above, with a couple exceptions: + +1. There's an `Haproxy` (or other load balancer) that decides which `PushGateway` will receive the `push`. +2. Each `Prometheus` pulls from every `PushGateway` in every node. That way each `Prometheus` has the latest metrics. +3. Each `Prometheus` pulls (federates) from other `Prometheus` nodes (all but itself) though less often. This ensures data integrity (sort of replication). + +![](./images/advanced_prometheus_cluster_setup.png) + +## Development + +Start single node `docker-compose up -d` + +Or multi node `docker-compose up -f docker-compose.multi-node.yml -d` + +Tests `docker exec $(docker ps | grep _php | awk '{print $1}') vendor/bin/phpunit tests` + +## License + +MIT diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..f80562b --- /dev/null +++ b/composer.json @@ -0,0 +1,37 @@ +{ + "name": "comsave/prometheus-pushgateway-bundle", + "description": "Symfony integration to push metrics to Prometheus + PushGateway", + "license": "MIT", + "require": { + "php": ">=7.3", + "comsave/dependency-injection-config-to-parameters": "^0.1.0", + "comsave/prometheus_client_php": "^1.0.5", + "jms/serializer": "^3.0|^2.0", + "symfony/config": "^3.4|^4.4", + "symfony/console": "^3.4|^4.4", + "symfony/http-kernel": "^3.4|^4.4" + }, + "require-dev": { + "phpunit/phpunit": "^8.0", + "spatie/phpunit-snapshot-assertions": "^3.0" + }, + "autoload": { + "psr-4": { + "Comsave\\PrometheusPushGatewayBundle\\": "src/PrometheusPushGatewayBundle" + } + }, + "autoload-dev": { + "psr-4": { + "Comsave\\PrometheusPushGatewayBundle\\Tests\\": "tests" + } + }, + "authors": [ + { + "name": "Vaidas Bagdonas", + "email": "vaidas.bagdonas@comsave.com" + } + ], + "config": { + "sort-packages": true + } +} diff --git a/docker-compose.multi-node.yml b/docker-compose.multi-node.yml new file mode 100644 index 0000000..630a33f --- /dev/null +++ b/docker-compose.multi-node.yml @@ -0,0 +1,101 @@ +version: "3" + +services: + php: + build: + dockerfile: ./docker/php-fpm/Dockerfile + context: . + depends_on: + - redis + volumes: + - ./:/app + + redis: + image: redis:alpine + + haproxy: + image: haproxy:alpine + depends_on: + - prometheus + - prometheus2 + - prometheus3 + - pushgateway + - pushgateway2 + - pushgateway3 + volumes: + - ./docker/haproxy:/usr/local/etc/haproxy:ro + + prometheus: + image: prom/prometheus + command: + - '--web.listen-address=:9091' + - '--config.file=/etc/prometheus/prometheus.yml' + - '--web.console.templates=/etc/prometheus/consoles' + - '--web.console.libraries=/etc/prometheus/console_libraries' + depends_on: + - pushgateway + ports: + - 9091:9091 + volumes: + - ./docker/prometheus/prometheus1.yml/:/etc/prometheus/prometheus.yml + + pushgateway: + image: prom/pushgateway + command: + - '--web.listen-address=:9191' + ports: + - 9191:9191 + + prometheus2: + image: prom/prometheus + command: + - '--web.listen-address=:9092' + - '--config.file=/etc/prometheus/prometheus.yml' + - '--web.console.templates=/etc/prometheus/consoles' + - '--web.console.libraries=/etc/prometheus/console_libraries' + depends_on: + - pushgateway2 + ports: + - 9092:9092 + volumes: + - ./docker/prometheus/prometheus2.yml/:/etc/prometheus/prometheus.yml + + pushgateway2: + image: prom/pushgateway + command: + - '--web.listen-address=:9192' + ports: + - 9192:9192 + + prometheus3: + image: prom/prometheus + command: + - '--web.listen-address=:9093' + - '--config.file=/etc/prometheus/prometheus.yml' + - '--web.console.templates=/etc/prometheus/consoles' + - '--web.console.libraries=/etc/prometheus/console_libraries' + depends_on: + - pushgateway3 + ports: + - 9093:9093 + volumes: + - ./docker/prometheus/prometheus3.yml/:/etc/prometheus/prometheus.yml + + pushgateway3: + image: prom/pushgateway + command: + - '--web.listen-address=:9193' + ports: + - 9193:9193 + + grafana: + image: grafana/grafana + depends_on: + - haproxy + ports: + - 3000:3000 + volumes: + - ./docker/grafana/datasources1.yml:/etc/grafana/provisioning/datasources/datasources.yml + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..798eeff --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,49 @@ +version: "3" + +services: + php: + build: + dockerfile: ./docker/php-fpm/Dockerfile + context: . + depends_on: + - redis + volumes: + - ./:/app + + redis: + image: redis:alpine + + prometheus: + image: prom/prometheus + command: + - '--web.listen-address=:9090' + - '--config.file=/etc/prometheus/prometheus.yml' + - '--web.console.templates=/etc/prometheus/consoles' + - '--web.console.libraries=/etc/prometheus/console_libraries' + depends_on: + - pushgateway + ports: + - 9090:9090 + volumes: + - ./docker/prometheus/prometheus.yml/:/etc/prometheus/prometheus.yml + + pushgateway: + image: prom/pushgateway + command: + - '--web.listen-address=:9191' + - '--push.disable-consistency-check' + - '--persistence.interval=5m' + ports: + - 9191:9191 + + grafana: + image: grafana/grafana + depends_on: + - prometheus + ports: + - 3000:3000 + volumes: + - ./docker/grafana/datasources.yml:/etc/grafana/provisioning/datasources/datasources.yml + environment: + - GF_SECURITY_ADMIN_PASSWORD=admin + - GF_USERS_ALLOW_SIGN_UP=false \ No newline at end of file diff --git a/docker/grafana/datasources.yml b/docker/grafana/datasources.yml new file mode 100644 index 0000000..86fd346 --- /dev/null +++ b/docker/grafana/datasources.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://prometheus:9090 + isDefault: true diff --git a/docker/grafana/datasources1.yml b/docker/grafana/datasources1.yml new file mode 100644 index 0000000..04cb172 --- /dev/null +++ b/docker/grafana/datasources1.yml @@ -0,0 +1,8 @@ +apiVersion: 1 + +datasources: + - name: Prometheus + type: prometheus + access: proxy + url: http://haproxy:9090 + isDefault: true diff --git a/docker/haproxy/haproxy.cfg b/docker/haproxy/haproxy.cfg new file mode 100644 index 0000000..89d793f --- /dev/null +++ b/docker/haproxy/haproxy.cfg @@ -0,0 +1,31 @@ +global +# global settings here + +defaults +# defaults here + +frontend pushgateway_fe + bind :9191 + mode tcp + default_backend pushgateway_be + +backend pushgateway_be + mode tcp + balance roundrobin + default-server inter 1s + server pushgateway pushgateway:9191 check id 1 + server pushgateway2 pushgateway2:9192 check id 2 + server pushgateway3 pushgateway3:9193 check id 3 + +frontend prometheus_fe + bind :9090 + mode tcp + default_backend prometheus_be + +backend prometheus_be + mode tcp + balance roundrobin + default-server inter 1s + server prometheus prometheus:9091 check id 1 + server prometheus2 prometheus2:9092 check id 2 + server prometheus3 prometheus3:9093 check id 3 \ No newline at end of file diff --git a/docker/php-fpm/Dockerfile b/docker/php-fpm/Dockerfile new file mode 100644 index 0000000..2926578 --- /dev/null +++ b/docker/php-fpm/Dockerfile @@ -0,0 +1,18 @@ +FROM php:7.3-fpm + +RUN apt-get update && apt-get install git -y + +RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" && \ + php -r "if (hash_file('sha384', 'composer-setup.php') === 'e0012edf3e80b6978849f5eff0d4b4e4c79ff1609dd1e613307e16318854d24ae64f26d17af3ef0bf7cfb710ca74755a') { echo 'Installer verified'; } else { echo 'Installer corrupt'; unlink('composer-setup.php'); } echo PHP_EOL;" && \ + php composer-setup.php && \ + php -r "unlink('composer-setup.php');" && \ + mv composer.phar /usr/local/bin/composer && \ + chmod +x /usr/local/bin/composer + +RUN pecl install redis && docker-php-ext-enable redis +RUN apt-get install zip unzip -y + +WORKDIR /app +COPY composer.* ./ +RUN composer install +COPY ./* ./ \ No newline at end of file diff --git a/docker/prometheus/prometheus.yml b/docker/prometheus/prometheus.yml new file mode 100644 index 0000000..f1aec0c --- /dev/null +++ b/docker/prometheus/prometheus.yml @@ -0,0 +1,9 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'pushgateway' + honor_labels: true + scrape_interval: 500ms + static_configs: + - targets: ['pushgateway:9191'] \ No newline at end of file diff --git a/docker/prometheus/prometheus1.yml b/docker/prometheus/prometheus1.yml new file mode 100644 index 0000000..e3fc92d --- /dev/null +++ b/docker/prometheus/prometheus1.yml @@ -0,0 +1,24 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'federate' + scrape_interval: 5s + honor_labels: true + metrics_path: '/federate' + params: + 'match[]': + - '{job=~"morty_.*"}' + static_configs: + - targets: + - 'prometheus2:9092' + - 'prometheus3:9093' + + - job_name: 'pushgateway' + scrape_interval: 500ms + honor_labels: true + static_configs: + - targets: + - 'pushgateway:9191' + - 'pushgateway2:9192' + - 'pushgateway3:9193' diff --git a/docker/prometheus/prometheus2.yml b/docker/prometheus/prometheus2.yml new file mode 100644 index 0000000..9fccc51 --- /dev/null +++ b/docker/prometheus/prometheus2.yml @@ -0,0 +1,24 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'federate' + scrape_interval: 5s + honor_labels: true + metrics_path: '/federate' + params: + 'match[]': + - '{job=~"morty_.*"}' + static_configs: + - targets: + - 'prometheus:9091' + - 'prometheus3:9093' + + - job_name: 'pushgateway' + scrape_interval: 500ms + honor_labels: true + static_configs: + - targets: + - 'pushgateway:9191' + - 'pushgateway2:9192' + - 'pushgateway3:9193' \ No newline at end of file diff --git a/docker/prometheus/prometheus3.yml b/docker/prometheus/prometheus3.yml new file mode 100644 index 0000000..cb868e1 --- /dev/null +++ b/docker/prometheus/prometheus3.yml @@ -0,0 +1,24 @@ +global: + scrape_interval: 15s + +scrape_configs: + - job_name: 'federate' + scrape_interval: 5s + honor_labels: true + metrics_path: '/federate' + params: + 'match[]': + - '{job=~"morty_.*"}' + static_configs: + - targets: + - 'prometheus:9091' + - 'prometheus2:9092' + + - job_name: 'pushgateway' + scrape_interval: 500ms + honor_labels: true + static_configs: + - targets: + - 'pushgateway:9191' + - 'pushgateway2:9192' + - 'pushgateway3:9193' \ No newline at end of file diff --git a/images/advanced_prometheus_cluster_setup.png b/images/advanced_prometheus_cluster_setup.png new file mode 100644 index 0000000..f76f1e2 Binary files /dev/null and b/images/advanced_prometheus_cluster_setup.png differ diff --git a/images/basic_prometheus_cluster_setup.png b/images/basic_prometheus_cluster_setup.png new file mode 100644 index 0000000..de30edf Binary files /dev/null and b/images/basic_prometheus_cluster_setup.png differ diff --git a/images/diagram.py b/images/diagram.py new file mode 100644 index 0000000..355e17b --- /dev/null +++ b/images/diagram.py @@ -0,0 +1,78 @@ +from diagrams import Cluster, Diagram, Edge +from diagrams.onprem.compute import Server +from diagrams.onprem.inmemory import Redis +from diagrams.onprem.monitoring import Prometheus +from diagrams.onprem.network import Haproxy +from diagrams.aws.compute import ECS + +with Diagram(name="Advanced Prometheus Cluster Setup", show=False): + haproxy = Haproxy("haproxy") + + with Cluster("App Cluster"): + app = Server("app") + app_redis = Redis("pushgateway_redis_buffer") + app - Edge(color="brown", style="dashed") - app_redis + + app_cluster = [ + app, + app_redis + ] + + with Cluster("Prometheus Cluster"): + with Cluster("Prom1"): + push1 = ECS('pushgateway') + prom1 = Prometheus('prometheus') + + with Cluster("Prom2"): + push2 = ECS('pushgateway') + prom2 = Prometheus('prometheus') + + with Cluster("Prom3"): + push3 = ECS('pushgateway') + prom3 = Prometheus('prometheus') + + push1 << Edge(label="pull", color="brown") << prom1 + push1 << Edge(color="brown") << prom2 + push1 << Edge(color="brown") << prom3 + + push2 << Edge(color="brown") << prom1 + push2 << Edge(label="pull", color="brown") << prom2 + push2 << Edge(color="brown") << prom3 + + push3 << Edge(color="brown") << prom1 + push3 << Edge(color="brown") << prom2 + push3 << Edge(label="pull", color="brown") << prom3 + + prom1 << Edge(label="pull") << prom2 + prom1 << Edge(label="pull") << prom3 + + prom2 << Edge(label="pull") << prom1 + prom2 << Edge(label="pull") << prom3 + + prom3 << Edge(label="pull") << prom1 + prom3 << Edge(label="pull") << prom2 + + app >> Edge(label="push_metrics") >> haproxy >> [ + push1, + push2, + push3 + ] + +with Diagram(name="Basic Prometheus Cluster Setup", show=False): + with Cluster("App Cluster"): + app = Server("app") + app_redis = Redis("pushgateway_redis_buffer") + app - Edge(color="brown", style="dashed") - app_redis + + app_cluster = [ + app, + app_redis + ] + + with Cluster("Prometheus Cluster"): + push1 = ECS('pushgateway') + prom1 = Prometheus('prometheus') + + push1 << Edge(label="pull") << prom1 + + app >> Edge(label="push_metrics") >> push1 \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Command/PrometheusPushCommand.php b/src/PrometheusPushGatewayBundle/Command/PrometheusPushCommand.php new file mode 100644 index 0000000..d0d383e --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Command/PrometheusPushCommand.php @@ -0,0 +1,59 @@ +pushGatewayClient = $pushGatewayClient; + $this->prometheusJobNames = $prometheusJobNames; + + parent::__construct(); + } + + public function configure(): void + { + $this + ->setName('comsave:prometheus:push') + ->setDescription('Pushes scheduled metris from PushGateway to Prometheus.'); + } + + /** + * @throws GuzzleException + * @throws StorageException + */ + protected function execute(InputInterface $input, OutputInterface $output): int + { + $output->writeln('Pushing metrics...'); + + $this->pushGatewayClient->pushAll($this->prometheusJobNames); + $this->pushGatewayClient->flush(); // todo: check if no new values came in before flushing + + // todo: add never ending process option for supervisor, push every n seconds; + // todo: keep in mind the smaller the interval the more latency prometheus + // todo: will introduce on getting counters initial value + + $output->writeln('Done.'); + + return 0; + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/ComsavePrometheusPushGatewayBundle.php b/src/PrometheusPushGatewayBundle/ComsavePrometheusPushGatewayBundle.php new file mode 100644 index 0000000..069fc85 --- /dev/null +++ b/src/PrometheusPushGatewayBundle/ComsavePrometheusPushGatewayBundle.php @@ -0,0 +1,9 @@ +processConfiguration(new Configuration(), $configs); + + DependencyInjectionConfigsToParams::setupConfigurationParameters( + $containerBuilder, + $processedConfigs, + Configuration::$configurationTreeRoot + ); + + $loader = new Loader\YamlFileLoader($containerBuilder, new FileLocator(__DIR__.'/../Resources/config')); + $loader->load('services.yml'); + + $this->registerMetrics($processedConfigs['metrics'], $containerBuilder); + } + + // todo: test this + public function registerMetrics(array $metricsConfig, ContainerBuilder $containerBuilder): void + { + /** @var CollectorRegistry $collectorRegistry */ + $collectorRegistry = $containerBuilder->get(CollectorRegistry::class); + + foreach ($metricsConfig as $namespace => $metricConfig) { + switch ($metricConfig['type']) { + case 'counter': + if($metricConfig['prefetch_query']) { + $collectorRegistry->addCounterPrefetchGroupLabel( + new CounterPrefetchQuery( + $namespace, + $metricConfig['name'], + $metricConfig['prefetch_query'] + ) + ); + } + + $collectorRegistry->registerCounter( + $namespace, + $metricConfig['name'], + $metricConfig['help'], + $metricConfig['labels'] + ); + break; + case 'gauge': + $collectorRegistry->registerGauge( + $namespace, + $metricConfig['name'], + $metricConfig['help'], + $metricConfig['labels'] + ); + break; + case 'histogram': // todo: add histogram + default: + break; + } + } + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/DependencyInjection/Configuration.php b/src/PrometheusPushGatewayBundle/DependencyInjection/Configuration.php new file mode 100644 index 0000000..1cc8444 --- /dev/null +++ b/src/PrometheusPushGatewayBundle/DependencyInjection/Configuration.php @@ -0,0 +1,85 @@ +root(static::$configurationTreeRoot) + ->children() + ->arrayNode('prometheus') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('host') + ->defaultValue('localhost:9090') + ->isRequired() + ->end() + ->scalarNode('username') + ->defaultNull() + ->end() + ->scalarNode('password') + ->defaultNull() + ->end() + ->scalarNode('instance') + ->defaultValue('localhost') + ->isRequired() + ->end() + ->end() + ->end() + ->arrayNode('pushgateway') + ->addDefaultsIfNotSet() + ->children() + ->scalarNode('host') + ->defaultValue('localhost:9191') + ->isRequired() + ->end() + ->scalarNode('redis') + ->defaultValue('localhost:6379') + ->isRequired() + ->end() + ->scalarNode('username') + ->defaultNull() + ->end() + ->scalarNode('password') + ->defaultNull() + ->end() + ->end() + ->end() + ->arrayNode('metrics') + ->prototype('array') + ->children() + ->scalarNode('name') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('type') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('help') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->scalarNode('prefetch_group_label') + ->defaultNull() + ->end() + ->arrayNode('labels') + ->prototype('scalar')->end() + ->end() + ->end() + ->end() + ->end() + ->end(); + + return $treeBuilder; + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Event/CounterMetricEvent.php b/src/PrometheusPushGatewayBundle/Event/CounterMetricEvent.php new file mode 100644 index 0000000..b7f74fd --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Event/CounterMetricEvent.php @@ -0,0 +1,28 @@ +counter = $counter; + } + + public function getCounter(): Counter + { + return $this->counter; + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/EventSubscriber/MetricEventSubscriber.php b/src/PrometheusPushGatewayBundle/EventSubscriber/MetricEventSubscriber.php new file mode 100644 index 0000000..77c00cd --- /dev/null +++ b/src/PrometheusPushGatewayBundle/EventSubscriber/MetricEventSubscriber.php @@ -0,0 +1,46 @@ +prometheusClient = $prometheusClient; + } + + public static function getSubscribedEvents(): array + { + return [ + CounterMetricEvent::BEFORE_REGISTER => 'onCounterBeforeRegister', + ]; + } + + public function onCounterBeforeRegister(CounterMetricEvent $event): void + { + $counter = $event->getCounter(); + + if($counter->getPrefetchGroupLabel()) { + $results = $this->prometheusClient->query(vsprintf('sum(%s{%s}) by (%s)', [ + sprintf('%s_%s', $counter->getLabelNames()['namespace'], $counter->getName()), + $this->prometheusClient->requireLabels($counter->getLabelNames()), + $counter->getPrefetchGroupLabel() + ])); + + if(count($results) > 0) { + $counter->setCurrentCount($results[0]->getValue()); + } + } + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Factory/HttpClientFactory.php b/src/PrometheusPushGatewayBundle/Factory/HttpClientFactory.php new file mode 100644 index 0000000..3a9c284 --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Factory/HttpClientFactory.php @@ -0,0 +1,14 @@ +build(); + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Factory/RedisStorageAdapterFactory.php b/src/PrometheusPushGatewayBundle/Factory/RedisStorageAdapterFactory.php new file mode 100644 index 0000000..c925403 --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Factory/RedisStorageAdapterFactory.php @@ -0,0 +1,20 @@ + $redisHost, + 'port' => (int)$redisPort, + ] + ); + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Model/PrometheusResponse.php b/src/PrometheusPushGatewayBundle/Model/PrometheusResponse.php new file mode 100644 index 0000000..cd4fb83 --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Model/PrometheusResponse.php @@ -0,0 +1,40 @@ +status; + } + + public function setStatus(string $status): void + { + $this->status = $status; + } + + public function getData(): PrometheusResponseData + { + return $this->data; + } + + public function setData(PrometheusResponseData $data): void + { + $this->data = $data; + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Model/PrometheusResponseData.php b/src/PrometheusPushGatewayBundle/Model/PrometheusResponseData.php new file mode 100644 index 0000000..d2751a1 --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Model/PrometheusResponseData.php @@ -0,0 +1,47 @@ +") + */ + private $results; + + public function getResultType(): string + { + return $this->resultType; + } + + public function setResultType(string $resultType): void + { + $this->resultType = $resultType; + } + + /** + * @return array|PrometheusResponseDataResult[] + */ + public function getResults(): array + { + return $this->results; + } + + /** + * @param array|PrometheusResponseDataResult[] $results + */ + public function setResults(array $results): void + { + $this->results = $results; + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Model/PrometheusResponseDataResult.php b/src/PrometheusPushGatewayBundle/Model/PrometheusResponseDataResult.php new file mode 100644 index 0000000..e84e6f6 --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Model/PrometheusResponseDataResult.php @@ -0,0 +1,46 @@ +metric; + } + + public function setMetric(array $metric): void + { + $this->metric = $metric; + } + + public function getValues(): array + { + return $this->values; + } + + public function setValues(array $values): void + { + $this->values = $values; + } + + public function getValue(): string + { + return $this->getValues()[1]; + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Prometheus/CollectorRegistry.php b/src/PrometheusPushGatewayBundle/Prometheus/CollectorRegistry.php new file mode 100644 index 0000000..d0e5ef9 --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Prometheus/CollectorRegistry.php @@ -0,0 +1,61 @@ +eventDispatcher = $eventDispatcher; + } + + public function registerCounter($namespace, $name, $help, $labels = []): CounterInterface + { + /** @var Counter $counter */ + $counter = parent::registerCounter($namespace, $name, $help, $labels); + + $counter->setPrefetchGroupLabel($this->getCounterPrefetchGroupLabel($namespace, $name))); + + $this->eventDispatcher->dispatch(new CounterMetricEvent($counter), CounterMetricEvent::BEFORE_REGISTER); + + return $counter; + } + + public function getCounterPrefetchGroupLabel(string $namespace, string $name): ?string + { + if(isset($this->counterPrefetchGroupLabels[$namespace]) && isset($this->counterPrefetchGroupLabels[$namespace][$name])) { + return $this->counterPrefetchGroupLabels[$namespace][$name]; + } + + return null; + } + + public function addCounterPrefetchGroupLabel(string $namespace, string $name, string $prefetchGroupLabel): void + { + if(!isset($this->counterPrefetchGroupLabels[$namespace])) { + $this->counterPrefetchGroupLabels[$namespace] = []; + } + + $this->counterPrefetchGroupLabels[$namespace][$name] = $prefetchGroupLabel; + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Prometheus/Counter.php b/src/PrometheusPushGatewayBundle/Prometheus/Counter.php new file mode 100644 index 0000000..99bd5a3 --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Prometheus/Counter.php @@ -0,0 +1,34 @@ +prefetchGroupLabel = $prefetchGroupLabel; + } + + public function getPrefetchGroupLabel(): ?string + { + return $this->prefetchGroupLabel; + } + + public function incBy($count, array $labels = []): void + { + $this->currentCount += $count; + + parent::incBy($this->currentCount, $labels); + } + + public function setCurrentCount(int $currentCount): void + { + $this->currentCount = $currentCount; + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Resources/config/services.yml b/src/PrometheusPushGatewayBundle/Resources/config/services.yml new file mode 100644 index 0000000..7626c6e --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Resources/config/services.yml @@ -0,0 +1,45 @@ +parameters: + +services: + _defaults: + autowire: true + autoconfigure: true + public: false + + Comsave\PrometheusPushGatewayBundle\: + resource: '../../*' + exclude: '../../{DependencyInjection,Event,Exception,Model,Kernel.php}' + + Prometheus\Storage\Redis: + factory: ['Comsave\PrometheusPushGatewayBundle\Factory\RedisStorageAdapterFactory', 'build'] + arguments: ['%comsave_prometheus_pushgateway.pushgateway.redis%'] + + Comsave\PrometheusPushGatewayBundle\Prometheus\CollectorRegistry: + autowire: false + arguments: ['@Symfony\Component\EventDispatcher\EventSubscriberInterface', '@Prometheus\Storage\Redis'] + + GuzzleHttp\Client: + factory: ['Comsave\PrometheusPushGatewayBundle\Factory\GuzzleHttpClientFactory', 'build'] + + JMS\Serializer\Serializer: + factory: ['Comsave\PrometheusPushGatewayBundle\Factory\JmsSerializerFactory', 'build'] + + Comsave\PrometheusPushGatewayBundle\Command\PrometheusPushCommand: + arguments: + $prometheusJobNames: '%comsave_prometheus_pushgateway.prometheus.jobs%' + + Comsave\PrometheusPushGatewayBundle\Services\PushGateway: + arguments: + - '%comsave_prometheus_pushgateway.pushgateway.host%' + - '%comsave_prometheus_pushgateway.pushgateway.username%' + - '%comsave_prometheus_pushgateway.pushgateway.password%' + + Comsave\PrometheusPushGatewayBundle\Services\PushGatewayClient: + arguments: + $prometheusInstanceName: '%comsave_prometheus_pushgateway.prometheus.instance%' + + Comsave\PrometheusPushGatewayBundle\Services\PrometheusClient: + arguments: + $prometheusUrl: '%comsave_prometheus_pushgateway.prometheus.host%' + $username: '%comsave_prometheus_pushgateway.prometheus.username%' + $password: '%comsave_prometheus_pushgateway.prometheus.password%' diff --git a/src/PrometheusPushGatewayBundle/Services/PrometheusClient.php b/src/PrometheusPushGatewayBundle/Services/PrometheusClient.php new file mode 100644 index 0000000..6978267 --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Services/PrometheusClient.php @@ -0,0 +1,102 @@ +prometheusUrl = $prometheusUrl; + $this->jmsSerializer = $jmsSerializer; + $this->httpClient = $httpClient; + $this->username = $username; + $this->password = $password; + } + + /** + * @param string $query + * @return array|PrometheusResponseDataResult[] + * @throws \GuzzleHttp\Exception\GuzzleException + */ + public function query(string $query): array + { + try { + $response = $this->httpClient->request( + 'POST', + sprintf('%s/api/v1/query', $this->prometheusUrl), + $this->buildRequestOptions( + [ + 'form_params' => [ + 'query' => $query, + ], +// 'content-type' => 'application/x-www-form-urlencoded', + // todo: include instance + ] + ) + ); + } + catch(ClientException $ex) { + // todo: + throw $ex; + } + + /** @var PrometheusResponse $response */ + $response = $this->jmsSerializer->deserialize((string)$response->getBody(), PrometheusResponse::class, 'json'); + + return $response->getData()->getResults(); + } + + public function requireLabels(array $labels): string + { + return implode(',', array_map(function(string $label) { + return sprintf('%s=~".+"', $label); + }, $labels)); + } + + private function buildRequestOptions(array $requestOptions): array + { + if ($this->username && $this->password) { + $requestOptions['auth'] = [ + $this->username, + $this->password, + ]; + } + + return $requestOptions; + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Services/PushGateway.php b/src/PrometheusPushGatewayBundle/Services/PushGateway.php new file mode 100644 index 0000000..f1ac02d --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Services/PushGateway.php @@ -0,0 +1,137 @@ + [ + 'Content-Type' => RenderTextFormat::MIME_TYPE, + ], + 'connect_timeout' => 10, + 'timeout' => 20, + ]; + + /** + * @param string $address + * @param null|string $username + * @param null|string $password + * @codeCoverageIgnore + */ + public function __construct(string $address, ?string $username = null, ?string $password = null) + { + $this->address = $address; + $this->username = $username; + $this->password = $password; + } + + private function buildServiceUrl(string $job, array $groupingKey): string + { + $url = vsprintf('http://%s/metrics/job/%s', [ + $this->address, + $job + ]); + + if (!empty($groupingKey)) { + foreach ($groupingKey as $label => $value) { + $url .= vsprintf('/%s/%s', [ + $label, + $value + ]); + } + } + + return $url; + } + + private function buildClient(): Client + { + return new Client(); + } + + + /** + * Pushes all metrics in a Collector, replacing all those with the same job. + * @param CollectorRegistry $collectorRegistry + * @param string $job + * @param array $groupingKey + * @throws GuzzleException + */ + public function push(CollectorRegistry $collectorRegistry, string $job, array $groupingKey = null): void + { + $this->doRequest($collectorRegistry, $job, $groupingKey, 'put'); + } + + /** + * Pushes all metrics in a Collector, replacing only previously pushed metrics of the same name and job. + * @param CollectorRegistry $collectorRegistry + * @param $job + * @param $groupingKey + * @throws GuzzleException + */ + public function pushAdd(CollectorRegistry $collectorRegistry, string $job, array $groupingKey = null): void + { + $this->doRequest($collectorRegistry, $job, $groupingKey, 'post'); + } + + /** + * @param string $job + * @param array $groupingKey + * @throws GuzzleException + */ + public function delete(string $job, array $groupingKey = null): void + { + $this->doRequest(null, $job, $groupingKey, 'delete'); + } + + /** + * @param CollectorRegistry $collectorRegistry + * @param string $job + * @param array $groupingKey + * @param string $method + * @throws GuzzleException + */ + private function doRequest(?CollectorRegistry $collectorRegistry, string $job, array $groupingKey, $method): void + { + $requestOptions = static::$requestOptions; + + if($this->username && $this->password) { + $requestOptions['auth'] = [ + $this->username, + $this->password + ]; + } + + if ($method != 'delete') { + $requestOptions['body'] = (new RenderTextFormat())->render($collectorRegistry->getMetricFamilySamples()); + } + + $response = $this->buildClient()->request( + $method, + $this->buildServiceUrl($job, $groupingKey), + $requestOptions + ); + + if (!in_array($response->getStatusCode(), [200, 202])) { + throw new \RuntimeException(vsprintf('Unexpected status code %s received from push gateway %s: $s', [ + $response->getStatusCode(), + $this->address, + $response->getBody() + ])); + } + } +} \ No newline at end of file diff --git a/src/PrometheusPushGatewayBundle/Services/PushGatewayClient.php b/src/PrometheusPushGatewayBundle/Services/PushGatewayClient.php new file mode 100644 index 0000000..3338b9d --- /dev/null +++ b/src/PrometheusPushGatewayBundle/Services/PushGatewayClient.php @@ -0,0 +1,139 @@ +registry = $registry; + $this->registryStorageAdapter = $registryStorageAdapter; + $this->pushGateway = $pushGateway; + $this->prometheusClient = $prometheusClient; + $this->prometheusInstanceName = $prometheusInstanceName; + } + + /** + * @throws GuzzleException + * @throws StorageException + */ + public function push(string $prometheusJobName): void + { + try { +// $this->pushGateway->pushAdd( + $this->pushGateway->push( + $this->registry, + sprintf('morty_%s', $prometheusJobName), + [ + 'instance' => $this->prometheusInstanceName, + ] + ); + } + catch (\RuntimeException $ex) { + if(strpos($ex->getMessage(), 'Unexpected status code 200 received from push gateway') === false) { + throw $ex; + } + } + } + + /** + * @throws GuzzleException + * @throws StorageException + */ + public function pushAll(array $prometheusJobNames): void + { + foreach ($prometheusJobNames as $jobName) { + $this->push($jobName); + } + } + + /** + * @throws MetricsRegistrationException + */ + public function counter(string $namespace, string $name, ?string $help = null, array $labels = [], bool $fetchCurrent = false): CounterInterface + { + return $this->registry->getOrRegisterCounter( + $namespace, + $name, + $help, + $labels + ); + } + + /** + * @throws MetricsRegistrationException + */ + public function gauge(string $namespace, string $name, ?string $help = null, array $labels = []): GaugeInterface + { + return $this->registry->getOrRegisterGauge( + $namespace, + $name, + $help, + $labels + ); + } + + /** + * @throws MetricsRegistrationException + */ + public function histogram(string $namespace, string $name, ?string $help = null, array $labels = [], ?array $buckets = null): Histogram + { + return $this->registry->getOrRegisterHistogram( + $namespace, + $name, + $help, + $labels, + $buckets + ); + } + + public function getPrometheusClient(): PrometheusClient + { + return $this->prometheusClient; + } + + /** + * @throws StorageException + */ + public function flush(): void + { + $this->registryStorageAdapter->flushRedis(); + } +} \ No newline at end of file diff --git a/tests/Integration/AbstractPrometheusPushGatewayTest.php b/tests/Integration/AbstractPrometheusPushGatewayTest.php new file mode 100644 index 0000000..5672ff9 --- /dev/null +++ b/tests/Integration/AbstractPrometheusPushGatewayTest.php @@ -0,0 +1,43 @@ +addSubscriber(new MetricEventSubscriber()); + + $registryStorageAdapter = RedisStorageAdapterFactory::build('redis:6379'); + $registry = new CollectorRegistry($registryStorageAdapter, $eventDispatcher); + + return new PushGatewayClient( + $registry, + $registryStorageAdapter, + new PushGateway($pushGatewayUrl), + $prometheusClient, + '127.0.0.1:9000' + ); + } +} \ No newline at end of file diff --git a/tests/Integration/PrometheusMultiNodeHaProxyPushTest.php b/tests/Integration/PrometheusMultiNodeHaProxyPushTest.php new file mode 100644 index 0000000..11179ab --- /dev/null +++ b/tests/Integration/PrometheusMultiNodeHaProxyPushTest.php @@ -0,0 +1,111 @@ + 'prometheus:9091', + 2 => 'prometheus2:9092', + 3 => 'prometheus3:9093', + ]; + + /** + * @throws GuzzleException + * @throws MetricsRegistrationException + * @throws StorageException + */ + public function testPushesOneCounterMetric(): void + { + $metricNamespace = 'test'; + $metricName = 'some_counter_1_'.date('YmdHis'); + $metricFullName = sprintf('%s_%s', $metricNamespace, $metricName); +// var_dump($metricFullName); + + $pushGateway1 = static::buildPushGatewayClient('haproxy:9191', static::buildPrometheusClient('haproxy:9090')); + $pushGateway1->flush(); + + $counter = $pushGateway1->counter( + $metricNamespace, + $metricName, + 'it increases', + ['type'] + ); + $counter->incBy(5, ['blue']); + $pushGateway1->push($this->jobName); + + sleep(2); // wait for Prometheus to pull the metrics from PushGateway + + foreach($this->nodes as $node => $server) { + $results = static::buildPrometheusClient($server)->query($metricFullName); + + $this->assertCount(1, $results, sprintf('Node %s results invalid.', $node)); + $this->assertEquals($metricFullName, $results[0]->getMetric()['__name__']); + $this->assertEquals('blue', $results[0]->getMetric()['type']); + $this->assertEquals(5, $results[0]->getValue()); + } + } + + /** + * @throws GuzzleException + * @throws MetricNotFoundException + * @throws MetricsRegistrationException + * @throws StorageException + */ + public function testPushesCounterMetricAndIncreases(): void + { + $metricNamespace = 'test'; + $metricName = 'some_counter_2_'.date('YmdHis'); + $metricFullName = sprintf('%s_%s', $metricNamespace, $metricName); +// var_dump($metricFullName); + + $pushGateway1 = static::buildPushGatewayClient('haproxy:9191', static::buildPrometheusClient('haproxy:9090')); + $pushGateway1->flush(); + + $counter = $pushGateway1->counter( + $metricNamespace, + $metricName, + 'it increases', + ['type'] + ); + $counter->incBy(5, ['blue']); + $pushGateway1->push($this->jobName.'_2'); + + sleep(2); // wait for Prometheus to pull the metrics from PushGateway + + foreach($this->nodes as $node => $server) { + $results = static::buildPrometheusClient($server)->query($metricFullName); + + $this->assertCount(1, $results, sprintf('Node %s results invalid.', $node)); + $this->assertEquals($metricFullName, $results[0]->getMetric()['__name__']); + $this->assertEquals('blue', $results[0]->getMetric()['type']); + $this->assertEquals(5, $results[0]->getValue()); + } + + $counter = $pushGateway1->counter( + $metricNamespace, + $metricName + ); + $counter->inc(['blue']); + $pushGateway1->push($this->jobName.'_2'); + + sleep(2); // wait for Prometheus to pull the metrics from PushGateway + + foreach($this->nodes as $node => $server) { + $results = static::buildPrometheusClient($server)->query($metricFullName); + + $this->assertCount(1, $results, sprintf('Node %s results invalid.', $node)); + $this->assertEquals($metricFullName, $results[0]->getMetric()['__name__']); + $this->assertEquals('blue', $results[0]->getMetric()['type']); + $this->assertEquals(6, $results[0]->getValue()); + } + } +} \ No newline at end of file diff --git a/tests/Integration/PrometheusSingleNodePushTest.php b/tests/Integration/PrometheusSingleNodePushTest.php new file mode 100644 index 0000000..82e301e --- /dev/null +++ b/tests/Integration/PrometheusSingleNodePushTest.php @@ -0,0 +1,161 @@ +prometheusClient = static::buildPrometheusClient('prometheus:9090'); + $this->pushGatewayClient = self::buildPushGatewayClient('pushgateway:9191', $this->prometheusClient); + $this->pushGatewayClient->flush(); + } + + /** + * @throws GuzzleException + * @throws MetricsRegistrationException + * @throws StorageException + */ + public function testPushesOneCounterMetric(): void + { + $metricNamespace = 'test'; + $metricName = 'some_counter_1_'.date('YmdHis'); + $metricFullName = sprintf('%s_%s', $metricNamespace, $metricName); + + $counter = $this->pushGatewayClient->counter( + $metricNamespace, + $metricName, + 'it increases', + ['type'] + ); + $counter->incBy(5, ['blue']); + $this->pushGatewayClient->push($this->jobName); + + sleep(2); // wait for Prometheus to pull the metrics from PushGateway + + $results = $this->prometheusClient->query($metricFullName); + + $this->assertCount(1, $results); + $this->assertEquals($metricFullName, $results[0]->getMetric()['__name__']); + $this->assertEquals('blue', $results[0]->getMetric()['type']); + $this->assertEquals(5, $results[0]->getValue()); + } + + /** + * @throws GuzzleException + * @throws MetricNotFoundException + * @throws MetricsRegistrationException + * @throws StorageException + */ + public function testPushesCounterMetricAndIncreases(): void + { + $metricNamespace = 'test'; + $metricName = 'some_counter_2_'.date('YmdHis'); + $metricFullName = sprintf('%s_%s', $metricNamespace, $metricName); + + $counter = $this->pushGatewayClient->counter( + $metricNamespace, + $metricName, + 'it increases', + ['type'] + ); + $counter->incBy(5, ['blue']); + $this->pushGatewayClient->push($this->jobName.'_2'); + + sleep(2); // wait for Prometheus to pull the metrics from PushGateway + + $results = $this->prometheusClient->query($metricFullName); + + $this->assertCount(1, $results); + $this->assertEquals($metricFullName, $results[0]->getMetric()['__name__']); + $this->assertEquals('blue', $results[0]->getMetric()['type']); + $this->assertEquals(5, $results[0]->getValue()); + + $counter = $this->pushGatewayClient->counter( + $metricNamespace, + $metricName + ); + $counter->inc(['blue']); + $this->pushGatewayClient->push($this->jobName.'_2'); + + sleep(2); // wait for Prometheus to pull the metrics from PushGateway + + $results = $this->prometheusClient->query($metricFullName); + + $this->assertCount(1, $results); + $this->assertEquals($metricFullName, $results[0]->getMetric()['__name__']); + $this->assertEquals('blue', $results[0]->getMetric()['type']); + $this->assertEquals(6, $results[0]->getValue()); + } + + /** + * @throws GuzzleException + * @throws MetricsRegistrationException + * @throws StorageException + */ + public function testPushesGetsExistingMetricFromPrometheusToIncrease(): void + { + $metricNamespace = 'test'; + $metricName = 'some_counter_3_'.date('YmdHis'); + $metricFullName = sprintf('%s_%s', $metricNamespace, $metricName); + + $labels = ['order_id', 'user_id']; + $counter = $this->pushGatewayClient->counter( + $metricNamespace, + $metricName, + 'it increases', + $labels + ); + $counter->inc([md5(mt_rand()), 'user_id_1']); + $counter->inc([md5(mt_rand()), 'user_id_2']); + $counter->inc([md5(mt_rand()), 'user_id_2']); + $this->pushGatewayClient->push($this->jobName); + $this->pushGatewayClient->flush(); + + sleep(2); // wait for Prometheus to pull the metrics from PushGateway + + $results = $this->prometheusClient->query( + sprintf('sum(%s{%s})', $metricFullName, $this->prometheusClient->requireLabels($labels)), + ); + var_dump($results); + + $this->assertCount(1, $results); + $this->assertEquals(3, $results[0]->getValue()); + + $counter = $this->pushGatewayClient->counter( + $metricNamespace, + $metricName, + 'it increases', + $labels, + true + ); + $counter->inc([md5(mt_rand()), 'user_id_2']); + $this->pushGatewayClient->push($this->jobName); + + sleep(2); // wait for Prometheus to pull the metrics frwom PushGateway + + $results = $this->prometheusClient->query( + sprintf('sum(%s{%s})', $metricFullName, $this->prometheusClient->requireLabels($labels)), + ); + var_dump($results); + + $this->assertCount(1, $results); + $this->assertEquals(4, $results[0]->getValue()); + } +} \ No newline at end of file diff --git a/tests/Unit/DependencyInjection/ConfigurationTest.php b/tests/Unit/DependencyInjection/ConfigurationTest.php new file mode 100644 index 0000000..a37df67 --- /dev/null +++ b/tests/Unit/DependencyInjection/ConfigurationTest.php @@ -0,0 +1,95 @@ + [ + 'host' => 'prometheus:9090', + 'username' => 'admin', + 'password' => 'duuude', + 'instance' => 'moms_basement:6666', + ], + 'pushgateway' => [ + 'host' => 'pushgateway:9191', + 'username' => 'admin2', + 'password' => 'duuude2', + 'redis' => 'redis:6379', + ], + ]; + + $configuration = new Configuration(); + + $configNode = $configuration->getConfigTreeBuilder()->buildTree(); + $resultConfig = $configNode->finalize($configNode->normalize($inputOutput)); + + $this->assertMatchesJsonSnapshot($resultConfig); + } + + public function testConfigurationNoCredentials(): void + { + $inputOutput = [ + 'prometheus' => [ + 'host' => 'prometheus:9090', + 'instance' => 'moms_basement:6666', + ], + 'pushgateway' => [ + 'host' => 'pushgateway:9191', + 'redis' => 'redis:6379', + ], + ]; + + $configuration = new Configuration(); + + $configNode = $configuration->getConfigTreeBuilder()->buildTree(); + $resultConfig = $configNode->finalize($configNode->normalize($inputOutput)); + + $this->assertMatchesJsonSnapshot($resultConfig); + } + + public function testConfigurationWithMetrics(): void + { + $inputOutput = [ + 'prometheus' => [ + 'host' => 'prometheus:9090', + 'username' => 'admin', + 'password' => 'duuude', + 'instance' => 'moms_basement:6666', + ], + 'pushgateway' => [ + 'host' => 'pushgateway:9191', + 'username' => 'admin2', + 'password' => 'duuude2', + 'redis' => 'redis:6379', + ], + 'metrics' => [ + 'api' => [ + 'name' => 'orders', + 'type' => 'counter', + 'help' => 'counts number of orders', + 'prefetch_group_label' => 'user_id', + 'labels' => [ + 'order_id', + 'user_id', + ], + ], + ], + ]; + + $configuration = new Configuration(); + + $configNode = $configuration->getConfigTreeBuilder()->buildTree(); + $resultConfig = $configNode->finalize($configNode->normalize($inputOutput)); + + $this->assertMatchesJsonSnapshot($resultConfig); + } +} \ No newline at end of file diff --git a/tests/Unit/DependencyInjection/__snapshots__/ConfigurationTest__testConfigurationNoCredentials__1.json b/tests/Unit/DependencyInjection/__snapshots__/ConfigurationTest__testConfigurationNoCredentials__1.json new file mode 100644 index 0000000..4a8288d --- /dev/null +++ b/tests/Unit/DependencyInjection/__snapshots__/ConfigurationTest__testConfigurationNoCredentials__1.json @@ -0,0 +1,15 @@ +{ + "prometheus": { + "host": "prometheus:9090", + "instance": "moms_basement:6666", + "username": null, + "password": null + }, + "pushgateway": { + "host": "pushgateway:9191", + "redis": "redis:6379", + "username": null, + "password": null + }, + "metrics": [] +} diff --git a/tests/Unit/DependencyInjection/__snapshots__/ConfigurationTest__testConfigurationWithMetrics__1.json b/tests/Unit/DependencyInjection/__snapshots__/ConfigurationTest__testConfigurationWithMetrics__1.json new file mode 100644 index 0000000..e137ae9 --- /dev/null +++ b/tests/Unit/DependencyInjection/__snapshots__/ConfigurationTest__testConfigurationWithMetrics__1.json @@ -0,0 +1,26 @@ +{ + "prometheus": { + "host": "prometheus:9090", + "username": "admin", + "password": "duuude", + "instance": "moms_basement:6666" + }, + "pushgateway": { + "host": "pushgateway:9191", + "username": "admin2", + "password": "duuude2", + "redis": "redis:6379" + }, + "metrics": { + "api": { + "name": "orders", + "type": "counter", + "help": "counts number of orders", + "prefetch_group_label": "user_id", + "labels": [ + "order_id", + "user_id" + ] + } + } +} diff --git a/tests/Unit/DependencyInjection/__snapshots__/ConfigurationTest__testConfiguration__1.json b/tests/Unit/DependencyInjection/__snapshots__/ConfigurationTest__testConfiguration__1.json new file mode 100644 index 0000000..9c767fd --- /dev/null +++ b/tests/Unit/DependencyInjection/__snapshots__/ConfigurationTest__testConfiguration__1.json @@ -0,0 +1,15 @@ +{ + "prometheus": { + "host": "prometheus:9090", + "username": "admin", + "password": "duuude", + "instance": "moms_basement:6666" + }, + "pushgateway": { + "host": "pushgateway:9191", + "username": "admin2", + "password": "duuude2", + "redis": "redis:6379" + }, + "metrics": [] +} diff --git a/tests/Unit/MetricSerializerTest.php b/tests/Unit/MetricSerializerTest.php new file mode 100644 index 0000000..7ad1dd5 --- /dev/null +++ b/tests/Unit/MetricSerializerTest.php @@ -0,0 +1,50 @@ +deserialize($responseJson, PrometheusResponse::class, 'json'); + /** @var PrometheusResponseDataResult $prometheusDataResult */ + $prometheusDataResult = $prometheusResponse->getData()->getResults()[0]; + + $this->assertEquals([ + '__name__' => 'test_some_counter', + 'instance' => '127.0.0.1:9000', + 'job' => 'my_custom_service_job', + 'type' => 'blue' + ], $prometheusDataResult->getMetric()); + $this->assertEquals(5, $prometheusDataResult->getValue()); + } +} \ No newline at end of file