852 lines
42 KiB
HTML
852 lines
42 KiB
HTML
<!doctype html>
|
||
<html lang="zh-CN">
|
||
|
||
<head>
|
||
<meta charset="utf-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||
<title>HY App Monitor</title>
|
||
<link rel="stylesheet" href="/app.css" />
|
||
<script defer src="/vue.js"></script>
|
||
<script defer src="/app.js"></script>
|
||
</head>
|
||
|
||
<body>
|
||
<div id="boot-indicator" class="boot-indicator">加载中...</div>
|
||
<noscript>
|
||
<div class="boot-indicator">请启用 JavaScript</div>
|
||
</noscript>
|
||
|
||
<div id="app" v-cloak class="shell">
|
||
<header class="hero">
|
||
<div class="hero-copy">
|
||
<p class="eyebrow">HY APP MONITOR</p>
|
||
<h1>服务 / 主机 / Nacos / MongoDB</h1>
|
||
</div>
|
||
<div class="hero-side">
|
||
<div class="stat-block">
|
||
<span>服务总数</span>
|
||
<strong>{{ summary.total }}</strong>
|
||
</div>
|
||
<div class="stat-block ok">
|
||
<span>服务正常</span>
|
||
<strong>{{ summary.ok }}</strong>
|
||
</div>
|
||
<div class="stat-block bad">
|
||
<span>服务异常</span>
|
||
<strong>{{ summary.down }}</strong>
|
||
</div>
|
||
<div class="stat-block neutral">
|
||
<span>主机数</span>
|
||
<strong>{{ summary.hostTotal }}</strong>
|
||
</div>
|
||
<div class="stat-block neutral">
|
||
<span>Nacos 服务</span>
|
||
<strong>{{ summary.nacosServiceTotal }}</strong>
|
||
</div>
|
||
<div :class="['stat-block', summary.nacosUnhealthyInstanceTotal > 0 ? 'bad' : 'ok']">
|
||
<span>Nacos 异常实例</span>
|
||
<strong>{{ summary.nacosUnhealthyInstanceTotal }}</strong>
|
||
</div>
|
||
<div :class="['stat-block', payload.mongo && payload.mongo.ok ? 'ok' : 'bad']">
|
||
<span>Mongo 状态</span>
|
||
<strong>{{ payload.mongo && payload.mongo.ok ? 'OK' : 'ERR' }}</strong>
|
||
</div>
|
||
<div class="stat-block neutral">
|
||
<span>Mongo 集合</span>
|
||
<strong>{{ summary.mongoCollectionTotal }}</strong>
|
||
</div>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="toolbar">
|
||
<div class="toolbar-left">
|
||
<button :class="['filter-chip', filter === 'all' ? 'active' : '']" @click="filter = 'all'">全部</button>
|
||
<button :class="['filter-chip', filter === 'down' ? 'active' : '']" @click="filter = 'down'">异常</button>
|
||
<button :class="['filter-chip', filter === 'java' ? 'active' : '']" @click="filter = 'java'">Java</button>
|
||
<button :class="['filter-chip', filter === 'golang' ? 'active' : '']" @click="filter = 'golang'">Golang</button>
|
||
</div>
|
||
<div class="toolbar-right">
|
||
<input v-model.trim="keyword" class="search" placeholder="搜索主机 / 服务 / IP / Nacos" />
|
||
<label class="interval-field">
|
||
<span>刷新</span>
|
||
<select v-model.number="refreshIntervalSeconds" class="interval-select">
|
||
<option v-for="seconds in refreshIntervalOptions" :key="seconds" :value="seconds">{{ seconds }}s</option>
|
||
</select>
|
||
</label>
|
||
<button class="refresh" @click="refresh" :disabled="loading">{{ loading ? '刷新中...' : '立即刷新' }}</button>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="meta-strip">
|
||
<span>最后刷新:{{ formattedUpdatedAt }}</span>
|
||
<span>主机指标正常:{{ summary.hostMetricOk }} / {{ summary.hostTotal }}</span>
|
||
<span>Nacos 实例:{{ summary.nacosInstanceTotal }}</span>
|
||
<span>Mongo 数据库:{{ summary.mongoDatabaseTotal }}</span>
|
||
<span v-if="error" class="bad-text">加载失败:{{ error }}</span>
|
||
</section>
|
||
|
||
<section class="section-head">
|
||
<h2>主机与服务</h2>
|
||
</section>
|
||
<section class="hosts">
|
||
<article v-for="group in groupedHosts" :key="group.host" class="host-panel">
|
||
<header class="host-header">
|
||
<div class="host-identity">
|
||
<h2>{{ group.host }}</h2>
|
||
<p>{{ group.ip }} · {{ group.groupLabel }}</p>
|
||
</div>
|
||
<div class="host-badges">
|
||
<span class="badge neutral">{{ group.serviceSummary.total }} 项服务</span>
|
||
<span :class="['badge', group.downCount > 0 ? 'bad' : 'ok']">
|
||
{{ group.downCount > 0 ? `${group.downCount} 异常` : '服务正常' }}
|
||
</span>
|
||
<span :class="['badge', group.metrics && group.metrics.ok ? 'ok' : 'bad']">
|
||
{{ group.metrics && group.metrics.ok ? '主机指标正常' : '主机指标异常' }}
|
||
</span>
|
||
</div>
|
||
</header>
|
||
|
||
<section class="host-metrics">
|
||
<div :class="['metric-card', toneClass(group.metrics.cpuLevel)]">
|
||
<small>CPU</small>
|
||
<strong>{{ formatPercent(group.metrics.cpuPercent) }}</strong>
|
||
</div>
|
||
<div :class="['metric-card', toneClass(group.metrics.memoryLevel)]">
|
||
<small>内存</small>
|
||
<strong>{{ formatPercent(group.metrics.memoryPercent) }}</strong>
|
||
<span>{{ formatGb(group.metrics.memoryUsedGb, group.metrics.memoryTotalGb) }}</span>
|
||
</div>
|
||
<div :class="['metric-card', toneClass(group.metrics.diskLevel)]">
|
||
<small>磁盘</small>
|
||
<strong>{{ formatPercent(group.metrics.diskPercent) }}</strong>
|
||
<span>{{ formatGb(group.metrics.diskUsedGb, group.metrics.diskTotalGb) }}</span>
|
||
</div>
|
||
<div :class="['metric-card', toneClass(group.metrics.processLevel)]">
|
||
<small>进程数</small>
|
||
<strong>{{ group.metrics.processCount ?? '-' }}</strong>
|
||
<span>{{ group.metrics.hostname || group.instanceId }}</span>
|
||
</div>
|
||
</section>
|
||
|
||
<p v-if="group.metrics && group.metrics.error" class="metric-error">{{ group.metrics.error }}</p>
|
||
|
||
<div v-if="group.items.length > 0" class="service-grid">
|
||
<div v-for="item in group.items" :key="item.host + '-' + item.service" class="service-card">
|
||
<div class="service-top">
|
||
<div>
|
||
<div class="service-name">{{ item.service }}</div>
|
||
<div class="service-kind">{{ item.kind }}</div>
|
||
</div>
|
||
<div :class="['status-pill', toneClass(item.level)]">
|
||
<span class="dot"></span>
|
||
{{ item.ok ? 'UP' : 'DOWN' }}
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-data">
|
||
<div>
|
||
<small>HTTP</small>
|
||
<strong>{{ item.statusCode || '-' }}</strong>
|
||
</div>
|
||
<div>
|
||
<small>耗时</small>
|
||
<strong>{{ item.latencyMs }}ms</strong>
|
||
</div>
|
||
<div>
|
||
<small>端口</small>
|
||
<strong>{{ item.port }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="service-url">{{ item.url }}</div>
|
||
<div class="service-detail">{{ item.detail || '无返回内容' }}</div>
|
||
</div>
|
||
</div>
|
||
</article>
|
||
</section>
|
||
|
||
<section class="section-head">
|
||
<h2>Nacos 注册</h2>
|
||
</section>
|
||
<section class="infra-panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h3>Nacos</h3>
|
||
<p>{{ payload.nacos.baseUrl }} · {{ payload.nacos.namespaceName }} · {{ payload.nacos.namespaceId }}</p>
|
||
</div>
|
||
<span :class="['badge', payload.nacos && payload.nacos.ok ? 'ok' : 'bad']">
|
||
{{ payload.nacos && payload.nacos.ok ? '连接正常' : '连接异常' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="summary-grid">
|
||
<div class="summary-card">
|
||
<small>命名空间</small>
|
||
<strong>{{ payload.nacos.summary.namespaceTotal || 0 }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>服务数</small>
|
||
<strong>{{ payload.nacos.summary.serviceTotal || 0 }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>实例数</small>
|
||
<strong>{{ payload.nacos.summary.instanceTotal || 0 }}</strong>
|
||
</div>
|
||
<div :class="['summary-card', (payload.nacos.summary.unhealthyInstanceTotal || 0) > 0 ? 'tone-bad' : 'tone-ok']">
|
||
<small>异常实例</small>
|
||
<strong>{{ payload.nacos.summary.unhealthyInstanceTotal || 0 }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<p v-if="payload.nacos.error" class="metric-error">{{ payload.nacos.error }}</p>
|
||
|
||
<div class="table-wrap">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>命名空间</th>
|
||
<th>ID</th>
|
||
<th>配置数</th>
|
||
<th>类型</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="item in payload.nacos.namespaces" :key="item.namespaceId">
|
||
<td>{{ item.name }}</td>
|
||
<td class="mono">{{ item.namespaceId }}</td>
|
||
<td>{{ item.configCount }}</td>
|
||
<td>{{ item.type }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="nacos-service-grid">
|
||
<article v-for="service in filteredNacosServices" :key="service.rawServiceName" class="nacos-service-card">
|
||
<header class="nacos-service-head">
|
||
<div>
|
||
<h4>{{ service.serviceName }}</h4>
|
||
<p>{{ service.namespaceName || 'public' }} · {{ service.groupName }}</p>
|
||
</div>
|
||
<span :class="['badge', service.level === 'ok' ? 'ok' : service.level === 'warn' ? 'warn' : 'bad']">
|
||
{{ service.instanceTotal }} / {{ service.healthyTotal }}
|
||
</span>
|
||
</header>
|
||
<div class="table-wrap compact">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>IP</th>
|
||
<th>端口</th>
|
||
<th>集群</th>
|
||
<th>健康</th>
|
||
<th>启用</th>
|
||
<th>权重</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="instance in service.instances" :key="service.rawServiceName + '-' + instance.ip + '-' + instance.port">
|
||
<td class="mono">{{ instance.ip }}</td>
|
||
<td>{{ instance.port }}</td>
|
||
<td>{{ instance.clusterName }}</td>
|
||
<td :class="instance.healthy ? 'ok-text' : 'bad-text'">{{ instance.healthy ? 'Y' : 'N' }}</td>
|
||
<td :class="instance.enabled ? 'ok-text' : 'bad-text'">{{ instance.enabled ? 'Y' : 'N' }}</td>
|
||
<td>{{ instance.weight }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</article>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-head">
|
||
<h2>Nacos 配置</h2>
|
||
</section>
|
||
<section class="infra-panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h3>Nacos Config</h3>
|
||
<p>读取当前命名空间下的配置并直接编辑保存</p>
|
||
</div>
|
||
<span :class="['badge', nacosConfig.error || nacosConfig.detailError ? 'bad' : 'ok']">
|
||
{{ nacosConfig.error || nacosConfig.detailError ? '存在错误' : '可操作' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="config-toolbar">
|
||
<input
|
||
v-model.trim="nacosConfig.keyword"
|
||
class="search config-search"
|
||
placeholder="搜索 dataId / group / appName"
|
||
/>
|
||
<button class="refresh" @click="refreshNacosConfigs({ preserveSelection: true, reloadSelected: false })" :disabled="nacosConfig.loading">
|
||
{{ nacosConfig.loading ? '加载中...' : '刷新配置列表' }}
|
||
</button>
|
||
</div>
|
||
|
||
<p v-if="nacosConfig.error" class="metric-error">{{ nacosConfig.error }}</p>
|
||
<p v-if="nacosConfig.detailError" class="metric-error">{{ nacosConfig.detailError }}</p>
|
||
|
||
<div class="config-layout">
|
||
<aside class="config-list">
|
||
<button
|
||
v-for="item in filteredNacosConfigs"
|
||
:key="configKey(item.group, item.dataId)"
|
||
:class="['config-item', configKey(item.group, item.dataId) === nacosConfig.selectedKey ? 'active' : '']"
|
||
@click="openNacosConfig(item)"
|
||
>
|
||
<strong>{{ item.dataId }}</strong>
|
||
<span>{{ item.group }} · {{ item.type || 'text' }}</span>
|
||
<small v-if="item.appName">{{ item.appName }}</small>
|
||
</button>
|
||
<div v-if="!nacosConfig.loading && filteredNacosConfigs.length === 0" class="config-empty">
|
||
当前没有可展示的配置项
|
||
</div>
|
||
</aside>
|
||
|
||
<section class="config-editor">
|
||
<template v-if="nacosConfig.editor.dataId">
|
||
<div class="config-meta-grid">
|
||
<div class="summary-card">
|
||
<small>Data ID</small>
|
||
<strong class="mono">{{ nacosConfig.editor.dataId }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>Group</small>
|
||
<strong class="mono">{{ nacosConfig.editor.group }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>Type</small>
|
||
<strong>{{ nacosConfig.editor.type || 'text' }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>最后修改</small>
|
||
<strong>{{ formatDateTime(nacosConfig.editor.lastModifiedTime) }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<p v-if="nacosConfig.editor.description" class="config-description">{{ nacosConfig.editor.description }}</p>
|
||
<p v-if="nacosConfig.editor.appName" class="config-description">appName:{{ nacosConfig.editor.appName }}</p>
|
||
<p v-if="nacosConfig.editor.md5" class="config-description mono">md5:{{ nacosConfig.editor.md5 }}</p>
|
||
|
||
<textarea
|
||
v-model="nacosConfig.editor.content"
|
||
class="config-textarea mono"
|
||
spellcheck="false"
|
||
:disabled="nacosConfig.detailLoading || nacosConfig.saving"
|
||
></textarea>
|
||
|
||
<div class="config-actions">
|
||
<span v-if="nacosConfig.saveMessage" class="ok-text">{{ nacosConfig.saveMessage }}</span>
|
||
<span v-else-if="nacosConfig.validationMessage" class="ok-text">{{ nacosConfig.validationMessage }}</span>
|
||
<span v-else-if="nacosConfig.detailLoading" class="badge neutral">配置加载中</span>
|
||
<span v-else-if="nacosConfigDirty" class="badge warn">有未保存修改</span>
|
||
<span v-else class="badge neutral">内容已同步</span>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="validateNacosConfig" :disabled="nacosConfig.validating || nacosConfig.saving || nacosConfig.detailLoading">
|
||
{{ nacosConfig.validating ? '校验中...' : '校验格式' }}
|
||
</button>
|
||
<button class="refresh" @click="saveNacosConfig" :disabled="nacosConfig.saving || nacosConfig.detailLoading">
|
||
{{ nacosConfig.saving ? '保存中...' : '保存配置' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-else class="config-empty">
|
||
请选择左侧配置项
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-head">
|
||
<h2>Golang 配置</h2>
|
||
</section>
|
||
<section class="infra-panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h3>Golang Runtime Env</h3>
|
||
<p>读取 app 节点当前运行中的 service.env,只暴露 chatapp3-golang 实际消费的环境变量</p>
|
||
</div>
|
||
<span :class="['badge', goConfig.error ? 'bad' : goConfig.diffKeys.length > 0 ? 'warn' : 'ok']">
|
||
{{ goConfig.error ? '存在错误' : goConfig.diffKeys.length > 0 ? '双机有差异' : '已同步' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="config-toolbar">
|
||
<div class="host-tags">
|
||
<span v-for="host in goConfig.hosts" :key="host.host" class="badge neutral">
|
||
{{ host.host }} · {{ host.path }}
|
||
</span>
|
||
</div>
|
||
<button class="refresh" @click="refreshGoConfig" :disabled="goConfig.loading">
|
||
{{ goConfig.loading ? '加载中...' : '刷新 Go 配置' }}
|
||
</button>
|
||
</div>
|
||
|
||
<p v-if="goConfig.error" class="metric-error">{{ goConfig.error }}</p>
|
||
<p v-if="goConfig.diffKeys.length > 0" class="metric-error">
|
||
当前 app 节点存在差异 key:{{ goConfig.diffKeys.join(', ') }}
|
||
</p>
|
||
|
||
<section class="config-editor">
|
||
<textarea
|
||
v-model="goConfig.editorContent"
|
||
class="config-textarea mono"
|
||
spellcheck="false"
|
||
:disabled="goConfig.loading || goConfig.saving"
|
||
></textarea>
|
||
|
||
<div class="config-actions">
|
||
<span v-if="goConfig.saveMessage" class="ok-text">{{ goConfig.saveMessage }}</span>
|
||
<span v-else-if="goConfig.validationMessage" class="ok-text">{{ goConfig.validationMessage }}</span>
|
||
<span v-else-if="goConfigDirty" class="badge warn">有未保存修改</span>
|
||
<span v-else class="badge neutral">内容已同步</span>
|
||
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="validateGoConfig" :disabled="goConfig.validating || goConfig.saving || goConfig.loading">
|
||
{{ goConfig.validating ? '校验中...' : '校验格式' }}
|
||
</button>
|
||
<button class="refresh" @click="saveGoConfig" :disabled="goConfig.saving || goConfig.loading">
|
||
{{ goConfig.saving ? '保存中...' : '保存 Go 配置' }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
</section>
|
||
|
||
<section class="section-head">
|
||
<h2>服务更新</h2>
|
||
</section>
|
||
<section class="infra-panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h3>Service Update</h3>
|
||
<p>本机构建选中的 Java / Golang 服务,推到 Harbor,更新完整 compose,并只重建你选中的服务</p>
|
||
</div>
|
||
<span :class="['badge', updateStatusBadgeClass()]">
|
||
{{ updateStatusLabel() }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="config-toolbar">
|
||
<div class="restart-pills">
|
||
<button
|
||
v-for="item in updateOps.items"
|
||
:key="item.service"
|
||
:class="['config-item', updateOps.selectedServices.includes(item.service) ? 'active' : '']"
|
||
@click="toggleUpdateService(item.service)"
|
||
>
|
||
<strong>{{ item.service }}</strong>
|
||
<span>{{ item.kind }} · {{ item.hosts.map((host) => host.host).join(' / ') }}</span>
|
||
<small v-if="item.currentImages && item.currentImages.length > 0" class="mono">
|
||
{{ item.currentImages.length === 1 ? item.currentImages[0] : `${item.currentImages.length} 个当前镜像` }}
|
||
</small>
|
||
</button>
|
||
</div>
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="refreshUpdateTargets" :disabled="updateOps.loading || updateOps.running">
|
||
{{ updateOps.loading ? '加载中...' : '刷新更新目标' }}
|
||
</button>
|
||
<button class="refresh danger" @click="runServiceUpdate" :disabled="updateOps.running || updateOps.selectedServices.length === 0">
|
||
{{ updateOps.running ? '更新中...' : `更新 ${updateOps.selectedServices.length} 项服务` }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="host-tags">
|
||
<span class="badge neutral mono">
|
||
Harbor:{{ updateOps.builder.harborRegistry }} / {{ updateOps.builder.harborProject }}
|
||
</span>
|
||
<span class="badge neutral mono">Java 仓:{{ updateOps.builder.javaRepoRoot }}</span>
|
||
<span class="badge neutral mono">Go 仓:{{ updateOps.builder.golangRepoRoot }}</span>
|
||
<span class="badge neutral">平台:{{ updateOps.defaults.dockerPlatform }}</span>
|
||
</div>
|
||
|
||
<div class="update-form-grid">
|
||
<label class="update-field">
|
||
<span>Java Git Ref</span>
|
||
<input v-model.trim="updateOps.javaGitRef" class="search" placeholder="main / branch / tag / commit" />
|
||
</label>
|
||
<label class="update-field">
|
||
<span>Go Git Ref</span>
|
||
<input v-model.trim="updateOps.goGitRef" class="search" placeholder="main / branch / tag / commit" />
|
||
</label>
|
||
<label class="update-field">
|
||
<span>Image Tag</span>
|
||
<input v-model.trim="updateOps.imageTag" class="search" placeholder="可选,不填则自动生成" />
|
||
</label>
|
||
<label class="update-check">
|
||
<input v-model="updateOps.skipMavenBuild" type="checkbox" />
|
||
<span>复用已有 Java Jar,跳过 Maven</span>
|
||
</label>
|
||
</div>
|
||
|
||
<p v-if="updateOps.error" class="metric-error">{{ updateOps.error }}</p>
|
||
<p v-if="updateOps.message" :class="updateOps.result && updateOps.result.ok ? 'ok-text' : 'metric-error'">{{ updateOps.message }}</p>
|
||
|
||
<div v-if="updateOps.operationId" class="host-tags operation-meta">
|
||
<span class="badge neutral mono">任务:{{ updateOps.operationId }}</span>
|
||
<span :class="['badge', updateStatusBadgeClass()]">状态:{{ updateStatusLabel() }}</span>
|
||
<span class="badge neutral mono">阶段:{{ updateOps.stage || '-' }}</span>
|
||
<span class="badge neutral">开始:{{ formatDateTime(updateOps.startedAt) }}</span>
|
||
<span v-if="updateOps.finishedAt" class="badge neutral">结束:{{ formatDateTime(updateOps.finishedAt) }}</span>
|
||
</div>
|
||
|
||
<section v-if="updateOps.operationId || updateOps.running || updateOps.logs.length > 0" class="update-log-panel">
|
||
<div class="panel-head update-log-head">
|
||
<div>
|
||
<h3>Update Log</h3>
|
||
<p>按阶段展示构建、推 Harbor、同步 compose、预拉镜像和滚动重建的实时输出,可下载完整日志</p>
|
||
</div>
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="fetchUpdateOperationStatus(updateOps.operationId)" :disabled="!updateOps.operationId || updateOps.running">
|
||
刷新日志
|
||
</button>
|
||
<a
|
||
v-if="updateOps.operationId"
|
||
class="refresh ghost"
|
||
:href="updateLogDownloadUrl()"
|
||
target="_blank"
|
||
rel="noopener"
|
||
download
|
||
>
|
||
下载完整日志
|
||
</a>
|
||
</div>
|
||
</div>
|
||
<pre id="service-update-log-output" class="update-log-output mono">{{ updateLogText || '等待更新日志...' }}</pre>
|
||
</section>
|
||
|
||
<div v-if="updateOps.result && updateOps.result.builds && updateOps.result.builds.length > 0" class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>仓库类型</th>
|
||
<th>Git Ref</th>
|
||
<th>Commit</th>
|
||
<th>Image Tag</th>
|
||
<th>服务</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="build in updateOps.result.builds" :key="build.kind + '-' + build.commit">
|
||
<td>{{ build.kind }}</td>
|
||
<td class="mono">{{ build.gitRef }}</td>
|
||
<td class="mono">{{ build.commit }}</td>
|
||
<td class="mono">{{ build.imageTag }}</td>
|
||
<td>{{ (build.services || []).join(', ') }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="updateOps.result && updateOps.result.preparedHosts && updateOps.result.preparedHosts.length > 0" class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>主机</th>
|
||
<th>服务</th>
|
||
<th>Compose</th>
|
||
<th>预拉镜像</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="item in updateOps.result.preparedHosts" :key="item.host">
|
||
<td>{{ item.host }}</td>
|
||
<td>{{ (item.services || []).join(', ') }}</td>
|
||
<td>{{ item.compose && item.compose.changed ? '已更新' : '未变化' }}</td>
|
||
<td :class="item.prepull && item.prepull.status === 'SUCCESS' ? 'ok-text' : 'bad-text'">
|
||
{{ item.prepull ? item.prepull.status : '-' }}
|
||
</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="updateOps.result && updateOps.result.steps && updateOps.result.steps.length > 0" class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>服务</th>
|
||
<th>主机</th>
|
||
<th>镜像</th>
|
||
<th>状态</th>
|
||
<th>HTTP</th>
|
||
<th>Nacos</th>
|
||
<th>耗时</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="step in updateOps.result.steps" :key="step.service + '-' + step.host + '-' + step.image">
|
||
<td>{{ step.service }}</td>
|
||
<td>{{ step.host }}</td>
|
||
<td class="mono">{{ step.image }}</td>
|
||
<td :class="step.status === 'success' ? 'ok-text' : 'bad-text'">{{ step.status }}</td>
|
||
<td>
|
||
<span v-if="step.http">{{ step.http.statusCode }} / {{ step.http.latencyMs }}ms</span>
|
||
<span v-else>-</span>
|
||
</td>
|
||
<td>
|
||
<span v-if="step.nacos">{{ step.nacos.source || 'ok' }}</span>
|
||
<span v-else>-</span>
|
||
</td>
|
||
<td>{{ step.durationSeconds || '-' }}s</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-head">
|
||
<h2>滚动重启</h2>
|
||
</section>
|
||
<section class="infra-panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h3>Rolling Restart</h3>
|
||
<p>选择要发布生效的 Java / Golang 服务,按宿主机顺序逐台重启并等待健康恢复</p>
|
||
</div>
|
||
<span :class="['badge', restartOps.error ? 'bad' : restartOps.running ? 'warn' : 'ok']">
|
||
{{ restartOps.error ? '存在错误' : restartOps.running ? '执行中' : '可操作' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="config-toolbar">
|
||
<div class="restart-pills">
|
||
<button
|
||
v-for="item in restartOps.items"
|
||
:key="item.service"
|
||
:class="['config-item', restartOps.selectedServices.includes(item.service) ? 'active' : '']"
|
||
@click="toggleRestartService(item.service)"
|
||
>
|
||
<strong>{{ item.service }}</strong>
|
||
<span>{{ item.kind }} · {{ item.hosts.map((host) => host.host).join(' / ') }}</span>
|
||
</button>
|
||
</div>
|
||
<div class="action-group">
|
||
<button class="refresh ghost" @click="refreshRestartTargets" :disabled="restartOps.loading || restartOps.running">
|
||
{{ restartOps.loading ? '加载中...' : '刷新服务列表' }}
|
||
</button>
|
||
<button class="refresh danger" @click="runRollingRestart" :disabled="restartOps.running || restartOps.selectedServices.length === 0">
|
||
{{ restartOps.running ? '重启中...' : `滚动重启 ${restartOps.selectedServices.length} 项服务` }}
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<p v-if="restartOps.error" class="metric-error">{{ restartOps.error }}</p>
|
||
<p v-if="restartOps.message" :class="restartOps.result && restartOps.result.ok ? 'ok-text' : 'metric-error'">{{ restartOps.message }}</p>
|
||
|
||
<div v-if="restartOps.result && restartOps.result.steps && restartOps.result.steps.length > 0" class="table-wrap">
|
||
<table class="table compact">
|
||
<thead>
|
||
<tr>
|
||
<th>服务</th>
|
||
<th>主机</th>
|
||
<th>类型</th>
|
||
<th>状态</th>
|
||
<th>HTTP</th>
|
||
<th>Nacos</th>
|
||
<th>耗时</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="step in restartOps.result.steps" :key="step.service + '-' + step.host">
|
||
<td>{{ step.service }}</td>
|
||
<td>{{ step.host }}</td>
|
||
<td>{{ step.kind }}</td>
|
||
<td :class="step.status === 'success' ? 'ok-text' : 'bad-text'">{{ step.status }}</td>
|
||
<td>
|
||
<span v-if="step.http">{{ step.http.statusCode }} / {{ step.http.latencyMs }}ms</span>
|
||
<span v-else>-</span>
|
||
</td>
|
||
<td>
|
||
<span v-if="step.nacos">{{ step.nacos.source || 'ok' }}</span>
|
||
<span v-else>-</span>
|
||
</td>
|
||
<td>{{ step.durationSeconds || '-' }}s</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
|
||
<section class="section-head">
|
||
<h2>MongoDB 指标</h2>
|
||
</section>
|
||
<section class="infra-panel">
|
||
<div class="panel-head">
|
||
<div>
|
||
<h3>MongoDB</h3>
|
||
<p>{{ payload.mongo.info.host || '-' }} · {{ payload.mongo.info.version || '-' }} · {{ payload.mongo.info.process || '-' }}</p>
|
||
</div>
|
||
<span :class="['badge', payload.mongo && payload.mongo.ok ? 'ok' : 'bad']">
|
||
{{ payload.mongo && payload.mongo.ok ? '连接正常' : '连接异常' }}
|
||
</span>
|
||
</div>
|
||
|
||
<div class="summary-grid">
|
||
<div class="summary-card">
|
||
<small>运行时长</small>
|
||
<strong>{{ formatDuration(payload.mongo.info.uptimeSeconds) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>当前连接</small>
|
||
<strong>{{ formatNumber(payload.mongo.connections.current) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>可用连接</small>
|
||
<strong>{{ formatNumber(payload.mongo.connections.available) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>数据库</small>
|
||
<strong>{{ formatNumber(payload.mongo.summary.databaseTotal) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>集合</small>
|
||
<strong>{{ formatNumber(payload.mongo.summary.collectionTotal) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>对象数</small>
|
||
<strong>{{ formatNumber(payload.mongo.summary.objectTotal) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>数据量</small>
|
||
<strong>{{ formatMb(payload.mongo.summary.dataSizeMb) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>存储量</small>
|
||
<strong>{{ formatMb(payload.mongo.summary.storageSizeMb) }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<p v-if="payload.mongo.error" class="metric-error">{{ payload.mongo.error }}</p>
|
||
|
||
<div class="metric-grid">
|
||
<div class="summary-card">
|
||
<small>Resident</small>
|
||
<strong>{{ formatMb(payload.mongo.memory.residentMb) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>Virtual</small>
|
||
<strong>{{ formatMb(payload.mongo.memory.virtualMb) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>Mapped</small>
|
||
<strong>{{ formatMb(payload.mongo.memory.mappedMb) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>Requests</small>
|
||
<strong>{{ formatNumber(payload.mongo.network.numRequests) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>Bytes In</small>
|
||
<strong>{{ formatBytes(payload.mongo.network.bytesIn) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>Bytes Out</small>
|
||
<strong>{{ formatBytes(payload.mongo.network.bytesOut) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>WT Cache</small>
|
||
<strong>{{ formatBytes(payload.mongo.wiredTigerCache.bytesCurrentlyInCache) }}</strong>
|
||
</div>
|
||
<div class="summary-card">
|
||
<small>WT Dirty</small>
|
||
<strong>{{ formatBytes(payload.mongo.wiredTigerCache.trackedDirtyBytesInCache) }}</strong>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="table-wrap">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>操作</th>
|
||
<th>次数</th>
|
||
<th>操作</th>
|
||
<th>次数</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr>
|
||
<td>insert</td>
|
||
<td>{{ formatNumber(payload.mongo.opcounters.insert) }}</td>
|
||
<td>query</td>
|
||
<td>{{ formatNumber(payload.mongo.opcounters.query) }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>update</td>
|
||
<td>{{ formatNumber(payload.mongo.opcounters.update) }}</td>
|
||
<td>delete</td>
|
||
<td>{{ formatNumber(payload.mongo.opcounters.delete) }}</td>
|
||
</tr>
|
||
<tr>
|
||
<td>getmore</td>
|
||
<td>{{ formatNumber(payload.mongo.opcounters.getmore) }}</td>
|
||
<td>command</td>
|
||
<td>{{ formatNumber(payload.mongo.opcounters.command) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div v-if="payload.mongo.replicaSet && payload.mongo.replicaSet.configured" class="table-wrap">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>成员</th>
|
||
<th>状态</th>
|
||
<th>健康</th>
|
||
<th>运行时长</th>
|
||
<th>复制延迟</th>
|
||
<th>最后 Optime</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="member in payload.mongo.replicaSet.members" :key="member.name">
|
||
<td class="mono">{{ member.name }}</td>
|
||
<td>{{ member.stateStr }}</td>
|
||
<td :class="member.health >= 1 ? 'ok-text' : 'bad-text'">{{ member.health }}</td>
|
||
<td>{{ formatDuration(member.uptime) }}</td>
|
||
<td>{{ member.lagSeconds == null ? '-' : `${member.lagSeconds}s` }}</td>
|
||
<td>{{ formatDateTime(member.optimeDate) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
|
||
<div class="table-wrap">
|
||
<table class="table">
|
||
<thead>
|
||
<tr>
|
||
<th>数据库</th>
|
||
<th>集合</th>
|
||
<th>对象</th>
|
||
<th>数据量</th>
|
||
<th>存储量</th>
|
||
<th>索引数</th>
|
||
<th>索引量</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
<tr v-for="db in payload.mongo.databases" :key="db.name">
|
||
<td>{{ db.name }}</td>
|
||
<td>{{ formatNumber(db.collections) }}</td>
|
||
<td>{{ formatNumber(db.objects) }}</td>
|
||
<td>{{ formatMb(db.dataSizeMb) }}</td>
|
||
<td>{{ formatMb(db.storageSizeMb) }}</td>
|
||
<td>{{ formatNumber(db.indexCount) }}</td>
|
||
<td>{{ formatMb(db.indexSizeMb) }}</td>
|
||
</tr>
|
||
</tbody>
|
||
</table>
|
||
</div>
|
||
</section>
|
||
</div>
|
||
</body>
|
||
|
||
</html>
|