Skip to content
41 changes: 40 additions & 1 deletion lib/agent.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,46 @@ Object.defineProperty(Agent.prototype, 'currentTraceIds', {
}
})

// Destroy this agent. This prevents any new agent processing, communication
// with APM server, and resets changed global state *as much as is possible*.
//
// In the typical uses case -- a singleton Agent running for the full process
// lifetime -- it is *not* necessary to call `agent.destroy()`. It is used
// for some testing.
//
// Limitations:
// - Patching/wrapping of functions for instrumentation *is* undone, but
// references to the wrapped versions can remain.
// - There may be in-flight tasks (in ins.addEndedSpan() and
// agent.captureError() for example) that will complete after this destroy
// completes. They should have no impact other than CPU/resource use.
Agent.prototype.destroy = function () {
if (this._transport) this._transport.destroy()
if (this._transport && this._transport.destroy) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I did a quick scan of the work done in both the agent's constructor and agent.start() and the values reset here appear to be sufficient.

this._transport.destroy()
}
// So in-flight tasks in ins.addEndedSpan() and agent.captureError() do
// not use the destroyed transport.
this._transport = null

// So in-flight tasks do not call user-added filters after the agent has
// been destroyed.
this._errorFilters = new Filters()
this._transactionFilters = new Filters()
this._spanFilters = new Filters()

if (this._uncaughtExceptionListener) {
process.removeListener('uncaughtException', this._uncaughtExceptionListener)
}
this._metrics.stop()
this._instrumentation.stop()

// Allow a new Agent instance to `.start()`. Typically this is only relevant
// for tests that may use multiple Agent instances in a single test process.
global[symbols.agentInitialized] = null

if (Error.stackTraceLimit === this._conf.stackTraceLimit) {
Error.stackTraceLimit = this._origStackTraceLimit
}
}

