集群支持
This commit is contained in:
parent
35aeecd2c7
commit
50763da6dc
@ -48,17 +48,23 @@ public function handle(): int
|
|||||||
return CommandAlias::SUCCESS;
|
return CommandAlias::SUCCESS;
|
||||||
}
|
}
|
||||||
|
|
||||||
private function format(string $event, array $message = [])
|
public function format(string $event, array $message = [], $stdout = true): ?string
|
||||||
{
|
{
|
||||||
$status = $this->switch($event, $message['data']);
|
$status = $this->switch($event, $message['data']);
|
||||||
|
|
||||||
if (! $status) {
|
if (! $status) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$message = "[{$message['node']['type']}] {$message['node']['id']}:$event: ".$status;
|
$message = "[{$message['node']['type']}] {$message['node']['id']}:$event: ".$status;
|
||||||
|
|
||||||
|
if ($stdout) {
|
||||||
$this->info($message);
|
$this->info($message);
|
||||||
|
|
||||||
|
return null;
|
||||||
|
} else {
|
||||||
|
return $message;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public function switch($event, $message = []): string|null
|
public function switch($event, $message = []): string|null
|
||||||
|
101
app/Http/Controllers/Admin/NodeController.php
Normal file
101
app/Http/Controllers/Admin/NodeController.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers\Admin;
|
||||||
|
|
||||||
|
use App\Console\Commands\Cluster\Monitor;
|
||||||
|
use App\Http\Controllers\Controller;
|
||||||
|
use App\Support\ClusterSupport;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Http\JsonResponse;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||||
|
|
||||||
|
class NodeController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*
|
||||||
|
* @return View
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
$nodes = ClusterSupport::nodes();
|
||||||
|
|
||||||
|
return view('admin.cluster.nodes', compact('nodes'));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param string $node
|
||||||
|
* @return JsonResponse
|
||||||
|
*/
|
||||||
|
public function update(Request $request, string $node)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'weight' => 'sometimes|integer|min:0|max:100',
|
||||||
|
]);
|
||||||
|
|
||||||
|
ClusterSupport::update($node, [
|
||||||
|
'weight' => $request->input('weight'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return $this->success('Updated');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function event(Request $request)
|
||||||
|
{
|
||||||
|
$request->validate([
|
||||||
|
'restart' => 'nullable|string|max:10|in:web,queue',
|
||||||
|
]);
|
||||||
|
|
||||||
|
if ($service = $request->input('restart')) {
|
||||||
|
if ($service === 'web') {
|
||||||
|
ClusterSupport::publish('cluster.restart.'.$service);
|
||||||
|
} elseif ($service === 'queue') {
|
||||||
|
Artisan::call('queue:restart');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return back()->with('success', '已经向集群广播命令。');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function stream()
|
||||||
|
{
|
||||||
|
$response = new StreamedResponse(function () {
|
||||||
|
ClusterSupport::publish('monitor.started');
|
||||||
|
|
||||||
|
ClusterSupport::listen('*', function ($event, $message) {
|
||||||
|
$monitor = new Monitor();
|
||||||
|
if (connection_aborted()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$msg = $monitor->format($event, $message, false);
|
||||||
|
|
||||||
|
echo 'data: '.$msg."\n\n";
|
||||||
|
|
||||||
|
ob_flush();
|
||||||
|
flush();
|
||||||
|
}, false);
|
||||||
|
});
|
||||||
|
$response->headers->set('Content-Type', 'text/event-stream');
|
||||||
|
$response->headers->set('X-Accel-Buffering', 'no');
|
||||||
|
$response->headers->set('Cache-Control', 'no-cache');
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
// /**
|
||||||
|
// * Remove the specified resource from storage.
|
||||||
|
// *
|
||||||
|
// * @param int $id
|
||||||
|
// * @return Response
|
||||||
|
// */
|
||||||
|
// public function destroy($id)
|
||||||
|
// {
|
||||||
|
// //
|
||||||
|
// }
|
||||||
|
}
|
@ -66,6 +66,11 @@ public static function updateThisNode($data = []): void
|
|||||||
{
|
{
|
||||||
$node_id = config('settings.node.id');
|
$node_id = config('settings.node.id');
|
||||||
|
|
||||||
|
self::update($node_id, $data);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function update($node_id, $data = []): void
|
||||||
|
{
|
||||||
$node = self::hget('nodes', $node_id, '[]');
|
$node = self::hget('nodes', $node_id, '[]');
|
||||||
$node = json_decode($node, true);
|
$node = json_decode($node, true);
|
||||||
|
|
||||||
|
24
resources/views/admin/cluster/events.blade.php
Normal file
24
resources/views/admin/cluster/events.blade.php
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', '事件广播')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<h3>事件广播</h3>
|
||||||
|
<p>对集群广播命令。</p>
|
||||||
|
|
||||||
|
|
||||||
|
<h4>重启</h4>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('admin.cluster.events.send') }}" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="restart" value="web" />
|
||||||
|
<button type="submit" class="btn btn-primary">Web 服务</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
<form method="POST" action="{{ route('admin.cluster.events.send') }}" class="d-inline">
|
||||||
|
@csrf
|
||||||
|
<input type="hidden" name="restart" value="queue" />
|
||||||
|
<button type="submit" class="btn btn-primary">队列服务</button>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
@endsection
|
53
resources/views/admin/cluster/monitor.blade.php
Normal file
53
resources/views/admin/cluster/monitor.blade.php
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
@php use Illuminate\Support\Carbon; @endphp
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', '监视器')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<h3>集群日志监视器</h3>
|
||||||
|
|
||||||
|
<div id="logs"></div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
const eventSource = new EventSource('stream');
|
||||||
|
|
||||||
|
let line = 0;
|
||||||
|
let currentLine = 0;
|
||||||
|
let maxLine = 100;
|
||||||
|
|
||||||
|
const logs = document.getElementById('logs');
|
||||||
|
|
||||||
|
eventSource.onmessage = function (event) {
|
||||||
|
const text = event.data
|
||||||
|
|
||||||
|
if (text === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 将 text 填充到 logs 中,如果超过 maxLine 行,则删除最早的一行
|
||||||
|
const lines = text.split("\n");
|
||||||
|
for (let i = 0; i < lines.length; i++) {
|
||||||
|
const line = lines[i];
|
||||||
|
if (line === '') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = document.createElement('span');
|
||||||
|
const localDateTimeString = new Date().toLocaleString();
|
||||||
|
span.innerHTML = `[${localDateTimeString}]` + line + '<br/>';
|
||||||
|
logs.appendChild(span);
|
||||||
|
|
||||||
|
if (currentLine > maxLine) {
|
||||||
|
logs.removeChild(logs.firstChild);
|
||||||
|
currentLine--;
|
||||||
|
}
|
||||||
|
|
||||||
|
currentLine++;
|
||||||
|
|
||||||
|
window.scrollTo(0, document.body.scrollHeight);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@endsection
|
139
resources/views/admin/cluster/nodes.blade.php
Normal file
139
resources/views/admin/cluster/nodes.blade.php
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
@php use Illuminate\Support\Carbon; @endphp
|
||||||
|
@extends('layouts.admin')
|
||||||
|
|
||||||
|
@section('title', '节点')
|
||||||
|
|
||||||
|
@section('content')
|
||||||
|
<h3>Cluster Ready!</h3>
|
||||||
|
<p>节点管理</p>
|
||||||
|
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead>
|
||||||
|
<th>
|
||||||
|
类型
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
标识
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
对端地址
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
权重(可更改)
|
||||||
|
</th>
|
||||||
|
<th>
|
||||||
|
上次心跳
|
||||||
|
</th>
|
||||||
|
{{-- <th>--}}
|
||||||
|
{{-- 管理--}}
|
||||||
|
{{-- </th>--}}
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
|
||||||
|
@foreach($nodes as $node)
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
@if ($node['type'] == 'master')
|
||||||
|
<span class="text-success">主节点</span>
|
||||||
|
@elseif ($node['type'] == 'slave')
|
||||||
|
<span class="text-secondary">工作节点</span>
|
||||||
|
@elseif ($node['type'] == 'edge')
|
||||||
|
<span class="text-warning">边缘节点</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ $node['id'] }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ $node['ip'] }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<span class="editable" node-id="{{ $node['id'] }}" value=" {{ $node['weight'] ?? '' }}"></span>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
@php($time = Carbon::createFromTimestamp($node['last_heartbeat']))
|
||||||
|
@if ($time->diffInMinutes() > 1)
|
||||||
|
<span class="text-danger">{{ $time->diffForHumans() }}</span>
|
||||||
|
@else
|
||||||
|
<span class="text-success">{{ $time->diffForHumans() }}</span>
|
||||||
|
@endif
|
||||||
|
</td>
|
||||||
|
{{-- <td>--}}
|
||||||
|
{{-- <a>--}}
|
||||||
|
{{-- 清除数据--}}
|
||||||
|
{{-- </a>--}}
|
||||||
|
{{-- </td>--}}
|
||||||
|
</tr>
|
||||||
|
@endforeach
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<p>注意: 权重为 <span class="text-danger">0</span> 则不调度。</p>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let editables = document.querySelectorAll('.editable');
|
||||||
|
editables.forEach(function (editable) {
|
||||||
|
// fill :value
|
||||||
|
editable.innerText = editable.getAttribute('value');
|
||||||
|
|
||||||
|
editable.addEventListener('click', function () {
|
||||||
|
let input = document.createElement('input');
|
||||||
|
input.value = editable.innerText;
|
||||||
|
input.classList.add('form-control')
|
||||||
|
editable.innerText = '';
|
||||||
|
editable.appendChild(input);
|
||||||
|
input.focus();
|
||||||
|
|
||||||
|
input.addEventListener('blur', function () {
|
||||||
|
editable.innerText = input.value;
|
||||||
|
input.remove();
|
||||||
|
|
||||||
|
// 不能为空,负数
|
||||||
|
if (input.value === '' || input.value < 0) {
|
||||||
|
editable.innerText = editable.getAttribute('value');
|
||||||
|
} else {
|
||||||
|
let node_id = editable.getAttribute('node-id');
|
||||||
|
|
||||||
|
axios.patch('nodes/' + node_id, {
|
||||||
|
weight: input.value
|
||||||
|
}).then(function () {
|
||||||
|
editable.setAttribute('value', input.value);
|
||||||
|
}).catch(function (error) {
|
||||||
|
editable.innerText = editable.getAttribute('value');
|
||||||
|
|
||||||
|
if (error.response.status === 422) {
|
||||||
|
let errors = error.response.data.errors;
|
||||||
|
let message = '';
|
||||||
|
for (let key in errors) {
|
||||||
|
message += errors[key][0] + '\n';
|
||||||
|
}
|
||||||
|
alert(message);
|
||||||
|
} else {
|
||||||
|
alert('服务器错误');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
input.addEventListener('keyup', function (e) {
|
||||||
|
if (e.key === 'Enter') {
|
||||||
|
input.blur();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.editable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.editable input {
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
@endsection
|
@ -87,7 +87,6 @@
|
|||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{{ route('admin.devices.index') }}">物联设备</a>
|
<a class="dropdown-item" href="{{ route('admin.devices.index') }}">物联设备</a>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
<li>
|
<li>
|
||||||
<a class="dropdown-item" href="{{ route('admin.applications.index') }}">应用程序</a>
|
<a class="dropdown-item" href="{{ route('admin.applications.index') }}">应用程序</a>
|
||||||
</li>
|
</li>
|
||||||
@ -113,6 +112,31 @@
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
</li>
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item dropdown">
|
||||||
|
<a class="nav-link dropdown-toggle" href="#" id="navbarDropdownCluster" role="button"
|
||||||
|
data-bs-toggle="dropdown" aria-expanded="false">
|
||||||
|
集群
|
||||||
|
</a>
|
||||||
|
<ul class="dropdown-menu" aria-labelledby="navbarDropdownCluster">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ route('admin.cluster.nodes') }}">
|
||||||
|
节点
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ route('admin.cluster.events') }}">
|
||||||
|
广播
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{{ route('admin.cluster.monitor') }}">
|
||||||
|
监视器
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
@endauth
|
@endauth
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@
|
|||||||
use App\Http\Controllers\Admin\HomeController;
|
use App\Http\Controllers\Admin\HomeController;
|
||||||
use App\Http\Controllers\Admin\HostController;
|
use App\Http\Controllers\Admin\HostController;
|
||||||
use App\Http\Controllers\Admin\ModuleController;
|
use App\Http\Controllers\Admin\ModuleController;
|
||||||
|
use App\Http\Controllers\Admin\NodeController;
|
||||||
use App\Http\Controllers\Admin\NotificationController;
|
use App\Http\Controllers\Admin\NotificationController;
|
||||||
use App\Http\Controllers\Admin\ReplyController;
|
use App\Http\Controllers\Admin\ReplyController;
|
||||||
use App\Http\Controllers\Admin\UserController;
|
use App\Http\Controllers\Admin\UserController;
|
||||||
@ -41,6 +42,13 @@
|
|||||||
Route::get('devices', [DeviceController::class, 'index'])->name('devices.index');
|
Route::get('devices', [DeviceController::class, 'index'])->name('devices.index');
|
||||||
Route::delete('devices', [DeviceController::class, 'destroy'])->name('devices.destroy');
|
Route::delete('devices', [DeviceController::class, 'destroy'])->name('devices.destroy');
|
||||||
|
|
||||||
|
Route::get('cluster/nodes', [NodeController::class, 'index'])->name('cluster.nodes');
|
||||||
|
Route::patch('cluster/nodes/{node}', [NodeController::class, 'update'])->name('cluster.nodes.update');
|
||||||
|
Route::view('cluster/events', 'admin.cluster.events')->name('cluster.events');
|
||||||
|
Route::post('cluster/events', [NodeController::class, 'event'])->name('cluster.events.send');
|
||||||
|
Route::view('cluster/monitor', 'admin.cluster.monitor')->name('cluster.monitor');
|
||||||
|
Route::get('cluster/stream', [NodeController::class, 'stream'])->name('cluster.stream');
|
||||||
|
|
||||||
Route::resource('notifications', NotificationController::class)->only(['create', 'store']);
|
Route::resource('notifications', NotificationController::class)->only(['create', 'store']);
|
||||||
|
|
||||||
Route::view('commands', 'admin.commands')->name('commands');
|
Route::view('commands', 'admin.commands')->name('commands');
|
||||||
@ -51,4 +59,5 @@
|
|||||||
Route::get('/login', [AuthController::class, 'index'])->name('login');
|
Route::get('/login', [AuthController::class, 'index'])->name('login');
|
||||||
Route::post('/login', [AuthController::class, 'login']);
|
Route::post('/login', [AuthController::class, 'login']);
|
||||||
});
|
});
|
||||||
|
|
||||||
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
|
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
|
||||||
|
Loading…
Reference in New Issue
Block a user