From f75e755f3d9d3ba8ce18d858c6b4729750e3d0bd Mon Sep 17 00:00:00 2001 From: CN-JS-HuiBai Date: Sat, 4 Apr 2026 19:33:22 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0valkey=20support?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 17 ++++---- Install.sh | 4 +- package-lock.json | 102 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 1 + server/cache.js | 59 +++++++++++++++++++++++++++ server/index.js | 17 ++++++-- 6 files changed, 184 insertions(+), 16 deletions(-) create mode 100644 server/cache.js diff --git a/.env.example b/.env.example index fe88e7d..a151907 100644 --- a/.env.example +++ b/.env.example @@ -1,13 +1,10 @@ -# MySQL Configuration -MYSQL_HOST=localhost -MYSQL_PORT=3306 -MYSQL_USER=root -MYSQL_PASSWORD=your_password -MYSQL_DATABASE=display_wall - -# Server Configuration HOST=0.0.0.0 PORT=3000 - -# Data refresh interval (ms) REFRESH_INTERVAL=5000 + +# Valkey/Redis Cache Configuration +VALKEY_HOST=localhost +VALKEY_PORT=6379 +VALKEY_PASSWORD= +VALKEY_DB=dashboard +VALKEY_TTL=30 diff --git a/Install.sh b/Install.sh index ddd7746..98d4640 100644 --- a/Install.sh +++ b/Install.sh @@ -67,9 +67,9 @@ RestartSec=10 StandardOutput=syslog StandardError=syslog SyslogIdentifier=data-wall +# Pass environment via .env file injection for flexibility +EnvironmentFile=-$PROJECT_DIR/.env Environment=NODE_ENV=production -Environment=PORT=3000 -# If you have specific env vars in .env, they will be loaded by the app's dotenv.config() [Install] WantedBy=multi-user.target diff --git a/package-lock.json b/package-lock.json index 874a1d7..fb548e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,9 +12,16 @@ "cors": "^2.8.5", "dotenv": "^16.4.0", "express": "^4.21.0", + "ioredis": "^5.10.1", "mysql2": "^3.11.0" } }, + "node_modules/@ioredis/commands": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@ioredis/commands/-/commands-1.5.1.tgz", + "integrity": "sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "25.5.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-25.5.2.tgz", @@ -132,6 +139,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/cluster-key-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", + "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", + "license": "Apache-2.0", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/combined-stream": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", @@ -606,6 +622,53 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", "license": "ISC" }, + "node_modules/ioredis": { + "version": "5.10.1", + "resolved": "https://registry.npmjs.org/ioredis/-/ioredis-5.10.1.tgz", + "integrity": "sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==", + "license": "MIT", + "dependencies": { + "@ioredis/commands": "1.5.1", + "cluster-key-slot": "^1.1.0", + "debug": "^4.3.4", + "denque": "^2.1.0", + "lodash.defaults": "^4.2.0", + "lodash.isarguments": "^3.1.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0", + "standard-as-callback": "^2.1.0" + }, + "engines": { + "node": ">=12.22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/ioredis" + } + }, + "node_modules/ioredis/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/ioredis/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -621,6 +684,18 @@ "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==", "license": "MIT" }, + "node_modules/lodash.defaults": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/lodash.defaults/-/lodash.defaults-4.2.0.tgz", + "integrity": "sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==", + "license": "MIT" + }, + "node_modules/lodash.isarguments": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/lodash.isarguments/-/lodash.isarguments-3.1.0.tgz", + "integrity": "sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==", + "license": "MIT" + }, "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -885,6 +960,27 @@ "node": ">= 0.8" } }, + "node_modules/redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==", + "license": "MIT", + "dependencies": { + "redis-errors": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/safe-buffer": { "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", @@ -1049,6 +1145,12 @@ "url": "https://github.com/mysqljs/sql-escaper?sponsor=1" } }, + "node_modules/standard-as-callback": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/standard-as-callback/-/standard-as-callback-2.1.0.tgz", + "integrity": "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==", + "license": "MIT" + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index bedab7d..5d0d058 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "cors": "^2.8.5", "dotenv": "^16.4.0", "express": "^4.21.0", + "ioredis": "^5.10.1", "mysql2": "^3.11.0" } } diff --git a/server/cache.js b/server/cache.js new file mode 100644 index 0000000..0feac6d --- /dev/null +++ b/server/cache.js @@ -0,0 +1,59 @@ +const Redis = require('ioredis'); + +const host = process.env.VALKEY_HOST || 'localhost'; +const port = parseInt(process.env.VALKEY_PORT) || 6379; +const password = process.env.VALKEY_PASSWORD || undefined; +const db = parseInt(process.env.VALKEY_DB) || 0; +const ttl = parseInt(process.env.VALKEY_TTL) || 30; + +let redis = null; + +try { + redis = new Redis({ + host, + port, + password, + db, + lazyConnect: true, + maxRetriesPerRequest: 1 + }); + + redis.on('error', (err) => { + // Fail silently after one retry, we just won't cache + console.warn('[Cache] Valkey connection failed, caching disabled:', err.message); + }); +} catch (err) { + console.warn('[Cache] Valkey init failed:', err.message); +} + +const cache = { + async get(key) { + if (!redis) return null; + try { + const data = await redis.get(key); + return data ? JSON.parse(data) : null; + } catch (e) { + return null; + } + }, + + async set(key, value, customTtl = ttl) { + if (!redis) return; + try { + await redis.set(key, JSON.stringify(value), 'EX', customTtl); + } catch (e) { + // ignore + } + }, + + async del(key) { + if (!redis) return; + try { + await redis.del(key); + } catch (e) { + // ignore + } + } +}; + +module.exports = cache; diff --git a/server/index.js b/server/index.js index eca9a37..6a14122 100644 --- a/server/index.js +++ b/server/index.js @@ -4,6 +4,7 @@ const cors = require('cors'); const path = require('path'); const db = require('./db'); const prometheusService = require('./prometheus-service'); +const cache = require('./cache'); const checkAndFixDatabase = require('./db-integrity-check'); const app = express(); @@ -462,12 +463,20 @@ app.get('/api/metrics/overview', async (req, res) => { }); } - const allMetrics = await Promise.all(sources.map(source => - prometheusService.getOverviewMetrics(source.url, source.name).catch(err => { + const allMetrics = await Promise.all(sources.map(async (source) => { + const cacheKey = `source_metrics:${source.url}:${source.name}`; + const cached = await cache.get(cacheKey); + if (cached) return cached; + + try { + const metrics = await prometheusService.getOverviewMetrics(source.url, source.name); + await cache.set(cacheKey, metrics, 15); // Cache for 15s + return metrics; + } catch (err) { console.error(`Error fetching metrics from ${source.name}:`, err.message); return null; - }) - )); + } + })); const validMetrics = allMetrics.filter(m => m !== null);