UNPKG

heroku-debug

Version:
281 lines (230 loc) 9.14 kB
'use strict' var http = require('http') var util = require('util') var events = require('events') var uuid = require('node-uuid') var OpbeatHttpClient = require('opbeat-http-client') var ReleaseTracker = require('opbeat-release-tracker') var debug = require('debug')('opbeat') var config = require('./config') var logger = require('./logger') var filters = require('./filters') var parsers = require('./parsers') var request = require('./request') var connect = require('./middleware/connect') var Instrumentation = require('./instrumentation') var version = require('../package').version var userAgent = 'opbeat-nodejs/' + version var warnedAboutFilter = false var noop = function () {} module.exports = Agent function Agent () { events.EventEmitter.call(this) var boundConnect = connect.bind(this) this.middleware = { connect: boundConnect, express: boundConnect } var ins = new Instrumentation(this) this._instrumentation = ins this.startTransaction = ins.startTransaction.bind(ins) this.endTransaction = ins.endTransaction.bind(ins) this.setTransactionName = ins.setTransactionName.bind(ins) this.buildTrace = ins.buildTrace.bind(ins) this._filters = [] // configure the agent with default values this._config() } util.inherits(Agent, events.EventEmitter) Agent.prototype._config = function (opts) { opts = config(opts) logger.init({ level: opts.logLevel }) this.appId = opts.appId this.organizationId = opts.organizationId this.secretToken = opts.secretToken this.active = opts.active this.logLevel = opts.logLevel this.hostname = opts.hostname this.stackTraceLimit = opts.stackTraceLimit this.captureExceptions = opts.captureExceptions this.exceptionLogLevel = opts.exceptionLogLevel this.timeout = { active: opts.timeout, errorResult: opts.timeoutErrorResult, errorThreshold: opts.timeoutErrorThreshold } this.instrument = opts.instrument this.filter = opts.filter // deprecated this.ff_captureFrame = opts.ff_captureFrame this._apiHost = opts._apiHost if (this.filter) { warnedAboutFilter = true logger.warn('Opbeat `filter` option is deprecated. See `opbeat.addFilter()` instead.') } return opts } Agent.prototype.start = function (opts) { if (global.__opbeat_initialized) throw new Error('Do not call opbeat.start() more than once') global.__opbeat_initialized = true opts = this._config(opts) if (opts.filterHttpHeaders) this.addFilter(filters.httpHeaders) if (!this.active) { logger.info('Opbeat agent is inactive due to configuration') return this } else if (!this.appId || !this.organizationId || !this.secretToken) { logger.error('Opbeat isn\'t correctly configured: Missing organizationId, appId or secretToken') this.active = false return this } else { debug('agent configured correctly %o', { node: process.version, agent: version, org: this.organizationId, app: this.appId, instrument: this.instrument }) } this._instrumentation.start() this._httpClient = OpbeatHttpClient({ appId: this.appId, organizationId: this.organizationId, secretToken: this.secretToken, userAgent: userAgent, _apiHost: this._apiHost }) Error.stackTraceLimit = this.stackTraceLimit if (this.captureExceptions) this.handleUncaughtExceptions() this.on('error', errorLogger) this.on('logged', function (url, uuid) { logger.info('Opbeat logged error successfully at %s', url, { uuid: uuid }) }) return this } Agent.prototype.addFilter = function (fn) { if (typeof fn !== 'function') { logger.error('Can\'t add filter of type %s', typeof fn) return } this._filters.push(fn) } Agent.prototype._runFilters = function (data, err) { // deprecated functionality if (this.filter) { if (!warnedAboutFilter) { warnedAboutFilter = true logger.warn('Opbeat `filter` option is deprecated. See `opbeat.addFilter()` instead.') } data = this.filter(err, data) if (!data) return } // abort if a filter function doesn't return an object this._filters.some(function (filter) { data = filter(data) return !data }) return data } Agent.prototype.captureError = function (err, data, cb) { var agent = this var captureTime = new Date() if (typeof data === 'function') { cb = data data = {} } else if (!data) { data = {} } else if (data.request instanceof http.IncomingMessage) { data.http = parsers.parseRequest(data.request) } delete data.request var trans = this._instrumentation.currentTransaction if (!data.http && trans && trans.req) data.http = parsers.parseRequest(trans.req) var errUUID = data.extra && data.extra.uuid || uuid.v4() if (!util.isError(err)) { var isMessage = true var customCulprit = 'culprit' in data parsers.parseMessage(err, data) logger.error(data.message, { uuid: errUUID }) err = new Error(data.message) } else if (this.ff_captureFrame && !err.uncaught) { var captureFrameError = new Error() } if (!isMessage) { logger.error('logging error with Opbeat:', { uuid: errUUID }) logger.error(err.stack) } parsers.parseError(err, data, function (data) { if (isMessage) { // Messages shouldn't have an exception and the algorithm for finding the // culprit might show the Opbeat agent and we don't want that delete data.exception if (!customCulprit) delete data.culprit if (data.stacktrace) data.stacktrace.frames.shift() } var done = function () { if (data.stacktrace) data.stacktrace.frames.reverse() // opbeat expects frames in reverse order data.machine = { hostname: agent.hostname } data.extra = data.extra || {} data.extra.node = process.version if (!data.extra.uuid) data.extra.uuid = errUUID data.timestamp = captureTime.toISOString() data = agent._runFilters(data, err) if (!data) logger.info('Error not sent to Opbeat - Ignored by filter') if (data && agent.active) request.error(agent, data, cb) // In v3.x, the captureError callback will only be called if the agent is // active. This is scheduled to change in v4.x. When using the middleware // we want to call the next function even if the agent is inactive, so // until we release v4.x we need a way to tell the captureError function // that the callback should be called even if the agent is inactive. This // is done by naming the callback `_opbeatMiddleware`. // TODO: Undo this hack in v4.x else if (cb && cb.name === '_opbeatMiddleware') cb() } if (captureFrameError && (!data.stacktrace || !data.stacktrace.frames.some(function (frame) { return frame.in_app }))) { // prepare to add a top frame to the stack trace specifying the location // where captureError was called from. This can make it easier to debug // async stack traces. parsers.parseError(captureFrameError, {}, function (result) { // ignore the first frame as it will be the opbeat module if (result.stacktrace) var frame = result.stacktrace.frames[1] if (data.stacktrace) data.stacktrace.frames.unshift(frame) else if (frame) data.stacktrace = { frames: [frame] } done() }) } else { done() } }) } // The optional callback will be called with the error object after the // error have been logged to Opbeat. 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) { var data = { extra: { uuid: uuid.v4() }, level: agent.exceptionLogLevel } debug('Opbeat caught unhandled exception %o', { uuid: data.extra.uuid }) // Since we exit the node-process we cannot guarantee that the // listeners will be called, so to ensure a uniform result, // we'll remove all event listeners if an uncaught exception is // found agent.removeAllListeners() // But make sure emitted errors doesn't cause yet another uncaught // exception agent.on('error', errorLogger) err.uncaught = true agent.captureError(err, data, function (opbeatErr, url) { if (opbeatErr) { logger.error('Could not notify Opbeat', { uuid: data.extra.uuid }) logger.error(opbeatErr.stack) } else { logger.info('Opbeat logged error successfully at %s', url, { uuid: data.extra.uuid }) } cb ? cb(err, url) : process.exit(1) }) } process.on('uncaughtException', this._uncaughtExceptionListener) } Agent.prototype.trackRelease = function (data, cb) { if (!this._releaseTracker) this._releaseTracker = ReleaseTracker(this._httpClient) this._releaseTracker(data, cb || noop) } function errorLogger (err, uuid) { logger.error('Could not notify Opbeat', { uuid: uuid }) logger.error(err.stack) }