diff --git a/public/index.html b/public/index.html
index 95c7dca..943d0c8 100644
--- a/public/index.html
+++ b/public/index.html
@@ -587,7 +587,7 @@
`).join('');
- // Handle partitions integration: Move the expandable partition section UNDER the Disk Usage metric
+ // Render Custom Data
+ const customDataContainer = dom.customDataContainer;
+ if (customDataContainer) {
+ customDataContainer.innerHTML = '';
+ if (data.custom_data && data.custom_data.length > 0) {
+ data.custom_data.forEach(item => {
+ const card = document.createElement('div');
+ card.className = 'detail-metric-card';
+ card.style.flex = '1 1 calc(50% - 10px)';
+ card.innerHTML = `
+
${item.name}
+
${item.value || '-'}
+ `;
+ customDataContainer.appendChild(card);
+ });
+ }
+ }
+
+ // Partitions
if (data.partitions && data.partitions.length > 0) {
dom.detailPartitionsContainer.style.display = 'block';
dom.partitionSummary.textContent = `${data.partitions.length} 个本地分区`;
@@ -1997,8 +2087,11 @@
// Update IP visibility input
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';
+ if (dom.ipLabelNameInput) dom.ipLabelNameInput.value = settings.ip_label_name || '';
+ // Load Custom Metrics
+ loadCustomMetricsUI(settings.custom_metrics);
+
// Sync security tab dependency
updateSecurityDependency();
} catch (err) {
@@ -2118,7 +2211,8 @@
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,
ip_metric_name: dom.ipMetricNameInput ? dom.ipMetricNameInput.value.trim() : null,
- ip_label_name: dom.ipLabelNameInput ? dom.ipLabelNameInput.value.trim() : 'address'
+ ip_label_name: dom.ipLabelNameInput ? dom.ipLabelNameInput.value.trim() : 'address',
+ custom_metrics: getCustomMetricsFromUI()
};
// UI Feedback for both potential save buttons
diff --git a/server/db-integrity-check.js b/server/db-integrity-check.js
deleted file mode 100644
index 8006f17..0000000
--- a/server/db-integrity-check.js
+++ /dev/null
@@ -1,197 +0,0 @@
-/**
- * Database Integrity Check
- * Runs at startup to ensure all required tables exist.
- * Recreates the database if any tables are missing.
- */
-require('dotenv').config();
-const mysql = require('mysql2/promise');
-const db = require('./db');
-const path = require('path');
-const fs = require('fs');
-
-const REQUIRED_TABLES = [
- 'users',
- 'prometheus_sources',
- 'site_settings',
- 'traffic_stats',
- 'server_locations',
- 'latency_routes'
-];
-
-async function checkAndFixDatabase() {
- const envPath = path.join(__dirname, '..', '.env');
- if (!fs.existsSync(envPath)) return;
-
- try {
- // Check tables
- const [rows] = await db.query("SHOW TABLES");
- const existingTables = rows.map(r => Object.values(r)[0]);
-
- const missingTables = REQUIRED_TABLES.filter(t => !existingTables.includes(t));
-
- if (missingTables.length > 0) {
- console.log(`[Database Integrity] ⚠️ Missing tables: ${missingTables.join(', ')}. Creating them...`);
-
- for (const table of missingTables) {
- await createTable(table);
- }
- console.log(`[Database Integrity] ✅ Missing tables created.`);
- }
-
- // Check for is_server_source and type in prometheus_sources
- const [promColumns] = await db.query("SHOW COLUMNS FROM prometheus_sources");
- const promColumnNames = promColumns.map(c => c.Field);
-
- if (!promColumnNames.includes('is_server_source')) {
- console.log(`[Database Integrity] ⚠️ Missing column 'is_server_source' in 'prometheus_sources'. Adding it...`);
- await db.query("ALTER TABLE prometheus_sources ADD COLUMN is_server_source TINYINT(1) DEFAULT 1 AFTER description");
- console.log(`[Database Integrity] ✅ Column 'is_server_source' added.`);
- }
-
- if (!promColumnNames.includes('type')) {
- console.log(`[Database Integrity] ⚠️ Missing column 'type' in 'prometheus_sources'. Adding it...`);
- await db.query("ALTER TABLE prometheus_sources ADD COLUMN type VARCHAR(50) DEFAULT 'prometheus' AFTER is_server_source");
- console.log(`[Database Integrity] ✅ Column 'type' added.`);
- }
-
- // Check for new columns in site_settings
- const [columns] = await db.query("SHOW COLUMNS FROM site_settings");
- const columnNames = columns.map(c => c.Field);
- const addColumn = async (columnName, sql) => {
- if (!columnNames.includes(columnName)) {
- try {
- console.log(`[Database Integrity] ⚠️ Missing column '${columnName}' in 'site_settings'. Adding it...`);
- await db.query(sql);
- console.log(`[Database Integrity] ✅ Column '${columnName}' added.`);
- } catch (err) {
- console.error(`[Database Integrity] ❌ Failed to add column '${columnName}':`, err.message);
- // Try without AFTER if it exists
- if (sql.includes('AFTER')) {
- try {
- const fallback = sql.split(' AFTER')[0];
- console.log(`[Database Integrity] 🔄 Retrying column '${columnName}' WITHOUT 'AFTER'...`);
- await db.query(fallback);
- console.log(`[Database Integrity] ✅ Column '${columnName}' added via fallback.`);
- } catch (err2) {
- console.error(`[Database Integrity] ❌ Fallback also failed:`, err2.message);
- }
- }
- }
- }
- };
-
- await addColumn('show_page_name', "ALTER TABLE site_settings ADD COLUMN show_page_name TINYINT(1) DEFAULT 1 AFTER page_name");
- await addColumn('show_95_bandwidth', "ALTER TABLE site_settings ADD COLUMN show_95_bandwidth TINYINT(1) DEFAULT 0 AFTER default_theme");
- await addColumn('p95_type', "ALTER TABLE site_settings ADD COLUMN p95_type VARCHAR(20) DEFAULT 'tx' AFTER show_95_bandwidth");
- await addColumn('require_login_for_server_details', "ALTER TABLE site_settings ADD COLUMN require_login_for_server_details TINYINT(1) DEFAULT 1 AFTER p95_type");
- await addColumn('blackbox_source_id', "ALTER TABLE site_settings ADD COLUMN blackbox_source_id INT AFTER p95_type");
- await addColumn('latency_source', "ALTER TABLE site_settings ADD COLUMN latency_source VARCHAR(100) AFTER blackbox_source_id");
- await addColumn('latency_dest', "ALTER TABLE site_settings ADD COLUMN latency_dest VARCHAR(100) AFTER latency_source");
- await addColumn('latency_target', "ALTER TABLE site_settings ADD COLUMN latency_target VARCHAR(255) AFTER latency_dest");
- await addColumn('icp_filing', "ALTER TABLE site_settings ADD COLUMN icp_filing VARCHAR(255) AFTER latency_target");
- await addColumn('ps_filing', "ALTER TABLE site_settings ADD COLUMN ps_filing VARCHAR(255) AFTER icp_filing");
- await addColumn('logo_url_dark', "ALTER TABLE site_settings ADD COLUMN logo_url_dark TEXT AFTER logo_url");
- await addColumn('favicon_url', "ALTER TABLE site_settings ADD COLUMN favicon_url TEXT AFTER logo_url_dark");
- } catch (err) {
- console.error('[Database Integrity] ❌ Overall site_settings check error:', err.message);
- }
-}
-
-async function createTable(tableName) {
- console.log(` - Creating table "${tableName}"...`);
- switch (tableName) {
- case 'users':
- await db.query(`
- CREATE TABLE IF NOT EXISTS users (
- id INT AUTO_INCREMENT PRIMARY KEY,
- username VARCHAR(255) NOT NULL UNIQUE,
- password VARCHAR(255) NOT NULL,
- salt VARCHAR(255) NOT NULL,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- break;
- case 'prometheus_sources':
- await db.query(`
- CREATE TABLE IF NOT EXISTS prometheus_sources (
- id INT AUTO_INCREMENT PRIMARY KEY,
- name VARCHAR(255) NOT NULL,
- url VARCHAR(500) NOT NULL,
- description TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- break;
- case 'site_settings':
- await db.query(`
- CREATE TABLE IF NOT EXISTS site_settings (
- id INT PRIMARY KEY DEFAULT 1,
- page_name VARCHAR(255) DEFAULT '数据可视化展示大屏',
- show_page_name TINYINT(1) DEFAULT 1,
- title VARCHAR(255) DEFAULT '数据可视化展示大屏',
- logo_url TEXT,
- logo_url_dark TEXT,
- favicon_url TEXT,
- default_theme VARCHAR(20) DEFAULT 'dark',
- show_95_bandwidth TINYINT(1) DEFAULT 0,
- p95_type VARCHAR(20) DEFAULT 'tx',
- require_login_for_server_details TINYINT(1) DEFAULT 1,
- blackbox_source_id INT,
- latency_source VARCHAR(100),
- latency_dest VARCHAR(100),
- latency_target VARCHAR(255),
- icp_filing VARCHAR(255),
- ps_filing VARCHAR(255),
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- await db.query(`
- INSERT IGNORE INTO site_settings (id, page_name, title, default_theme, show_95_bandwidth)
- VALUES (1, '数据可视化展示大屏', '数据可视化展示大屏', 'dark', 0)
- `);
- break;
- case 'traffic_stats':
- await db.query(`
- CREATE TABLE IF NOT EXISTS traffic_stats (
- id INT AUTO_INCREMENT PRIMARY KEY,
- rx_bytes BIGINT UNSIGNED DEFAULT 0,
- tx_bytes BIGINT UNSIGNED DEFAULT 0,
- rx_bandwidth DOUBLE DEFAULT 0,
- tx_bandwidth DOUBLE DEFAULT 0,
- timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- UNIQUE INDEX (timestamp)
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- break;
- case 'latency_routes':
- await db.query(`
- CREATE TABLE IF NOT EXISTS latency_routes (
- id INT AUTO_INCREMENT PRIMARY KEY,
- source_id INT NOT NULL,
- latency_source VARCHAR(100) NOT NULL,
- latency_dest VARCHAR(100) NOT NULL,
- latency_target VARCHAR(255) NOT NULL,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- break;
- case 'server_locations':
- await db.query(`
- CREATE TABLE IF NOT EXISTS server_locations (
- id INT AUTO_INCREMENT PRIMARY KEY,
- ip VARCHAR(255) NOT NULL UNIQUE,
- country CHAR(2),
- country_name VARCHAR(100),
- region VARCHAR(100),
- city VARCHAR(100),
- latitude DOUBLE,
- longitude DOUBLE,
- last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- break;
- }
-}
-
-module.exports = checkAndFixDatabase;
diff --git a/server/db-schema-check.js b/server/db-schema-check.js
index ec2909b..0b76471 100644
--- a/server/db-schema-check.js
+++ b/server/db-schema-check.js
@@ -68,6 +68,7 @@ const SCHEMA = {
show_server_ip TINYINT(1) DEFAULT 0,
ip_metric_name VARCHAR(100) DEFAULT NULL,
ip_label_name VARCHAR(100) DEFAULT 'address',
+ custom_metrics JSON DEFAULT NULL,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
`,
@@ -142,6 +143,10 @@ const SCHEMA = {
{
name: 'ip_label_name',
sql: "ALTER TABLE site_settings ADD COLUMN ip_label_name VARCHAR(100) DEFAULT 'address' AFTER ip_metric_name"
+ },
+ {
+ name: 'custom_metrics',
+ sql: "ALTER TABLE site_settings ADD COLUMN custom_metrics JSON DEFAULT NULL AFTER ip_label_name"
}
]
},
diff --git a/server/index.js b/server/index.js
index 711218d..6279052 100644
--- a/server/index.js
+++ b/server/index.js
@@ -148,7 +148,8 @@ function getPublicSiteSettings(settings = {}) {
network_data_sources: settings.network_data_sources || null,
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'
+ ip_label_name: settings.ip_label_name || 'address',
+ custom_metrics: settings.custom_metrics || []
};
}
@@ -905,7 +906,7 @@ app.post('/api/settings', requireAuth, async (req, res) => {
const {
page_name, show_page_name, title, logo_url, logo_url_dark, favicon_url,
default_theme, show_95_bandwidth, p95_type, require_login_for_server_details,
- icp_filing, ps_filing, network_data_sources, show_server_ip
+ icp_filing, ps_filing, network_data_sources, show_server_ip, ip_metric_name, ip_label_name, custom_metrics
} = req.body;
// 3. Prepare parameters, prioritizing body but falling back to current
@@ -931,7 +932,8 @@ app.post('/api/settings', requireAuth, async (req, res) => {
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),
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')
+ ip_label_name: ip_label_name !== undefined ? ip_label_name : (current.ip_label_name || 'address'),
+ custom_metrics: custom_metrics !== undefined ? JSON.stringify(custom_metrics) : (current.custom_metrics || '[]')
};
await db.query(`
@@ -939,8 +941,8 @@ app.post('/api/settings', requireAuth, async (req, res) => {
id, page_name, show_page_name, title, logo_url, logo_url_dark, favicon_url,
default_theme, show_95_bandwidth, p95_type, require_login_for_server_details,
blackbox_source_id, latency_source, latency_dest, latency_target,
- icp_filing, ps_filing, network_data_sources, show_server_ip
- ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
+ icp_filing, ps_filing, network_data_sources, show_server_ip, ip_metric_name, ip_label_name
+ ) VALUES (1, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
page_name = VALUES(page_name),
show_page_name = VALUES(show_page_name),
@@ -959,12 +961,15 @@ app.post('/api/settings', requireAuth, async (req, res) => {
icp_filing = VALUES(icp_filing),
ps_filing = VALUES(ps_filing),
network_data_sources = VALUES(network_data_sources),
- show_server_ip = VALUES(show_server_ip)`,
+ show_server_ip = VALUES(show_server_ip),
+ ip_metric_name = VALUES(ip_metric_name),
+ ip_label_name = VALUES(ip_label_name)`,
[
settings.page_name, settings.show_page_name, settings.title, settings.logo_url, settings.logo_url_dark, settings.favicon_url,
settings.default_theme, settings.show_95_bandwidth, settings.p95_type, settings.require_login_for_server_details,
settings.blackbox_source_id, settings.latency_source, settings.latency_dest, settings.latency_target,
- settings.icp_filing, settings.ps_filing, settings.network_data_sources, settings.show_server_ip
+ settings.icp_filing, settings.ps_filing, settings.network_data_sources, settings.show_server_ip,
+ settings.ip_metric_name, settings.ip_label_name
]
);
@@ -1421,6 +1426,16 @@ async function start() {
const REFRESH_INT = parseInt(process.env.REFRESH_INTERVAL) || 5000;
setInterval(broadcastMetrics, REFRESH_INT);
+ // Periodic cleanup of rate limit buckets to prevent memory leak
+ setInterval(() => {
+ const now = Date.now();
+ for (const [key, bucket] of requestBuckets.entries()) {
+ if (bucket.resetAt <= now) {
+ requestBuckets.delete(key);
+ }
+ }
+ }, 3600000); // Once per hour
+
server.listen(PORT, HOST, () => {
console.log(`\n 🚀 Data Visualization Display Wall (WebSocket Enabled)`);
console.log(` 📊 Server running at http://${HOST === '0.0.0.0' ? 'localhost' : HOST}:${PORT}`);
diff --git a/server/init-db.js b/server/init-db.js
index eeeb410..16ff346 100644
--- a/server/init-db.js
+++ b/server/init-db.js
@@ -1,102 +1,38 @@
-/**
- * Database Initialization Script
- * Run: npm run init-db
- * Creates the required MySQL database and tables.
- */
-require('dotenv').config();
const mysql = require('mysql2/promise');
+const checkAndFixDatabase = require('./db-schema-check');
+const db = require('./db');
async function initDatabase() {
- const connection = await mysql.createConnection({
- host: process.env.MYSQL_HOST || 'localhost',
- port: parseInt(process.env.MYSQL_PORT) || 3306,
- user: process.env.MYSQL_USER || 'root',
- password: process.env.MYSQL_PASSWORD || ''
- });
-
+ const host = process.env.MYSQL_HOST || 'localhost';
+ const port = parseInt(process.env.MYSQL_PORT) || 3306;
+ const user = process.env.MYSQL_USER || 'root';
+ const password = process.env.MYSQL_PASSWORD || '';
const dbName = process.env.MYSQL_DATABASE || 'display_wall';
- console.log('🔧 Initializing database...\n');
+ // 1. Create connection without database selected to create the DB itself
+ const connection = await mysql.createConnection({
+ host,
+ port,
+ user,
+ password
+ });
+
+ console.log('🔧 Initializing database environment...\n');
// Create database
await connection.query(`CREATE DATABASE IF NOT EXISTS \`${dbName}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci`);
console.log(` ✅ Database "${dbName}" ready`);
+ await connection.end();
- await connection.query(`USE \`${dbName}\``);
+ // 2. Re-initialize the standard pool so it can see the new DB
+ db.initPool();
- // Create users table
- await connection.query(`
- CREATE TABLE IF NOT EXISTS users (
- id INT AUTO_INCREMENT PRIMARY KEY,
- username VARCHAR(255) NOT NULL UNIQUE,
- password VARCHAR(255) NOT NULL,
- salt VARCHAR(255) NOT NULL,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- console.log(' ✅ Table "users" ready');
-
- // Create prometheus_sources table
- await connection.query(`
- CREATE TABLE IF NOT EXISTS prometheus_sources (
- id INT AUTO_INCREMENT PRIMARY KEY,
- name VARCHAR(255) NOT NULL,
- url VARCHAR(500) NOT NULL,
- description TEXT,
- created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- console.log(' ✅ Table "prometheus_sources" ready');
-
- // Create site_settings table
- await connection.query(`
- CREATE TABLE IF NOT EXISTS site_settings (
- id INT PRIMARY KEY DEFAULT 1,
- page_name VARCHAR(255) DEFAULT '数据可视化展示大屏',
- title VARCHAR(255) DEFAULT '数据可视化展示大屏',
- logo_url TEXT,
- default_theme VARCHAR(20) DEFAULT 'dark',
- show_page_name TINYINT(1) DEFAULT 1,
- logo_url_dark TEXT,
- favicon_url TEXT,
- show_95_bandwidth TINYINT(1) DEFAULT 0,
- p95_type VARCHAR(20) DEFAULT 'tx',
- require_login_for_server_details TINYINT(1) DEFAULT 1,
- blackbox_source_id INT,
- latency_source VARCHAR(100),
- latency_dest VARCHAR(100),
- latency_target VARCHAR(255),
- icp_filing VARCHAR(255),
- ps_filing VARCHAR(255),
- updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- // Insert default settings if not exists
- await connection.query(`
- INSERT IGNORE INTO site_settings (id, page_name, title, default_theme)
- VALUES (1, '数据可视化展示大屏', '数据可视化展示大屏', 'dark')
- `);
- console.log(' ✅ Table "site_settings" ready');
-
- // Create server_locations table
- await connection.query(`
- CREATE TABLE IF NOT EXISTS server_locations (
- id INT AUTO_INCREMENT PRIMARY KEY,
- ip VARCHAR(255) NOT NULL UNIQUE,
- country CHAR(2),
- country_name VARCHAR(100),
- region VARCHAR(100),
- city VARCHAR(100),
- latitude DOUBLE,
- longitude DOUBLE,
- last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci
- `);
- console.log(' ✅ Table "server_locations" ready');
+ // 3. Use the centralized schema tool to create/fix all tables
+ console.log(' 📦 Initializing tables using schema-check tool...');
+ await checkAndFixDatabase();
+ console.log(' ✅ Tables and columns ready');
console.log('\n🎉 Database initialization complete!\n');
- await connection.end();
}
initDatabase().catch(err => {
diff --git a/server/prometheus-service.js b/server/prometheus-service.js
index 9f9d68f..0d263fd 100644
--- a/server/prometheus-service.js
+++ b/server/prometheus-service.js
@@ -9,14 +9,30 @@ const crypto = require('crypto');
const httpAgent = new http.Agent({ keepAlive: true });
const httpsAgent = new https.Agent({ keepAlive: true });
-const serverIdMap = new Map(); // token -> { instance, job, source }
+const serverIdMap = new Map(); // token -> { instance, job, source, lastSeen }
const SECRET = process.env.APP_SECRET || crypto.randomBytes(32).toString('hex');
+// Periodic cleanup of serverIdMap to prevent infinite growth
+setInterval(() => {
+ const now = Date.now();
+ const TTL = 24 * 60 * 60 * 1000; // 24 hours
+ for (const [token, data] of serverIdMap.entries()) {
+ if (now - (data.lastSeen || 0) > TTL) {
+ serverIdMap.delete(token);
+ }
+ }
+}, 3600000); // Once per hour
+
function getServerToken(instance, job, source) {
const hash = crypto.createHmac('sha256', SECRET)
.update(`${instance}:${job}:${source}`)
.digest('hex')
.substring(0, 16);
+
+ // Update lastSeen timestamp
+ const data = serverIdMap.get(hash);
+ if (data) data.lastSeen = Date.now();
+
return hash;
}
@@ -222,7 +238,7 @@ async function getOverviewMetrics(url, sourceName) {
const token = getServerToken(originalInstance, job, sourceName);
// Store mapping for detail queries
- serverIdMap.set(token, { instance: originalInstance, source: sourceName, job });
+ serverIdMap.set(token, { instance: originalInstance, source: sourceName, job, lastSeen: Date.now() });
if (!instances.has(token)) {
instances.set(token, {
@@ -582,76 +598,85 @@ async function getServerDetails(baseUrl, instance, job, settings = {}) {
await Promise.all(queryPromises);
- // Add IP information
+ // Process custom metrics from settings
+ results.custom_data = [];
try {
- let foundIp = false;
+ const customMetrics = typeof settings.custom_metrics === 'string'
+ ? JSON.parse(settings.custom_metrics)
+ : (settings.custom_metrics || []);
- // 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 = [];
+ if (Array.isArray(customMetrics) && customMetrics.length > 0) {
+ const customPromises = customMetrics.map(async (cfg) => {
+ if (!cfg.metric) return null;
+ try {
+ const expr = `${cfg.metric}{instance="${node}",job="${job}"}`;
+ const res = await query(url, expr);
+ if (res && res.length > 0) {
+ const val = res[0].metric[cfg.label || 'address'] || res[0].value[1];
+
+ // If this metric is marked as an IP source, update the main IP fields
+ if (cfg.is_ip && !results.ipv4?.length && !results.ipv6?.length) {
+ if (val.includes(':')) {
+ results.ipv6 = [val];
+ results.ipv4 = [];
+ } else {
+ results.ipv4 = [val];
+ results.ipv6 = [];
+ }
}
- foundIp = true;
+
+ return {
+ name: cfg.name || cfg.metric,
+ value: val
+ };
}
+ } catch (e) {
+ console.error(`[Prometheus] Custom metric error (${cfg.metric}):`, e.message);
}
- } catch (e) {
- console.error(`[Prometheus] Error querying custom IP metric ${settings.ip_metric_name}:`, e.message);
- }
- }
-
- // 2. Fallback to Prometheus Targets API
- if (!foundIp) {
- try {
- const targets = await getTargets(baseUrl);
- const matchedTarget = targets.find(t => t.labels && t.labels.instance === node && t.labels.job === job);
- if (matchedTarget) {
- const scrapeUrl = matchedTarget.scrapeUrl || '';
- try {
- const urlObj = new URL(scrapeUrl);
- const host = urlObj.hostname;
- if (host.includes(':')) {
- results.ipv6 = [host];
- results.ipv4 = [];
- } else {
- results.ipv4 = [host];
- results.ipv6 = [];
- }
- foundIp = true;
- } catch (e) {
- // Simple fallback if URL parsing fails
- const host = scrapeUrl.split('//').pop().split('/')[0].split(':')[0];
- if (host) {
- results.ipv4 = [host];
- results.ipv6 = [];
- foundIp = true;
- }
- }
- }
- } catch (e) {
- console.error(`[Prometheus] Error fetching target info for ${node}:`, e.message);
- }
- }
-
- if (!foundIp) {
- results.ipv4 = [];
- results.ipv6 = [];
+ return null;
+ });
+
+ const customResults = await Promise.all(customPromises);
+ results.custom_data = customResults.filter(r => r !== null);
}
} catch (err) {
- console.error(`[Prometheus] Critical error resolving IPs for ${node}:`, err.message);
- results.ipv4 = results.ipv4 || [];
- results.ipv6 = results.ipv6 || [];
+ console.error('[Prometheus] Error processing custom metrics:', err.message);
}
+ // Ensure IP discovery fallback if no custom IP metric found
+ if ((!results.ipv4 || results.ipv4.length === 0) && (!results.ipv6 || results.ipv6.length === 0)) {
+ try {
+ const targets = await getTargets(baseUrl);
+ const matchedTarget = targets.find(t => t.labels && t.labels.instance === node && t.labels.job === job);
+ if (matchedTarget) {
+ const scrapeUrl = matchedTarget.scrapeUrl || '';
+ try {
+ const urlObj = new URL(scrapeUrl);
+ const host = urlObj.hostname;
+ if (host.includes(':')) {
+ results.ipv6 = [host];
+ results.ipv4 = [];
+ } else {
+ results.ipv4 = [host];
+ results.ipv6 = [];
+ }
+ } catch (e) {
+ const host = scrapeUrl.split('//').pop().split('/')[0].split(':')[0];
+ if (host) {
+ results.ipv4 = [host];
+ results.ipv6 = [];
+ }
+ }
+ }
+ } catch (e) {
+ console.error(`[Prometheus] Target fallback error for ${node}:`, e.message);
+ }
+ }
+
+ // Final sanitization
+ results.ipv4 = results.ipv4 || [];
+ results.ipv6 = results.ipv6 || [];
+
// Group partitions
const partitionsMap = {};
(results.partitions_size || []).forEach(p => {