Skip to content

Commit 6373342

Browse files
authored
Merge pull request #5 from leventcz/feature/recording-mode
2 parents 0c3562b + 85b0096 commit 6373342

File tree

11 files changed

+161
-51
lines changed

11 files changed

+161
-51
lines changed

README.md

+50-11
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,22 @@
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)
33
[![Licence](https://img.shields.io/github/license/leventcz/laravel-top.svg?style=flat-square)](https://github.com/leventcz/laravel-top/actions)
44

5+
<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>
6+
57
<p align="center"><img src="/art/top.gif" alt="Real-time monitoring with Laravel Top"></p>
68

79
```php
810
php artisan top
911
```
10-
**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.
12+
**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.
13+
14+
## How it works
1115

12-
## How it works?
16+
**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.
1317

14-
**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.
18+
**Top** only listens to events from incoming requests, so metrics from operations performed via queues or commands are not reflected.
19+
20+
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.
1521

1622
## Installation
1723

@@ -25,8 +31,6 @@ composer require leventcz/laravel-top
2531

2632
## Configuration
2733

28-
By default, **Top** uses the default Redis connection. To change the connection, you need to edit the configuration file.
29-
3034
You can publish the config file with:
3135

3236
```bash
@@ -37,12 +41,37 @@ php artisan vendor:publish --tag="top"
3741
<?php
3842

3943
return [
44+
4045
/*
41-
* Provide a redis connection from config/database.php
46+
|--------------------------------------------------------------------------
47+
| Redis Connection
48+
|--------------------------------------------------------------------------
49+
|
50+
| Specify the Redis database connection from config/database.php
51+
| that Top will use to save data.
52+
| The default value is suitable for most applications.
53+
|
4254
*/
43-
'connection' => env('TOP_REDIS_CONNECTION', 'default')
55+
56+
'connection' => env('TOP_REDIS_CONNECTION', 'default'),
57+
58+
/*
59+
|--------------------------------------------------------------------------
60+
| Recording Mode
61+
|--------------------------------------------------------------------------
62+
|
63+
| Determine when Top should record application metrics based on this value.
64+
| By default, Top only listens to your application when it is running.
65+
| If you want to access metrics through the facade, you can select the "always" mode.
66+
|
67+
| Available Modes: "runtime", "always"
68+
|
69+
*/
70+
71+
'recording_mode' => env('TOP_RECORDING_MODE', 'runtime'),
4472
];
4573

74+
4675
```
4776

4877
## Facade
@@ -55,32 +84,42 @@ If you want to access metrics in your application, you can use the **Top** facad
5584
use Leventcz\Top\Facades\Top;
5685
use Leventcz\Top\Data\Route;
5786

87+
// Retrieve HTTP request metrics
5888
$requestSummary = Top::http();
5989
$requestSummary->averageRequestPerSecond;
6090
$requestSummary->averageMemoryUsage;
6191
$requestSummary->averageDuration;
6292

93+
// Retrieve database query metrics
6394
$databaseSummary = Top::database();
6495
$databaseSummary->averageQueryPerSecond;
6596
$databaseSummary->averageQueryDuration;
6697

98+
// Retrieve cache operation metrics
6799
$cacheSummary = Top::cache();
68100
$cacheSummary->averageHitPerSecond;
69101
$cacheSummary->averageMissPerSecond;
70102
$cacheSummary->averageWritePerSecond;
71103

104+
// Retrieve the top 20 busiest routes
72105
$topRoutes = Top::routes();
73106
$topRoutes->each(function(Route $route) {
74107
$route->uri;
75108
$route->method;
76109
$route->averageRequestPerSecond;
77110
$route->averageMemoryUsage;
78111
$route->averageDuration;
79-
})
80-
```
81-
## Changelog
112+
});
82113

83-
Please see [CHANGELOG](CHANGELOG.md) for more information what has changed recently.
114+
// Force Top to start recording for the given duration (in seconds)
115+
Top::startRecording(int $duration = 5);
116+
117+
// Force Top to stop recording
118+
Top::stopRecording();
119+
120+
// Check if Top is currently recording
121+
Top::isRecording();
122+
```
84123

85124
## Testing
86125

config/top.php

+25-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,32 @@
11
<?php
22

33
return [
4+
45
/*
5-
* Provide a redis connection from config/database.php
6+
|--------------------------------------------------------------------------
7+
| Redis Connection
8+
|--------------------------------------------------------------------------
9+
|
10+
| Specify the Redis database connection from config/database.php
11+
| that Top will use to save data.
12+
| The default value is suitable for most applications.
13+
|
614
*/
15+
716
'connection' => env('TOP_REDIS_CONNECTION', 'default'),
17+
18+
/*
19+
|--------------------------------------------------------------------------
20+
| Recording Mode
21+
|--------------------------------------------------------------------------
22+
|
23+
| Determine when Top should record application metrics based on this value.
24+
| By default, Top only listens to your application when it is running.
25+
| If you want to access metrics through the facade, you can select the "always" mode.
26+
|
27+
| Available Modes: "runtime", "always"
28+
|
29+
*/
30+
31+
'recording_mode' => env('TOP_RECORDING_MODE', 'runtime'),
832
];

src/Commands/TopCommand.php

+3-1
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,14 @@ public function handle(GuiBuilder $guiBuilder): void
3232
$guiBuilder
3333
->exitAlternateScreen()
3434
->showCursor();
35-
exit();
35+
exit(0);
3636
});
3737
}
3838

