prometheus
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -256,6 +256,11 @@
|
||||
</svg>
|
||||
服务器详情
|
||||
</h2>
|
||||
<div class="chart-header-right">
|
||||
<select id="sourceFilter" class="source-select">
|
||||
<option value="all">所有数据源</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="server-table-wrap">
|
||||
<table class="server-table" id="serverTable">
|
||||
|
||||
@@ -70,7 +70,8 @@
|
||||
detailCpuCores: document.getElementById('detailCpuCores'),
|
||||
detailMemTotal: document.getElementById('detailMemTotal'),
|
||||
detailUptime: document.getElementById('detailUptime'),
|
||||
detailContainer: document.getElementById('detailContainer')
|
||||
detailContainer: document.getElementById('detailContainer'),
|
||||
sourceFilter: document.getElementById('sourceFilter')
|
||||
};
|
||||
|
||||
// ---- State ----
|
||||
@@ -78,6 +79,8 @@
|
||||
let networkChart = null;
|
||||
let user = null; // Currently logged in user
|
||||
let currentServerDetail = { instance: null, job: null, source: null, charts: {} };
|
||||
let allServersData = [];
|
||||
let currentSourceFilter = 'all';
|
||||
|
||||
// ---- Initialize ----
|
||||
function init() {
|
||||
@@ -155,6 +158,14 @@
|
||||
networkChart.draw();
|
||||
});
|
||||
}
|
||||
|
||||
// Source filter listener
|
||||
if (dom.sourceFilter) {
|
||||
dom.sourceFilter.addEventListener('change', () => {
|
||||
currentSourceFilter = dom.sourceFilter.value;
|
||||
renderFilteredServers();
|
||||
});
|
||||
}
|
||||
|
||||
// Check auth status
|
||||
checkAuthStatus();
|
||||
@@ -312,6 +323,7 @@
|
||||
try {
|
||||
const response = await fetch('/api/metrics/overview');
|
||||
const data = await response.json();
|
||||
allServersData = data.servers || [];
|
||||
updateDashboard(data);
|
||||
} catch (err) {
|
||||
console.error('Error fetching metrics:', err);
|
||||
@@ -349,7 +361,7 @@
|
||||
dom.traffic24hTotal.textContent = formatBytes(data.traffic24h.total || (data.traffic24h.rx + data.traffic24h.tx));
|
||||
|
||||
// Update server table
|
||||
updateServerTable(data.servers);
|
||||
renderFilteredServers();
|
||||
|
||||
// Flash animation
|
||||
if (previousMetrics) {
|
||||
@@ -363,6 +375,14 @@
|
||||
previousMetrics = data;
|
||||
}
|
||||
|
||||
function renderFilteredServers() {
|
||||
let filtered = allServersData;
|
||||
if (currentSourceFilter !== 'all') {
|
||||
filtered = allServersData.filter(s => s.source === currentSourceFilter);
|
||||
}
|
||||
updateServerTable(filtered);
|
||||
}
|
||||
|
||||
// ---- Server Table ----
|
||||
function updateServerTable(servers) {
|
||||
if (!servers || servers.length === 0) {
|
||||
@@ -390,7 +410,8 @@
|
||||
<span class="status-dot ${server.up ? 'status-dot-online' : 'status-dot-offline'}"></span>
|
||||
</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>${escapeHtml(server.source)}</td>
|
||||
<td>
|
||||
@@ -770,11 +791,28 @@
|
||||
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() {
|
||||
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);
|
||||
|
||||
@@ -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
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user