// These are metrics about the agent itself -- separate from the metrics
Expand Down Expand Up @@ -215,6 +253,7 @@ Agent.prototype.start = function (opts) {
this._instrumentation.start()
this._metrics.start()

this._origStackTraceLimit = Error.stackTraceLimit
Error.stackTraceLimit = this._conf.stackTraceLimit
if (this._conf.captureExceptions) this.handleUncaughtExceptions()

Expand Down
28 changes: 17 additions & 11 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,6 @@ if (packageName === 'elastic-apm-node') {
}
var userAgent = `${packageName}/${version}`

config.INTAKE_STRING_MAX_SIZE = 1024
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

REVIEWER NOTE: All the changes in this file were to export DEFAULTS to use in one of the tests below. Then, because the CAPTURE_... constants are used in the declaration of DEFAULTS, it was cleaner to declare the exports as consts and setup module.exports at the bottom.

config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER = 'never'
config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES = 'messages'
config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = 'always'

module.exports = config

let confFile = loadConfigFile()

let serviceName, serviceVersion
Expand All @@ -38,6 +31,11 @@ try {
serviceVersion = version
} catch (err) {}

const INTAKE_STRING_MAX_SIZE = 1024
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I generally prefer "export everything once at the bottom" approach, and I never liked relying on the config function being hoisted up

const CAPTURE_ERROR_LOG_STACK_TRACES_NEVER = 'never'
const CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES = 'messages'
const CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = 'always'

var DEFAULTS = {
abortedErrorThreshold: '25s',
active: true,
Expand All @@ -47,7 +45,7 @@ var DEFAULTS = {
asyncHooks: true,
breakdownMetrics: true,
captureBody: 'off',
captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES,
captureErrorLogStackTraces: CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES,
captureExceptions: true,
captureHeaders: true,
captureSpanStackTraces: true,
Expand Down Expand Up @@ -562,8 +560,8 @@ function normalizeBools (opts, logger) {
}

function truncateOptions (opts) {
if (opts.serviceVersion) opts.serviceVersion = truncate(String(opts.serviceVersion), config.INTAKE_STRING_MAX_SIZE)
if (opts.hostname) opts.hostname = truncate(String(opts.hostname), config.INTAKE_STRING_MAX_SIZE)
if (opts.serviceVersion) opts.serviceVersion = truncate(String(opts.serviceVersion), INTAKE_STRING_MAX_SIZE)
if (opts.hostname) opts.hostname = truncate(String(opts.hostname), INTAKE_STRING_MAX_SIZE)
}

function bytes (input) {
Expand Down Expand Up @@ -685,7 +683,7 @@ function getBaseClientConfig (conf, agent) {
environment: conf.environment,

// Sanitize conf
truncateKeywordsAt: config.INTAKE_STRING_MAX_SIZE,
truncateKeywordsAt: INTAKE_STRING_MAX_SIZE,
truncateErrorMessagesAt: conf.errorMessageMaxLength,

// HTTP conf
Expand Down Expand Up @@ -720,3 +718,11 @@ function getBaseClientConfig (conf, agent) {
cloudMetadataFetcher: (new CloudMetadata(cloudProvider, conf.logger, conf.serviceName))
}
}

// Exports.
module.exports = config
module.exports.INTAKE_STRING_MAX_SIZE = INTAKE_STRING_MAX_SIZE
module.exports.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER = CAPTURE_ERROR_LOG_STACK_TRACES_NEVER
module.exports.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES = CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES
module.exports.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS
module.exports.DEFAULTS = DEFAULTS
27 changes: 15 additions & 12 deletions lib/instrumentation/async-hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,12 @@
const asyncHooks = require('async_hooks')
const shimmer = require('./shimmer')

// FOR INTERNAL TESTING PURPOSES ONLY!
const resettable = process.env._ELASTIC_APM_ASYNC_HOOKS_RESETTABLE === 'true'
let _asyncHook

module.exports = function (ins) {
const asyncHook = asyncHooks.createHook({ init, before, destroy })
const contexts = new WeakMap()

if (resettable) {
if (_asyncHook) _asyncHook.disable()
_asyncHook = asyncHook
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This asyncHook.disable() moved to Instrumentation#stop(), removing the need for _ELASTIC_APM_ASYNC_HOOKS_RESETTABLE.

const activeSpans = new Map()
const activeTransactions = new Map()
let contexts = new WeakMap()

Object.defineProperty(ins, 'currentTransaction', {
get () {
const asyncId = asyncHooks.executionAsyncId()
Expand All @@ -32,7 +24,6 @@ module.exports = function (ins) {
}
})

const activeSpans = new Map()
Object.defineProperty(ins, 'activeSpan', {
get () {
const asyncId = asyncHooks.executionAsyncId()
Expand Down Expand Up @@ -63,6 +54,18 @@ module.exports = function (ins) {
}
})

shimmer.wrap(ins, 'stop', function (origStop) {
return function wrappedStop () {
asyncHook.disable()
activeTransactions.clear()
activeSpans.clear()
contexts = new WeakMap()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

shimmer.unwrap(ins, 'addEndedTransaction')
shimmer.unwrap(ins, 'stop')
return origStop.call(this)
}
})

asyncHook.enable()

function init (asyncId, type, triggerAsyncId, resource) {
Expand Down
19 changes: 19 additions & 0 deletions lib/instrumentation/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,25 @@ function Instrumentation (agent) {
}
}

// Stop active instrumentation and reset global state *as much as possible*.
//
// Limitations: Removing and re-applying 'require-in-the-middle'-based patches
// has no way to update existing references to patched or unpatched exports from
// those modules.
Instrumentation.prototype.stop = function () {
// Reset context tracking.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Appears to reset the appropriate properties.

this.currentTransaction = null
this.bindingSpan = null
this.activeSpan = null

// Reset patching.
this._started = false
if (this._hook) {
this._hook.unhook()
this._hook = null
}
}

Object.defineProperty(Instrumentation.prototype, 'ids', {
get () {
const current = this.currentSpan || this.currentTransaction
Expand Down
12 changes: 12 additions & 0 deletions test/_agent.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
'use strict'

// DEPRECATED: New tests should not use this wrapper. Instead using the
// real Agent directly, and its `agent.destroy()` method to clean up state
// and the end of tests. E.g.:
//
// const Agent = require('.../lib/agent')
// test('test name', t => {
// const agent = new Agent().start({ ... })
// ...
// agent.destroy()
// t.end()
// })

var Agent = require('../lib/agent')
var symbols = require('../lib/symbols')

Expand Down
93 changes: 0 additions & 93 deletions test/_apm_server.js

This file was deleted.

10 changes: 8 additions & 2 deletions test/_mock_apm_server.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
// // Test code using `serverUrl`...
// // - Events received on the intake API will be on `server.events`.
// // - Raw request data is on `server.requests`.
// // - Use `server.clear()` to clear `server.events` and `server.requests`
// // for re-use of the mock server in multiple test cases.
// // - Call `server.close()` when done.
// })

Expand All @@ -15,12 +17,16 @@ const zlib = require('zlib')

class MockAPMServer {
constructor () {
this.events = []
this.requests = []
this.clear()
this.serverUrl = null // set in .start()
this._http = http.createServer(this._onRequest.bind(this))
}

clear () {
this.events = []
this.requests = []
}

_onRequest (req, res) {
var parsedUrl = new URL(req.url, this.serverUrl)
var instream = req
Expand Down
Loading