Skip to content

Commit

Permalink
Merge pull request #5 from leventcz/feature/recording-mode
Browse files Browse the repository at this point in the history
  • Loading branch information
leventcz authored May 27, 2024
2 parents 0c3562b + 85b0096 commit 6373342
Show file tree
Hide file tree
Showing 11 changed files with 161 additions and 51 deletions.
61 changes: 50 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)

<a href="https://trendshift.io/repositories/10338" target="_blank"><img src="https://trendshift.io/api/badge/repositories/10338" alt="leventcz%2Flaravel-top | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>

<p align="center"><img src="/art/top.gif" alt="Real-time monitoring with Laravel Top"></p>

```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

## 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.

**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** 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

Expand All @@ -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
Expand All @@ -37,12 +41,37 @@ php artisan vendor:publish --tag="top"
<?php

return [

/*
* Provide a redis connection from config/database.php
|--------------------------------------------------------------------------
| Redis Connection
|--------------------------------------------------------------------------
|
| Specify the Redis database connection from config/database.php
| that Top will use to save data.
| The default value is suitable for most applications.
|
*/
'connection' => 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
Expand All @@ -55,32 +84,42 @@ 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;
$route->method;
$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

Expand Down
26 changes: 25 additions & 1 deletion config/top.php
Original file line number Diff line number Diff line change
@@ -1,8 +1,32 @@
<?php

return [

/*
* Provide a redis connection from config/database.php
|--------------------------------------------------------------------------
| Redis Connection
|--------------------------------------------------------------------------
|
| Specify the Redis database connection from config/database.php
| that Top will use to save data.
| The default value is suitable for most applications.
|
*/

'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'),
];
4 changes: 3 additions & 1 deletion src/Commands/TopCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,14 @@ public function handle(GuiBuilder $guiBuilder): void
$guiBuilder
->exitAlternateScreen()
->showCursor();
exit();
exit(0);
});
}

private function feed(GuiBuilder $guiBuilder): void
{
Top::startRecording();

$guiBuilder
->setRequestSummary(Top::http())
->setDatabaseSummary(Top::database())
Expand Down
6 changes: 6 additions & 0 deletions src/Contracts/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
3 changes: 3 additions & 0 deletions src/Facades/Top.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
{
Expand Down
10 changes: 10 additions & 0 deletions src/Listeners/RequestListener.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -33,4 +38,9 @@ public function subscribe(): array
RequestHandled::class => 'requestHandled',
];
}

private function shouldRecord(): bool
{
return config('top.recording_mode') === 'always' || Top::isRecording();
}
}
38 changes: 22 additions & 16 deletions src/Repositories/RedisRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -17,17 +15,18 @@

readonly class RedisRepository implements Repository
{
private const TOP_STATUS_KEY = 'top-status';

public function __construct(
private RedisFactory $factory,
private Config $config
private Connection $connection
) {
}

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";
Expand Down Expand Up @@ -208,16 +207,30 @@ public function getTopRoutes(): RouteCollection
return RouteCollection::fromArray($this->execute($script));
}

public function recorderExists(): bool
{
return $this->connection->exists(self::TOP_STATUS_KEY) === 1; // @phpstan-ignore-line
}

public function setRecorder(int $duration = 5): void
{
$this->connection->setex(self::TOP_STATUS_KEY, $duration, true); // @phpstan-ignore-line
}

public function deleteRecorder(): void
{
$this->connection->del(self::TOP_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++) {
Expand All @@ -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);
}
}
25 changes: 17 additions & 8 deletions src/ServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Leventcz\Top;

use Illuminate\Events\Dispatcher;
use Illuminate\Contracts\Redis\Factory;
use Illuminate\Foundation\Application;
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
use Leventcz\Top\Commands\TopCommand;
Expand All @@ -20,14 +20,23 @@ 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)
);
});
}

public function boot(Dispatcher $dispatcher): void
public function boot(): void
{
if ($this->app->runningInConsole()) {
$this->commands([TopCommand::class]);
Expand All @@ -36,8 +45,8 @@ public function boot(Dispatcher $dispatcher): void
return;
}

$dispatcher->subscribe(RequestListener::class);
$dispatcher->subscribe(CacheListener::class);
$dispatcher->subscribe(DatabaseListener::class);
$this->app->make('events')->subscribe(RequestListener::class);
$this->app->make('events')->subscribe(CacheListener::class);
$this->app->make('events')->subscribe(DatabaseListener::class);
}
}
15 changes: 15 additions & 0 deletions src/TopManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
}
14 changes: 1 addition & 13 deletions tests/Repositories/RedisRepository.php
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
<?php

use Illuminate\Config\Repository;
use Illuminate\Contracts\Redis\Factory as RedisFactory;
use Illuminate\Redis\Connections\Connection;
use Leventcz\Top\Data\CacheSummary;
use Leventcz\Top\Data\DatabaseSummary;
Expand All @@ -12,18 +10,8 @@
use Leventcz\Top\Repositories\RedisRepository;

beforeEach(function () {
$this->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 () {
Expand Down
Loading

0 comments on commit 6373342

Please sign in to comment.