UNPKG

elastic-apm-node

Version:

The official Elastic APM agent for Node.js

433 lines (357 loc) 13.2 kB
'use strict' var crypto = require('crypto') var http = require('http') var parseUrl = require('url').parse var path = require('path') var afterAll = require('after-all-results') var isError = require('core-util-is').isError var ancestors = require('require-ancestors') var Filters = require('object-filter-sequence') var config = require('./config') var connect = require('./middleware/connect') var Instrumentation = require('./instrumentation') var lambda = require('./lambda') var Metrics = require('./metrics') var parsers = require('./parsers') var stackman = require('./stackman') var symbols = require('./symbols') var IncomingMessage = http.IncomingMessage var ServerResponse = http.ServerResponse var version = require('../package').version module.exports = Agent function Agent () { this.middleware = { connect: connect.bind(this) } this._conf = null this._httpClient = null this._uncaughtExceptionListener = null this._config() this._instrumentation = new Instrumentation(this) this._metrics = new Metrics(this) this._errorFilters = new Filters() this._transactionFilters = new Filters() this._spanFilters = new Filters() this._transport = null this.lambda = lambda(this) } Object.defineProperty(Agent.prototype, 'logger', { get () { return this._conf.logger } }) Object.defineProperty(Agent.prototype, 'currentTransaction', { get () { return this._instrumentation.currentTransaction } }) Object.defineProperty(Agent.prototype, 'currentSpan', { get () { return this._instrumentation.currentSpan } }) Object.defineProperty(Agent.prototype, 'currentTraceparent', { get () { const current = this.currentSpan || this.currentTransaction return current ? current.traceparent : null } }) Agent.prototype.destroy = function () { if (this._transport) this._transport.destroy() } Agent.prototype.addPatch = function (name, handler) { return this._instrumentation.addPatch.apply(this._instrumentation, arguments) } Agent.prototype.removePatch = function (name, handler) { return this._instrumentation.removePatch.apply(this._instrumentation, arguments) } Agent.prototype.clearPatches = function (name) { return this._instrumentation.clearPatches.apply(this._instrumentation, arguments) } Agent.prototype.startTransaction = function (name, type, { startTime, childOf } = {}) { return this._instrumentation.startTransaction.apply(this._instrumentation, arguments) } Agent.prototype.endTransaction = function (result, endTime) { return this._instrumentation.endTransaction.apply(this._instrumentation, arguments) } Agent.prototype.setTransactionName = function (name) { return this._instrumentation.setTransactionName.apply(this._instrumentation, arguments) } Agent.prototype.startSpan = function (name, type, { childOf } = {}) { return this._instrumentation.startSpan.apply(this._instrumentation, arguments) } Agent.prototype._config = function (opts) { this._conf = config(opts) const url = this._conf.serverUrl ? parseUrl(this._conf.serverUrl) : { host: 'localhost:8200', port: '8200' } this._conf.serverHost = url.host this._conf.serverPort = parseInt(url.port, 10) } Agent.prototype.isStarted = function () { return global[symbols.agentInitialized] } Agent.prototype.start = function (opts) { if (this.isStarted()) throw new Error('Do not call .start() more than once') global[symbols.agentInitialized] = true this._config(opts) if (this._conf.filterHttpHeaders) { this.addFilter(require('./filters/http-headers')) } if (!this._conf.active) { this.logger.info('Elastic APM agent is inactive due to configuration') return this } else if (!this._conf.serviceName) { this.logger.error('Elastic APM isn\'t correctly configured: Missing serviceName') this._conf.active = false return this } else if (!/^[a-zA-Z0-9 _-]+$/.test(this._conf.serviceName)) { this.logger.error('Elastic APM isn\'t correctly configured: serviceName "%s" contains invalid characters! (allowed: a-z, A-Z, 0-9, _, -, <space>)', this._conf.serviceName) this._conf.active = false return this } else if (this._conf.serverPort < 1 || this._conf.serverPort > 65535) { this.logger.error('Elastic APM isn\'t correctly configured: serverUrl "%s" contains an invalid port! (allowed: 1-65535)', this._conf.serverUrl) this._conf.active = false return this } else if (this._conf.logLevel === 'trace') { var _ancestors = ancestors(module) var basedir = path.dirname(process.argv[1]) var stackObj = {} Error.captureStackTrace(stackObj) try { var pkg = require(path.join(basedir, 'package.json')) } catch (e) {} this.logger.trace('agent configured correctly %o', { pid: process.pid, ppid: process.ppid, arch: process.arch, platform: process.platform, node: process.version, agent: version, ancestors: _ancestors, startTrace: stackObj.stack.split(/\n */).slice(1), main: pkg && pkg.main, dependencies: pkg && pkg.dependencies, conf: this._conf }) } this._transport = this._conf.transport(this._conf, this) this._instrumentation.start() this._metrics.start() Error.stackTraceLimit = this._conf.stackTraceLimit if (this._conf.captureExceptions) this.handleUncaughtExceptions() return this } Agent.prototype.setFramework = function ({ name, version, overwrite = true }) { if (!this._transport || !this._conf) return const conf = {} if (name && (overwrite || !this._conf.frameworkName)) this._conf.frameworkName = conf.frameworkName = name if (version && (overwrite || !this._conf.frameworkVersion)) this._conf.frameworkVersion = conf.frameworkVersion = version this._transport.config(conf) } Agent.prototype.setUserContext = function (context) { var trans = this.currentTransaction if (!trans) return false trans.setUserContext(context) return true } Agent.prototype.setCustomContext = function (context) { var trans = this.currentTransaction if (!trans) return false trans.setCustomContext(context) return true } Agent.prototype.setTag = function (key, value) { this.logger.warn('Called deprecated method: apm.setTag(...)') return this.setLabel(key, value) } Agent.prototype.setLabel = function (key, value) { var trans = this.currentTransaction if (!trans) return false return trans.setLabel(key, value) } Agent.prototype.addTags = function (tags) { this.logger.warn('Called deprecated method: apm.addTags(...)') return this.addLabels(tags) } Agent.prototype.addLabels = function (labels) { var trans = this.currentTransaction if (!trans) return false return trans.addLabels(labels) } Agent.prototype.addFilter = function (fn) { this.addErrorFilter(fn) this.addTransactionFilter(fn) this.addSpanFilter(fn) } Agent.prototype.addErrorFilter = function (fn) { if (typeof fn !== 'function') { this.logger.error('Can\'t add filter of type %s', typeof fn) return } this._errorFilters.push(fn) } Agent.prototype.addTransactionFilter = function (fn) { if (typeof fn !== 'function') { this.logger.error('Can\'t add filter of type %s', typeof fn) return } this._transactionFilters.push(fn) } Agent.prototype.addSpanFilter = function (fn) { if (typeof fn !== 'function') { this.logger.error('Can\'t add filter of type %s', typeof fn) return } this._spanFilters.push(fn) } Agent.prototype.captureError = function (err, opts, cb) { if (typeof opts === 'function') return this.captureError(err, null, opts) var agent = this var trans = this.currentTransaction var span = this.currentSpan var timestamp = normalizeTimestamp(opts && opts.timestamp) var context = (span || trans || {})._context || {} var req = opts && opts.request instanceof IncomingMessage ? opts.request : trans && trans.req var res = opts && opts.response instanceof ServerResponse ? opts.response : trans && trans.res var _isError = isError(err) if ((!opts || opts.handled !== false) && (agent._conf.captureErrorLogStackTraces === config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS || (!_isError && agent._conf.captureErrorLogStackTraces === config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES)) ) { var captureLocation = {} Error.captureStackTrace(captureLocation, Agent.prototype.captureError) } if (!_isError) { prepareError(parsers.parseMessage(err)) } else { parsers.parseError(err, agent, function (_, error) { // As of now, parseError suppresses errors internally, but even if they // were passed on, we would want to suppress them here anyway prepareError(error) }) } function prepareError (error) { error.id = crypto.randomBytes(16).toString('hex') error.parent_id = context.id error.trace_id = context.traceId error.timestamp = timestamp error.context = { user: Object.assign( {}, req && parsers.getUserContextFromRequest(req), trans && trans._user, opts && opts.user ), tags: Object.assign( {}, trans && trans._labels, opts && opts.tags, opts && opts.labels ), custom: Object.assign( {}, trans && trans._custom, opts && opts.custom ) } if (trans) { error.transaction_id = trans.id error.transaction = { type: trans.type, sampled: trans.sampled } } if (error.exception) { error.exception.handled = !opts || opts.handled // Optional add an alternative error message as well as the exception message if (opts && opts.message && opts.message !== error.exception.message && !error.log) { error.log = { message: opts.message } } } if (req) { error.context.request = parsers.getContextFromRequest(req, agent._conf, 'errors') } if (res) { error.context.response = parsers.getContextFromResponse(res, agent._conf, true) } if (captureLocation) { // prepare to add a stack trace pointing to where captureError was called // from. This can make it easier to debug async stack traces. stackman.callsites(captureLocation, function (_err, callsites) { if (_err) { agent.logger.debug('error while getting capture location callsites: %s', _err.message) } var next = afterAll(function (_, frames) { // As of now, parseCallsite suppresses errors internally, but even if // they were passed on, we would want to suppress them here anyway if (frames) { // In case there isn't any log object, we'll make a dummy message // as the APM Server requires a message to be present if a // stacktrace also present if (!error.log) error.log = { message: error.exception.message } error.log.stacktrace = frames } send(error) }) if (callsites) { callsites.forEach(function (callsite) { parsers.parseCallsite(callsite, true, agent, next()) }) } }) } else { send(error) } } function send (error) { error = agent._errorFilters.process(error) if (!error) { agent.logger.debug('error ignored by filter %o', { id: error.id }) if (cb) cb(null, error.id) return } if (agent._transport) { agent.logger.info(`Sending error to Elastic APM`, { id: error.id }) agent._transport.sendError(error, function () { agent.flush(function (err) { if (cb) cb(err, error.id) }) }) } else if (cb) { // TODO: Swallow this error just as it's done in agent.flush()? process.nextTick(cb.bind(null, new Error('cannot capture error before agent is started'), error.id)) } } } // The optional callback will be called with the error object after the error // have been sent to the intake API. If no callback have been provided we will // automatically terminate the process, so if you provide a callback you must // remember to terminate the process manually. Agent.prototype.handleUncaughtExceptions = function (cb) { var agent = this if (this._uncaughtExceptionListener) { process.removeListener('uncaughtException', this._uncaughtExceptionListener) } this._uncaughtExceptionListener = function (err) { agent.logger.debug('Elastic APM caught unhandled exception: %s', err.message) agent.captureError(err, { handled: false }, function () { cb ? cb(err) : process.exit(1) }) } process.on('uncaughtException', this._uncaughtExceptionListener) } Agent.prototype.flush = function (cb) { if (this._transport) { // TODO: Only bind the callback if the transport can't use AsyncResource from async hooks this._transport.flush(cb && this._instrumentation.bindFunction(cb)) } else { this.logger.warn(new Error('cannot flush agent before it is started')) if (cb) process.nextTick(cb) } } function normalizeTimestamp (timestamp) { return (timestamp > 0 && Math.floor(timestamp * 1000)) || Date.now() * 1000 }