加入自定义指标支持

This commit is contained in:
CN-JS-HuiBai
2026-04-10 22:22:54 +08:00
parent 710b6a719e
commit cb27d1a249
5 changed files with 93 additions and 16 deletions

View File

@@ -593,7 +593,25 @@
<option value="1">显示 (Show)</option> <option value="1">显示 (Show)</option>
<option value="0">隐藏 (Hide)</option> <option value="0">隐藏 (Hide)</option>
</select> </select>
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 6px;">开启后,点击服务器详情时会显示该服务器的公网 IP 地址(需 node_exporter 提供支持)</p> <p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 6px;">开启后,点击服务器详情时会显示该服务器的公网 IP 地址。</p>
</div>
<div style="margin-top: 25px; padding-top: 15px; border-top: 1px dashed var(--border-color);">
<h4 style="margin-bottom: 12px; color: var(--text-secondary); font-size: 0.95rem;">高级指标配置 (可选)</h4>
<div class="form-group">
<label for="ipMetricNameInput">IP 发现指标 (Metric Name)</label>
<input type="text" id="ipMetricNameInput" placeholder="例如: node_network_address_info"
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary); width: 100%;">
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 6px;">留空则使用 Prometheus Target 自动发现。若需兼容 <code>node_exporter</code> 自定义指标(如 textfile请在此输入指标名。</p>
</div>
<div class="form-group" style="margin-top: 12px;">
<label for="ipLabelNameInput">IP 提取标签 (Label Name)</label>
<input type="text" id="ipLabelNameInput" placeholder="address"
style="padding: 10px 14px; background: var(--bg-input); border: 1px solid var(--border-color); border-radius: var(--radius-sm); color: var(--text-primary); width: 100%;">
<p style="font-size: 0.72rem; color: var(--text-muted); margin-top: 6px;">从该指标的哪个标签提取 IP默认为 <code>address</code></p>
</div>
</div> </div>
<div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end;"> <div class="form-actions" style="margin-top: 25px; display: flex; justify-content: flex-end;">
<button class="btn btn-add" id="btnSaveSecuritySettings">保存安全设置</button> <button class="btn btn-add" id="btnSaveSecuritySettings">保存安全设置</button>

View File

@@ -115,6 +115,8 @@
btnExpandGlobe: document.getElementById('btnExpandGlobe'), btnExpandGlobe: document.getElementById('btnExpandGlobe'),
btnRefreshNetwork: document.getElementById('btnRefreshNetwork'), btnRefreshNetwork: document.getElementById('btnRefreshNetwork'),
showServerIpInput: document.getElementById('showServerIpInput'), showServerIpInput: document.getElementById('showServerIpInput'),
ipMetricNameInput: document.getElementById('ipMetricNameInput'),
ipLabelNameInput: document.getElementById('ipLabelNameInput'),
// Footer & Filing // Footer & Filing
icpFilingInput: document.getElementById('icpFilingInput'), icpFilingInput: document.getElementById('icpFilingInput'),
psFilingInput: document.getElementById('psFilingInput'), psFilingInput: document.getElementById('psFilingInput'),
@@ -1994,6 +1996,8 @@
// Update IP visibility input // Update IP visibility input
if (dom.showServerIpInput) dom.showServerIpInput.value = settings.show_server_ip ? "1" : "0"; if (dom.showServerIpInput) dom.showServerIpInput.value = settings.show_server_ip ? "1" : "0";
if (dom.ipMetricNameInput) dom.ipMetricNameInput.value = settings.ip_metric_name || '';
if (dom.ipLabelNameInput) dom.ipLabelNameInput.value = settings.ip_label_name || 'address';
// Sync security tab dependency // Sync security tab dependency
updateSecurityDependency(); updateSecurityDependency();
@@ -2112,7 +2116,9 @@
ps_filing: dom.psFilingInput ? dom.psFilingInput.value.trim() : '', ps_filing: dom.psFilingInput ? dom.psFilingInput.value.trim() : '',
icp_filing: dom.icpFilingInput ? dom.icpFilingInput.value.trim() : '', icp_filing: dom.icpFilingInput ? dom.icpFilingInput.value.trim() : '',
network_data_sources: Array.from(dom.networkSourceSelector.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value).join(','), network_data_sources: Array.from(dom.networkSourceSelector.querySelectorAll('input[type="checkbox"]:checked')).map(cb => cb.value).join(','),
show_server_ip: dom.showServerIpInput ? (dom.showServerIpInput.value === "1") : false show_server_ip: dom.showServerIpInput ? (dom.showServerIpInput.value === "1") : false,
ip_metric_name: dom.ipMetricNameInput ? dom.ipMetricNameInput.value.trim() : null,
ip_label_name: dom.ipLabelNameInput ? dom.ipLabelNameInput.value.trim() : 'address'
}; };
// UI Feedback for both potential save buttons // UI Feedback for both potential save buttons

