UNPKG

elastic-apm-node

Version:

The official Elastic APM agent for Node.js

542 lines (492 loc) 17.3 kB
'use strict' /** * This file is extracted from the 'async-listener' project copyright by * Forrest L Norvell. It have been modified slightly to be used in the current * context and where possible changes have been contributed back to the * original project. * * https://github.com/othiym23/async-listener * * Original file: * * https://github.com/othiym23/async-listener/blob/master/index.js * * License: * * BSD-2-Clause, http://opensource.org/licenses/BSD-2-Clause */ var isNative = require('is-native') var semver = require('semver') var shimmer = require('./shimmer') var wrap = shimmer.wrap var massWrap = shimmer.massWrap var v7plus = semver.gte(process.version, '7.0.0') var v11plus = semver.gte(process.version, '11.0.0') module.exports = function (ins) { var net = require('net') // From Node.js v7.0.0, net._normalizeConnectArgs have been renamed net._normalizeArgs if (v7plus && !net._normalizeArgs) { // a polyfill in our polyfill etc so forth -- taken from node master on 2017/03/09 net._normalizeArgs = function (args) { if (args.length === 0) { return [{}, null] } var arg0 = args[0] var options = {} if (typeof arg0 === 'object' && arg0 !== null) { // (options[...][, cb]) options = arg0 } else if (isPipeName(arg0)) { // (path[...][, cb]) options.path = arg0 } else { // ([port][, host][...][, cb]) options.port = arg0 if (args.length > 1 && typeof args[1] === 'string') { options.host = args[1] } } var cb = args[args.length - 1] if (typeof cb !== 'function') { return [options, null] } else { return [options, cb] } } } else if (!v7plus && !net._normalizeConnectArgs) { // a polyfill in our polyfill etc so forth -- taken from node master on 2013/10/30 net._normalizeConnectArgs = function (args) { var options = {} function toNumber (x) { return (x = Number(x)) >= 0 ? x : false } if (typeof args[0] === 'object' && args[0] !== null) { // connect(options, [cb]) options = args[0] } else if (typeof args[0] === 'string' && toNumber(args[0]) === false) { // connect(path, [cb]) options.path = args[0] } else { // connect(port, [host], [cb]) options.port = args[0] if (typeof args[1] === 'string') { options.host = args[1] } } var cb = args[args.length - 1] return typeof cb === 'function' ? [options, cb] : [options] } } wrap(net.Server.prototype, '_listen2', function (original) { return function () { this.on('connection', function (socket) { if (socket._handle) { socket._handle.onread = ins.bindFunction(socket._handle.onread) } }) try { return original.apply(this, arguments) } finally { // the handle will only not be set in cases where there has been an error if (this._handle && this._handle.onconnection) { this._handle.onconnection = ins.bindFunction(this._handle.onconnection) } } } }) function patchOnRead (ctx) { if (ctx && ctx._handle) { var handle = ctx._handle if (!handle._obOriginalOnread) { handle._obOriginalOnread = handle.onread } handle.onread = ins.bindFunction(handle._obOriginalOnread) } } wrap(net.Socket.prototype, 'connect', function (original) { return function () { // From Node.js v7.0.0, net._normalizeConnectArgs have been renamed net._normalizeArgs var args = v7plus ? net._normalizeArgs(arguments) : net._normalizeConnectArgs(arguments) if (args[1]) args[1] = ins.bindFunction(args[1]) var result = original.apply(this, args) patchOnRead(this) return result } }) var http = require('http') // NOTE: A rewrite occurred in 0.11 that changed the addRequest signature // from (req, host, port, localAddress) to (req, options) // Here, I use the longer signature to maintain 0.10 support, even though // the rest of the arguments aren't actually used wrap(http.Agent.prototype, 'addRequest', function (original) { return function (req) { var onSocket = req.onSocket req.onSocket = ins.bindFunction(function (socket) { patchOnRead(socket) return onSocket.apply(this, arguments) }) return original.apply(this, arguments) } }) var childProcess = require('child_process') function wrapChildProcess (child) { if (Array.isArray(child.stdio)) { child.stdio.forEach(function (socket) { if (socket && socket._handle) { socket._handle.onread = ins.bindFunction(socket._handle.onread) wrap(socket._handle, 'close', activatorFirst) } }) } if (child._handle) { child._handle.onexit = ins.bindFunction(child._handle.onexit) } } // iojs v2.0.0+ if (childProcess.ChildProcess) { wrap(childProcess.ChildProcess.prototype, 'spawn', function (original) { return function () { var result = original.apply(this, arguments) wrapChildProcess(this) return result } }) } else { massWrap(childProcess, [ 'execFile', // exec is implemented in terms of execFile 'fork', 'spawn' ], function (original) { return function () { var result = original.apply(this, arguments) wrapChildProcess(result) return result } }) } // need unwrapped nextTick for use within < 0.9 async error handling if (!process._fatalException) { process._originalNextTick = process.nextTick } var processors = [] if (process._nextDomainTick) processors.push('_nextDomainTick') if (process._tickDomainCallback) processors.push('_tickDomainCallback') massWrap( process, processors, activator ) wrap(process, 'nextTick', activatorFirst) var asynchronizers = [ 'setTimeout', 'setInterval' ] if (global.setImmediate) asynchronizers.push('setImmediate') var timers = require('timers') var patchGlobalTimers = global.setTimeout === timers.setTimeout massWrap( timers, asynchronizers, activatorFirst ) if (patchGlobalTimers) { massWrap( global, asynchronizers, activatorFirst ) } var dns = require('dns') massWrap( dns, [ 'lookup', 'resolve', 'resolve4', 'resolve6', 'resolveCname', 'resolveMx', 'resolveNs', 'resolveTxt', 'resolveSrv', 'reverse' ], activator ) if (dns.resolveNaptr) wrap(dns, 'resolveNaptr', activator) var fs = require('fs') massWrap( fs, [ 'watch', 'rename', 'truncate', 'chown', 'fchown', 'chmod', 'fchmod', 'stat', 'lstat', 'fstat', 'link', 'symlink', 'readlink', 'realpath', 'unlink', 'rmdir', 'mkdir', 'readdir', 'close', 'open', 'utimes', 'futimes', 'fsync', 'write', 'read', 'readFile', 'writeFile', 'appendFile', 'watchFile', 'unwatchFile', 'exists' ], activator ) // only wrap lchown and lchmod on systems that have them. if (fs.lchown) wrap(fs, 'lchown', activator) // eslint-disable-line node/no-deprecated-api if (fs.lchmod) wrap(fs, 'lchmod', activator) // eslint-disable-line node/no-deprecated-api // only wrap ftruncate in versions of node that have it if (fs.ftruncate) wrap(fs, 'ftruncate', activator) // Wrap zlib streams var zlib try { zlib = require('zlib') } catch (err) { } if (zlib && zlib.Deflate && zlib.Deflate.prototype) { var proto = Object.getPrototypeOf(zlib.Deflate.prototype) if (proto._transform) { // streams2 wrap(proto, '_transform', activator) } else if (proto.write && proto.flush && proto.end) { // plain ol' streams massWrap( proto, [ 'write', 'flush', 'end' ], activator ) } } // Wrap Crypto var crypto try { crypto = require('crypto') } catch (err) { } if (crypto) { var cryptoFunctions = ['pbkdf2', 'randomBytes'] if (!v11plus) cryptoFunctions.push('pseudoRandomBytes') massWrap( crypto, cryptoFunctions, activator ) } var instrumentPromise = isNative(global.Promise) // In case it's a non-native Promise, but bind have been used so it // looks native. There's still a potential false positive if the // non-native Promise library have a `name` property set to "Promise". // But worst case, the non-native Promise library will be instrumented // twice. instrumentPromise = instrumentPromise && global.Promise.name === 'Promise' /* * Native promises use the microtask queue to make all callbacks run * asynchronously to avoid Zalgo issues. Since the microtask queue is not * exposed externally, promises need to be modified in a fairly invasive and * complex way. * * The async boundary in promises that must be patched is between the * fulfillment of the promise and the execution of any callback that is waiting * for that fulfillment to happen. This means that we need to trigger a create * when resolve or reject is called and trigger before, after and error handlers * around the callback execution. There may be multiple callbacks for each * fulfilled promise, so handlers will behave similar to setInterval where * there may be multiple before after and error calls for each create call. * * async-listener monkeypatching has one basic entry point: `wrapCallback`. * `wrapCallback` should be called when create should be triggered and be * passed a function to wrap, which will execute the body of the async work. * The resolve and reject calls can be modified fairly easily to call * `wrapCallback`, but at the time of resolve and reject all the work to be done * on fulfillment may not be defined, since a call to then, chain or fetch can * be made even after the promise has been fulfilled. To get around this, we * create a placeholder function which will call a function passed into it, * since the call to the main work is being made from within the wrapped * function, async-listener will work correctly. * * There is another complication with monkeypatching Promises. Calls to then, * chain and catch each create new Promises that are fulfilled internally in * different ways depending on the return value of the callback. When the * callback return a Promise, the new Promise is resolved asynchronously after * the returned Promise has been also been resolved. When something other than * a promise is resolved the resolve call for the new Promise is put in the * microtask queue and asynchronously resolved. * * Then must be wrapped so that its returned promise has a wrapper that can be * used to invoke further continuations. This wrapper cannot be created until * after the callback has run, since the callback may return either a promise * or another value. Fortunately we already have a wrapper function around the * callback we can use (the wrapper created by resolve or reject). * * By adding an additional argument to this wrapper, we can pass in the * returned promise so it can have its own wrapper appended. the wrapper * function can the call the callback, and take action based on the return * value. If a promise is returned, the new Promise can proxy the returned * Promise's wrapper (this wrapper may not exist yet, but will by the time the * wrapper needs to be invoked). Otherwise, a new wrapper can be create the * same way as in resolve and reject. Since this wrapper is created * synchronously within another wrapper, it will properly appear as a * continuation from within the callback. */ if (instrumentPromise) { wrapPromise() } function wrapPromise () { var Promise = global.Promise wrap(Promise.prototype, 'then', wrapThen) // Node.js <v7 only, alias for .then if (Promise.prototype.chain) { wrap(Promise.prototype, 'chain', wrapThen) } function wrapThen (original) { return function wrappedThen () { var promise = this var next = original.apply(promise, Array.prototype.map.call(arguments, bind)) return next // wrap callbacks (success, error) so that the callbacks will be called as a // continuations of the resolve or reject call using the __ob_wrapper created above. function bind (fn) { if (typeof fn !== 'function') return fn return ins.bindFunction(fn) } } } } // Shim activator for functions that have callback last function activator (fn) { var fallback = function () { var args var cbIdx = arguments.length - 1 if (typeof arguments[cbIdx] === 'function') { args = Array(arguments.length) for (var i = 0; i < arguments.length - 1; i++) { args[i] = arguments[i] } args[cbIdx] = ins.bindFunction(arguments[cbIdx]) } return fn.apply(this, args || arguments) } // Preserve function length for small arg count functions. switch (fn.length) { case 1: return function (cb) { if (arguments.length !== 1) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, cb) } case 2: return function (a, cb) { if (arguments.length !== 2) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, a, cb) } case 3: return function (a, b, cb) { if (arguments.length !== 3) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, a, b, cb) } case 4: return function (a, b, c, cb) { if (arguments.length !== 4) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, a, b, c, cb) } case 5: return function (a, b, c, d, cb) { if (arguments.length !== 5) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, a, b, c, d, cb) } case 6: return function (a, b, c, d, e, cb) { if (arguments.length !== 6) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, a, b, c, d, e, cb) } default: return fallback } } // Shim activator for functions that have callback first function activatorFirst (fn) { var fallback = function () { var args if (typeof arguments[0] === 'function') { args = Array(arguments.length) args[0] = ins.bindFunction(arguments[0]) for (var i = 1; i < arguments.length; i++) { args[i] = arguments[i] } } return fn.apply(this, args || arguments) } // Preserve function length for small arg count functions. switch (fn.length) { case 1: return function (cb) { if (arguments.length !== 1) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, cb) } case 2: return function (cb, a) { if (arguments.length !== 2) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, cb, a) } case 3: return function (cb, a, b) { if (arguments.length !== 3) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, cb, a, b) } case 4: return function (cb, a, b, c) { if (arguments.length !== 4) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, cb, a, b, c) } case 5: return function (cb, a, b, c, d) { if (arguments.length !== 5) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, cb, a, b, c, d) } case 6: return function (cb, a, b, c, d, e) { if (arguments.length !== 6) return fallback.apply(this, arguments) if (typeof cb === 'function') cb = ins.bindFunction(cb) return fn.call(this, cb, a, b, c, d, e) } default: return fallback } } } // taken from node master on 2017/03/09 function toNumber (x) { return (x = Number(x)) >= 0 ? x : false } // taken from node master on 2017/03/09 function isPipeName (s) { return typeof s === 'string' && toNumber(s) === false }