prometheus

This commit is contained in:
CN-JS-HuiBai
2026-04-04 23:08:30 +08:00
parent f3f49f2c8e
commit a1703e72be
4 changed files with 78 additions and 14 deletions

View File

@@ -613,6 +613,29 @@ input:checked+.slider:before {
gap: 16px; gap: 16px;
} }
.chart-header-right {
display: flex;
align-items: center;
gap: 12px;
}
.source-select {
padding: 6px 12px;
background: var(--bg-input);
border: 1px solid var(--border-color);
border-radius: var(--radius-sm);
color: var(--text-primary);
font-size: 0.85rem;
outline: none;
cursor: pointer;
transition: all 0.2s ease;
}
.source-select:hover {
border-color: var(--border-hover);
background: rgba(99, 102, 241, 0.05);
}
.chart-title { .chart-title {
display: flex; display: flex;
align-items: center; align-items: center;

View File

@@ -256,6 +256,11 @@
</svg> </svg>
服务器详情 服务器详情
</h2> </h2>
<div class="chart-header-right">
<select id="sourceFilter" class="source-select">
<option value="all">所有数据源</option>
</select>
</div>
</div> </div>
<div class="server-table-wrap"> <div class="server-table-wrap">
<table class="server-table" id="serverTable"> <table class="server-table" id="serverTable">

View File

@@ -70,7 +70,8 @@
detailCpuCores: document.getElementById('detailCpuCores'), detailCpuCores: document.getElementById('detailCpuCores'),
detailMemTotal: document.getElementById('detailMemTotal'), detailMemTotal: document.getElementById('detailMemTotal'),
detailUptime: document.getElementById('detailUptime'), detailUptime: document.getElementById('detailUptime'),
detailContainer: document.getElementById('detailContainer') detailContainer: document.getElementById('detailContainer'),
sourceFilter: document.getElementById('sourceFilter')
}; };
// ---- State ---- // ---- State ----
@@ -78,6 +79,8 @@
let networkChart = null; let networkChart = null;
let user = null; // Currently logged in user let user = null; // Currently logged in user
let currentServerDetail = { instance: null, job: null, source: null, charts: {} }; let currentServerDetail = { instance: null, job: null, source: null, charts: {} };
let allServersData = [];
let currentSourceFilter = 'all';
// ---- Initialize ---- // ---- Initialize ----
function init() { function init() {
@@ -156,6 +159,14 @@
}); });
} }
// Source filter listener
if (dom.sourceFilter) {
dom.sourceFilter.addEventListener('change', () => {
currentSourceFilter = dom.sourceFilter.value;
renderFilteredServers();
});
}
// Check auth status // Check auth status
checkAuthStatus(); checkAuthStatus();
@@ -312,6 +323,7 @@
try { try {
const response = await fetch('/api/metrics/overview'); const response = await fetch('/api/metrics/overview');
const data = await response.json(); const data = await response.json();
allServersData = data.servers || [];
updateDashboard(data); updateDashboard(data);
} catch (err) { } catch (err) {
console.error('Error fetching metrics:', err); console.error('Error fetching metrics:', err);
@@ -349,7 +361,7 @@
dom.traffic24hTotal.textContent = formatBytes(data.traffic24h.total || (data.traffic24h.rx + data.traffic24h.tx)); dom.traffic24hTotal.textContent = formatBytes(data.traffic24h.total || (data.traffic24h.rx + data.traffic24h.tx));
// Update server table // Update server table
updateServerTable(data.servers); renderFilteredServers();
// Flash animation // Flash animation
if (previousMetrics) { if (previousMetrics) {
@@ -363,6 +375,14 @@
previousMetrics = data; previousMetrics = data;
} }
function renderFilteredServers() {
let filtered = allServersData;
if (currentSourceFilter !== 'all') {
filtered = allServersData.filter(s => s.source === currentSourceFilter);
}
updateServerTable(filtered);
}
// ---- Server Table ---- // ---- Server Table ----
function updateServerTable(servers) { function updateServerTable(servers) {
if (!servers || servers.length === 0) { if (!servers || servers.length === 0) {
@@ -390,7 +410,8 @@
<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>
<td> <td>
<div style="color: var(--text-primary); font-weight: 600; font-family: var(--font-sans);">${escapeHtml(server.job)}</div> <div style="color: var(--text-primary); font-weight: 600; font-family: var(--font-sans); line-height: 1.2;">${escapeHtml(server.job)}</div>
<div style="color: var(--text-muted); font-size: 0.7rem; font-family: var(--font-mono); font-weight: 400; margin-top: 2px;">${escapeHtml(server.originalInstance)}</div>
</td> </td>
<td>${escapeHtml(server.source)}</td> <td>${escapeHtml(server.source)}</td>
<td> <td>
@@ -770,11 +791,28 @@
dom.siteSettingsMessage.className = 'form-message'; dom.siteSettingsMessage.className = 'form-message';
} }
function updateSourceFilterOptions(sources) {
if (!dom.sourceFilter) return;
const current = dom.sourceFilter.value;
let html = '<option value="all">所有数据源</option>';
sources.forEach(source => {
html += `<option value="${escapeHtml(source.name)}">${escapeHtml(source.name)}</option>`;
});
dom.sourceFilter.innerHTML = html;
if (sources.some(s => s.name === current)) {
dom.sourceFilter.value = current;
} else {
dom.sourceFilter.value = 'all';
currentSourceFilter = 'all';
}
}
async function loadSources() { async function loadSources() {
try { try {
const response = await fetch('/api/sources'); const response = await fetch('/api/sources');
const sources = await response.json(); const sources = await response.json();
dom.sourceCount.textContent = `${sources.length} 个数据源`; dom.sourceCount.textContent = `${sources.length} 个数据源`;
updateSourceFilterOptions(sources);
renderSources(sources); renderSources(sources);
} catch (err) { } catch (err) {
console.error('Error loading sources:', err); console.error('Error loading sources:', err);

View File

@@ -12,9 +12,9 @@ const httpsAgent = new https.Agent({ keepAlive: true, rejectUnauthorized: false
const serverIdMap = new Map(); // token -> { instance, job, source } const serverIdMap = new Map(); // token -> { instance, job, source }
const SECRET = crypto.randomBytes(16).toString('hex'); const SECRET = crypto.randomBytes(16).toString('hex');
function getServerToken(instance) { function getServerToken(instance, job, source) {
const hash = crypto.createHmac('sha256', SECRET) const hash = crypto.createHmac('sha256', SECRET)
.update(instance) .update(`${instance}:${job}:${source}`)
.digest('hex') .digest('hex')
.substring(0, 16); .substring(0, 16);
return hash; return hash;
@@ -220,11 +220,11 @@ async function getOverviewMetrics(url, sourceName) {
const instances = new Map(); const instances = new Map();
const getOrCreate = (metric) => { const getOrCreate = (metric) => {
const originalInstance = metric.instance; const job = metric.job || 'Unknown';
const token = getServerToken(originalInstance, sourceName); const token = getServerToken(originalInstance, job, sourceName);
// Store mapping for detail queries // Store mapping for detail queries
serverIdMap.set(token, { instance: originalInstance, source: sourceName, job: metric.job }); serverIdMap.set(token, { instance: originalInstance, source: sourceName, job });
if (!instances.has(token)) { if (!instances.has(token)) {
instances.set(token, { instances.set(token, {
@@ -252,14 +252,13 @@ async function getOverviewMetrics(url, sourceName) {
}; };
// Initialize instances from targets first (to ensure we have all servers even if they have no metrics) // Initialize instances from targets first (to ensure we have all servers even if they have no metrics)
const nodeJobRegex = /node|exporter|host/i;
for (const target of targetsResult) { for (const target of targetsResult) {
const labels = target.labels || {}; const labels = target.labels || {};
const instance = labels.instance; const instance = labels.instance;
const job = labels.job; const job = labels.job || '';
// Only include targets that look like node-exporters // Include every target from the activeTargets list
if (instance && (nodeJobRegex.test(job) || nodeJobRegex.test(target.scrapePool))) { if (instance) {
const inst = getOrCreate(labels); const inst = getOrCreate(labels);
inst.up = target.health === 'up'; inst.up = target.health === 'up';
} }
@@ -371,8 +370,7 @@ async function getOverviewMetrics(url, sourceName) {
total: totalTraffic24hRx + totalTraffic24hTx total: totalTraffic24hRx + totalTraffic24hTx
}, },
servers: allInstancesList.map(s => { servers: allInstancesList.map(s => {
const { originalInstance, ...rest } = s; return s; // Include all fields including originalInstance
return rest;
}) })
}; };
} }