From 9047890b7f140728e437b50835c3a9e3cacb986e Mon Sep 17 00:00:00 2001 From: Till Backhaus Date: Tue, 22 May 2018 14:31:18 +0200 Subject: [PATCH] Fix laravel lumen compatibility * lowercase namespace without dashes * implement a few basic test cases so this won't happen again * use getOrRegister... so multiple registration isn't an issue anymore --- .travis.yml | 3 +- README.md | 12 +-- composer.json | 25 +++-- src/LaravelController.php | 31 ++++++ ...rovider.php => LaravelServiceProvider.php} | 30 ++---- src/LpeController.php | 42 -------- src/LpeManager.php | 99 ------------------- src/LumenController.php | 31 ++++++ src/LumenServiceProvider.php | 60 +++++++++++ .../AbstractResponseTimeMiddleware.php | 93 ++++++++++++++--- .../LaravelResponseTimeMiddleware.php | 13 ++- .../LumenResponseTimeMiddleware.php | 34 ++++++- src/laravel_routes.php | 2 +- tests/Middleware/LaravelMiddlewareTest.php | 25 ++++- tests/Middleware/LumenMiddlewareTest.php | 44 +++++++++ 15 files changed, 340 insertions(+), 204 deletions(-) create mode 100644 src/LaravelController.php rename src/{LpeServiceProvider.php => LaravelServiceProvider.php} (55%) delete mode 100644 src/LpeController.php delete mode 100644 src/LpeManager.php create mode 100644 src/LumenController.php create mode 100644 src/LumenServiceProvider.php create mode 100644 tests/Middleware/LumenMiddlewareTest.php diff --git a/.travis.yml b/.travis.yml index c824548..1e60915 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,6 @@ language: php php: - - '7.0' + - '7.1' + - '7.2' install: composer install --no-interaction diff --git a/README.md b/README.md index bc7c7c0..f574d66 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ In `config/app.php` ``` 'providers' => [ ... - Traum-ferienwohnungen\PrometheusExporter\LpeServiceProvider::class, + traumferienwohnungen\PrometheusExporter\LpeServiceProvider::class, ]; ``` @@ -39,34 +39,34 @@ In `app/Http/Kernel.php` ``` protected $middleware = [ ... - \Traum-ferienwohnungen\PrometheusExporter\Middleware\LaravelResponseTimeMiddleware::class, + \traumferienwohnungen\PrometheusExporter\Middleware\LaravelResponseTimeMiddleware::class, ]; ``` #### Add an endpoint for the metrics ``` -Route::get('metrics', \Traum-ferienwohnungen\PrometheusExporter\LpeController::class . '@metrics'); +Route::get('metrics', \traumferienwohnungen\PrometheusExporter\LpeController::class . '@metrics'); ``` ### Lumen #### Register the ServiceProvider In `bootstrap/app.php` ``` -$app->register(\Traum-ferienwohnungen\PrometheusExporter\LpeServiceProvider::class); +$app->register(\traumferienwohnungen\PrometheusExporter\LpeServiceProvider::class); ``` #### Enable the Middleware In `bootstrap/app.php` ``` $app->middleware([ - \Traum-ferienwohnungen\PrometheusExporter\Middleware\LumenResponseTimeMiddleware::class + \traumferienwohnungen\PrometheusExporter\Middleware\LumenResponseTimeMiddleware::class ]); ``` #### Add an endpoint for the metrics In `bootstrap/app.php` ``` -$app->group(['namespace'=> '\Traum-ferienwohnungen\PrometheusExporter'], function() use ($app){ +$app->group(['namespace'=> '\traumferienwohnungen\PrometheusExporter'], function() use ($app){ $app->get('metrics', ['as' => 'metrics', 'uses'=> 'LpeController@metrics']); }); ``` diff --git a/composer.json b/composer.json index ab2ce6a..6e79c97 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "traum-ferienwohnungen/laravel-prometheus-exporter", - "description": "A prometheus exporter for the Laravel web framework", - "keywords": ["laravel", "prometheus"], + "description": "A prometheus exporter for Laravel & Lumen", + "keywords": ["laravel", "lumen", "prometheus"], "type": "library", "license": "MIT", "authors": [ @@ -12,28 +12,25 @@ ], "autoload": { "psr-4": { - "Traum-ferienwohnungen\\PrometheusExporter\\": "src/" + "traumferienwohnungen\\PrometheusExporter\\": "src/" } }, - "repositories": [ - { - "type" : "vcs", - "url": "https://github.com/traum-ferienwohnungen/laravel-prometheus-exporter" - } - ], "require": { - "jimdo/prometheus_client_php": "dev-store-initialized", - "illuminate/support": "^5.6" + "php" : "^7.1", + "jimdo/prometheus_client_php": "^0.9.1", + "illuminate/support": "^5.6", + "fzaninotto/faker": "~1.7" }, "require-dev": { "illuminate/http": "^5.6", - "phpunit/phpunit": "^6.0", - "orchestra/testbench": "~3.0" + "phpunit/phpunit": "^7.0", + "orchestra/testbench": "~3.6", + "mockery/mockery": "^1.1" }, "extra": { "laravel": { "providers": [ - "Traum-ferienwohnungen\\PrometheusExporter\\LpeServiceProvider" + "traumferienwohnungen\\PrometheusExporter\\LaravelServiceProvider" ] } } diff --git a/src/LaravelController.php b/src/LaravelController.php new file mode 100644 index 0000000..9239f86 --- /dev/null +++ b/src/LaravelController.php @@ -0,0 +1,31 @@ +render($registry->getMetricFamilySamples())) + ->header('Content-Type', $renderer::MIME_TYPE); + } +} diff --git a/src/LpeServiceProvider.php b/src/LaravelServiceProvider.php similarity index 55% rename from src/LpeServiceProvider.php rename to src/LaravelServiceProvider.php index a8d844e..7625376 100644 --- a/src/LpeServiceProvider.php +++ b/src/LaravelServiceProvider.php @@ -1,39 +1,30 @@ publishes([$source => config_path('prometheus_exporter.php')]); + $this->mergeConfigFrom($source, 'prometheus_exporter'); - if ($this->app instanceof LaravelApplication) { - $this->publishes([$source => config_path('prometheus_exporter.php')]); - $this->mergeConfigFrom($source, 'prometheus_exporter'); - - $kernel->pushMiddleware(LaravelResponseTimeMiddleware::class); - if(! $this->app->routesAreCached()){ - $this->registerMetricsRoute(); - } - } elseif (class_exists('Laravel\Lumen\Application', false)) { - $this->app->configure('prometheus_exporter'); - $this->mergeConfigFrom($source, 'prometheus_exporter'); - } + $kernel->pushMiddleware(LaravelResponseTimeMiddleware::class); + $this->registerMetricsRoute(); } /** @@ -46,7 +37,6 @@ public function register() 'prometheus_exporter' ); - switch (config('prometheus_exporter.adapter')) { case 'apc': $this->app->bind('Prometheus\Storage\Adapter', 'Prometheus\Storage\APC'); diff --git a/src/LpeController.php b/src/LpeController.php deleted file mode 100644 index 5ff69d9..0000000 --- a/src/LpeController.php +++ /dev/null @@ -1,42 +0,0 @@ -lpeManager = $lpeManager; - } - - /** - * metric - * - * Expose metrics for prometheus - * - * @return Response - */ - public function metrics() - { - $renderer = new RenderTextFormat(); - - return response($renderer->render($this->lpeManager->getMetricFamilySamples())) - ->header('Content-Type', $renderer::MIME_TYPE); - } -} diff --git a/src/LpeManager.php b/src/LpeManager.php deleted file mode 100644 index 4f7444a..0000000 --- a/src/LpeManager.php +++ /dev/null @@ -1,99 +0,0 @@ - - * @package Traum-ferienwohnungen\PrometheusExporter - */ -class LpeManager -{ - /** - * @var CollectorRegistry - */ - protected $registry; - - /** - * @var Counter - */ - protected $requestCounter; - - /** - * @var Counter - */ - protected $requestDurationCounter; - - /** - * LpeManager constructor. - * - * @param CollectorRegistry $registry - */ - public function __construct(CollectorRegistry $registry, Router $router) - { - $this->registry = $registry; - $routeNames = []; - foreach($router->getRoutes() as $route){ - $routeNames[] = $route->getName(); - } - $this->initRouteMetrics($routeNames); - } - - public function initRouteMetrics(array $routes) - { - static $run = false; - if (!$run){ - $run = true; - - $namespace = config('prometheus_exporter.namespace_http_server'); - $labelNames = $this->getRequestCounterLabelNames(); - - $name = 'requests_total'; - $help = 'number of http requests'; - $this->requestCounter = $this->registry->registerCounter($namespace, $name, $help, $labelNames); - - $name = 'requests_latency_milliseconds'; - $help = 'duration of http_requests'; - $this->requestDurationCounter = $this->registry->registerCounter($namespace, $name, $help, $labelNames); - - foreach ($routes as $route) { - foreach (config('prometheus_exporter.init_metrics_for_http_methods') as $method) { - foreach (config('prometheus_exporter.init_metrics_for_http_status_codes') as $statusCode) { - $labelValues = [(string)$route, (string)$method, (string) $statusCode]; - $this->requestCounter->incBy(0, $labelValues); - $this->requestDurationCounter->incBy(0.0, $labelValues); - } - } - } - } - } - - protected function getRequestCounterLabelNames() - { - return [ - 'route', 'method', 'status_code', - ]; - } - - public function countRequest($route, $method, $statusCode, $duration_milliseconds) - { - $labelValues = [(string)$route, (string)$method, (string) $statusCode]; - $this->requestCounter->inc($labelValues); - $this->requestDurationCounter->incBy($duration_milliseconds, $labelValues); - } - - /** - * Get metric family samples - * - * @return \Prometheus\MetricFamilySamples[] - */ - public function getMetricFamilySamples() - { - return $this->registry->getMetricFamilySamples(); - } -} diff --git a/src/LumenController.php b/src/LumenController.php new file mode 100644 index 0000000..79119a9 --- /dev/null +++ b/src/LumenController.php @@ -0,0 +1,31 @@ +render($registry->getMetricFamilySamples())) + ->header('Content-Type', $renderer::MIME_TYPE); + } +} diff --git a/src/LumenServiceProvider.php b/src/LumenServiceProvider.php new file mode 100644 index 0000000..a115ca5 --- /dev/null +++ b/src/LumenServiceProvider.php @@ -0,0 +1,60 @@ +app->configure('prometheus_exporter'); + $this->mergeConfigFrom($source, 'prometheus_exporter'); + + $this->app->middleware([ + 'prometheusexporter' => 'traumferienwohnungen\PrometheusExporter\Middleware\LumenResponseTimeMiddleware' + ]); + + } + + /** + * Register the service provider. + */ + public function register() + { + $this->mergeConfigFrom( + __DIR__.'/config/config.php', + 'prometheus_exporter' + ); + + switch (config('prometheus_exporter.adapter')) { + case 'apc': + $this->app->bind('Prometheus\Storage\Adapter', 'Prometheus\Storage\APC'); + break; + case 'redis': + $this->app->bind('Prometheus\Storage\Adapter', function($app){ + return new \Prometheus\Storage\Redis(config('prometheus_exporter.redis')); + }); + break; + default: + throw new \ErrorException('"prometheus_exporter.adapter" must be either apc or redis'); + } + + $this->app->singleton(CollectorRegistry::class, + function ($app){ + return new CollectorRegistry(app(\Prometheus\Storage\Adapter::class)); + }); + } + +} diff --git a/src/Middleware/AbstractResponseTimeMiddleware.php b/src/Middleware/AbstractResponseTimeMiddleware.php index 055484e..3b609ec 100644 --- a/src/Middleware/AbstractResponseTimeMiddleware.php +++ b/src/Middleware/AbstractResponseTimeMiddleware.php @@ -1,13 +1,14 @@ lpeManager = $lpeManager; + $this->registry = $registry; + $this->initRouteMetrics($this->getRouteNames()); } /** @@ -45,18 +57,75 @@ public function handle(Request $request, Closure $next) /** @var \Illuminate\Http\Response $response */ $response = $next($request); - $duration = microtime(true) - $start; - $duration_milliseconds = $duration * 1000.0; $route_name = $this->getRouteName(); $method = $request->getMethod(); $status = $response->getStatusCode(); - $this->lpeManager->countRequest($route_name, $method, $status, $duration_milliseconds); + $duration = microtime(true) - $start; + $duration_milliseconds = $duration * 1000.0; + $this->countRequest($route_name, $method, $status, $duration_milliseconds); return $response; } + /** + * @param $routeNames string[] + * @throws \Prometheus\Exception\MetricsRegistrationException + */ + public function initRouteMetrics($routeNames) + { + $namespace = config('prometheus_exporter.namespace_http_server'); + $labelNames = $this->getRequestCounterLabelNames(); + + $name = 'requests_total'; + $help = 'number of http requests'; + $this->requestCounter = $this->registry->getOrRegisterCounter($namespace, $name, $help, $labelNames); + + $name = 'requests_latency_milliseconds'; + $help = 'duration of http_requests'; + $this->requestDurationCounter = $this->registry->getOrRegisterCounter($namespace, $name, $help, $labelNames); + + foreach ($routeNames as $route) { + foreach (config('prometheus_exporter.init_metrics_for_http_methods') as $method) { + foreach (config('prometheus_exporter.init_metrics_for_http_status_codes') as $statusCode) { + $labelValues = [(string)$route, (string)$method, (string) $statusCode]; + $this->requestCounter->incBy(0, $labelValues); + $this->requestDurationCounter->incBy(0.0, $labelValues); + } + } + } + } + + protected function getRequestCounterLabelNames() + { + return [ + 'route', 'method', 'status_code', + ]; + } + + public function countRequest($route, $method, $statusCode, $duration_milliseconds) + { + $labelValues = [(string)$route, (string)$method, (string) $statusCode]; + $this->requestCounter->inc($labelValues); + $this->requestDurationCounter->incBy($duration_milliseconds, $labelValues); + } + + /** + * Get metric family samples + * + * @return \Prometheus\MetricFamilySamples[] + */ + public function getMetricFamilySamples() + { + return $this->registry->getMetricFamilySamples(); + } + + /** + * @return string[] + */ + abstract protected function getRouteNames(); + /** * Get route name * diff --git a/src/Middleware/LaravelResponseTimeMiddleware.php b/src/Middleware/LaravelResponseTimeMiddleware.php index f51e69f..b5c15cb 100644 --- a/src/Middleware/LaravelResponseTimeMiddleware.php +++ b/src/Middleware/LaravelResponseTimeMiddleware.php @@ -1,12 +1,21 @@ getName() ?: "unnamed"; + } + return $routeNames; + } + /** * Get route name * diff --git a/src/Middleware/LumenResponseTimeMiddleware.php b/src/Middleware/LumenResponseTimeMiddleware.php index 8f9741c..01b25ea 100644 --- a/src/Middleware/LumenResponseTimeMiddleware.php +++ b/src/Middleware/LumenResponseTimeMiddleware.php @@ -1,12 +1,25 @@ extractRouteName($route['action']); + }; + return array_unique($routeNames); + } + /** * Get route name * @@ -14,7 +27,20 @@ class LumenResponseTimeMiddleware extends AbstractResponseTimeMiddleware */ protected function getRouteName() { - $route_info = $this->request->route()[1]; - return array_key_exists('as', $route_info) ? $route_info['as']: 'unnamed'; + $route_info = $this->request->route(); + if ( NULL === $route_info){ + return 'unknown'; + } + + return $this->extractRouteName($route_info[1]); + } + + /** + * @param $routeInfo array + * @return string + */ + protected function extractRouteName($routeInfo) + { + return array_key_exists('as', $routeInfo) ? $routeInfo['as']: 'unnamed'; } } diff --git a/src/laravel_routes.php b/src/laravel_routes.php index 12f3155..9aedcd3 100644 --- a/src/laravel_routes.php +++ b/src/laravel_routes.php @@ -1,3 +1,3 @@ name('metrics'); +Route::get('metrics', \traumferienwohnungen\PrometheusExporter\LaravelController::class . '@metrics')->name('metrics'); diff --git a/tests/Middleware/LaravelMiddlewareTest.php b/tests/Middleware/LaravelMiddlewareTest.php index dbc3371..189f596 100644 --- a/tests/Middleware/LaravelMiddlewareTest.php +++ b/tests/Middleware/LaravelMiddlewareTest.php @@ -1,15 +1,34 @@ handle(new \Illuminate\Http\Request(), function(){ + $mockCounter = Mockery::mock(Counter::class); + $mockCounter->shouldReceive('inc')->once(); + $mockCounter->shouldReceive('incBy')->once(); + $mockRegistry = Mockery::mock(CollectorRegistry::class); + $mockRegistry->shouldReceive('getOrRegisterCounter')->twice()->andReturn( + $mockCounter + ); + + $middleware = new LaravelResponseTimeMiddleware($mockRegistry); + + Route::shouldReceive('getRoutes'); + Route::shouldReceive('currentRouteName'); + $requestMock = Mockery::mock(Request::class); + $requestMock->shouldReceive('getMethod')->once(); + + $middleware->handle( + $requestMock, function(){ return new \Illuminate\Http\Response(); }); } diff --git a/tests/Middleware/LumenMiddlewareTest.php b/tests/Middleware/LumenMiddlewareTest.php new file mode 100644 index 0000000..154cb11 --- /dev/null +++ b/tests/Middleware/LumenMiddlewareTest.php @@ -0,0 +1,44 @@ +shouldReceive('inc')->once(); + $counter->shouldReceive('incBy')->once(); + $registry = Mockery::mock(CollectorRegistry::class); + $registry->shouldReceive('getOrRegisterCounter')->twice()->andReturn( + $counter + ); + + $middleware = new LumenResponseTimeMiddleware($registry); + + $request = Mockery::mock(Request::class); + $request->shouldReceive('route')->once()->andReturn($this->getTestRouteObject()); + $request->shouldReceive('getMethod')->once(); + + $middleware->handle( + $request, function(){ + return new \Illuminate\Http\Response(); + }); + } + + private function getTestRouteObject() + { + return [ + 0 => true, + 1 => [ + 'as' => 'root', + 0 => [] + ], + 2 => [] + ]; + } +}