Skip to content

Commit

Permalink
feat: option to enable inspector api through http endpoints
Browse files Browse the repository at this point in the history
  • Loading branch information
dulguun0225 committed Dec 4, 2023
1 parent e233288 commit 1145c1b
Show file tree
Hide file tree
Showing 6 changed files with 163 additions and 1 deletion.
3 changes: 2 additions & 1 deletion cli/commands/dev.js
Expand Up @@ -40,7 +40,8 @@ module.exports.devCmd = async program => {
RABBITMQ_HOST: 'amqp://127.0.0.1',
ELASTICSEARCH_URL: 'http://127.0.0.1:9200',
ENABLED_SERVICES_JSON: enabledServicesJson,
ALLOWED_ORIGINS: configs.allowed_origins
ALLOWED_ORIGINS: configs.allowed_origins,
NODE_INSPECTOR: 'enabled',
};

let port = 3300;
Expand Down
5 changes: 5 additions & 0 deletions cli/commands/docker/utils.js
Expand Up @@ -127,6 +127,7 @@ const generatePluginBlock = (configs, plugin) => {
PORT: plugin.port || SERVICE_INTERNAL_PORT || 80,
API_MONGO_URL: api_mongo_url,
MONGO_URL: mongo_url,
NODE_INSPECTOR: configs.nodeInspector ? 'enabled' : undefined,
LOAD_BALANCER_ADDRESS: generateLBaddress(`http://plugin-${plugin.name}-api`),
...commonEnvs(configs),
...(plugin.extra_env || {})
Expand Down Expand Up @@ -492,6 +493,7 @@ const up = async ({ uis, downloadLocales, fromInstaller }) => {
JWT_TOKEN_SECRET: configs.jwt_token_secret,
LOAD_BALANCER_ADDRESS: generateLBaddress('http://plugin-core-api'),
MONGO_URL: mongoEnv(configs),
NODE_INSPECTOR: configs.nodeInspector ? 'enabled' : undefined,
EMAIL_VERIFIER_ENDPOINT:
configs.email_verifier_endpoint ||
'https://email-verifier.erxes.io',
Expand All @@ -514,6 +516,7 @@ const up = async ({ uis, downloadLocales, fromInstaller }) => {
JWT_TOKEN_SECRET: configs.jwt_token_secret,
CLIENT_PORTAL_DOMAINS: configs.client_portal_domains || '',
MONGO_URL: mongoEnv(configs),
NODE_INSPECTOR: configs.nodeInspector ? 'enabled' : undefined,
...commonEnvs(configs),
...((configs.gateway || {}).extra_env || {})
},
Expand All @@ -525,6 +528,7 @@ const up = async ({ uis, downloadLocales, fromInstaller }) => {
crons: {
image: `erxes/crons:${image_tag}`,
environment: {
NODE_INSPECTOR: configs.nodeInspector ? 'enabled' : undefined,
MONGO_URL: mongoEnv(configs),
...commonEnvs(configs)
},
Expand All @@ -538,6 +542,7 @@ const up = async ({ uis, downloadLocales, fromInstaller }) => {
JWT_TOKEN_SECRET: configs.jwt_token_secret,
LOAD_BALANCER_ADDRESS: generateLBaddress('http://plugin-workers-api'),
MONGO_URL: mongoEnv(configs),
NODE_INSPECTOR: configs.nodeInspector ? 'enabled' : undefined,
...commonEnvs(configs),
...((configs.workers || {}).extra_env || {})
},
Expand Down
147 changes: 147 additions & 0 deletions packages/api-utils/src/inspect.ts
@@ -0,0 +1,147 @@
import * as dotenv from 'dotenv';
dotenv.config();
import { promisify } from 'util';

// TODO: replace it with "node:inspector/promises" after migrating to Node version >= 20
import { HeapProfiler, Profiler, Session } from 'node:inspector';
import { Express } from 'express';
const session = new Session() as any;
session.connect();

const post = promisify(session.post.bind(session));

const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));

const { NODE_INSPECTOR } = process.env;

const nodeInspector = NODE_INSPECTOR === 'enabled';

export async function cpuPreciseCoverage(
seconds = 10
): Promise<Profiler.ScriptCoverage[]> {
// session.connect();
await post('Profiler.enable');

await post('Profiler.startPreciseCoverage', {
callCount: true,
detailed: true
});

await sleep(seconds * 1000);

const { result } = await post('Profiler.takePreciseCoverage');

await post('Profiler.stopPreciseCoverage');
await post('Profiler.disable');
// session.disconnect();
return result;
}

export async function cpuProfile(seconds = 10): Promise<Profiler.Profile> {
// session.connect();
await post('Profiler.enable');
await post('Profiler.start');

await sleep(seconds * 1000);

const { profile } = await post('Profiler.stop');

await post('Profiler.disable');
// session.disconnect();
return profile;
}

export async function heapProfile(
seconds = 10
): Promise<HeapProfiler.SamplingHeapProfile> {
// session.connect();
await post('HeapProfiler.enable');
await post('HeapProfiler.startSampling');

await sleep(seconds * 1000);

const { profile } = await post('HeapProfiler.stopSampling');

await post('HeapProfiler.disable');
// session.disconnect();
return profile;
}

export async function heapSnapshot(
chunkCb: (chunk: string) => void
): Promise<void> {
return new Promise<void>(async resolve => {
// session.connect();
const getChunk = m => {
chunkCb(m.params.chunk);
};
session.on('HeapProfiler.addHeapSnapshotChunk', getChunk);

await post('HeapProfiler.takeHeapSnapshot');

session.removeListener('HeapProfiler.addHeapSnapshotChunk', getChunk);
// session.disconnect();
return resolve();
});
}

export function applyInspectorEndpoints(app: Express, name: string) {
app.use('/node-inspector', (req, res, next) => {
if (nodeInspector) {
return next();
}
return res.status(403).send('Node inspector is not enabled');
});

app.get('/node-inspector/precise-coverage/:seconds', async (req, res) => {
try {
const seconds = Number(req.params.seconds) || 10;

const coverage = await cpuPreciseCoverage(seconds);

res.header('Content-Type', 'application/json');
return res.send(JSON.stringify(coverage, null, 4));
} catch (e) {
return res.status(500).send(e.message);
}
});

app.get('/node-inspector/cpu-profile/:seconds', async (req, res) => {
try {
const seconds = Number(req.params.seconds) || 10;

const profile = await cpuProfile(seconds);

res.attachment(`${name}-cpu-profile-${Date.now()}.cpuprofile`);
res.type('application/json');
return res.send(JSON.stringify(profile));
} catch (e) {
return res.status(500).send(e.message);
}
});

app.get('/node-inspector/heap-profile/:seconds', async (req, res) => {
try {
const seconds = Number(req.params.seconds) || 10;

const profile = await heapProfile(seconds);

res.attachment(`${name}-heap-profile-${Date.now()}.heapprofile`);
res.type('application/json');
return res.send(JSON.stringify(profile));
} catch (e) {
return res.status(500).send(e.message);
}
});

app.get('/node-inspector/heap-snapshot/', async (req, res) => {
try {
res.attachment(`${name}-heap-snapshot-${Date.now()}.heapsnapshot`);
res.type('json');
await heapSnapshot(chunk => res.write(chunk));
res.end();
} catch (e) {
return res.status(500).send(e.message);
}
});
}
3 changes: 3 additions & 0 deletions packages/api-utils/src/start-plugin/index.ts
Expand Up @@ -36,6 +36,7 @@ import {
leave,
redis
} from '@erxes/api-utils/src/serviceDiscovery';
import { applyInspectorEndpoints } from '../inspect';

const {
MONGO_URL,
Expand Down Expand Up @@ -687,6 +688,8 @@ export async function startPlugin(configs: any): Promise<express.Express> {
}
});

applyInspectorEndpoints(app, configs.name);

debugInfo(`${configs.name} server is running on port: ${PORT}`);

return app;
Expand Down
3 changes: 3 additions & 0 deletions packages/core/src/index.ts
Expand Up @@ -50,6 +50,7 @@ import exporter from './exporter';
import { moduleObjects } from './data/permissions/actions/permission';
import dashboards from './dashboards';
import { getEnabledServices } from '@erxes/api-utils/src/serviceDiscovery';
import { applyInspectorEndpoints } from '@erxes/api-utils/src/inspect';

const {
JWT_TOKEN_SECRET,
Expand Down Expand Up @@ -326,6 +327,8 @@ app.get('/plugins/enabled', async (_req, res) => {
// The error handler must be before any other error middleware and after all controllers
app.use(Sentry.Handlers.errorHandler());

applyInspectorEndpoints(app, 'core');

// Wrap the Express server
const httpServer = createServer(app);

Expand Down
3 changes: 3 additions & 0 deletions packages/gateway/src/index.ts
Expand Up @@ -25,6 +25,7 @@ import {
startSubscriptionServer,
stopSubscriptionServer
} from './subscription';
import { applyInspectorEndpoints } from '@erxes/api-utils/src/inspect';

const {
DOMAIN,
Expand Down Expand Up @@ -116,6 +117,8 @@ const {

app.use(express.urlencoded({ limit: '15mb', extended: true }));

applyInspectorEndpoints(app, 'gateway');

const port = PORT || 4000;

await new Promise<void>(resolve => httpServer.listen({ port }, resolve));
Expand Down

0 comments on commit 1145c1b

Please sign in to comment.