集群支持
This commit is contained in:
parent
35aeecd2c7
commit
50763da6dc
@ -48,17 +48,23 @@ public function handle(): int
|
||||
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']);
|
||||
|
||||
if (! $status) {
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
$message = "[{$message['node']['type']}] {$message['node']['id']}:$event: ".$status;
|
||||
|
||||
$this->info($message);
|
||||
if ($stdout) {
|
||||
$this->info($message);
|
||||
|
||||
return null;
|
||||
} else {
|
||||
return $message;
|
||||
}
|
||||
}
|
||||
|
||||
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');
|
||||
|
||||
self::update($node_id, $data);
|
||||
}
|
||||
|
||||
public static function update($node_id, $data = []): void
|
||||
{
|
||||
$node = self::hget('nodes', $node_id, '[]');
|
||||
$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>
|
||||
<a class="dropdown-item" href="{{ route('admin.devices.index') }}">物联设备</a>
|
||||
</li>
|
||||
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('admin.applications.index') }}">应用程序</a>
|
||||
</li>
|
||||
@ -113,6 +112,31 @@
|
||||
</li>
|
||||
</ul>
|
||||
</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>
|
||||
@endauth
|
||||
|
||||
|
@ -7,6 +7,7 @@
|
||||
use App\Http\Controllers\Admin\HomeController;
|
||||
use App\Http\Controllers\Admin\HostController;
|
||||
use App\Http\Controllers\Admin\ModuleController;
|
||||
use App\Http\Controllers\Admin\NodeController;
|
||||
use App\Http\Controllers\Admin\NotificationController;
|
||||
use App\Http\Controllers\Admin\ReplyController;
|
||||
use App\Http\Controllers\Admin\UserController;
|
||||
@ -41,6 +42,13 @@
|
||||
Route::get('devices', [DeviceController::class, 'index'])->name('devices.index');
|
||||
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::view('commands', 'admin.commands')->name('commands');
|
||||
@ -51,4 +59,5 @@
|
||||
Route::get('/login', [AuthController::class, 'index'])->name('login');
|
||||
Route::post('/login', [AuthController::class, 'login']);
|
||||
});
|
||||
|
||||
Route::post('/logout', [AuthController::class, 'logout'])->name('logout');
|
||||
|
Loading…
Reference in New Issue
Block a user