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:
parent
bb7c0e0e66
commit
1fdff43ae7
@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
namespace App\Console;
|
namespace App\Console;
|
||||||
|
|
||||||
|
use App\Jobs\NodeStatistics;
|
||||||
use App\Models\ActivityLog;
|
use App\Models\ActivityLog;
|
||||||
use Illuminate\Console\Scheduling\Schedule;
|
use Illuminate\Console\Scheduling\Schedule;
|
||||||
use Illuminate\Database\Console\PruneCommand;
|
use Illuminate\Database\Console\PruneCommand;
|
||||||
@ -32,6 +33,8 @@ class Kernel extends ConsoleKernel
|
|||||||
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
|
$schedule->command(ProcessRunnableCommand::class)->everyMinute()->withoutOverlapping();
|
||||||
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
|
$schedule->command(CleanServiceBackupFilesCommand::class)->daily();
|
||||||
|
|
||||||
|
$schedule->job(new NodeStatistics())->everyFiveSeconds()->withoutOverlapping();
|
||||||
|
|
||||||
if (config('backups.prune_age')) {
|
if (config('backups.prune_age')) {
|
||||||
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
|
// Every 30 minutes, run the backup pruning command so that any abandoned backups can be deleted.
|
||||||
$schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes();
|
$schedule->command(PruneOrphanedBackupsCommand::class)->everyThirtyMinutes();
|
||||||
|
@ -3,12 +3,11 @@
|
|||||||
namespace App\Filament\Resources\NodeResource\Pages;
|
namespace App\Filament\Resources\NodeResource\Pages;
|
||||||
|
|
||||||
use App\Filament\Resources\NodeResource;
|
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\Models\Node;
|
||||||
use App\Services\Nodes\NodeUpdateService;
|
use App\Services\Nodes\NodeUpdateService;
|
||||||
use Filament\Actions;
|
use Filament\Actions;
|
||||||
use Filament\Forms;
|
use Filament\Forms;
|
||||||
|
use Filament\Forms\Components\Fieldset;
|
||||||
use Filament\Forms\Components\Grid;
|
use Filament\Forms\Components\Grid;
|
||||||
use Filament\Forms\Components\Placeholder;
|
use Filament\Forms\Components\Placeholder;
|
||||||
use Filament\Forms\Components\Tabs;
|
use Filament\Forms\Components\Tabs;
|
||||||
@ -17,6 +16,7 @@ use Filament\Forms\Components\TagsInput;
|
|||||||
use Filament\Forms\Components\Textarea;
|
use Filament\Forms\Components\Textarea;
|
||||||
use Filament\Forms\Components\TextInput;
|
use Filament\Forms\Components\TextInput;
|
||||||
use Filament\Forms\Components\ToggleButtons;
|
use Filament\Forms\Components\ToggleButtons;
|
||||||
|
use Filament\Forms\Components\View;
|
||||||
use Filament\Forms\Get;
|
use Filament\Forms\Get;
|
||||||
use Filament\Forms\Set;
|
use Filament\Forms\Set;
|
||||||
use Filament\Notifications\Notification;
|
use Filament\Notifications\Notification;
|
||||||
@ -41,6 +41,32 @@ class EditNode extends EditRecord
|
|||||||
->persistTabInQueryString()
|
->persistTabInQueryString()
|
||||||
->columnSpanFull()
|
->columnSpanFull()
|
||||||
->tabs([
|
->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')
|
Tab::make('Basic Settings')
|
||||||
->icon('tabler-server')
|
->icon('tabler-server')
|
||||||
->schema([
|
->schema([
|
||||||
@ -437,16 +463,17 @@ class EditNode extends EditRecord
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getFooterWidgets(): array
|
|
||||||
{
|
|
||||||
return [
|
|
||||||
NodeStorageChart::class,
|
|
||||||
NodeMemoryChart::class,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
protected function afterSave(): void
|
protected function afterSave(): void
|
||||||
{
|
{
|
||||||
$this->fillForm();
|
$this->fillForm();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function getColumnSpan()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
protected function getColumnStart()
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
81
app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php
Normal file
81
app/Filament/Resources/NodeResource/Widgets/NodeCpuChart.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -3,66 +3,83 @@
|
|||||||
namespace App\Filament\Resources\NodeResource\Widgets;
|
namespace App\Filament\Resources\NodeResource\Widgets;
|
||||||
|
|
||||||
use App\Models\Node;
|
use App\Models\Node;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Filament\Support\RawJs;
|
||||||
use Filament\Widgets\ChartWidget;
|
use Filament\Widgets\ChartWidget;
|
||||||
use Illuminate\Database\Eloquent\Model;
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
|
||||||
class NodeMemoryChart extends ChartWidget
|
class NodeMemoryChart extends ChartWidget
|
||||||
{
|
{
|
||||||
protected static ?string $heading = 'Memory';
|
protected static ?string $pollingInterval = '5s';
|
||||||
|
protected static ?string $maxHeight = '300px';
|
||||||
protected static ?string $pollingInterval = '60s';
|
|
||||||
|
|
||||||
public ?Model $record = null;
|
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
|
protected function getData(): array
|
||||||
{
|
{
|
||||||
/** @var Node $node */
|
/** @var Node $node */
|
||||||
$node = $this->record;
|
$node = $this->record;
|
||||||
|
|
||||||
$total = ($node->statistics()['memory_total'] ?? 0) / 1024 / 1024 / 1024;
|
$memUsed = collect(cache()->get("nodes.$node->id.memory_used"))->slice(-10)
|
||||||
$used = ($node->statistics()['memory_used'] ?? 0) / 1024 / 1024 / 1024;
|
->map(fn ($value, $key) => [
|
||||||
$unused = $total - $used;
|
'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 [
|
return [
|
||||||
'datasets' => [
|
'datasets' => [
|
||||||
[
|
[
|
||||||
'label' => 'Data Cool',
|
'data' => array_column($memUsed, 'memory'),
|
||||||
'data' => [$used, $unused],
|
|
||||||
'backgroundColor' => [
|
'backgroundColor' => [
|
||||||
'rgb(255, 99, 132)',
|
'rgba(96, 165, 250, 0.3)',
|
||||||
'rgb(54, 162, 235)',
|
],
|
||||||
'rgb(255, 205, 86)',
|
'tension' => '0.3',
|
||||||
|
'fill' => true,
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
// 'backgroundColor' => [],
|
'labels' => array_column($memUsed, 'timestamp'),
|
||||||
],
|
|
||||||
'labels' => ['Used', 'Unused'],
|
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getType(): string
|
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,8 @@ use Illuminate\Database\Eloquent\Model;
|
|||||||
class NodeStorageChart extends ChartWidget
|
class NodeStorageChart extends ChartWidget
|
||||||
{
|
{
|
||||||
protected static ?string $heading = 'Storage';
|
protected static ?string $heading = 'Storage';
|
||||||
|
|
||||||
protected static ?string $pollingInterval = '60s';
|
protected static ?string $pollingInterval = '60s';
|
||||||
|
protected static ?string $maxHeight = '300px';
|
||||||
|
|
||||||
public ?Model $record = null;
|
public ?Model $record = null;
|
||||||
|
|
||||||
@ -47,7 +47,6 @@ class NodeStorageChart extends ChartWidget
|
|||||||
return [
|
return [
|
||||||
'datasets' => [
|
'datasets' => [
|
||||||
[
|
[
|
||||||
'label' => 'Data Cool',
|
|
||||||
'data' => [$used, $unused],
|
'data' => [$used, $unused],
|
||||||
'backgroundColor' => [
|
'backgroundColor' => [
|
||||||
'rgb(255, 99, 132)',
|
'rgb(255, 99, 132)',
|
||||||
@ -55,7 +54,6 @@ class NodeStorageChart extends ChartWidget
|
|||||||
'rgb(255, 205, 86)',
|
'rgb(255, 205, 86)',
|
||||||
],
|
],
|
||||||
],
|
],
|
||||||
// 'backgroundColor' => [],
|
|
||||||
],
|
],
|
||||||
'labels' => ['Used', 'Unused'],
|
'labels' => ['Used', 'Unused'],
|
||||||
];
|
];
|
||||||
|
46
app/Jobs/NodeStatistics.php
Normal file
46
app/Jobs/NodeStatistics.php
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,3 @@
|
|||||||
|
<x-filament::widget>
|
||||||
|
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeCpuChart::class, ['record'=> $getRecord()])
|
||||||
|
</x-filament::widget>
|
@ -0,0 +1,3 @@
|
|||||||
|
<x-filament::widget>
|
||||||
|
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeMemoryChart::class, ['record'=> $getRecord()])
|
||||||
|
</x-filament::widget>
|
@ -0,0 +1,3 @@
|
|||||||
|
<x-filament::widget>
|
||||||
|
@livewire(\App\Filament\Resources\NodeResource\Widgets\NodeStorageChart::class, ['record'=> $getRecord()])
|
||||||
|
</x-filament::widget>
|
Loading…
x
Reference in New Issue
Block a user