View File

@@ -66,6 +66,8 @@ const SCHEMA = {
ps_filing VARCHAR(255), ps_filing VARCHAR(255),
network_data_sources TEXT, network_data_sources TEXT,
show_server_ip TINYINT(1) DEFAULT 0, show_server_ip TINYINT(1) DEFAULT 0,
ip_metric_name VARCHAR(100) DEFAULT NULL,
ip_label_name VARCHAR(100) DEFAULT 'address',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`, `,
@@ -132,6 +134,14 @@ const SCHEMA = {
{ {
name: 'show_server_ip', name: 'show_server_ip',
sql: "ALTER TABLE site_settings ADD COLUMN show_server_ip TINYINT(1) DEFAULT 0 AFTER network_data_sources" sql: "ALTER TABLE site_settings ADD COLUMN show_server_ip TINYINT(1) DEFAULT 0 AFTER network_data_sources"
},
{
name: 'ip_metric_name',
sql: "ALTER TABLE site_settings ADD COLUMN ip_metric_name VARCHAR(100) DEFAULT NULL AFTER show_server_ip"
},
{
name: 'ip_label_name',
sql: "ALTER TABLE site_settings ADD COLUMN ip_label_name VARCHAR(100) DEFAULT 'address' AFTER ip_metric_name"
} }
] ]
}, },

View File

