commit bf1a4e1879165fb8e44234827c545eef6889dc5b Author: Matthew Goslett Date: Thu Jul 27 11:11:02 2017 +0200 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..ba93dc5 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +.idea +composer.lock +vendor +bin +coverage +coverage.xml \ No newline at end of file diff --git a/.styleci.yml b/.styleci.yml new file mode 100644 index 0000000..3d42cee --- /dev/null +++ b/.styleci.yml @@ -0,0 +1,76 @@ +preset: psr2 + +enabled: + - alpha_ordered_imports + - binary_operator_spaces + - blank_line_after_opening_tag + - cast_spaces + - concat_with_spaces + - const_visibility_required + - declare_equal_normalize + - function_typehint_space + - hash_to_slash_comment + - heredoc_to_nowdoc + - include + - lowercase_cast + - method_separation + - native_function_casing + - new_with_braces + - no_blank_lines_after_class_opening + - no_blank_lines_after_phpdoc + - no_blank_lines_after_return + - no_blank_lines_after_throw + - no_blank_lines_between_imports + - no_blank_lines_between_traits + - no_empty_statement + - no_extra_consecutive_blank_lines + - no_leading_import_slash + - no_leading_namespace_whitespace + - no_multiline_whitespace_around_double_arrow + - no_short_bool_cast + - no_short_echo_tag + - no_singleline_whitespace_before_semicolons + - no_spaces_inside_offset + - no_spaces_outside_offset + - no_trailing_comma_in_list_call + - no_trailing_comma_in_singleline_array + - no_unneeded_control_parentheses + - no_unreachable_default_argument_value + - no_unused_imports + - no_useless_return + - no_whitespace_before_comma_in_array + - no_whitespace_in_blank_line + - normalize_index_brace + - object_operator_without_whitespace + - phpdoc_add_missing_param_annotation + - phpdoc_indent + - phpdoc_inline_tag + - phpdoc_link_to_see + - phpdoc_no_access + - phpdoc_no_empty_return + - phpdoc_no_package + - phpdoc_order + - phpdoc_property + - phpdoc_scalar + - phpdoc_separation + - phpdoc_single_line_var_spacing + - phpdoc_to_comment + - phpdoc_trim + - phpdoc_type_to_var + - phpdoc_types + - phpdoc_var_without_name + - print_to_echo + - self_accessor + - short_array_syntax + - short_scalar_cast + - single_blank_line_before_namespace + - single_quote + - space_after_semicolon + - standardize_not_equals + - ternary_operator_spaces + - trailing_comma_in_multiline_array + - trim_array_spaces + - unalign_double_arrow + - unalign_equals + - unary_operator_spaces + - whitespace_after_comma_in_array diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..b12489d --- /dev/null +++ b/.travis.yml @@ -0,0 +1,12 @@ +language: php + +php: + - 5.6 + - 7.0 + - 7.1 + - nightly + +before_script: + - composer install + +script: ./vendor/bin/phpunit --configuration phpunit.xml \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..baa7a4a --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2017 Superbalist.com a division of Takealot Online (Pty) Ltd + +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. \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..76b0436 --- /dev/null +++ b/README.md @@ -0,0 +1,65 @@ +# laravel-prometheus-exporter + +A prometheus exporter for Laravel. + +[![Author](http://img.shields.io/badge/author-@superbalist-blue.svg?style=flat-square)](https://twitter.com/superbalist) +[![Build Status](https://img.shields.io/travis/Superbalist/laravel-prometheus-exporter/master.svg?style=flat-square)](https://travis-ci.org/Superbalist/laravel-prometheus-exporter) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Packagist Version](https://img.shields.io/packagist/v/superbalist/laravel-prometheus-exporter.svg?style=flat-square)](https://packagist.org/packages/superbalist/laravel-prometheus-exporter) +[![Total Downloads](https://img.shields.io/packagist/dt/superbalist/laravel-prometheus-exporter.svg?style=flat-square)](https://packagist.org/packages/superbalist/laravel-prometheus-exporter) + +This package is a wrapper bridging [jimdo/prometheus_client_php](https://github.com/Jimdo/prometheus_client_php) into Laravel. + +## Installation + +```bash +composer require superbalist/laravel-prometheus-exporter +``` + +Register the service provider in app.php +```php +'providers' => [ + // ... + Superbalist\LaravelPrometheusExporter\PrometheusServiceProvider::class, +] +``` + +Register the facade in app.php +```php +'aliases' => [ + // ... + 'PubSub' => Superbalist\LaravelPrometheusExporter\PrometheusFacade::class, +] +``` + +## Configuration + +The package has a default configuration which uses the following environment variables. +``` +PROMETHEUS_NAMESPACE=app + +PROMETHEUS_METRICS_ROUTE_ENABLED=true +PROMETHEUS_METRICS_ROUTE_PATH=metrics +PROMETHEUS_METRICS_ROUTE_MIDDLEWARE=null + +PROMETHEUS_STORAGE_ADAPTER=memory + +REDIS_HOST=localhost +REDIS_PORT=6379 +PROMETHEUS_REDIS_PREFIX=PROMETHEUS_ +``` + +To customize the configuration file, publish the package configuration using Artisan. +```bash +php artisan vendor:publish --provider="Superbalist\LaravelPrometheusExporter\PrometheusServiceProvider" +``` + +You can then edit the generated config at `app/config/prometheus.php`. + +// TODO: + +## Usage + +```php +// TODO: +``` diff --git a/changelog.md b/changelog.md new file mode 100644 index 0000000..a848111 --- /dev/null +++ b/changelog.md @@ -0,0 +1,5 @@ +# Changelog + +## 1.0.0 - ? + +* Initial release \ No newline at end of file diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..e45a96f --- /dev/null +++ b/composer.json @@ -0,0 +1,32 @@ +{ + "name": "superbalist/laravel-prometheus-exporter", + "description": "A prometheus exporter for Laravel", + "license": "MIT", + "authors": [ + { + "name": "Superbalist.com a division of Takealot Online (Pty) Ltd", + "email": "info@superbalist.com" + } + ], + "require": { + "php": ">=5.6.0", + "illuminate/support": "^5.3", + "illuminate/routing": "^5.3", + "jimdo/prometheus_client_php": "^0.9.0" + }, + "autoload": { + "psr-4": { + "Superbalist\\LaravelPrometheusExporter\\": "src/", + "Tests\\": "tests/" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.0-dev" + } + }, + "require-dev": { + "phpunit/phpunit": "^5.5", + "mockery/mockery": "^0.9.5" + } +} diff --git a/config/prometheus.php b/config/prometheus.php new file mode 100644 index 0000000..4c4bdb5 --- /dev/null +++ b/config/prometheus.php @@ -0,0 +1,107 @@ + env('PROMETHEUS_NAMESPACE', 'app'), + + /* + |-------------------------------------------------------------------------- + | Metrics Route Enabled? + |-------------------------------------------------------------------------- + | + | If enabled, a /metrics route will be registered to export prometheus + | metrics. + | + */ + + 'metrics_route_enabled' => env('PROMETHEUS_METRICS_ROUTE_ENABLED', true), + + /* + |-------------------------------------------------------------------------- + | Metrics Route Path + |-------------------------------------------------------------------------- + | + | The path at which prometheus metrics are exported. + | + | This is only applicable if metrics_route_enabled is set to true. + | + */ + + 'metrics_route_path' => env('PROMETHEUS_METRICS_ROUTE_PATH', 'metrics'), + + /* + |-------------------------------------------------------------------------- + | Metrics Route Middleware + |-------------------------------------------------------------------------- + | + | The middleware to assign to the metrics route. + | + | This can be used to protect the /metrics end-point to authenticated users, + | a specific ip address, etc. + | You are responsible for writing the middleware and implementing any + | business logic needed by your application. + | + */ + + 'metrics_route_middleware' => env('PROMETHEUS_METRICS_ROUTE_MIDDLEWARE'), + + /* + |-------------------------------------------------------------------------- + | Storage Adapter + |-------------------------------------------------------------------------- + | + | The storage adapter to use. + | + | Supported: "memory", "redis", "apc" + | + */ + + 'storage_adapter' => env('PROMETHEUS_STORAGE_ADAPTER', 'memory'), + + /* + |-------------------------------------------------------------------------- + | Storage Adapters + |-------------------------------------------------------------------------- + | + | The storage adapter configs. + | + */ + + 'storage_adapters' => [ + + 'redis' => [ + 'host' => env('REDIS_HOST', 'localhost'), + 'port' => env('REDIS_PORT', 6379), + 'timeout' => 0.1, + 'read_timeout' => 10, + 'persistent_connections' => false, + 'prefix' => env('PROMETHEUS_REDIS_PREFIX', 'PROMETHEUS_'), + ], + + ], + + /* + |-------------------------------------------------------------------------- + | Collectors + |-------------------------------------------------------------------------- + | + | The collectors specified here will be auto-registered in the exporter. + | + */ + + 'collectors' => [ + // \Your\ExporterClass::class, + ], + +]; diff --git a/phpunit.php b/phpunit.php new file mode 100644 index 0000000..043183f --- /dev/null +++ b/phpunit.php @@ -0,0 +1,3 @@ + + + + + ./tests/ + + + + + ./src/ + + + + + + + + \ No newline at end of file diff --git a/src/CollectorInterface.php b/src/CollectorInterface.php new file mode 100644 index 0000000..5ac17a9 --- /dev/null +++ b/src/CollectorInterface.php @@ -0,0 +1,34 @@ +registerCounter('search_requests_total', 'The total number of search requests.'); + * ``` + * + * @param PrometheusExporter $exporter + */ + public function registerMetrics(PrometheusExporter $exporter); + + /** + * Collect metrics data, if need be, before exporting. + * + * As an example, this may be used to perform time consuming database queries and set the value of a counter + * or gauge. + */ + public function collect(); +} diff --git a/src/MetricsController.php b/src/MetricsController.php new file mode 100644 index 0000000..3ac750e --- /dev/null +++ b/src/MetricsController.php @@ -0,0 +1,41 @@ +prometheusExporter = $prometheusExporter; + } + + /** + * GET /metrics + * + * The route path is configurable in the prometheus.metrics_route_path config var, or the + * PROMETHEUS_METRICS_ROUTE_PATH env var. + * + * @return \Symfony\Component\HttpFoundation\Response + */ + public function getMetrics() + { + $metrics = $this->prometheusExporter->export(); + + $renderer = new RenderTextFormat(); + $result = $renderer->render($metrics); + + return response($result) + ->header('Content-Type', RenderTextFormat::MIME_TYPE); + } +} diff --git a/src/PrometheusExporter.php b/src/PrometheusExporter.php new file mode 100644 index 0000000..6663a66 --- /dev/null +++ b/src/PrometheusExporter.php @@ -0,0 +1,235 @@ +namespace = $namespace; + $this->prometheus = $prometheus; + + foreach ($collectors as $collector) { + /** @var CollectorInterface $collector */ + $this->registerCollector($collector); + } + } + + /** + * Return the metric namespace. + * + * @return string + */ + public function getNamespace() + { + return $this->namespace; + } + + /** + * Return the CollectorRegistry. + * + * @return CollectorRegistry + */ + public function getPrometheus() + { + return $this->prometheus; + } + + /** + * Register a collector. + * + * @param CollectorInterface $collector + */ + public function registerCollector(CollectorInterface $collector) + { + $name = $collector->getName(); + + if (!isset($this->collectors[$name])) { + $this->collectors[$name] = $collector; + + $collector->registerMetrics($this->prometheus); + } + } + + /** + * Return all collectors. + * + * @return array + */ + public function getCollectors() + { + return $this->collectors; + } + + /** + * Return a collector by name. + * + * @param string $name + * @return CollectorInterface + */ + public function getCollector($name) + { + if (!isset($this->collectors[$name])) { + throw new InvalidArgumentException(sprintf('The collector "%s" is not registered.', $name)); + } + + return $this->collectors[$name]; + } + + /** + * Register a counter. + * + * @param string $name + * @param string $help + * @param array $labels + * @return \Prometheus\Counter + * @see https://prometheus.io/docs/concepts/metric_types/#counter + */ + public function registerCounter($name, $help, $labels = []) + { + return $this->prometheus->registerCounter($this->namespace, $name, $help, $labels); + } + + /** + * Return a counter. + * + * @param string $name + * @return \Prometheus\Counter + */ + public function getCounter($name) + { + return $this->prometheus->getCounter($this->namespace, $name); + } + + /** + * Return or register a counter. + * + * @param string $name + * @param string $help + * @param array $labels + * @return \Prometheus\Counter + * @see https://prometheus.io/docs/concepts/metric_types/#counter + */ + public function getOrRegisterCounter($name, $help, $labels = []) + { + return $this->prometheus->getOrRegisterCounter($this->namespace, $name, $help, $labels); + } + + /** + * Register a gauge. + * + * @param string $name + * @param string $help + * @param array $labels + * @return \Prometheus\Gauge + * @see https://prometheus.io/docs/concepts/metric_types/#gauge + */ + public function registerGauge($name, $help, $labels = []) + { + return $this->prometheus->registerGauge($this->namespace, $name, $help, $labels); + } + + /** + * Return a gauge. + * + * @param string $name + * @return \Prometheus\Counter + */ + public function getGauge($name) + { + return $this->prometheus->getCounter($this->namespace, $name); + } + + /** + * Return or register a gauge. + * + * @param string $name + * @param string $help + * @param array $labels + * @return \Prometheus\Gauge + * @see https://prometheus.io/docs/concepts/metric_types/#gauge + */ + public function getOrRegisterGauge($name, $help, $labels = []) + { + return $this->prometheus->getOrRegisterGauge($this->namespace, $name, $help, $labels); + } + + /** + * Register a histogram. + * + * @param string $name + * @param string $help + * @param array $labels + * @param array $buckets + * @return \Prometheus\Histogram + * @see https://prometheus.io/docs/concepts/metric_types/#histogram + */ + public function registerHistogram($name, $help, $labels = [], $buckets = null) + { + return $this->prometheus->registerHistogram($this->namespace, $name, $help, $labels, $buckets); + } + + /** + * Return a histogram. + * + * @param string $name + * @return \Prometheus\Histogram + */ + public function getHistogram($name) + { + return $this->prometheus->getHistogram($this->namespace, $name); + } + + /** + * Return or register a histogram. + * + * @param string $name + * @param string $help + * @param array $labels + * @param array $buckets + * @return \Prometheus\Histogram + * @see https://prometheus.io/docs/concepts/metric_types/#histogram + */ + public function getOrRegisterHistogram($name, $help, $labels = [], $buckets = null) + { + return $this->prometheus->getOrRegisterHistogram($this->namespace, $name, $help, $labels, $buckets); + } + + /** + * Export the metrics from all collectors. + * + * @return \Prometheus\MetricFamilySamples[] + */ + public function export() + { + foreach ($this->collectors as $collector) { + /** @var CollectorInterface $collector */ + $collector->collect(); + } + + return $this->prometheus->getMetricFamilySamples(); + } +} diff --git a/src/PrometheusFacade.php b/src/PrometheusFacade.php new file mode 100644 index 0000000..3867399 --- /dev/null +++ b/src/PrometheusFacade.php @@ -0,0 +1,18 @@ +publishes([ + __DIR__ . '/../config/prometheus.php' => config_path('prometheus.php'), + ]); + + if (config('prometheus.metrics_route_enabled')) { + $this->loadRoutesFrom(__DIR__ . '/routes.php'); + } + + $exporter = $this->app->make(PrometheusExporter::class); /** @var PrometheusExporter $exporter */ + foreach (config('prometheus.collectors') as $class) { + $collector = $this->app->make($class); + $exporter->registerCollector($collector); + } + } + + /** + * Register bindings in the container. + */ + public function register() + { + $this->mergeConfigFrom(__DIR__ . '/../config/prometheus.php', 'prometheus'); + + $this->app->singleton(PrometheusExporter::class, function ($app) { + $adapter = $app['prometheus.storage_adapter']; + $prometheus = new CollectorRegistry($adapter); + return new PrometheusExporter(config('prometheus.namespace'), $prometheus); + }); + $this->app->alias(PrometheusExporter::class, 'prometheus'); + + $this->app->bind('prometheus.storage_adapter_factory', function ($app) { + return new StorageAdapterFactory($app); + }); + + $this->app->bind(Adapter::class, function ($app) { + $factory = $app['prometheus.storage_adapter_factory']; /** @var StorageAdapterFactory $factory */ + $driver = config('prometheus.storage_adapter'); + $configs = config('storage_adapters'); + $config = array_get($configs, $driver, []); + return $factory->make($driver, $config); + }); + $this->app->alias(Adapter::class, 'prometheus.storage_adapter'); + } + + /** + * Get the services provided by the provider. + * + * @return array + */ + public function provides() + { + return [ + 'prometheus', + 'prometheus.storage_adapter_factory', + 'prometheus.storage_adapter', + ]; + } +} diff --git a/src/StorageAdapterFactory.php b/src/StorageAdapterFactory.php new file mode 100644 index 0000000..8427130 --- /dev/null +++ b/src/StorageAdapterFactory.php @@ -0,0 +1,61 @@ +container = $container; + } + + /** + * Factory a storage adapter. + * + * @param string $driver + * @param array $config + * @return Adapter + */ + public function make($driver, array $config = []) + { + switch ($driver) { + case 'memory': + return new InMemory(); + case 'redis': + return $this->makeRedisAdapter($config); + case 'apc': + return new APC(); + } + + throw new InvalidArgumentException(sprintf('The driver [%s] is not supported.', $driver)); + } + + /** + * Factory a redis storage adapter. + * + * @param array $config + * @return Redis + */ + protected function makeRedisAdapter(array $config) + { + if (isset($config['prefix'])) { + Redis::setPrefix($config['prefix']); + } + return new Redis($config); + } +} diff --git a/src/routes.php b/src/routes.php new file mode 100644 index 0000000..740b501 --- /dev/null +++ b/src/routes.php @@ -0,0 +1,5 @@ +middleware(config('prometheus.metrics_route_middleware')) + ->name('metrics');