完善serverless部署环境
This commit is contained in:
242
server/index.js
242
server/index.js
@@ -15,13 +15,23 @@ const net = require('net');
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 3000;
|
||||
const HOST = process.env.HOST || '0.0.0.0';
|
||||
|
||||
const IS_SERVERLESS = [
|
||||
process.env.SERVERLESS,
|
||||
process.env.VERCEL,
|
||||
process.env.AWS_LAMBDA_FUNCTION_NAME,
|
||||
process.env.NETLIFY,
|
||||
process.env.FUNCTION_TARGET,
|
||||
process.env.K_SERVICE
|
||||
].some(Boolean);
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
const fs = require('fs');
|
||||
const crypto = require('crypto');
|
||||
|
||||
let isDbInitialized = false;
|
||||
let bootstrapPromise = null;
|
||||
let backgroundServicesStarted = false;
|
||||
const sessions = new Map(); // Fallback session store when Valkey is unavailable
|
||||
const requestBuckets = new Map();
|
||||
const SESSION_TTL_SECONDS = parseInt(process.env.SESSION_TTL_SECONDS, 10) || 86400;
|
||||
@@ -148,6 +158,21 @@ function getPublicSiteSettings(settings = {}) {
|
||||
};
|
||||
}
|
||||
|
||||
function getRuntimeConfig() {
|
||||
return {
|
||||
serverless: IS_SERVERLESS,
|
||||
realtimeMode: IS_SERVERLESS ? 'polling' : 'websocket'
|
||||
};
|
||||
}
|
||||
|
||||
function hasDatabaseConfig() {
|
||||
return Boolean(
|
||||
process.env.MYSQL_HOST &&
|
||||
process.env.MYSQL_USER &&
|
||||
process.env.MYSQL_DATABASE
|
||||
);
|
||||
}
|
||||
|
||||
async function getSiteSettingsRow() {
|
||||
const [rows] = await db.query('SELECT * FROM site_settings WHERE id = 1');
|
||||
return rows.length > 0 ? rows[0] : {};
|
||||
@@ -298,21 +323,49 @@ function getCookie(req, name) {
|
||||
return matches ? decodeURIComponent(matches[1]) : undefined;
|
||||
}
|
||||
|
||||
async function checkDb() {
|
||||
try {
|
||||
const fs = require('fs');
|
||||
if (!fs.existsSync(path.join(__dirname, '..', '.env'))) {
|
||||
isDbInitialized = false;
|
||||
return;
|
||||
}
|
||||
const [rows] = await db.query("SHOW TABLES LIKE 'prometheus_sources'");
|
||||
async function checkDb() {
|
||||
try {
|
||||
if (!hasDatabaseConfig()) {
|
||||
isDbInitialized = false;
|
||||
return;
|
||||
}
|
||||
const [rows] = await db.query("SHOW TABLES LIKE 'prometheus_sources'");
|
||||
isDbInitialized = rows.length > 0;
|
||||
} catch (err) {
|
||||
isDbInitialized = false;
|
||||
}
|
||||
}
|
||||
|
||||
checkDb();
|
||||
checkDb();
|
||||
|
||||
async function bootstrapServices({ enableBackgroundTasks = !IS_SERVERLESS } = {}) {
|
||||
if (!bootstrapPromise) {
|
||||
bootstrapPromise = (async () => {
|
||||
await checkAndFixDatabase();
|
||||
await checkDb();
|
||||
})().catch((err) => {
|
||||
bootstrapPromise = null;
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
||||
await bootstrapPromise;
|
||||
|
||||
if (enableBackgroundTasks && !backgroundServicesStarted) {
|
||||
latencyService.start();
|
||||
backgroundServicesStarted = true;
|
||||
}
|
||||
}
|
||||
|
||||
app.use(async (req, res, next) => {
|
||||
try {
|
||||
await bootstrapServices({ enableBackgroundTasks: !IS_SERVERLESS });
|
||||
next();
|
||||
} catch (err) {
|
||||
console.error('Service bootstrap failed:', err);
|
||||
res.status(500).json({ error: 'Service initialization failed' });
|
||||
}
|
||||
});
|
||||
|
||||
// --- Health API ---
|
||||
app.get('/health', async (req, res) => {
|
||||
@@ -741,7 +794,8 @@ const serveIndex = async (req, res) => {
|
||||
|
||||
// Inject settings
|
||||
const settingsJson = escapeJsonForInlineScript(getPublicSiteSettings(settings));
|
||||
const injection = `<script>window.SITE_SETTINGS = ${settingsJson};</script>`;
|
||||
const runtimeJson = escapeJsonForInlineScript(getRuntimeConfig());
|
||||
const injection = `<script>window.SITE_SETTINGS = ${settingsJson}; window.APP_RUNTIME = ${runtimeJson};</script>`;
|
||||
|
||||
// Replace <head> with <head> + injection
|
||||
html = html.replace('<head>', '<head>' + injection);
|
||||
@@ -977,7 +1031,7 @@ app.post('/api/settings', requireAuth, async (req, res) => {
|
||||
// ==================== Metrics Aggregation ====================
|
||||
|
||||
// Reusable function to get overview metrics
|
||||
async function getOverview(force = false) {
|
||||
async function getOverview(force = false) {
|
||||
const [sources] = await db.query('SELECT * FROM prometheus_sources WHERE is_server_source = 1 AND type != "blackbox"');
|
||||
if (sources.length === 0) {
|
||||
return {
|
||||
@@ -1101,12 +1155,43 @@ async function getOverview(force = false) {
|
||||
return safeServer;
|
||||
}));
|
||||
|
||||
overview.servers = geoServers;
|
||||
return overview;
|
||||
}
|
||||
|
||||
// Get all aggregated metrics from all Prometheus sources
|
||||
app.get('/api/metrics/overview', async (req, res) => {
|
||||
overview.servers = geoServers;
|
||||
return overview;
|
||||
}
|
||||
|
||||
async function getLatencyResults() {
|
||||
const [routes] = await db.query(`
|
||||
SELECT r.*, s.url, s.type as source_type
|
||||
FROM latency_routes r
|
||||
JOIN prometheus_sources s ON r.source_id = s.id
|
||||
`);
|
||||
|
||||
if (routes.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return Promise.all(routes.map(async (route) => {
|
||||
let latency = await cache.get(`latency:route:${route.id}`);
|
||||
|
||||
if (latency === null) {
|
||||
if (route.source_type === 'prometheus') {
|
||||
latency = await prometheusService.getLatency(route.url, route.latency_target);
|
||||
} else if (route.source_type === 'blackbox') {
|
||||
latency = await latencyService.resolveLatencyForRoute(route);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: route.id,
|
||||
source: route.latency_source,
|
||||
dest: route.latency_dest,
|
||||
latency
|
||||
};
|
||||
}));
|
||||
}
|
||||
|
||||
// Get all aggregated metrics from all Prometheus sources
|
||||
app.get('/api/metrics/overview', async (req, res) => {
|
||||
try {
|
||||
const force = req.query.force === 'true';
|
||||
const overview = await getOverview(force);
|
||||
@@ -1234,6 +1319,24 @@ app.get('/api/metrics/server-history', requireServerDetailsAccess, async (req, r
|
||||
} catch (err) {
|
||||
res.status(500).json({ error: err.message });
|
||||
}
|
||||
});
|
||||
|
||||
app.get('/api/realtime/overview', async (req, res) => {
|
||||
try {
|
||||
const force = req.query.force === 'true';
|
||||
const [overview, latencies] = await Promise.all([
|
||||
getOverview(force),
|
||||
getLatencyResults()
|
||||
]);
|
||||
|
||||
res.json({
|
||||
...overview,
|
||||
latencies
|
||||
});
|
||||
} catch (err) {
|
||||
console.error('Error fetching realtime overview:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch realtime overview' });
|
||||
}
|
||||
});
|
||||
// SPA fallback
|
||||
app.get('*', (req, res, next) => {
|
||||
@@ -1291,50 +1394,25 @@ app.put('/api/latency-routes/:id', requireAuth, async (req, res) => {
|
||||
});
|
||||
|
||||
// ==================== Metrics Latency ====================
|
||||
|
||||
app.get('/api/metrics/latency', async (req, res) => {
|
||||
try {
|
||||
const [routes] = await db.query(`
|
||||
SELECT r.*, s.url, s.type as source_type
|
||||
FROM latency_routes r
|
||||
JOIN prometheus_sources s ON r.source_id = s.id
|
||||
`);
|
||||
|
||||
if (routes.length === 0) {
|
||||
return res.json({ routes: [] });
|
||||
}
|
||||
|
||||
const results = await Promise.all(routes.map(async (route) => {
|
||||
// Try to get from Valkey first (filled by background latencyService)
|
||||
let latency = await cache.get(`latency:route:${route.id}`);
|
||||
|
||||
// Fallback if not in cache (only for prometheus sources, blackbox sources rely on the background service)
|
||||
if (latency === null && route.source_type === 'prometheus') {
|
||||
latency = await prometheusService.getLatency(route.url, route.latency_target);
|
||||
}
|
||||
|
||||
return {
|
||||
id: route.id,
|
||||
source: route.latency_source,
|
||||
dest: route.latency_dest,
|
||||
latency: latency
|
||||
};
|
||||
}));
|
||||
|
||||
res.json({ routes: results });
|
||||
} catch (err) {
|
||||
console.error('Error fetching latencies:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch latency' });
|
||||
|
||||
app.get('/api/metrics/latency', async (req, res) => {
|
||||
try {
|
||||
const results = await getLatencyResults();
|
||||
res.json({ routes: results });
|
||||
} catch (err) {
|
||||
console.error('Error fetching latencies:', err);
|
||||
res.status(500).json({ error: 'Failed to fetch latency' });
|
||||
}
|
||||
});
|
||||
|
||||
// ==================== WebSocket Server ====================
|
||||
|
||||
const server = http.createServer(app);
|
||||
|
||||
const server = IS_SERVERLESS ? null : http.createServer(app);
|
||||
const wss = IS_SERVERLESS ? null : new WebSocket.Server({ server });
|
||||
let isBroadcastRunning = false;
|
||||
|
||||
function broadcast(data) {
|
||||
|
||||
function broadcast(data) {
|
||||
if (!wss) return;
|
||||
const message = JSON.stringify(data);
|
||||
wss.clients.forEach(client => {
|
||||
if (client.readyState === WebSocket.OPEN) {
|
||||
@@ -1344,33 +1422,15 @@ function broadcast(data) {
|
||||
}
|
||||
|
||||
// Broadcast loop
|
||||
async function broadcastMetrics() {
|
||||
if (IS_SERVERLESS || !wss) return;
|
||||
if (isBroadcastRunning) return;
|
||||
isBroadcastRunning = true;
|
||||
try {
|
||||
const overview = await getOverview();
|
||||
|
||||
// Also include latencies in the broadcast to make map lines real-time
|
||||
const [routes] = await db.query(`
|
||||
SELECT r.*, s.url, s.type as source_type
|
||||
FROM latency_routes r
|
||||
JOIN prometheus_sources s ON r.source_id = s.id
|
||||
`);
|
||||
|
||||
const latencyResults = await Promise.all(routes.map(async (route) => {
|
||||
let latency = await cache.get(`latency:route:${route.id}`);
|
||||
if (latency === null && route.source_type === 'prometheus') {
|
||||
latency = await prometheusService.getLatency(route.url, route.latency_target);
|
||||
}
|
||||
return {
|
||||
id: route.id,
|
||||
source: route.latency_source,
|
||||
dest: route.latency_dest,
|
||||
latency: latency
|
||||
};
|
||||
}));
|
||||
|
||||
broadcast({
|
||||
const overview = await getOverview();
|
||||
const latencyResults = await getLatencyResults();
|
||||
|
||||
broadcast({
|
||||
type: 'overview',
|
||||
data: {
|
||||
...overview,
|
||||
@@ -1387,11 +1447,7 @@ async function broadcastMetrics() {
|
||||
// Start server and services
|
||||
async function start() {
|
||||
try {
|
||||
console.log('🔧 Initializing services...');
|
||||
// Ensure DB is ready before starting anything else
|
||||
await checkAndFixDatabase();
|
||||
|
||||
// Start services
|
||||
console.log('🔧 Initializing services...');
|
||||
await bootstrapServices({ enableBackgroundTasks: true });
|
||||
|
||||
const REFRESH_INT = parseInt(process.env.REFRESH_INTERVAL) || 5000;
|
||||
@@ -1407,4 +1463,20 @@ async function start() {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (require.main === module) {
|
||||
if (IS_SERVERLESS) {
|
||||
bootstrapServices({ enableBackgroundTasks: false }).catch((err) => {
|
||||
console.error('Service bootstrap failed:', err.message);
|
||||
process.exit(1);
|
||||
});
|
||||
} else {
|
||||
start();
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
app,
|
||||
start,
|
||||
bootstrapServices,
|
||||
isServerless: IS_SERVERLESS
|
||||
|
||||
Reference in New Issue
Block a user