310 lines
11 KiB
JavaScript
310 lines
11 KiB
JavaScript
const dgram = require('dgram');
|
|
const state = require('../../infrastructure/state');
|
|
const { getMetrics, getHistoricalData, trackRequestWithTiming, trackRequest } = require('../../maintenance/metrics');
|
|
const { getHashForDomain } = require('../../core/core');
|
|
const { logDebug, logError } = require('../../infrastructure/logger');
|
|
const { createErrorResponse } = require('../../infrastructure/error_handler');
|
|
const { parseMinutesToMs } = require('../../infrastructure/utils');
|
|
const pidusage = require('pidusage');
|
|
|
|
async function handleStatsRoutes(req, res) {
|
|
const urlPath = req.urlPath || new URL(req.url, `https://${req.headers.host}`).pathname;
|
|
const method = req.method;
|
|
|
|
if (method === 'GET' && urlPath === '/api/stats') {
|
|
try {
|
|
const startTime = Date.now();
|
|
const stats = getMetrics();
|
|
|
|
const holesailChildren = [];
|
|
const pidStatsPromises = [];
|
|
const pidToChildMap = new Map();
|
|
|
|
for (const [id, child] of state.holesailChildren.entries()) {
|
|
try {
|
|
const opts = state.holesailOpts.get(id) || {};
|
|
const info = state.holesailInfos.get(id) || {};
|
|
const startTime = state.holesailChildStartTimes.get(id);
|
|
const uptime = startTime ? Date.now() - startTime : 0;
|
|
const status = child && !child.killed ? 'running' : 'stopped';
|
|
const pid = child ? child.pid : null;
|
|
|
|
if (pid && child && !child.killed) {
|
|
pidStatsPromises.push(
|
|
pidusage(pid).then(stats => ({ id, type: 'server', stats })).catch(err => {
|
|
logDebug('Admin', `Failed to get stats for server child ${id} (PID ${pid}): ${err.message}`);
|
|
return { id, type: 'server', stats: null };
|
|
})
|
|
);
|
|
}
|
|
|
|
pidToChildMap.set(id, {
|
|
id,
|
|
type: 'server',
|
|
status,
|
|
pid,
|
|
uptime,
|
|
cpuUsage: null,
|
|
memoryUsage: null,
|
|
opts: {
|
|
port: opts.port,
|
|
host: opts.host || '0.0.0.0',
|
|
protocol: opts.udp ? 'udp' : 'tcp',
|
|
secure: opts.secure || false,
|
|
domain: opts.domain || null
|
|
},
|
|
info: info
|
|
});
|
|
} catch (err) {
|
|
logError('Admin', `Error collecting stats for server child ${id}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
for (const [id, child] of state.holesailClientChildren.entries()) {
|
|
try {
|
|
const opts = state.holesailClientOpts.get(id) || {};
|
|
const info = state.holesailClientInfos.get(id) || {};
|
|
const startTime = state.holesailChildStartTimes.get(id);
|
|
const uptime = startTime ? Date.now() - startTime : 0;
|
|
const status = child && !child.killed ? 'running' : 'stopped';
|
|
const pid = child ? child.pid : null;
|
|
|
|
if (pid && child && !child.killed) {
|
|
pidStatsPromises.push(
|
|
pidusage(pid).then(stats => ({ id, type: 'client', stats })).catch(err => {
|
|
logDebug('Admin', `Failed to get stats for client child ${id} (PID ${pid}): ${err.message}`);
|
|
return { id, type: 'client', stats: null };
|
|
})
|
|
);
|
|
}
|
|
|
|
pidToChildMap.set(id, {
|
|
id,
|
|
type: 'client',
|
|
status,
|
|
pid,
|
|
uptime,
|
|
cpuUsage: null,
|
|
memoryUsage: null,
|
|
opts: {
|
|
domain: opts.domain || null,
|
|
port: opts.port,
|
|
host: opts.host || null,
|
|
protocol: opts.protocol || 'tcp'
|
|
},
|
|
info: info
|
|
});
|
|
} catch (err) {
|
|
logError('Admin', `Error collecting stats for client child ${id}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
const pidStatsResults = await Promise.all(pidStatsPromises);
|
|
|
|
for (const result of pidStatsResults) {
|
|
if (result.stats) {
|
|
const childData = pidToChildMap.get(result.id);
|
|
if (childData) {
|
|
childData.cpuUsage = {
|
|
user: result.stats.cpu / 2,
|
|
system: result.stats.cpu / 2,
|
|
percentage: result.stats.cpu
|
|
};
|
|
childData.memoryUsage = {
|
|
rss: result.stats.memory,
|
|
heapUsed: result.stats.memory * 0.8,
|
|
heapTotal: result.stats.memory,
|
|
external: 0,
|
|
arrayBuffers: 0
|
|
};
|
|
}
|
|
}
|
|
}
|
|
|
|
for (const childData of pidToChildMap.values()) {
|
|
holesailChildren.push(childData);
|
|
}
|
|
|
|
const managedConnections = new Set();
|
|
for (const child of holesailChildren) {
|
|
if (child.opts && child.opts.domain && child.opts.port) {
|
|
managedConnections.add(`${child.opts.domain}:${child.opts.port}`);
|
|
}
|
|
}
|
|
|
|
const p2pDomainEntries = [];
|
|
const hashPromises = [];
|
|
|
|
for (const [key, holesail] of state.holesails.entries()) {
|
|
try {
|
|
if (managedConnections.has(key)) {
|
|
continue;
|
|
}
|
|
|
|
const keyParts = key.split(':');
|
|
if (keyParts.length !== 2) {
|
|
logDebug('Admin', `Skipping invalid holesail key format: ${key}`);
|
|
continue;
|
|
}
|
|
|
|
const domain = keyParts[0];
|
|
const port = parseInt(keyParts[1], 10);
|
|
|
|
if (isNaN(port)) {
|
|
logDebug('Admin', `Skipping holesail key with invalid port: ${key}`);
|
|
continue;
|
|
}
|
|
|
|
const ip = state.domainToIPMap.get ? state.domainToIPMap.get(domain) : state.domainToIPMap[domain];
|
|
const isPersistent = state.persistentConnections && state.persistentConnections.has(key);
|
|
const status = holesail && typeof holesail === 'object' ? 'running' : 'stopped';
|
|
|
|
let holesailInfo = null;
|
|
try {
|
|
if (holesail && typeof holesail === 'object' && holesail.info) {
|
|
holesailInfo = holesail.info;
|
|
}
|
|
} catch (e) {
|
|
// Info not available
|
|
}
|
|
|
|
p2pDomainEntries.push({
|
|
key,
|
|
domain,
|
|
port,
|
|
ip,
|
|
isPersistent,
|
|
status,
|
|
holesailInfo
|
|
});
|
|
|
|
hashPromises.push(
|
|
getHashForDomain(domain).catch(err => {
|
|
logDebug('Admin', `Could not get hash for domain ${domain}: ${err.message}`);
|
|
return null;
|
|
})
|
|
);
|
|
} catch (err) {
|
|
logError('Admin', `Error processing p2p domain connection ${key}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
const hashResults = await Promise.all(hashPromises);
|
|
|
|
let mainProcessStats = null;
|
|
try {
|
|
mainProcessStats = await pidusage(process.pid);
|
|
} catch (err) {
|
|
logDebug('Admin', `Failed to get main process stats for p2p connections: ${err.message}`);
|
|
}
|
|
|
|
const p2pConnectionCount = p2pDomainEntries.length || 1;
|
|
let perConnectionCpu = null;
|
|
let perConnectionMemory = null;
|
|
|
|
if (mainProcessStats) {
|
|
perConnectionCpu = {
|
|
user: mainProcessStats.cpu / (p2pConnectionCount * 2),
|
|
system: mainProcessStats.cpu / (p2pConnectionCount * 2),
|
|
percentage: mainProcessStats.cpu / p2pConnectionCount
|
|
};
|
|
|
|
perConnectionMemory = {
|
|
rss: Math.floor(mainProcessStats.memory / p2pConnectionCount),
|
|
heapUsed: Math.floor((mainProcessStats.memory * 0.8) / p2pConnectionCount),
|
|
heapTotal: Math.floor(mainProcessStats.memory / p2pConnectionCount),
|
|
external: 0,
|
|
arrayBuffers: 0
|
|
};
|
|
}
|
|
|
|
for (let i = 0; i < p2pDomainEntries.length; i++) {
|
|
try {
|
|
const entry = p2pDomainEntries[i];
|
|
const hash = hashResults[i];
|
|
const startTime = state.holesailStartTimes && state.holesailStartTimes.get(entry.key);
|
|
const uptime = startTime ? Date.now() - startTime : 0;
|
|
|
|
let timeRemaining = null;
|
|
if (!entry.isPersistent && process.env.FULL_PERSISTENCE !== 'true' && startTime) {
|
|
const timeoutDuration = parseMinutesToMs(process.env.HOLESAIL_TIMEOUT || '5');
|
|
const elapsed = Date.now() - startTime;
|
|
const remaining = Math.max(0, timeoutDuration - elapsed);
|
|
timeRemaining = remaining;
|
|
}
|
|
|
|
holesailChildren.push({
|
|
id: entry.key,
|
|
type: 'p2p-domain',
|
|
status: entry.status,
|
|
pid: process.pid,
|
|
uptime: uptime,
|
|
timeRemaining: timeRemaining,
|
|
cpuUsage: perConnectionCpu,
|
|
memoryUsage: perConnectionMemory,
|
|
opts: {
|
|
domain: entry.domain,
|
|
port: entry.port,
|
|
host: entry.ip || null,
|
|
protocol: 'tcp'
|
|
},
|
|
persistent: entry.isPersistent,
|
|
hash: hash || null,
|
|
info: entry.holesailInfo || null
|
|
});
|
|
} catch (err) {
|
|
logError('Admin', `Error creating stats entry for p2p domain connection ${p2pDomainEntries[i].key}: ${err.message}`);
|
|
}
|
|
}
|
|
|
|
stats.holesailChildren = holesailChildren;
|
|
|
|
const responseTime = Date.now() - startTime;
|
|
trackRequestWithTiming('/api/stats', true, responseTime);
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(stats));
|
|
} catch (err) {
|
|
logError('Admin', `Error in /api/stats: ${err.message}`, err);
|
|
trackRequest('/api/stats', false);
|
|
const errorResponse = createErrorResponse(err, 500);
|
|
res.writeHead(errorResponse.statusCode, errorResponse.headers);
|
|
res.end(errorResponse.body);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
if (method === 'GET' && urlPath === '/api/stats/historical') {
|
|
try {
|
|
let minutes = 5;
|
|
if (req.url.includes('?')) {
|
|
const queryString = req.url.split('?')[1];
|
|
const params = new URLSearchParams(queryString);
|
|
const minutesParam = params.get('minutes');
|
|
if (minutesParam) {
|
|
minutes = parseInt(minutesParam, 10);
|
|
if (isNaN(minutes) || minutes < 1) minutes = 5;
|
|
if (minutes > 2880) minutes = 2880;
|
|
}
|
|
}
|
|
const startTime = Date.now();
|
|
const historical = getHistoricalData(minutes);
|
|
const responseTime = Date.now() - startTime;
|
|
trackRequestWithTiming('/api/stats/historical', true, responseTime);
|
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
res.end(JSON.stringify(historical));
|
|
} catch (err) {
|
|
logError('Admin', `Error in /api/stats/historical: ${err.message}`, err);
|
|
trackRequest('/api/stats/historical', false);
|
|
const errorResponse = createErrorResponse(err, 500);
|
|
res.writeHead(errorResponse.statusCode, errorResponse.headers);
|
|
res.end(errorResponse.body);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
module.exports = { handleStatsRoutes };
|
|
|