3939
private function feed(GuiBuilder $guiBuilder): void
4040
{
41+
Top::startRecording();
42+
4143
$guiBuilder
4244
->setRequestSummary(Top::http())
4345
->setDatabaseSummary(Top::database())

src/Contracts/Repository.php

+6
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,10 @@ public function getDatabaseSummary(): DatabaseSummary;
2222
public function getCacheSummary(): CacheSummary;
2323

2424
public function getTopRoutes(): RouteCollection;
25+
26+
public function recorderExists(): bool;
27+
28+
public function setRecorder(int $ttl = 5): void;
29+
30+
public function deleteRecorder(): void;
2531
}

src/Facades/Top.php

+3
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
* @method static DatabaseSummary database()
1616
* @method static CacheSummary cache()
1717
* @method static RouteCollection routes()
18+
* @method static void startRecording(int $duration = 5)
19+
* @method static void stopRecording()
20+
* @method static bool isRecording()
1821
*/
1922
class Top extends Facade
2023
{

src/Listeners/RequestListener.php

+10
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,16 @@
77
use Illuminate\Foundation\Http\Events\RequestHandled;
88
use Leventcz\Top\Data\HandledRequest;
99
use Leventcz\Top\Facades\State;
10+
use Leventcz\Top\Facades\Top;
1011

1112
readonly class RequestListener
1213
{
1314
public function requestHandled(RequestHandled $event): void
1415
{
16+
if (! $this->shouldRecord()) {
17+
return;
18+
}
19+
1520
$startTime = defined('LARAVEL_START') ? LARAVEL_START : $event->request->server('REQUEST_TIME_FLOAT');
1621
$memory = memory_get_peak_usage(true) / 1024 / 1024;
1722
$duration = $startTime ? floor((microtime(true) - $startTime) * 1000) : null;
@@ -33,4 +38,9 @@ public function subscribe(): array
3338
RequestHandled::class => 'requestHandled',
3439
];
3540
}
41+
42+
private function shouldRecord(): bool
43+
{
44+
return config('top.recording_mode') === 'always' || Top::isRecording();
45+
}
3646
}

src/Repositories/RedisRepository.php

+22-16
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@
44

55
namespace Leventcz\Top\Repositories;
66

7-
use Illuminate\Config\Repository as Config;
8-
use Illuminate\Contracts\Redis\Factory as RedisFactory;
97
use Illuminate\Redis\Connections\Connection;
108
use Leventcz\Top\Contracts\Repository;
119
use Leventcz\Top\Data\CacheSummary;
@@ -17,17 +15,18 @@
1715

