UNPKG

@contrast/route-metrics

Version:

`route-metrics` allows server performance, exclusive of network time, to be compared on a route-by-route basis. It was created to compare server performance with and without `@contrast/agent` being loaded and active.

208 lines (187 loc) 8.03 kB
'use strict'; const {isMainThread} = require('node:worker_threads'); const package_json = require('../package.json'); // // this is the startup code. it's only run in the main thread. // // it sets up the writer, the patcher, and listeners for all the events that // will be written to the log file. // module.exports = function initRouteMetrics({type}) { if (!isMainThread) { // for some reason, main is invoked in the worker thread after it's // invoked in the main thread. i guess we could treat this as a meta- // esm-type and enabled the patcher in the worker thread, but there // is not communications channel then, so we can't forward messages // to the main thread. this may not be the best solution, but the optimal // solution is for the user to --import the esm version. maybe this // should only be an esm module? would really simplify things. return; } // it's only one thing, but use an array so it's possible to add a user- // specified agent at runtime (at some point in the future). const AGENTS = ['@contrast/agent']; // node 16.19.1 made -r @contrast/agent not accessible via require patching // and it's not visible to patching or loader hooks when using --import, so // look at execArgv. it's a hack, but it's more straightforward than the way // it was done before (look for things required by the agent). This approach // also has no cost after startup. // https://github.com/nodejs/node/commit/b02d895137 const agentsToBeEmitted = []; // let's find @contrast/agent if it's there. for (let i = 0; i < process.execArgv.length; i++) { if (['-r', '--require', '--import'].includes(process.execArgv[i])) { const ix = AGENTS.indexOf(process.execArgv[i + 1]); if (ix >= 0) { agentsToBeEmitted.push(AGENTS[ix]); } } } // // startup code. all code in this file is executed in the main thread. // thread-specific code is in other files, e.g., setup-patcher.js. // const makeHeader = require('./writer/make-header'); // // get the configuration and create the log file writer // const {config, errors} = require('./config/agent-config').get(); const Writer = require('./writer/make-writer'); const options = { streamOptions: { // if logging all loads this generates many log entries very fast. the // end var is the highWaterMark multiplier if set: e.g., 1 means 1MB, 2 // means 2097152, etc. if not, default of 16K is usually fine. 0 is the // default value for LOG_ALL_LOADS. highWaterMark: config.LOG_ALL_LOADS ? config.LOG_ALL_LOADS * 1048576 : undefined } }; const writer = new Writer(config.LOG_FILE, options); // // write the header to the log file, then write any errors detected // in the configuration. nothing is written to the console if the // log file is writable. // const {header, errors: headerErrors} = makeHeader(package_json.version, config); writer.write({type: 'header'}, header); if (errors.unknown.length) { writer.write({type: 'unknown-config-items'}, errors.unknown.join(', ')); } if (errors.invalid.length) { writer.write({type: 'invalid-config-values'}, errors.invalid.join(', ')); } if (headerErrors.length) { writer.write({type: 'header-errors'}, headerErrors.join(', ')); } // // routes are emitted by the patched http/https/http2 code. type 'route' used // to be known as 'metric' but that was too generic and confusing. // const routeListener = (m) => { // main only writer.write({type: 'route'}, m); }; const routeEmitter = require('./route-emitter'); routeEmitter.on('route', routeListener); // // timeseries capture data that is sampled on a regular interval, unlike // patches, loads, and routes, which are generated by events. // // the always-enabled cpu and memory time series function tsCallback(cpuAndMem) { writer.write({type: 'proc'}, cpuAndMem); } const timeSeriesOptions = {tsCallback}; // optional GC time series if (config.GARBAGE_COLLECTION) { timeSeriesOptions.gcCallback = function gcListener(gcStats) { writer.write({type: 'gc'}, gcStats); }; } // optional event loop time series if (config.EVENTLOOP) { timeSeriesOptions.elCallback = function elListener(elPercentiles) { writer.write({type: 'eventloop'}, elPercentiles); }; } // undocumented interval setting, primarily for testing. let interval = process.env.CSI_ROUTE_METRICS_TIME_SERIES_INTERVAL || 1000; interval = Number(interval); const TimeSeries = require('./time-series'); // eslint-disable-next-line no-unused-vars const timeSeries = new TimeSeries(interval, timeSeriesOptions); for (const agent of agentsToBeEmitted) { writer.write({type: 'patch'}, {name: agent}); } // // setup the patcher for the main thread. it will also be set up in the // loader thread, but the loader thread event listeners just augment the // events then forward them to the main thread to be written to the log file. // // THIS IS DONE LAST SO WE DON'T SEE ALL OF OUR OWN LOADS. // const patchListener = (m) => { // if it's an error make it concise; there's only one place in each thread // where files are patched, so there's no need for a stack trace. // a stack trace. if (m instanceof Error) { const {code, message} = m; const patchError = {}; if (code) { patchError.code = code; } patchError.message = message; writer.write({type: 'patch-error'}, patchError); return; } // normal case - just write the log writer.write({type: 'patch'}, m); }; let loadListener; if (config.LOG_ALL_LOADS) { // listen for load events and write them to the log. a file being // loaded will emit either a load or a patch event, not both. packages // preloaded by node, whether using --require or --import, will emit // a patch event (currently only @contrast/agent is checked). loadListener = (m) => { writer.write({type: 'load'}, m); }; } const setupPatcher = require('./setup-patcher'); setupPatcher(patchListener, loadListener); // this should always be true now; this mostly serves as delineation that the // following code is all about setting up and handling the loader thread. if (type === 'esm') { // function to receive messages from the loader thread. all it does is pass // them to the writer. the esm initialize() hook enables patching and // listens for the same 'patch' and 'load' events as above. but the hooks // in the ESM loader thread just forward the event to the main thread, as // a message, where portMessageHandler() receives it and writes it to the // log file. // // why? so we don't try to write to the same file from multiple threads. // why does the ESM loader thread add a timestamp? so the timestamp doesn't // include the port transfer time. async function portMessageHandler(message) { // log it? yes, but slightly more complex because we're receiving the // message from the loader thread which added a threadId and timestamp. const {type, ts, tid, m: entry} = message; await writer.write({type, ts, tid}, entry); } // ideally we'd wait on this, but once register has been called the hooks are // in place and nothing remains to be done. (async() => { const transferData = { app_dir: header.app_dir, // this probably should pass the entire config to the loader thread but // at this time, this is the only item needed. it's used by patcher. // N.B. it's used as an integer in the loader thread even though it does // get assigned to process.env too. env: { CSI_RM_LOG_ALL_LOADS: Number(process.env.CSI_RM_LOG_ALL_LOADS) || 0, }, }; const registerHooks = (await import('./esm-hooks/index.mjs')).default; await registerHooks(transferData, portMessageHandler); })(); } };