From c7c7059fe62922d1e1383c8cf396039f745f22e3 Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Sun, 26 May 2024 16:48:19 +0300 Subject: [PATCH 01/15] new methods --- src/Contracts/Repository.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Contracts/Repository.php b/src/Contracts/Repository.php index 259124f..6263583 100644 --- a/src/Contracts/Repository.php +++ b/src/Contracts/Repository.php @@ -22,4 +22,10 @@ public function getDatabaseSummary(): DatabaseSummary; public function getCacheSummary(): CacheSummary; public function getTopRoutes(): RouteCollection; + + public function recorderExists(): bool; + + public function setRecorder(int $ttl = 5): void; + + public function deleteRecorder(): void; } From 6d89589acb8dd33d21defedcebbc1cd5ed33cbb0 Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Sun, 26 May 2024 16:48:56 +0300 Subject: [PATCH 02/15] recorder methods implemented --- src/Repositories/RedisRepository.php | 38 ++++++++++++++++------------ 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/Repositories/RedisRepository.php b/src/Repositories/RedisRepository.php index 5283e5d..96dcd88 100644 --- a/src/Repositories/RedisRepository.php +++ b/src/Repositories/RedisRepository.php @@ -4,8 +4,6 @@ namespace Leventcz\Top\Repositories; -use Illuminate\Config\Repository as Config; -use Illuminate\Contracts\Redis\Factory as RedisFactory; use Illuminate\Redis\Connections\Connection; use Leventcz\Top\Contracts\Repository; use Leventcz\Top\Data\CacheSummary; @@ -17,9 +15,10 @@ readonly class RedisRepository implements Repository { + private const STATUS_KEY = 'top-status'; + public function __construct( - private RedisFactory $factory, - private Config $config + private Connection $connection ) { } @@ -27,7 +26,7 @@ public function save(HandledRequest $request, EventCounter $eventCounter): void { // @phpstan-ignore-next-line $this - ->connection() + ->connection ->pipeline(function ($pipe) use ($request, $eventCounter) { $key = "top-requests:$request->timestamp"; $routeKey = "$request->method:$request->uri:data"; @@ -208,16 +207,30 @@ public function getTopRoutes(): RouteCollection return RouteCollection::fromArray($this->execute($script)); } + public function recorderExists(): bool + { + return $this->connection->exists(self::STATUS_KEY) === 1; // @phpstan-ignore-line + } + + public function setRecorder(int $duration = 5): void + { + $this->connection->setex(self::STATUS_KEY, $duration, true); // @phpstan-ignore-line + } + + public function deleteRecorder(): void + { + $this->connection->del(self::STATUS_KEY); // @phpstan-ignore-line + } + private function execute(string $script): array { - $keys = $this->buildKeys(now()->getTimestamp()); - // @phpstan-ignore-next-line - $result = $this->connection()->eval($script, count($keys), ...$keys); + $keys = $this->generateKeys(now()->getTimestamp()); + $result = $this->connection->eval($script, count($keys), ...$keys); // @phpstan-ignore-line return json_decode($result, true); } - private function buildKeys(int $timestamp): array + private function generateKeys(int $timestamp): array { $keys = []; for ($i = 0; $i < 5; $i++) { @@ -226,11 +239,4 @@ private function buildKeys(int $timestamp): array return $keys; } - - private function connection(): Connection - { - $connection = $this->config->get('top.connection'); - - return $this->factory->connection($connection); - } } From 75c6759e500f702b6830a8702babc8966e989020 Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Sun, 26 May 2024 16:49:30 +0300 Subject: [PATCH 03/15] tests updated with recorder implementation --- tests/Repositories/RedisRepository.php | 14 +------------- tests/ServiceProvider.php | 10 +++++++++- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/tests/Repositories/RedisRepository.php b/tests/Repositories/RedisRepository.php index c389339..70f66eb 100644 --- a/tests/Repositories/RedisRepository.php +++ b/tests/Repositories/RedisRepository.php @@ -1,7 +1,5 @@ redisFactory = Mockery::mock(RedisFactory::class); $this->connection = Mockery::mock(Connection::class); - $this->config = Mockery::mock(Repository::class); - - $this->redisFactory - ->shouldReceive('connection') - ->andReturn($this->connection); - $this->config - ->shouldReceive('get') - ->andReturn('top.connection'); - - $this->repository = new RedisRepository($this->redisFactory, $this->config); + $this->repository = new RedisRepository($this->connection); }); afterEach(function () { diff --git a/tests/ServiceProvider.php b/tests/ServiceProvider.php index 98648b1..8c2a2a1 100644 --- a/tests/ServiceProvider.php +++ b/tests/ServiceProvider.php @@ -3,6 +3,7 @@ use Illuminate\Config\Repository; use Illuminate\Contracts\Redis\Factory; use Illuminate\Foundation\Application; +use Illuminate\Redis\Connections\Connection; use Illuminate\Redis\RedisManager; use Leventcz\Top\Facades\State; use Leventcz\Top\Facades\Top; @@ -11,9 +12,16 @@ use Leventcz\Top\TopManager; beforeEach(function () { + $this->redis = Mockery::mock(RedisManager::class); + $this->connection = Mockery::mock(Connection::class); + $this + ->redis + ->shouldReceive('connection') + ->andReturn($this->connection); + $this->app = new Application(); $this->app->bind('config', fn () => new Repository()); - $this->app->bind(Factory::class, fn () => Mockery::mock(RedisManager::class)); + $this->app->bind(Factory::class, fn () => $this->redis); $this->app->register(ServiceProvider::class); }); From 25b6742ccf7c66eaf3cbe417b9394f793bb20de9 Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Sun, 26 May 2024 16:49:50 +0300 Subject: [PATCH 04/15] wip - recorder --- src/ServiceProvider.php | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index ec801d3..9bc0fe8 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -4,6 +4,7 @@ namespace Leventcz\Top; +use Illuminate\Contracts\Redis\Factory; use Illuminate\Events\Dispatcher; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider as BaseServiceProvider; @@ -20,10 +21,19 @@ class ServiceProvider extends BaseServiceProvider public function register(): void { $this->mergeConfigFrom(__DIR__.'/../config/top.php', 'top'); - $this->app->singleton(Repository::class, RedisRepository::class); $this->app->singleton('top', TopManager::class); - $this->app->bind('top.state', function (Application $app) { - return new StateManager($app->make(EventCounter::class), $app->make(Repository::class)); + $this->app->singleton(Repository::class, function (Application $application) { + $connection = $application + ->make(Factory::class) + ->connection($application['config']->get('top.connection')); + + return new RedisRepository($connection); + }); + $this->app->bind('top.state', function (Application $application) { + return new StateManager( + $application->make(EventCounter::class), + $application->make(Repository::class) + ); }); } @@ -36,8 +46,16 @@ public function boot(Dispatcher $dispatcher): void return; } - $dispatcher->subscribe(RequestListener::class); - $dispatcher->subscribe(CacheListener::class); - $dispatcher->subscribe(DatabaseListener::class); + // todo: octane support + if ($this->shouldRecord()) { + $dispatcher->subscribe(RequestListener::class); + $dispatcher->subscribe(CacheListener::class); + $dispatcher->subscribe(DatabaseListener::class); + } + } + + private function shouldRecord(): bool + { + return $this->app['config']->get('top.recording_mode') === 'always' || $this->app['top']->isRecording(); } } From fafc3b7c9191d83f2d0ef1878d5887f923bb4f02 Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Sun, 26 May 2024 16:50:18 +0300 Subject: [PATCH 05/15] new methods for top --- src/Facades/Top.php | 3 +++ src/TopManager.php | 15 +++++++++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Facades/Top.php b/src/Facades/Top.php index b20108b..2700fcc 100644 --- a/src/Facades/Top.php +++ b/src/Facades/Top.php @@ -15,6 +15,9 @@ * @method static DatabaseSummary database() * @method static CacheSummary cache() * @method static RouteCollection routes() + * @method static void startRecording(int $duration = 5) + * @method static void stopRecording() + * @method static bool isRecording() */ class Top extends Facade { diff --git a/src/TopManager.php b/src/TopManager.php index 779f432..e75f732 100644 --- a/src/TopManager.php +++ b/src/TopManager.php @@ -36,4 +36,19 @@ public function routes(): RouteCollection { return $this->repository->getTopRoutes(); } + + public function startRecording(int $duration = 5): void + { + $this->repository->setRecorder($duration); + } + + public function stopRecording(): void + { + $this->repository->deleteRecorder(); + } + + public function isRecording(): bool + { + return $this->repository->recorderExists(); + } } From 51c4a719c68fd4faec4aea1fbbd9e1e84dcebc2e Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Sun, 26 May 2024 16:51:03 +0300 Subject: [PATCH 06/15] force recording while command is running --- src/Commands/TopCommand.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Commands/TopCommand.php b/src/Commands/TopCommand.php index 60f4178..7775185 100644 --- a/src/Commands/TopCommand.php +++ b/src/Commands/TopCommand.php @@ -32,12 +32,14 @@ public function handle(GuiBuilder $guiBuilder): void $guiBuilder ->exitAlternateScreen() ->showCursor(); - exit(); + exit(1); }); } private function feed(GuiBuilder $guiBuilder): void { + Top::startRecording(); + $guiBuilder ->setRequestSummary(Top::http()) ->setDatabaseSummary(Top::database()) From afb8e5634257f7bf21b354aadba0973e6709678f Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Sun, 26 May 2024 19:18:59 +0300 Subject: [PATCH 07/15] configure recording mode --- config/top.php | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/config/top.php b/config/top.php index 1d111a8..15b95ea 100644 --- a/config/top.php +++ b/config/top.php @@ -1,8 +1,32 @@ env('TOP_REDIS_CONNECTION', 'default'), + + /* + |-------------------------------------------------------------------------- + | Recording Mode + |-------------------------------------------------------------------------- + | + | Determine when Top should record application metrics based on this value. + | By default, Top only listens to your application when it is running. + | If you want to access metrics through the facade, you can select the 'always' mode. + | + | Available Modes: "while_running", "always" + | + */ + + 'recording_mode' => env('TOP_RECORDING_MODE', 'while_running'), ]; From d0ff2c0f9e434ee3f983e5795aa9eaaff6f46ef8 Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Mon, 27 May 2024 03:27:02 +0300 Subject: [PATCH 08/15] check recording mode --- src/Listeners/RequestListener.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Listeners/RequestListener.php b/src/Listeners/RequestListener.php index f2de84d..f569d88 100644 --- a/src/Listeners/RequestListener.php +++ b/src/Listeners/RequestListener.php @@ -7,11 +7,16 @@ use Illuminate\Foundation\Http\Events\RequestHandled; use Leventcz\Top\Data\HandledRequest; use Leventcz\Top\Facades\State; +use Leventcz\Top\Facades\Top; readonly class RequestListener { public function requestHandled(RequestHandled $event): void { + if (! $this->shouldRecord()) { + return; + } + $startTime = defined('LARAVEL_START') ? LARAVEL_START : $event->request->server('REQUEST_TIME_FLOAT'); $memory = memory_get_peak_usage(true) / 1024 / 1024; $duration = $startTime ? floor((microtime(true) - $startTime) * 1000) : null; @@ -33,4 +38,9 @@ public function subscribe(): array RequestHandled::class => 'requestHandled', ]; } + + private function shouldRecord(): bool + { + return config('top.recording_mode') === 'always' || Top::isRecording(); + } } From dee8aa040a312d5e0455ac848a797f3a5970e109 Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Mon, 27 May 2024 03:28:20 +0300 Subject: [PATCH 09/15] octane compatibility --- src/ServiceProvider.php | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/ServiceProvider.php b/src/ServiceProvider.php index 9bc0fe8..0d4e517 100644 --- a/src/ServiceProvider.php +++ b/src/ServiceProvider.php @@ -5,7 +5,6 @@ namespace Leventcz\Top; use Illuminate\Contracts\Redis\Factory; -use Illuminate\Events\Dispatcher; use Illuminate\Foundation\Application; use Illuminate\Support\ServiceProvider as BaseServiceProvider; use Leventcz\Top\Commands\TopCommand; @@ -37,7 +36,7 @@ public function register(): void }); } - public function boot(Dispatcher $dispatcher): void + public function boot(): void { if ($this->app->runningInConsole()) { $this->commands([TopCommand::class]); @@ -46,16 +45,8 @@ public function boot(Dispatcher $dispatcher): void return; } - // todo: octane support - if ($this->shouldRecord()) { - $dispatcher->subscribe(RequestListener::class); - $dispatcher->subscribe(CacheListener::class); - $dispatcher->subscribe(DatabaseListener::class); - } - } - - private function shouldRecord(): bool - { - return $this->app['config']->get('top.recording_mode') === 'always' || $this->app['top']->isRecording(); + $this->app->make('events')->subscribe(RequestListener::class); + $this->app->make('events')->subscribe(CacheListener::class); + $this->app->make('events')->subscribe(DatabaseListener::class); } } From 65cffa21fbb5f3d31ad9413c10a4499268583734 Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Mon, 27 May 2024 04:05:30 +0300 Subject: [PATCH 10/15] config for recording mode --- config/top.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/config/top.php b/config/top.php index 15b95ea..36d1f51 100644 --- a/config/top.php +++ b/config/top.php @@ -4,7 +4,7 @@ /* |-------------------------------------------------------------------------- - | Connection + | Redis Connection |-------------------------------------------------------------------------- | | Specify the Redis database connection from config/database.php @@ -24,9 +24,9 @@ | By default, Top only listens to your application when it is running. | If you want to access metrics through the facade, you can select the 'always' mode. | - | Available Modes: "while_running", "always" + | Available Modes: "runtime", "always" | */ - 'recording_mode' => env('TOP_RECORDING_MODE', 'while_running'), + 'recording_mode' => env('TOP_RECORDING_MODE', 'runtime'), ]; From 64d13232c59d32aaf985d63990d6cae60c296d67 Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Mon, 27 May 2024 04:07:32 +0300 Subject: [PATCH 11/15] config for recording mode --- config/top.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/top.php b/config/top.php index 36d1f51..7bb5484 100644 --- a/config/top.php +++ b/config/top.php @@ -22,7 +22,7 @@ | | Determine when Top should record application metrics based on this value. | By default, Top only listens to your application when it is running. - | If you want to access metrics through the facade, you can select the 'always' mode. + | If you want to access metrics through the facade, you can select the "always" mode. | | Available Modes: "runtime", "always" | From 08d2ece493d4d3c77c20ae351020cc9c081d2501 Mon Sep 17 00:00:00 2001 From: Levent Corapsiz Date: Mon, 27 May 2024 04:35:20 +0300 Subject: [PATCH 12/15] readme updated --- README.md | 59 +++++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 49 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 11c91be..d5b5e8b 100644 --- a/README.md +++ b/README.md @@ -2,16 +2,22 @@ [![GitHub Tests Action Status](https://img.shields.io/github/actions/workflow/status/leventcz/laravel-top/tests.yml?branch=1.x&label=tests&style=flat-square)](https://github.com/leventcz/laravel-top/actions) [![Licence](https://img.shields.io/github/license/leventcz/laravel-top.svg?style=flat-square)](https://github.com/leventcz/laravel-top/actions) +leventcz%2Flaravel-top | Trendshift +

Real-time monitoring with Laravel Top

```php php artisan top ``` -**Top** provides real-time monitoring directly from the command line for Laravel applications. It is designed for production environments, enabling you to effortlessly track essential metrics and identify the busiest routes. +**Top** provides a lightweight solution for real-time monitoring directly from the command line for Laravel applications. It is designed for production environments, enabling you to effortlessly track essential metrics and identify the busiest routes. ## How it works? -**Top** listens to Laravel events and saves aggregated data to Redis hashes behind the scenes to calculate metrics. The aggregated data is stored with a short TTL, ensuring that historical data is not retained and preventing Redis from becoming overloaded. During display, metrics are calculated based on the average of the last 5 seconds of data. +**Top** listens to Laravel events and saves aggregated data to Redis behind the scenes to calculate metrics. The aggregated data is stored with a short TTL, ensuring that historical data is not retained and preventing Redis from becoming overloaded. During display, metrics are calculated based on the average of the last 5 seconds of data. + +**Top** only listens to events from incoming requests, so metrics from operations performed via queues or commands are not reflected. + +Since the data is stored in Redis, the output of the top command reflects data from all application servers, not just the server where you run the command. ## Installation @@ -25,8 +31,6 @@ composer require leventcz/laravel-top ## Configuration -By default, **Top** uses the default Redis connection. To change the connection, you need to edit the configuration file. - You can publish the config file with: ```bash @@ -37,12 +41,37 @@ php artisan vendor:publish --tag="top" env('TOP_REDIS_CONNECTION', 'default') + + 'connection' => env('TOP_REDIS_CONNECTION', 'default'), + + /* + |-------------------------------------------------------------------------- + | Recording Mode + |-------------------------------------------------------------------------- + | + | Determine when Top should record application metrics based on this value. + | By default, Top only listens to your application when it is running. + | If you want to access metrics through the facade, you can select the "always" mode. + | + | Available Modes: "runtime", "always" + | + */ + + 'recording_mode' => env('TOP_RECORDING_MODE', 'runtime'), ]; + ``` ## Facade @@ -55,20 +84,24 @@ If you want to access metrics in your application, you can use the **Top** facad use Leventcz\Top\Facades\Top; use Leventcz\Top\Data\Route; +// Retrieve HTTP request metrics $requestSummary = Top::http(); $requestSummary->averageRequestPerSecond; $requestSummary->averageMemoryUsage; $requestSummary->averageDuration; +// Retrieve database query metrics $databaseSummary = Top::database(); $databaseSummary->averageQueryPerSecond; $databaseSummary->averageQueryDuration; +// Retrieve cache operation metrics $cacheSummary = Top::cache(); $cacheSummary->averageHitPerSecond; $cacheSummary->averageMissPerSecond; $cacheSummary->averageWritePerSecond; +// Retrieve the top 20 busiest routes $topRoutes = Top::routes(); $topRoutes->each(function(Route $route) { $route->uri; @@ -76,11 +109,17 @@ $topRoutes->each(function(Route $route) { $route->averageRequestPerSecond; $route->averageMemoryUsage; $route->averageDuration; -}) -``` -## Changelog +}); -Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently. +// Force Top to start recording for the given duration (in seconds) +Top::startRecording(int $duration = 5); + +// Force Top to stop recording +Top::stopRecording(); + +// Check if Top is currently recording +Top::isRecording(); +``` ## Testing From 23cbcafeb08ac277955ea451a285be3c98d98c46 Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Mon, 27 May 2024 05:53:16 +0300 Subject: [PATCH 13/15] small fix --- src/Commands/TopCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Commands/TopCommand.php b/src/Commands/TopCommand.php index 7775185..d7a9d80 100644 --- a/src/Commands/TopCommand.php +++ b/src/Commands/TopCommand.php @@ -32,7 +32,7 @@ public function handle(GuiBuilder $guiBuilder): void $guiBuilder ->exitAlternateScreen() ->showCursor(); - exit(1); + exit(0); }); } From 840b90f034d2d45d9aa65c0180e108554c22f27b Mon Sep 17 00:00:00 2001 From: Levent Corapsiz Date: Mon, 27 May 2024 05:56:26 +0300 Subject: [PATCH 14/15] readme updated --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index d5b5e8b..2db27a2 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ php artisan top ``` **Top** provides a lightweight solution for real-time monitoring directly from the command line for Laravel applications. It is designed for production environments, enabling you to effortlessly track essential metrics and identify the busiest routes. -## How it works? +## How it works **Top** listens to Laravel events and saves aggregated data to Redis behind the scenes to calculate metrics. The aggregated data is stored with a short TTL, ensuring that historical data is not retained and preventing Redis from becoming overloaded. During display, metrics are calculated based on the average of the last 5 seconds of data. From 85b0096c3b220e4b4e04c02e2cb05f2fdc400d8e Mon Sep 17 00:00:00 2001 From: leventcorapsiz Date: Mon, 27 May 2024 06:13:44 +0300 Subject: [PATCH 15/15] const renamed --- src/Repositories/RedisRepository.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Repositories/RedisRepository.php b/src/Repositories/RedisRepository.php index 96dcd88..93497ee 100644 --- a/src/Repositories/RedisRepository.php +++ b/src/Repositories/RedisRepository.php @@ -15,7 +15,7 @@ readonly class RedisRepository implements Repository { - private const STATUS_KEY = 'top-status'; + private const TOP_STATUS_KEY = 'top-status'; public function __construct( private Connection $connection @@ -209,17 +209,17 @@ public function getTopRoutes(): RouteCollection public function recorderExists(): bool { - return $this->connection->exists(self::STATUS_KEY) === 1; // @phpstan-ignore-line + return $this->connection->exists(self::TOP_STATUS_KEY) === 1; // @phpstan-ignore-line } public function setRecorder(int $duration = 5): void { - $this->connection->setex(self::STATUS_KEY, $duration, true); // @phpstan-ignore-line + $this->connection->setex(self::TOP_STATUS_KEY, $duration, true); // @phpstan-ignore-line } public function deleteRecorder(): void { - $this->connection->del(self::STATUS_KEY); // @phpstan-ignore-line + $this->connection->del(self::TOP_STATUS_KEY); // @phpstan-ignore-line } private function execute(string $script): array