1816
readonly class RedisRepository implements Repository
1917
{
18+
private const TOP_STATUS_KEY = 'top-status';
19+
2020
public function __construct(
21-
private RedisFactory $factory,
22-
private Config $config
21+
private Connection $connection
2322
) {
2423
}
2524

2625
public function save(HandledRequest $request, EventCounter $eventCounter): void
2726
{
2827
// @phpstan-ignore-next-line
2928
$this
30-
->connection()
29+
->connection
3130
->pipeline(function ($pipe) use ($request, $eventCounter) {
3231
$key = "top-requests:$request->timestamp";
3332
$routeKey = "$request->method:$request->uri:data";
@@ -208,16 +207,30 @@ public function getTopRoutes(): RouteCollection
208207
return RouteCollection::fromArray($this->execute($script));
209208
}
210209

210+
public function recorderExists(): bool
211+
{
212+
return $this->connection->exists(self::TOP_STATUS_KEY) === 1; // @phpstan-ignore-line
213+
}
214+
215+
public function setRecorder(int $duration = 5): void
216+
{
217+
$this->connection->setex(self::TOP_STATUS_KEY, $duration, true); // @phpstan-ignore-line
218+
}
219+
220+
public function deleteRecorder(): void
221+
{
222+
$this->connection->del(self::TOP_STATUS_KEY); // @phpstan-ignore-line
223+
}
224+
211225
private function execute(string $script): array
212226
{
213-
$keys = $this->buildKeys(now()->getTimestamp());
214-
// @phpstan-ignore-next-line
215-
$result = $this->connection()->eval($script, count($keys), ...$keys);
227+
$keys = $this->generateKeys(now()->getTimestamp());
228+
$result = $this->connection->eval($script, count($keys), ...$keys); // @phpstan-ignore-line
216229

217230
return json_decode($result, true);
218231
}
219232

220-
private function buildKeys(int $timestamp): array
233+
private function generateKeys(int $timestamp): array
221234
{
222235
$keys = [];
223236
for ($i = 0; $i < 5; $i++) {
@@ -226,11 +239,4 @@ private function buildKeys(int $timestamp): array
226239

227240
return $keys;
228241
}
229-
230-
private function connection(): Connection
231-
{
232-
$connection = $this->config->get('top.connection');
233-
234-
return $this->factory->connection($connection);
235-
}
236242
}

src/ServiceProvider.php

+17-8
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
namespace Leventcz\Top;
66

7-
use Illuminate\Events\Dispatcher;
7+
use Illuminate\Contracts\Redis\Factory;
88
use Illuminate\Foundation\Application;
99
use Illuminate\Support\ServiceProvider as BaseServiceProvider;
1010
use Leventcz\Top\Commands\TopCommand;
@@ -20,14 +20,23 @@ class ServiceProvider extends BaseServiceProvider
2020
public function register(): void
2121
{
2222
$this->mergeConfigFrom(__DIR__.'/../config/top.php', 'top');
23-
$this->app->singleton(Repository::class, RedisRepository::class);
2423
$this->app->singleton('top', TopManager::class);
25-
$this->app->bind('top.state', function (Application $app) {
26-
return new StateManager($app->make(EventCounter::class), $app->make(Repository::class));
24+
$this->app->singleton(Repository::class, function (Application $application) {
25+
$connection = $application
26+
->make(Factory::class)
27+
->connection($application['config']->get('top.connection'));
28+
29+
return new RedisRepository($connection);
30+
});
31+
$this->app->bind('top.state', function (Application $application) {
32+
return new StateManager(
33+
$application->make(EventCounter::class),
34+
$application->make(Repository::class)
35+
);
2736
});
2837
}
2938

30-
public function boot(Dispatcher $dispatcher): void
39+
public function boot(): void
3140
{
3241
if ($this->app->runningInConsole()) {
3342
$this->commands([TopCommand::class]);
@@ -36,8 +45,8 @@ public function boot(Dispatcher $dispatcher): void
3645
return;
3746
}
3847

39-
$dispatcher->subscribe(RequestListener::class);
40-
$dispatcher->subscribe(CacheListener::class);
41-
$dispatcher->subscribe(DatabaseListener::class);
48+
$this->app->make('events')->subscribe(RequestListener::class);
49+
$this->app->make('events')->subscribe(CacheListener::class);
50+
$this->app->make('events')->subscribe(DatabaseListener::class);
4251
}
4352
}

src/TopManager.php

+15
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,19 @@ public function routes(): RouteCollection
3636
{
3737
return $this->repository->getTopRoutes();
3838
}
39+
40+
public function startRecording(int $duration = 5): void
41+
{
42+
$this->repository->setRecorder($duration);
43+
}
44+
45+
public function stopRecording(): void
46+
{
47+
$this->repository->deleteRecorder();
48+
}
49+
50+
public function isRecording(): bool
51+
{
52+
return $this->repository->recorderExists();
53+
}
3954
}

tests/Repositories/RedisRepository.php

+1-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
<?php
22

3-
use Illuminate\Config\Repository;
4-
use Illuminate\Contracts\Redis\Factory as RedisFactory;
53
use Illuminate\Redis\Connections\Connection;
64
use Leventcz\Top\Data\CacheSummary;
75
use Leventcz\Top\Data\DatabaseSummary;
@@ -12,18 +10,8 @@
1210
use Leventcz\Top\Repositories\RedisRepository;
1311

1412
beforeEach(function () {
15-
$this->redisFactory = Mockery::mock(RedisFactory::class);
1613
$this->connection = Mockery::mock(Connection::class);
17-
$this->config = Mockery::mock(Repository::class);
18-
19-
$this->redisFactory
20-
->shouldReceive('connection')
21-
->andReturn($this->connection);
22-
$this->config
23-
->shouldReceive('get')
24-
->andReturn('top.connection');
25-
26-
$this->repository = new RedisRepository($this->redisFactory, $this->config);
14+
$this->repository = new RedisRepository($this->connection);
2715
});
2816

2917
afterEach(function () {

0 commit comments

Comments
 (0)