diff --git a/public/css/style.css b/public/css/style.css index 6d3f246..c5e473b 100644 --- a/public/css/style.css +++ b/public/css/style.css @@ -613,6 +613,29 @@ input:checked+.slider:before { 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 { display: flex; align-items: center; diff --git a/public/index.html b/public/index.html index 94e704f..aa4b43e 100644 --- a/public/index.html +++ b/public/index.html @@ -256,6 +256,11 @@ 服务器详情 +
|
- ${escapeHtml(server.job)}
+ ${escapeHtml(server.job)}
+ ${escapeHtml(server.originalInstance)}
|
${escapeHtml(server.source)} | @@ -770,11 +791,28 @@ dom.siteSettingsMessage.className = 'form-message'; } + function updateSourceFilterOptions(sources) { + if (!dom.sourceFilter) return; + const current = dom.sourceFilter.value; + let html = ''; + sources.forEach(source => { + html += ``; + }); + 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() { try { const response = await fetch('/api/sources'); const sources = await response.json(); dom.sourceCount.textContent = `${sources.length} 个数据源`; + updateSourceFilterOptions(sources); renderSources(sources); } catch (err) { console.error('Error loading sources:', err); diff --git a/server/prometheus-service.js b/server/prometheus-service.js index 647a76f..efd02bd 100644 --- a/server/prometheus-service.js +++ b/server/prometheus-service.js @@ -12,9 +12,9 @@ const httpsAgent = new https.Agent({ keepAlive: true, rejectUnauthorized: false const serverIdMap = new Map(); // token -> { instance, job, source } const SECRET = crypto.randomBytes(16).toString('hex'); -function getServerToken(instance) { +function getServerToken(instance, job, source) { const hash = crypto.createHmac('sha256', SECRET) - .update(instance) + .update(`${instance}:${job}:${source}`) .digest('hex') .substring(0, 16); return hash; @@ -220,11 +220,11 @@ async function getOverviewMetrics(url, sourceName) { const instances = new Map(); const getOrCreate = (metric) => { - const originalInstance = metric.instance; - const token = getServerToken(originalInstance, sourceName); + const job = metric.job || 'Unknown'; + const token = getServerToken(originalInstance, job, sourceName); // 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)) { 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) - const nodeJobRegex = /node|exporter|host/i; for (const target of targetsResult) { const labels = target.labels || {}; const instance = labels.instance; - const job = labels.job; + const job = labels.job || ''; - // Only include targets that look like node-exporters - if (instance && (nodeJobRegex.test(job) || nodeJobRegex.test(target.scrapePool))) { + // Include every target from the activeTargets list + if (instance) { const inst = getOrCreate(labels); inst.up = target.health === 'up'; } @@ -371,8 +370,7 @@ async function getOverviewMetrics(url, sourceName) { total: totalTraffic24hRx + totalTraffic24hTx }, servers: allInstancesList.map(s => { - const { originalInstance, ...rest } = s; - return rest; + return s; // Include all fields including originalInstance }) }; } |