UNPKG

elastic-apm-node

Version:

The official Elastic APM agent for Node.js

331 lines (271 loc) 9.26 kB
'use strict' var fs = require('fs') var path = require('path') var hook = require('require-in-the-middle') var semver = require('semver') var NamedArray = require('./named-array') var shimmer = require('./shimmer') var Transaction = require('./transaction') var MODULES = [ 'apollo-server-core', 'bluebird', 'cassandra-driver', 'elasticsearch', 'express', 'express-graphql', 'express-queue', 'fastify', 'finalhandler', 'generic-pool', 'graphql', 'handlebars', 'hapi', 'http', 'https', 'http2', 'ioredis', 'jade', 'knex', 'koa', 'koa-router', 'mimic-response', 'mongodb-core', 'mysql', 'mysql2', 'pg', 'pug', 'redis', 'restify', 'tedious', 'ws' ] module.exports = Instrumentation function Instrumentation (agent) { this._agent = agent this._hook = null // this._hook is only exposed for testing purposes this._started = false this.currentTransaction = null // Span for binding callbacks this.bindingSpan = null // Span which is actively bound this.activeSpan = null Object.defineProperty(this, 'currentSpan', { get () { return this.bindingSpan || this.activeSpan } }) // NOTE: we need to track module names for patches // in a separate array rather than using Object.keys() // because the array is given to the hook(...) call. this._patches = new NamedArray() for (let mod of MODULES) { this.addPatch(mod, (exports, name, version, enabled) => { // Lazy require so that we don't have to use `require.resolve` which // would fail in combination with Webpack. For more info see: // https://github.com/elastic/apm-agent-nodejs/pull/957 const patch = require(`./modules/${mod}`) return patch(exports, name, version, enabled) }) } } Instrumentation.prototype.addPatch = function (name, handler) { const type = typeof handler if (type !== 'function' && type !== 'string') { this._agent.logger.error('Invalid patch handler type:', type) return } this._patches.add(name, handler) this._startHook() } Instrumentation.prototype.removePatch = function (name, handler) { this._patches.delete(name, handler) this._startHook() } Instrumentation.prototype.clearPatches = function (name) { this._patches.clear(name) this._startHook() } Instrumentation.modules = Object.freeze(MODULES) Instrumentation.prototype.start = function () { if (!this._agent._conf.instrument) return if (this._started) return this._started = true if (this._agent._conf.asyncHooks && semver.gte(process.version, '8.2.0')) { require('./async-hooks')(this) } else { require('./patch-async')(this) } const patches = this._agent._conf.addPatch if (Array.isArray(patches)) { for (let [mod, path] of patches) { this.addPatch(mod, path) } } this._startHook() } Instrumentation.prototype._startHook = function () { if (!this._started) return if (this._hook) { this._agent.logger.debug('removing hook to Node.js module loader') this._hook.unhook() } var self = this var disabled = new Set(this._agent._conf.disableInstrumentations) this._agent.logger.debug('adding hook to Node.js module loader') this._hook = hook(this._patches.keys, function (exports, name, basedir) { var enabled = !disabled.has(name) var pkg, version if (basedir) { pkg = path.join(basedir, 'package.json') try { version = JSON.parse(fs.readFileSync(pkg)).version } catch (e) { self._agent.logger.debug('could not shim %s module: %s', name, e.message) return exports } } else { version = process.versions.node } return self._patchModule(exports, name, version, enabled) }) } Instrumentation.prototype._patchModule = function (exports, name, version, enabled) { this._agent.logger.debug('shimming %s@%s module', name, version) var patches = this._patches.get(name) if (patches) { for (let patch of patches) { if (typeof patch === 'string') { if (patch[0] === '.') { patch = path.resolve(process.cwd(), patch) } patch = require(patch) } const type = typeof patch if (type !== 'function') { this._agent.logger.error('Invalid patch handler type "%s" for module "%s"', type, name) continue } exports = patch(exports, this._agent, { version, enabled }) } } return exports } Instrumentation.prototype.addEndedTransaction = function (transaction) { var agent = this._agent if (this._started) { var payload = agent._transactionFilters.process(transaction._encode()) if (!payload) return agent.logger.debug('transaction ignored by filter %o', { trans: transaction.id, trace: transaction.traceId }) agent.logger.debug('sending transaction %o', { trans: transaction.id, trace: transaction.traceId }) agent._transport.sendTransaction(payload) } else { agent.logger.debug('ignoring transaction %o', { trans: transaction.id, trace: transaction.traceId }) } } Instrumentation.prototype.addEndedSpan = function (span) { var agent = this._agent if (this._started) { agent.logger.debug('encoding span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) span._encode(function (err, payload) { if (err) { agent.logger.error('error encoding span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type, error: err.message }) return } payload = agent._spanFilters.process(payload) if (!payload) { agent.logger.debug('span ignored by filter %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) return } agent.logger.debug('sending span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) if (agent._transport) agent._transport.sendSpan(payload) }) } else { agent.logger.debug('ignoring span %o', { span: span.id, parent: span.parentId, trace: span.traceId, name: span.name, type: span.type }) } } Instrumentation.prototype.startTransaction = function (name, type, opts) { // To be backwards compatible with the old API, we also accept a `traceparent` string if (typeof opts === 'string') opts = { childOf: opts } return new Transaction(this._agent, name, type, opts) } Instrumentation.prototype.endTransaction = function (result, endTime) { if (!this.currentTransaction) { this._agent.logger.debug('cannot end transaction - no active transaction found') return } this.currentTransaction.end(result, endTime) } Instrumentation.prototype.setDefaultTransactionName = function (name) { var trans = this.currentTransaction if (!trans) { this._agent.logger.debug('no active transaction found - cannot set default transaction name') return } trans.setDefaultName(name) } Instrumentation.prototype.setTransactionName = function (name) { var trans = this.currentTransaction if (!trans) { this._agent.logger.debug('no active transaction found - cannot set transaction name') return } trans.name = name } Instrumentation.prototype.startSpan = function () { if (!this.currentTransaction) { this._agent.logger.debug('no active transaction found - cannot build new span') return null } return this.currentTransaction.startSpan.apply(this.currentTransaction, arguments) } var wrapped = Symbol('elastic-apm-wrapped-function') Instrumentation.prototype.bindFunction = function (original) { if (typeof original !== 'function' || original.name === 'elasticAPMCallbackWrapper') return original var ins = this var trans = this.currentTransaction var span = this.currentSpan if (trans && !trans.sampled) { return original } original[wrapped] = elasticAPMCallbackWrapper return elasticAPMCallbackWrapper function elasticAPMCallbackWrapper () { var prevTrans = ins.currentTransaction ins.currentTransaction = trans ins.bindingSpan = null ins.activeSpan = span if (trans) trans.sync = false if (span) span.sync = false var result = original.apply(this, arguments) ins.currentTransaction = prevTrans return result } } Instrumentation.prototype.bindEmitter = function (emitter) { var ins = this var addMethods = [ 'on', 'addListener' ] var removeMethods = [ 'off', 'removeListener' ] if (semver.satisfies(process.versions.node, '>=6')) { addMethods.push('prependListener') } shimmer.massWrap(emitter, addMethods, (original) => function (name, handler) { return original.call(this, name, ins.bindFunction(handler)) }) shimmer.massWrap(emitter, removeMethods, (original) => function (name, handler) { return original.call(this, name, handler[wrapped] || handler) }) } Instrumentation.prototype._recoverTransaction = function (trans) { if (this.currentTransaction === trans) return this._agent.logger.debug('recovering from wrong currentTransaction %o', { wrong: this.currentTransaction ? this.currentTransaction.id : undefined, correct: trans.id, trace: trans.traceId }) this.currentTransaction = trans }