Add Node CPU/Memory Graphs (#459)

* Update Node Stats

Soon TM

* Update

* Make these smaller

* Change graphs

* Remove this.

Didn't work anyways.

* Update Graphs

* Use User TZ and config var

* Fix math

* Change to per thread.
This commit is contained in:
Charles 2024-07-14 16:48:14 -04:00 committed by GitHub
parent bb7c0e0e66
commit 1fdff43ae7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 229 additions and 48 deletions

View File

@ -2,6 +2,7 @@
namespace App\Console;
use App\Jobs\NodeStatistics;
use App\Models\ActivityLog;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Database\Console\PruneCommand;
@ -32,6 +33,8 @@ class Kernel extends ConsoleKernel
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
if (config('backups.prune_age')) {
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
$schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes();

View File

@ -3,12 +3,11 @@
namespace App\Filament\Resources\NodeResource\Pages;
use App\Filament\Resources\NodeResource;
use App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart;
use App\Filament\Resources\NodeResource\Widgets\NodeStorageChart;
use App\Models\Node;
use App\Services\Nodes\NodeUpdateService;
use Filament\Actions;
use Filament\Forms;
use Filament\Forms\Components\Fieldset;
use Filament\Forms\Components\Grid;
use Filament\Forms\Components\Placeholder;
use Filament\Forms\Components\Tabs;
@ -17,6 +16,7 @@ use Filament\Forms\Components\TagsInput;
use Filament\Forms\Components\Textarea;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\ToggleButtons;
use Filament\Forms\Components\View;
use Filament\Forms\Get;
use Filament\Forms\Set;
use Filament\Notifications\Notification;
@ -41,6 +41,32 @@ class EditNode extends EditRecord
->persistTabInQueryString()
->columnSpanFull()
->tabs([
Tab::make('')
->label('Overview')
->icon('tabler-chart-area-line-filled')
->columns(6)
->schema([
Fieldset::make()
->label('Node Information')
->columns(4)
->schema([
Placeholder::make('')
->label('Wings Version')
->content(fn (Node $node) => $node->systemInformation()['version']),
Placeholder::make('')
->label('CPU Threads')
->content(fn (Node $node) => $node->systemInformation()['cpu_count']),
Placeholder::make('')
->label('Architecture')
->content(fn (Node $node) => $node->systemInformation()['architecture']),
Placeholder::make('')
->label('Kernel')
->content(fn (Node $node) => $node->systemInformation()['kernel_version']),
]),
View::make('filament.components.node-cpu-chart')->columnSpan(3),
View::make('filament.components.node-memory-chart')->columnSpan(3),
// TODO: Make purdy View::make('filament.components.node-storage-chart')->columnSpan(3),
]),
Tab::make('Basic Settings')
->icon('tabler-server')
->schema([
@ -437,16 +463,17 @@ class EditNode extends EditRecord
];
}
protected function getFooterWidgets(): array
{
return [
NodeStorageChart::class,
NodeMemoryChart::class,
];
}
protected function afterSave(): void
{
$this->fillForm();
}
protected function getColumnSpan()
{
return null;
}
protected function getColumnStart()
{
return null;
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace App\Filament\Resources\NodeResource\Widgets;
use App\Models\Node;
use Carbon\Carbon;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Database\Eloquent\Model;
class NodeCpuChart extends ChartWidget
{
protected static ?string $pollingInterval = '5s';
protected static ?string $maxHeight = '300px';
public ?Model $record = null;
protected function getData(): array
{
/** @var Node $node */
$node = $this->record;
$threads = $node->systemInformation()['cpu_count'];
$cpu = collect(cache()->get("nodes.$node->id.cpu_percent"))
->slice(-10)
->map(fn ($value, $key) => [
'cpu' => number_format($value * $threads, 2),
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
])
->all();
return [
'datasets' => [
[
'data' => array_column($cpu, 'cpu'),
'backgroundColor' => [
'rgba(96, 165, 250, 0.3)',
],
'tension' => '0.3',
'fill' => true,
],
],
'labels' => array_column($cpu, 'timestamp'),
];
}
protected function getType(): string
{
return 'line';
}
protected function getOptions(): RawJs
{
return RawJs::make(<<<'JS'
{
scales: {
y: {
min: 0,
},
},
plugins: {
legend: {
display: false,
}
}
}
JS);
}
public function getHeading(): string
{
/** @var Node $node */
$node = $this->record;
$threads = $node->systemInformation()['cpu_count'];
$cpu = number_format(collect(cache()->get("nodes.$node->id.cpu_percent"))->last() * $threads, 2);
$max = number_format($threads * 100) . '%';
return 'CPU - ' . $cpu . '% Of ' . $max;
}
}

View File

@ -3,66 +3,83 @@
namespace App\Filament\Resources\NodeResource\Widgets;
use App\Models\Node;
use Carbon\Carbon;
use Filament\Support\RawJs;
use Filament\Widgets\ChartWidget;
use Illuminate\Database\Eloquent\Model;
class NodeMemoryChart extends ChartWidget
{
protected static ?string $heading = 'Memory';
protected static ?string $pollingInterval = '60s';
protected static ?string $pollingInterval = '5s';
protected static ?string $maxHeight = '300px';
public ?Model $record = null;
protected static ?array $options = [
'scales' => [
'x' => [
'grid' => [
'display' => false,
],
'ticks' => [
'display' => false,
],
],
'y' => [
'grid' => [
'display' => false,
],
'ticks' => [
'display' => false,
],
],
],
];
protected function getData(): array
{
/** @var Node $node */
$node = $this->record;
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
$unused = $total - $used;
$memUsed = collect(cache()->get("nodes.$node->id.memory_used"))->slice(-10)
->map(fn ($value, $key) => [
'memory' => config('panel.use_binary_prefix') ? $value / 1024 / 1024 / 1024 : $value / 1000 / 1000 / 1000,
'timestamp' => Carbon::createFromTimestamp($key, (auth()->user()->timezone ?? 'UTC'))->format('H:i:s'),
])
->all();
return [
'datasets' => [
[
'label' => 'Data Cool',
'data' => [$used, $unused],
'data' => array_column($memUsed, 'memory'),
'backgroundColor' => [
'rgb(255, 99, 132)',
'rgb(54, 162, 235)',
'rgb(255, 205, 86)',
'rgba(96, 165, 250, 0.3)',
],
'tension' => '0.3',
'fill' => true,
],
// 'backgroundColor' => [],
],
'labels' => ['Used', 'Unused'],
'labels' => array_column($memUsed, 'timestamp'),
];
}
protected function getType(): string
{
return 'pie';
return 'line';
}
protected function getOptions(): RawJs
{
return RawJs::make(<<<'JS'
{
scales: {
y: {
min: 0,
},
},
plugins: {
legend: {
display: false,
}
}
}
JS);
}
public function getHeading(): string
{
/** @var Node $node */
$node = $this->record;
$latestMemoryUsed = collect(cache()->get("nodes.$node->id.memory_used"))->last();
$totalMemory = collect(cache()->get("nodes.$node->id.memory_total"))->last();
$used = config('panel.use_binary_prefix')
? number_format($latestMemoryUsed / 1024 / 1024 / 1024, 2) .' GiB'
: number_format($latestMemoryUsed / 1000 / 1000 / 1000, 2) . ' GB';
$total = config('panel.use_binary_prefix')
? number_format($totalMemory / 1024 / 1024 / 1024, 2) .' GiB'
: number_format($totalMemory / 1000 / 1000 / 1000, 2) . ' GB';
return 'Memory - ' . $used . ' Of ' . $total;
}
}

View File

@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Model;
class NodeStorageChart extends ChartWidget
{
protected static ?string $heading = 'Storage';
protected static ?string $pollingInterval = '60s';
protected static ?string $maxHeight = '300px';
public ?Model $record = null;
@ -47,7 +47,6 @@ class NodeStorageChart extends ChartWidget
return [
'datasets' => [
[
'label' => 'Data Cool',
'data' => [$used, $unused],
'backgroundColor' => [
'rgb(255, 99, 132)',
@ -55,7 +54,6 @@ class NodeStorageChart extends ChartWidget
'rgb(255, 205, 86)',
],
],
// 'backgroundColor' => [],
],
'labels' => ['Used', 'Unused'],
];

View File

@ -0,0 +1,46 @@
<?php
namespace App\Jobs;
use App\Models\Node;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class NodeStatistics implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
/**
* Create a new job instance.
*/
public function __construct()
{
//
}
/**
* Execute the job.
*/
public function handle(): void
{
foreach (Node::all() as $node) {
$stats = $node->statistics();
$timestamp = now()->getTimestamp();
foreach ($stats as $key => $value) {
$cacheKey = "nodes.{$node->id}.$key";
$data = cache()->get($cacheKey, []);
// Add current timestamp and value to the data array
$data[$timestamp] = $value;
// Update the cache with the new data, expires in 1 minute
cache()->put($cacheKey, $data, now()->addMinute());
}
}
}
}

View File

@ -0,0 +1,3 @@
<x-filament::widget>
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeCpuChart::class, ['record'=> $getRecord()])
</x-filament::widget>

View File

@ -0,0 +1,3 @@
<x-filament::widget>
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart::class, ['record'=> $getRecord()])
</x-filament::widget>

View File

@ -0,0 +1,3 @@
<x-filament::widget>
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeStorageChart::class, ['record'=> $getRecord()])
</x-filament::widget>