prometheus
This commit is contained in:
@@ -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;
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user