添加更详细的信息查询
This commit is contained in:
@@ -1254,6 +1254,43 @@ input:checked+.slider:before {
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ---- Server Detail Grid ---- */
|
||||||
|
.detail-grid {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: repeat(2, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-item {
|
||||||
|
background: var(--bg-card);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-label {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-muted);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.detail-value {
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: 1.1rem;
|
||||||
|
font-weight: 700;
|
||||||
|
color: var(--accent-indigo);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light-theme .detail-value {
|
||||||
|
color: var(--accent-blue);
|
||||||
|
}
|
||||||
|
|
||||||
/* ---- Animations ---- */
|
/* ---- Animations ---- */
|
||||||
@keyframes fadeInUp {
|
@keyframes fadeInUp {
|
||||||
from {
|
from {
|
||||||
|
|||||||
@@ -364,6 +364,64 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Server Detail Modal -->
|
||||||
|
<div class="modal-overlay" id="serverDetailModal">
|
||||||
|
<div class="modal" style="max-width: 600px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 id="serverDetailTitle">服务器详情</h2>
|
||||||
|
<button class="modal-close" id="serverDetailClose">×</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body" id="serverDetailBody">
|
||||||
|
<div id="detailLoading" style="text-align: center; padding: 20px; display: none;">
|
||||||
|
<div class="dot dot-pulse" style="display: inline-block; width: 12px; height: 12px; background: var(--accent-indigo);"></div>
|
||||||
|
<span style="margin-left: 10px; color: var(--text-secondary);">正在从数据库读取详情...</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-grid" id="detailGrid">
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">CPU 忙碌率 (Busy)</span>
|
||||||
|
<span class="detail-value" id="detailCpuBusy">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">系统负载 (Load)</span>
|
||||||
|
<span class="detail-value" id="detailSysLoad">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">内存使用率 (RAM)</span>
|
||||||
|
<span class="detail-value" id="detailMemUsedPct">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">SWAP 使用率</span>
|
||||||
|
<span class="detail-value" id="detailSwapUsedPct">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">根分区使用率 (/)</span>
|
||||||
|
<span class="detail-value" id="detailRootFsUsedPct">0%</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">CPU 核心总数</span>
|
||||||
|
<span class="detail-value" id="detailCpuCores">0 核心</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">物理内存总量</span>
|
||||||
|
<span class="detail-value" id="detailMemTotal">0 GB</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">运行时间 (Uptime)</span>
|
||||||
|
<span class="detail-value" id="detailUptime">0天 0小时</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">网络接收速率 (RX)</span>
|
||||||
|
<span class="detail-value" id="detailNetRx">0 Mbps</span>
|
||||||
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">网络发送速率 (TX)</span>
|
||||||
|
<span class="detail-value" id="detailNetTx">0 Mbps</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Login Modal -->
|
<!-- Login Modal -->
|
||||||
|
|||||||
@@ -60,7 +60,23 @@
|
|||||||
loginForm: document.getElementById('loginForm'),
|
loginForm: document.getElementById('loginForm'),
|
||||||
loginError: document.getElementById('loginError'),
|
loginError: document.getElementById('loginError'),
|
||||||
footerTime: document.getElementById('footerTime'),
|
footerTime: document.getElementById('footerTime'),
|
||||||
legendP95: document.getElementById('legendP95')
|
legendP95: document.getElementById('legendP95'),
|
||||||
|
// Server Details Modal
|
||||||
|
serverDetailModal: document.getElementById('serverDetailModal'),
|
||||||
|
serverDetailClose: document.getElementById('serverDetailClose'),
|
||||||
|
serverDetailTitle: document.getElementById('serverDetailTitle'),
|
||||||
|
detailGrid: document.getElementById('detailGrid'),
|
||||||
|
detailLoading: document.getElementById('detailLoading'),
|
||||||
|
detailCpuBusy: document.getElementById('detailCpuBusy'),
|
||||||
|
detailSysLoad: document.getElementById('detailSysLoad'),
|
||||||
|
detailMemUsedPct: document.getElementById('detailMemUsedPct'),
|
||||||
|
detailSwapUsedPct: document.getElementById('detailSwapUsedPct'),
|
||||||
|
detailRootFsUsedPct: document.getElementById('detailRootFsUsedPct'),
|
||||||
|
detailCpuCores: document.getElementById('detailCpuCores'),
|
||||||
|
detailMemTotal: document.getElementById('detailMemTotal'),
|
||||||
|
detailUptime: document.getElementById('detailUptime'),
|
||||||
|
detailNetRx: document.getElementById('detailNetRx'),
|
||||||
|
detailNetTx: document.getElementById('detailNetTx')
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- State ----
|
// ---- State ----
|
||||||
@@ -113,6 +129,26 @@
|
|||||||
if (e.key === 'Escape') {
|
if (e.key === 'Escape') {
|
||||||
closeSettings();
|
closeSettings();
|
||||||
closeLoginModal();
|
closeLoginModal();
|
||||||
|
closeServerDetail();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Server detail modal listeners
|
||||||
|
dom.serverDetailClose.addEventListener('click', closeServerDetail);
|
||||||
|
dom.serverDetailModal.addEventListener('click', (e) => {
|
||||||
|
if (e.target === dom.serverDetailModal) closeServerDetail();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Server table row click delegator
|
||||||
|
dom.serverTableBody.addEventListener('click', (e) => {
|
||||||
|
const row = e.target.closest('tr');
|
||||||
|
if (row && !row.classList.contains('empty-row')) {
|
||||||
|
const instance = row.getAttribute('data-instance');
|
||||||
|
const job = row.getAttribute('data-job');
|
||||||
|
const source = row.getAttribute('data-source');
|
||||||
|
if (instance && job && source) {
|
||||||
|
openServerDetail(instance, job, source);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -354,7 +390,7 @@
|
|||||||
const diskPct = server.diskTotal > 0 ? (server.diskUsed / server.diskTotal * 100) : 0;
|
const diskPct = server.diskTotal > 0 ? (server.diskUsed / server.diskTotal * 100) : 0;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr>
|
<tr data-instance="${escapeHtml(server.instance)}" data-job="${escapeHtml(server.job)}" data-source="${escapeHtml(server.source)}" style="cursor: pointer;">
|
||||||
<td>
|
<td>
|
||||||
<span class="status-dot ${server.up ? 'status-dot-online' : 'status-dot-offline'}"></span>
|
<span class="status-dot ${server.up ? 'status-dot-online' : 'status-dot-offline'}"></span>
|
||||||
</td>
|
</td>
|
||||||
@@ -393,6 +429,54 @@
|
|||||||
}).join('');
|
}).join('');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Server Detail ----
|
||||||
|
async function openServerDetail(instance, job, source) {
|
||||||
|
dom.serverDetailTitle.textContent = `${job} - 服务器详情`;
|
||||||
|
dom.serverDetailModal.classList.add('active');
|
||||||
|
|
||||||
|
// Show loading
|
||||||
|
dom.detailGrid.style.opacity = '0.3';
|
||||||
|
dom.detailLoading.style.display = 'block';
|
||||||
|
|
||||||
|
try {
|
||||||
|
const url = `/api/metrics/server-details?instance=${encodeURIComponent(instance)}&job=${encodeURIComponent(job)}&source=${encodeURIComponent(source)}`;
|
||||||
|
const response = await fetch(url);
|
||||||
|
if (!response.ok) throw new Error('Fetch failed');
|
||||||
|
const data = await response.json();
|
||||||
|
|
||||||
|
renderServerDetail(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Error fetching server details:', err);
|
||||||
|
} finally {
|
||||||
|
dom.detailGrid.style.opacity = '1';
|
||||||
|
dom.detailLoading.style.display = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeServerDetail() {
|
||||||
|
dom.serverDetailModal.classList.remove('active');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderServerDetail(data) {
|
||||||
|
dom.detailCpuBusy.textContent = formatPercent(data.cpuBusy);
|
||||||
|
dom.detailSysLoad.textContent = data.sysLoad.toFixed(1) + '%';
|
||||||
|
dom.detailMemUsedPct.textContent = formatPercent(data.memUsedPct);
|
||||||
|
dom.detailSwapUsedPct.textContent = formatPercent(data.swapUsedPct);
|
||||||
|
dom.detailRootFsUsedPct.textContent = formatPercent(data.rootFsUsedPct);
|
||||||
|
dom.detailCpuCores.textContent = data.cpuCores + ' 核心';
|
||||||
|
dom.detailMemTotal.textContent = formatBytes(data.memTotal);
|
||||||
|
|
||||||
|
// Uptime formatting
|
||||||
|
const uptimeSec = data.uptime;
|
||||||
|
const days = Math.floor(uptimeSec / 86400);
|
||||||
|
const hours = Math.floor((uptimeSec % 86400) / 3600);
|
||||||
|
const mins = Math.floor((uptimeSec % 3600) / 60);
|
||||||
|
dom.detailUptime.textContent = `${days}天 ${hours}小时 ${mins}分`;
|
||||||
|
|
||||||
|
dom.detailNetRx.textContent = formatBandwidth(data.netRx);
|
||||||
|
dom.detailNetTx.textContent = formatBandwidth(data.netTx);
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Network History ----
|
// ---- Network History ----
|
||||||
async function fetchNetworkHistory() {
|
async function fetchNetworkHistory() {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -601,6 +601,31 @@ app.get('/api/metrics/cpu-history', async (req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Get detailed metrics for a specific server
|
||||||
|
app.get('/api/metrics/server-details', async (req, res) => {
|
||||||
|
const { instance, job, source } = req.query;
|
||||||
|
|
||||||
|
if (!instance || !job || !source) {
|
||||||
|
return res.status(400).json({ error: 'instance, job, and source name are required' });
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Find the source URL by name
|
||||||
|
const [rows] = await db.query('SELECT url FROM prometheus_sources WHERE name = ?', [source]);
|
||||||
|
if (rows.length === 0) {
|
||||||
|
return res.status(404).json({ error: 'Prometheus source not found' });
|
||||||
|
}
|
||||||
|
const sourceUrl = rows[0].url;
|
||||||
|
|
||||||
|
// Fetch detailed metrics
|
||||||
|
const details = await prometheusService.getServerDetails(sourceUrl, instance, job);
|
||||||
|
res.json(details);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`Error fetching server details for ${instance}:`, err.message);
|
||||||
|
res.status(500).json({ error: 'Failed to fetch server details' });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// SPA fallback
|
// SPA fallback
|
||||||
app.get('*', (req, res, next) => {
|
app.get('*', (req, res, next) => {
|
||||||
if (req.path.startsWith('/api/') || req.path.includes('.')) return next();
|
if (req.path.startsWith('/api/') || req.path.includes('.')) return next();
|
||||||
|
|||||||
@@ -412,6 +412,42 @@ function mergeCpuHistories(histories) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get detailed metrics for a specific server (node)
|
||||||
|
*/
|
||||||
|
async function getServerDetails(baseUrl, instance, job) {
|
||||||
|
const url = normalizeUrl(baseUrl);
|
||||||
|
const node = instance;
|
||||||
|
|
||||||
|
// Queries based on the requested dashboard structure
|
||||||
|
const queries = {
|
||||||
|
cpuBusy: `100 * (1 - avg(rate(node_cpu_seconds_total{mode="idle", instance="${node}"}[1m])))`,
|
||||||
|
sysLoad: `node_load1{instance="${node}",job="${job}"} * 100 / count(count(node_cpu_seconds_total{instance="${node}",job="${job}"}) by (cpu))`,
|
||||||
|
memUsedPct: `(1 - (node_memory_MemAvailable_bytes{instance="${node}", job="${job}"} / node_memory_MemTotal_bytes{instance="${node}", job="${job}"})) * 100`,
|
||||||
|
swapUsedPct: `((node_memory_SwapTotal_bytes{instance="${node}",job="${job}"} - node_memory_SwapFree_bytes{instance="${node}",job="${job}"}) / (node_memory_SwapTotal_bytes{instance="${node}",job="${job}"})) * 100`,
|
||||||
|
rootFsUsedPct: `100 - ((node_filesystem_avail_bytes{instance="${node}",job="${job}",mountpoint="/",fstype!="rootfs"} * 100) / node_filesystem_size_bytes{instance="${node}",job="${job}",mountpoint="/",fstype!="rootfs"})`,
|
||||||
|
cpuCores: `count(count(node_cpu_seconds_total{instance="${node}",job="${job}"}) by (cpu))`,
|
||||||
|
memTotal: `node_memory_MemTotal_bytes{instance="${node}",job="${job}"}`,
|
||||||
|
uptime: `node_time_seconds{instance="${node}",job="${job}"} - node_boot_time_seconds{instance="${node}",job="${job}"}`,
|
||||||
|
netRx: `sum(rate(node_network_receive_bytes_total{instance="${node}",job="${job}",device!~'tap.*|veth.*|br.*|docker.*|virbr*|podman.*|lo.*|vmbr.*|fwbr.|ip.*|gre.*|virbr.*|vnet.*'}[1m]))`,
|
||||||
|
netTx: `sum(rate(node_network_transmit_bytes_total{instance="${node}",job="${job}",device!~'tap.*|veth.*|br.*|docker.*|virbr*|podman.*|lo.*|vmbr.*|fwbr.|ip.*|gre.*|virbr.*|vnet.*'}[1m]))`
|
||||||
|
};
|
||||||
|
|
||||||
|
const results = {};
|
||||||
|
const queryPromises = Object.entries(queries).map(async ([key, expr]) => {
|
||||||
|
try {
|
||||||
|
const res = await query(url, expr);
|
||||||
|
results[key] = res.length > 0 ? parseFloat(res[0].value[1]) : 0;
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`[Prometheus] Error querying ${key} for ${node}:`, e.message);
|
||||||
|
results[key] = 0;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await Promise.all(queryPromises);
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
testConnection,
|
testConnection,
|
||||||
query,
|
query,
|
||||||
@@ -420,5 +456,6 @@ module.exports = {
|
|||||||
getNetworkHistory,
|
getNetworkHistory,
|
||||||
mergeNetworkHistories,
|
mergeNetworkHistories,
|
||||||
getCpuHistory,
|
getCpuHistory,
|
||||||
mergeCpuHistories
|
mergeCpuHistories,
|
||||||
|
getServerDetails
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user