@@ -146,7 +146,9 @@ function getPublicSiteSettings(settings = {}) {
icp_filing: settings.icp_filing || null, icp_filing: settings.icp_filing || null,
ps_filing: settings.ps_filing || null, ps_filing: settings.ps_filing || null,
network_data_sources: settings.network_data_sources || null, network_data_sources: settings.network_data_sources || null,
show_server_ip: settings.show_server_ip ? 1 : 0 show_server_ip: settings.show_server_ip ? 1 : 0,
ip_metric_name: settings.ip_metric_name || null,
ip_label_name: settings.ip_label_name || 'address'
}; };
} }
@@ -551,6 +553,8 @@ app.post('/api/setup/init', ensureSetupAccess, async (req, res) => {
ps_filing VARCHAR(255), ps_filing VARCHAR(255),
network_data_sources TEXT, network_data_sources TEXT,
show_server_ip TINYINT(1) DEFAULT 0, show_server_ip TINYINT(1) DEFAULT 0,
ip_metric_name VARCHAR(100) DEFAULT NULL,
ip_label_name VARCHAR(100) DEFAULT 'address',
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`); `);
@@ -566,6 +570,8 @@ app.post('/api/setup/init', ensureSetupAccess, async (req, res) => {
await connection.query("ALTER TABLE site_settings ADD COLUMN IF NOT EXISTS require_login_for_server_details TINYINT(1) DEFAULT 1 AFTER p95_type"); await connection.query("ALTER TABLE site_settings ADD COLUMN IF NOT EXISTS require_login_for_server_details TINYINT(1) DEFAULT 1 AFTER p95_type");
await connection.query("ALTER TABLE site_settings ADD COLUMN IF NOT EXISTS network_data_sources TEXT AFTER ps_filing"); await connection.query("ALTER TABLE site_settings ADD COLUMN IF NOT EXISTS network_data_sources TEXT AFTER ps_filing");
await connection.query("ALTER TABLE site_settings ADD COLUMN IF NOT EXISTS show_server_ip TINYINT(1) DEFAULT 0 AFTER network_data_sources"); await connection.query("ALTER TABLE site_settings ADD COLUMN IF NOT EXISTS show_server_ip TINYINT(1) DEFAULT 0 AFTER network_data_sources");
await connection.query("ALTER TABLE site_settings ADD COLUMN IF NOT EXISTS ip_metric_name VARCHAR(100) DEFAULT NULL AFTER show_server_ip");
await connection.query("ALTER TABLE site_settings ADD COLUMN IF NOT EXISTS ip_label_name VARCHAR(100) DEFAULT 'address' AFTER ip_metric_name");
await connection.query(` await connection.query(`
CREATE TABLE IF NOT EXISTS latency_routes ( CREATE TABLE IF NOT EXISTS latency_routes (
id INT AUTO_INCREMENT PRIMARY KEY, id INT AUTO_INCREMENT PRIMARY KEY,
@@ -923,7 +929,9 @@ app.post('/api/settings', requireAuth, async (req, res) => {
icp_filing: icp_filing !== undefined ? icp_filing : (current.icp_filing || null), icp_filing: icp_filing !== undefined ? icp_filing : (current.icp_filing || null),
ps_filing: ps_filing !== undefined ? ps_filing : (current.ps_filing || null), ps_filing: ps_filing !== undefined ? ps_filing : (current.ps_filing || null),
network_data_sources: network_data_sources !== undefined ? network_data_sources : (current.network_data_sources || null), network_data_sources: network_data_sources !== undefined ? network_data_sources : (current.network_data_sources || null),
show_server_ip: show_server_ip !== undefined ? (show_server_ip ? 1 : 0) : (current.show_server_ip || 0) show_server_ip: show_server_ip !== undefined ? (show_server_ip ? 1 : 0) : (current.show_server_ip || 0),
ip_metric_name: ip_metric_name !== undefined ? ip_metric_name : (current.ip_metric_name || null),
ip_label_name: ip_label_name !== undefined ? ip_label_name : (current.ip_label_name || 'address')
}; };
await db.query(` await db.query(`
@@ -1216,8 +1224,8 @@ app.get('/api/metrics/server-details', requireServerDetailsAccess, async (req, r
} }
const sourceUrl = rows[0].url; const sourceUrl = rows[0].url;
// Fetch detailed metrics // Fetch detailed metrics with custom metric configuration if present
const details = await prometheusService.getServerDetails(sourceUrl, instance, job); const details = await prometheusService.getServerDetails(sourceUrl, instance, job, req.siteSettings);
// Dynamic field removal based on security settings: PHYSICAL DATA STRIPPING // Dynamic field removal based on security settings: PHYSICAL DATA STRIPPING
if (!req.siteSettings || !req.siteSettings.show_server_ip) { if (!req.siteSettings || !req.siteSettings.show_server_ip) {

View File

@@ -535,7 +535,7 @@ function resolveToken(token) {
/** /**
* Get detailed metrics for a specific server (node) * Get detailed metrics for a specific server (node)
*/ */
async function getServerDetails(baseUrl, instance, job) { async function getServerDetails(baseUrl, instance, job, settings = {}) {
const url = normalizeUrl(baseUrl); const url = normalizeUrl(baseUrl);
const node = resolveToken(instance); const node = resolveToken(instance);
@@ -582,12 +582,38 @@ async function getServerDetails(baseUrl, instance, job) {
await Promise.all(queryPromises); await Promise.all(queryPromises);
// Add IP information from Prometheus target (discovered labels or scrape URL) // Add IP information
try { try {
let foundIp = false;
// 1. Try Custom Node Exporter Metric if configured
if (settings.ip_metric_name) {
try {
const expr = `${settings.ip_metric_name}{instance="${node}",job="${job}"}`;
const res = await query(url, expr);
if (res && res.length > 0) {
const address = res[0].metric[settings.ip_label_name || 'address'];
if (address) {
if (address.includes(':')) {
results.ipv6 = [address];
results.ipv4 = [];
} else {
results.ipv4 = [address];
results.ipv6 = [];
}
foundIp = true;
}
}
} catch (e) {
console.error(`[Prometheus] Error querying custom IP metric ${settings.ip_metric_name}:`, e.message);
}
}
// 2. Fallback to Prometheus Targets API
if (!foundIp) {
const targets = await getTargets(baseUrl); const targets = await getTargets(baseUrl);
const matchedTarget = targets.find(t => t.labels && t.labels.instance === node && t.labels.job === job); const matchedTarget = targets.find(t => t.labels && t.labels.instance === node && t.labels.job === job);
if (matchedTarget) { if (matchedTarget) {
// Extract address from scrapeUrl or instance label
const scrapeUrl = matchedTarget.scrapeUrl || ''; const scrapeUrl = matchedTarget.scrapeUrl || '';
try { try {
const urlObj = new URL(scrapeUrl); const urlObj = new URL(scrapeUrl);
@@ -599,18 +625,27 @@ async function getServerDetails(baseUrl, instance, job) {
results.ipv4 = [host]; results.ipv4 = [host];
results.ipv6 = []; results.ipv6 = [];
} }
foundIp = true;
} catch (e) { } catch (e) {
// Fallback to instance label without port results.ipv4 = [];
const inst = matchedTarget.labels.instance.split(':')[0];
results.ipv4 = [inst];
results.ipv6 = []; results.ipv6 = [];
} }
} else { } else {
results.ipv4 = []; results.ipv4 = [];
results.ipv6 = []; results.ipv6 = [];
} }
} catch (e) { } else {
console.error(`[Prometheus] Error fetching target info for ${node}:`, e.message); results.ipv4 = [];
results.ipv6 = [];
}
}
if (!foundIp) {
results.ipv4 = [];
results.ipv6 = [];
}
} catch (err) {
console.error(`[Prometheus] Critical error resolving IPs for ${node}:`, err.message);
results.ipv4 = []; results.ipv4 = [];
results.ipv6 = []; results.ipv6 = [];
} }