534 lines
26 KiB
HTML
534 lines
26 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.detailLoading" class="badge neutral">配置加载中</span>
|
||
<span v-else-if="nacosConfigDirty" class="badge warn">有未保存修改</span>
|
||
<span v-else class="badge neutral">内容已同步</span>
|
||
|
||
<button class="refresh" @click="saveNacosConfig" :disabled="nacosConfig.saving || nacosConfig.detailLoading">
|
||
{{ nacosConfig.saving ? '保存中...' : '保存配置' }}
|
||
</button>
|
||
</div>
|
||
</template>
|
||
|
||
<div v-else class="config-empty">
|
||
请选择左侧配置项
|
||
</div>
|
||
</section>
|
||
</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>
|