diff --git a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php index 175e4a1c2..29aedeb01 100644 --- a/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php +++ b/app/Filament/Admin/Resources/Nodes/Pages/EditNode.php @@ -4,7 +4,7 @@ namespace App\Filament\Admin\Resources\Nodes\Pages; use App\Filament\Admin\Resources\Nodes\NodeResource; use App\Models\Node; -use App\Repositories\Daemon\DaemonConfigurationRepository; +use App\Repositories\Daemon\DaemonSystemRepository; use App\Services\Helpers\SoftwareVersionService; use App\Services\Nodes\NodeAutoDeployService; use App\Services\Nodes\NodeUpdateService; @@ -14,6 +14,8 @@ use Exception; use Filament\Actions\Action; use Filament\Actions\DeleteAction; use Filament\Forms\Components\Hidden; +use Filament\Forms\Components\Slider; +use Filament\Forms\Components\Slider\Enums\PipsMode; use Filament\Forms\Components\TagsInput; use Filament\Forms\Components\Textarea; use Filament\Forms\Components\TextInput; @@ -25,6 +27,7 @@ use Filament\Resources\Pages\EditRecord; use Filament\Schemas\Components\Actions; use Filament\Schemas\Components\Fieldset; use Filament\Schemas\Components\Grid; +use Filament\Schemas\Components\Section; use Filament\Schemas\Components\StateCasts\BooleanStateCast; use Filament\Schemas\Components\Tabs; use Filament\Schemas\Components\Tabs\Tab; @@ -33,7 +36,10 @@ use Filament\Schemas\Components\Utilities\Set; use Filament\Schemas\Components\View; use Filament\Schemas\Schema; use Filament\Support\Enums\Alignment; +use Filament\Support\Enums\IconSize; +use Filament\Support\RawJs; use Illuminate\Http\Client\ConnectionException; +use Illuminate\Support\Facades\Http; use Illuminate\Support\HtmlString; use Phiki\Grammar\Grammar; use Throwable; @@ -45,13 +51,13 @@ class EditNode extends EditRecord protected static string $resource = NodeResource::class; - private DaemonConfigurationRepository $daemonConfigurationRepository; + private DaemonSystemRepository $daemonSystemRepository; private NodeUpdateService $nodeUpdateService; - public function boot(DaemonConfigurationRepository $daemonConfigurationRepository, NodeUpdateService $nodeUpdateService): void + public function boot(DaemonSystemRepository $daemonSystemRepository, NodeUpdateService $nodeUpdateService): void { - $this->daemonConfigurationRepository = $daemonConfigurationRepository; + $this->daemonSystemRepository = $daemonSystemRepository; $this->nodeUpdateService = $nodeUpdateService; } @@ -624,6 +630,154 @@ class EditNode extends EditRecord ])->fullWidth(), ]), ]), + Tab::make('diagnostics') + ->label(trans('admin/node.tabs.diagnostics')) + ->icon('tabler-heart-search') + ->schema([ + Section::make('diag') + ->heading(trans('admin/node.tabs.diagnostics')) + ->columnSpanFull() + ->columns(4) + ->disabled(fn (Get $get) => $get('pulled')) + ->headerActions([ + Action::make('pull') + ->label(trans('admin/node.diagnostics.pull')) + ->icon('tabler-cloud-download')->iconButton()->iconSize(IconSize::ExtraLarge) + ->hidden(fn (Get $get) => $get('pulled')) + ->action(function (Get $get, Set $set, Node $node) { + $includeEndpoints = $get('include_endpoints') ?? true; + $includeLogs = $get('include_logs') ?? true; + $logLines = $get('log_lines') ?? 200; + + try { + $response = $this->daemonSystemRepository->setNode($node)->getDiagnostics($logLines, $includeEndpoints, $includeLogs); + + if ($response->status() === 404) { + Notification::make() + ->title(trans('admin/node.diagnostics.404')) + ->warning() + ->send(); + + return; + } + + $set('pulled', true); + $set('uploaded', false); + $set('log', $response->body()); + + Notification::make() + ->title(trans('admin/node.diagnostics.logs_pulled')) + ->success() + ->send(); + } catch (ConnectionException $e) { + Notification::make() + ->title(trans('admin/node.error_connecting', ['node' => $node->name])) + ->body($e->getMessage()) + ->danger() + ->send(); + + } + }), + Action::make('upload') + ->label(trans('admin/node.diagnostics.upload')) + ->visible(fn (Get $get) => $get('pulled') ?? false) + ->icon('tabler-cloud-upload')->iconButton()->iconSize(IconSize::ExtraLarge) + ->action(function (Get $get, Set $set) { + try { + $response = Http::asMultipart()->post('https://logs.pelican.dev', [ + [ + 'name' => 'c', + 'contents' => $get('log'), + ], + [ + 'name' => 'e', + 'contents' => '14d', + ], + ]); + + if ($response->failed()) { + Notification::make() + ->title(trans('admin/node.diagnostics.upload_failed')) + ->body(fn () => $response->status() . ' - ' . $response->body()) + ->danger() + ->send(); + + return; + } + + $data = $response->json(); + $url = $data['url']; + + Notification::make() + ->title(trans('admin/node.diagnostics.logs_uploaded')) + ->body("{$url}") + ->success() + ->actions([ + Action::make('viewLogs') + ->label(trans('admin/node.diagnostics.view_logs')) + ->url($url) + ->openUrlInNewTab(true), + ]) + ->persistent() + ->send(); + $set('log', $url); + $set('pulled', false); + $set('uploaded', true); + + } catch (\Exception $e) { + Notification::make() + ->title(trans('admin/node.diagnostics.upload_failed')) + ->body($e->getMessage()) + ->danger() + ->send(); + } + }), + Action::make('clear') + ->label(trans('admin/node.diagnostics.clear')) + ->visible(fn (Get $get) => $get('pulled') ?? false) + ->icon('tabler-trash')->iconButton()->iconSize(IconSize::ExtraLarge)->color('danger') + ->action(function (Get $get, Set $set) { + $set('pulled', false); + $set('uploaded', false); + $set('log', null); + $this->refresh(); + } + ), + ]) + ->schema([ + ToggleButtons::make('include_endpoints') + ->hintIcon('tabler-question-mark')->inline() + ->hintIconTooltip(trans('admin/node.diagnostics.include_endpoints_hint')) + ->formatStateUsing(fn () => 1) + ->boolean(), + ToggleButtons::make('include_logs') + ->live() + ->hintIcon('tabler-question-mark')->inline() + ->hintIconTooltip(trans('admin/node.diagnostics.include_logs_hint')) + ->formatStateUsing(fn () => 1) + ->boolean(), + Slider::make('log_lines') + ->columnSpan(2) + ->hiddenLabel() + ->live() + ->tooltips(RawJs::make(<<<'JS' + `${$value} lines` + JS)) + ->visible(fn (Get $get) => $get('include_logs')) + ->range(minValue: 100, maxValue: 500) + ->pips(PipsMode::Steps, density: 10) + ->step(50) + ->formatStateUsing(fn () => 200) + ->fillTrack(), + Hidden::make('pulled'), + Hidden::make('uploaded'), + ]), + Textarea::make('log') + ->hiddenLabel() + ->columnSpanFull() + ->rows(35) + ->visible(fn (Get $get) => ($get('pulled') ?? false) || ($get('uploaded') ?? false)), + ]), ]), ]); } @@ -681,7 +835,7 @@ class EditNode extends EditRecord try { if ($changed) { - $this->daemonConfigurationRepository->setNode($node)->update($node); + $this->daemonSystemRepository->setNode($node)->update($node); } parent::getSavedNotification()?->send(); } catch (ConnectionException) { diff --git a/app/Models/Node.php b/app/Models/Node.php index 52b02ac11..e4f127d84 100644 --- a/app/Models/Node.php +++ b/app/Models/Node.php @@ -4,7 +4,7 @@ namespace App\Models; use App\Contracts\Validatable; use App\Exceptions\Service\HasActiveServersException; -use App\Repositories\Daemon\DaemonConfigurationRepository; +use App\Repositories\Daemon\DaemonSystemRepository; use App\Traits\HasValidation; use Carbon\Carbon; use Exception; @@ -316,7 +316,7 @@ class Node extends Model implements Validatable { return once(function () { try { - return (new DaemonConfigurationRepository()) + return (new DaemonSystemRepository()) ->setNode($this) ->getSystemInformation(); } catch (Exception $exception) { diff --git a/app/Repositories/Daemon/DaemonConfigurationRepository.php b/app/Repositories/Daemon/DaemonSystemRepository.php similarity index 69% rename from app/Repositories/Daemon/DaemonConfigurationRepository.php rename to app/Repositories/Daemon/DaemonSystemRepository.php index 916e7d459..71d88a138 100644 --- a/app/Repositories/Daemon/DaemonConfigurationRepository.php +++ b/app/Repositories/Daemon/DaemonSystemRepository.php @@ -6,7 +6,7 @@ use App\Models\Node; use Illuminate\Http\Client\ConnectionException; use Illuminate\Http\Client\Response; -class DaemonConfigurationRepository extends DaemonRepository +class DaemonSystemRepository extends DaemonRepository { /** * Returns system information from the daemon instance. @@ -30,6 +30,23 @@ class DaemonConfigurationRepository extends DaemonRepository })->json(); } + /** + * Retrieve diagnostics from the daemon for the current node. + * + * + * @throws ConnectionException + */ + public function getDiagnostics(int $lines, bool $includeEndpoints, bool $includeLogs): Response + { + return $this->getHttpClient() + ->timeout(5) + ->get('/api/diagnostics', [ + 'log_lines' => $lines, + 'include_endpoints' => $includeEndpoints ? 'true' : 'false', + 'include_logs' => $includeLogs ? 'true' : 'false', + ]); + } + /** * Updates the configuration information for a daemon. Updates the information for * this instance using a passed-in model. This allows us to change plenty of information diff --git a/app/Services/Nodes/NodeUpdateService.php b/app/Services/Nodes/NodeUpdateService.php index 13b094979..6ef132a46 100644 --- a/app/Services/Nodes/NodeUpdateService.php +++ b/app/Services/Nodes/NodeUpdateService.php @@ -4,7 +4,7 @@ namespace App\Services\Nodes; use App\Exceptions\Service\Node\ConfigurationNotPersistedException; use App\Models\Node; -use App\Repositories\Daemon\DaemonConfigurationRepository; +use App\Repositories\Daemon\DaemonSystemRepository; use Illuminate\Database\ConnectionInterface; use Illuminate\Http\Client\ConnectionException; use Illuminate\Support\Str; @@ -17,7 +17,7 @@ class NodeUpdateService */ public function __construct( private ConnectionInterface $connection, - private DaemonConfigurationRepository $configurationRepository, + private DaemonSystemRepository $configurationRepository, ) {} /** diff --git a/lang/en/admin/node.php b/lang/en/admin/node.php index a726300ee..961ca5169 100644 --- a/lang/en/admin/node.php +++ b/lang/en/admin/node.php @@ -10,6 +10,7 @@ return [ 'basic_settings' => 'Basic Settings', 'advanced_settings' => 'Advanced Settings', 'config_file' => 'Configuration File', + 'diagnostics' => 'Diagnostics', ], 'table' => [ 'health' => 'Health', @@ -117,6 +118,24 @@ return [ 'error_connecting_description' => 'The configuration could not be automatically updated on Wings, you will need to manually update the configuration file.', 'allocation' => 'Allocation', + 'diagnostics' => [ + 'header' => 'Node Diagnostics', + 'include_endpoints' => 'Include Endpoints', + 'include_endpoints_hint' => 'Including endpoints will show panel urls within the logs and NOT obscure them.', + 'include_logs' => 'Include Logs', + 'include_logs_hint' => 'Including logs will show recent logs and help track down possible issues.', + 'run_diagnostics' => 'Run Diagnostics', + 'upload_to_pelican' => 'Upload Logs', + 'logs_pulled' => 'Logs Pulled!', + 'logs_uploaded' => 'Logs Uploaded', + 'upload_failed' => 'Logs Upload Failed', + 'view_logs' => 'View Logs', + 'pull' => 'Pull', + 'upload' => 'Upload', + 'clear' => 'Clear', + '404' => 'The requested diagnostic report could not be found. Make sure wings is up to date and try again.', + ], + 'cloudflare_issue' => [ 'title' => 'Cloudflare Issue', 'body' => 'Your Node is not accessible by Cloudflare',