Skip to content

Commit 986b6a4

Browse files
authored
test: refactor to move knowledge for in-process Agent re-use into Agent methods (#2305)
Move away from using the test/_agent.js wrapper in tests, in favour of using the real Agent class directly and agent.destroy() at the end of a test to clean up. Agent#destroy() has been improved and Instrumentation#stop() added to properly handle agent clean-up (within reason) such that tests can serially create multiple instances of the Agent.
1 parent ed7cd28 commit 986b6a4

File tree

9 files changed

+1381
-1131
lines changed

9 files changed

+1381
-1131
lines changed

lib/agent.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,46 @@ Object.defineProperty(Agent.prototype, 'currentTraceIds', {
7171
}
7272
})
7373

74+
// Destroy this agent. This prevents any new agent processing, communication
75+
// with APM server, and resets changed global state *as much as is possible*.
76+
//
77+
// In the typical uses case -- a singleton Agent running for the full process
78+
// lifetime -- it is *not* necessary to call `agent.destroy()`. It is used
79+
// for some testing.
80+
//
81+
// Limitations:
82+
// - Patching/wrapping of functions for instrumentation *is* undone, but
83+
// references to the wrapped versions can remain.
84+
// - There may be in-flight tasks (in ins.addEndedSpan() and
85+
// agent.captureError() for example) that will complete after this destroy
86+
// completes. They should have no impact other than CPU/resource use.
7487
Agent.prototype.destroy = function () {
75-
if (this._transport) this._transport.destroy()
88+
if (this._transport && this._transport.destroy) {
89+
this._transport.destroy()
90+
}
91+
// So in-flight tasks in ins.addEndedSpan() and agent.captureError() do
92+
// not use the destroyed transport.
93+
this._transport = null
94+
95+
// So in-flight tasks do not call user-added filters after the agent has
96+
// been destroyed.
97+
this._errorFilters = new Filters()
98+
this._transactionFilters = new Filters()
99+
this._spanFilters = new Filters()
100+
101+
if (this._uncaughtExceptionListener) {
102+
process.removeListener('uncaughtException', this._uncaughtExceptionListener)
103+
}
104+
this._metrics.stop()
105+
this._instrumentation.stop()
106+
107+
// Allow a new Agent instance to `.start()`. Typically this is only relevant
108+
// for tests that may use multiple Agent instances in a single test process.
109+
global[symbols.agentInitialized] = null
110+
111+
if (Error.stackTraceLimit === this._conf.stackTraceLimit) {
112+
Error.stackTraceLimit = this._origStackTraceLimit
113+
}
76114
}
77115

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

256+
this._origStackTraceLimit = Error.stackTraceLimit
218257
Error.stackTraceLimit = this._conf.stackTraceLimit
219258
if (this._conf.captureExceptions) this.handleUncaughtExceptions()
220259

lib/config.js

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,6 @@ if (packageName === 'elastic-apm-node') {
2222
}
2323
var userAgent = `${packageName}/${version}`
2424

25-
config.INTAKE_STRING_MAX_SIZE = 1024
26-
config.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER = 'never'
27-
config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES = 'messages'
28-
config.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = 'always'
29-
30-
module.exports = config
31-
3225
let confFile = loadConfigFile()
3326

3427
let serviceName, serviceVersion
@@ -38,6 +31,11 @@ try {
3831
serviceVersion = version
3932
} catch (err) {}
4033

34+
const INTAKE_STRING_MAX_SIZE = 1024
35+
const CAPTURE_ERROR_LOG_STACK_TRACES_NEVER = 'never'
36+
const CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES = 'messages'
37+
const CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = 'always'
38+
4139
var DEFAULTS = {
4240
abortedErrorThreshold: '25s',
4341
active: true,
@@ -47,7 +45,7 @@ var DEFAULTS = {
4745
asyncHooks: true,
4846
breakdownMetrics: true,
4947
captureBody: 'off',
50-
captureErrorLogStackTraces: config.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES,
48+
captureErrorLogStackTraces: CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES,
5149
captureExceptions: true,
5250
captureHeaders: true,
5351
captureSpanStackTraces: true,
@@ -562,8 +560,8 @@ function normalizeBools (opts, logger) {
562560
}
563561

564562
function truncateOptions (opts) {
565-
if (opts.serviceVersion) opts.serviceVersion = truncate(String(opts.serviceVersion), config.INTAKE_STRING_MAX_SIZE)
566-
if (opts.hostname) opts.hostname = truncate(String(opts.hostname), config.INTAKE_STRING_MAX_SIZE)
563+
if (opts.serviceVersion) opts.serviceVersion = truncate(String(opts.serviceVersion), INTAKE_STRING_MAX_SIZE)
564+
if (opts.hostname) opts.hostname = truncate(String(opts.hostname), INTAKE_STRING_MAX_SIZE)
567565
}
568566

569567
function bytes (input) {
@@ -685,7 +683,7 @@ function getBaseClientConfig (conf, agent) {
685683
environment: conf.environment,
686684

687685
// Sanitize conf
688-
truncateKeywordsAt: config.INTAKE_STRING_MAX_SIZE,
686+
truncateKeywordsAt: INTAKE_STRING_MAX_SIZE,
689687
truncateErrorMessagesAt: conf.errorMessageMaxLength,
690688

691689
// HTTP conf
@@ -720,3 +718,11 @@ function getBaseClientConfig (conf, agent) {
720718
cloudMetadataFetcher: (new CloudMetadata(cloudProvider, conf.logger, conf.serviceName))
721719
}
722720
}
721+
722+
// Exports.
723+
module.exports = config
724+
module.exports.INTAKE_STRING_MAX_SIZE = INTAKE_STRING_MAX_SIZE
725+
module.exports.CAPTURE_ERROR_LOG_STACK_TRACES_NEVER = CAPTURE_ERROR_LOG_STACK_TRACES_NEVER
726+
module.exports.CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES = CAPTURE_ERROR_LOG_STACK_TRACES_MESSAGES
727+
module.exports.CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS = CAPTURE_ERROR_LOG_STACK_TRACES_ALWAYS
728+
module.exports.DEFAULTS = DEFAULTS

lib/instrumentation/async-hooks.js

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,12 @@
33
const asyncHooks = require('async_hooks')
44
const shimmer = require('./shimmer')
55

6-
// FOR INTERNAL TESTING PURPOSES ONLY!
7-
const resettable = process.env._ELASTIC_APM_ASYNC_HOOKS_RESETTABLE === 'true'
8-
let _asyncHook
9-
106
module.exports = function (ins) {
117
const asyncHook = asyncHooks.createHook({ init, before, destroy })
12-
const contexts = new WeakMap()
13-
14-
if (resettable) {
15-
if (_asyncHook) _asyncHook.disable()
16-
_asyncHook = asyncHook
17-
}
18-
8+
const activeSpans = new Map()
199
const activeTransactions = new Map()
10+
let contexts = new WeakMap()
11+
2012
Object.defineProperty(ins, 'currentTransaction', {
2113
get () {
2214
const asyncId = asyncHooks.executionAsyncId()
@@ -32,7 +24,6 @@ module.exports = function (ins) {
3224
}
3325
})
3426

35-
const activeSpans = new Map()
3627
Object.defineProperty(ins, 'activeSpan', {
3728
get () {
3829
const asyncId = asyncHooks.executionAsyncId()
@@ -63,6 +54,18 @@ module.exports = function (ins) {
6354
}
6455
})
6556

57+
shimmer.wrap(ins, 'stop', function (origStop) {
58+
return function wrappedStop () {
59+
asyncHook.disable()
60+
activeTransactions.clear()
61+
activeSpans.clear()
62+
contexts = new WeakMap()
63+
shimmer.unwrap(ins, 'addEndedTransaction')
64+
shimmer.unwrap(ins, 'stop')
65+
return origStop.call(this)
66+
}
67+
})
68+
6669
asyncHook.enable()
6770

6871
function init (asyncId, type, triggerAsyncId, resource) {

lib/instrumentation/index.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,25 @@ function Instrumentation (agent) {
8686
}
8787
}
8888

89+
// Stop active instrumentation and reset global state *as much as possible*.
90+
//
91+
// Limitations: Removing and re-applying 'require-in-the-middle'-based patches
92+
// has no way to update existing references to patched or unpatched exports from
93+
// those modules.
94+
Instrumentation.prototype.stop = function () {
95+
// Reset context tracking.
96+
this.currentTransaction = null
97+
this.bindingSpan = null
98+
this.activeSpan = null
99+
100+
// Reset patching.
101+
this._started = false
102+
if (this._hook) {
103+
this._hook.unhook()
104+
this._hook = null
105+
}
106+
}
107+
89108
Object.defineProperty(Instrumentation.prototype, 'ids', {
90109
get () {
91110
const current = this.currentSpan || this.currentTransaction

test/_agent.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,17 @@
11
'use strict'
22

3+
// DEPRECATED: New tests should not use this wrapper. Instead using the
4+
// real Agent directly, and its `agent.destroy()` method to clean up state
5+
// and the end of tests. E.g.:
6+
//
7+
// const Agent = require('.../lib/agent')
8+
// test('test name', t => {
9+
// const agent = new Agent().start({ ... })
10+
// ...
11+
// agent.destroy()
12+
// t.end()
13+
// })
14+
315
var Agent = require('../lib/agent')
416
var symbols = require('../lib/symbols')
517

test/_apm_server.js

Lines changed: 0 additions & 93 deletions
This file was deleted.

test/_mock_apm_server.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
// // Test code using `serverUrl`...
77
// // - Events received on the intake API will be on `server.events`.
88
// // - Raw request data is on `server.requests`.
9+
// // - Use `server.clear()` to clear `server.events` and `server.requests`
10+
// // for re-use of the mock server in multiple test cases.
911
// // - Call `server.close()` when done.
1012
// })
1113

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

1618
class MockAPMServer {
1719
constructor () {
18-
this.events = []
19-
this.requests = []
20+
this.clear()
2021
this.serverUrl = null // set in .start()
2122
this._http = http.createServer(this._onRequest.bind(this))
2223
}
2324

25+
clear () {
26+
this.events = []
27+
this.requests = []
28+
}
29+
2430
_onRequest (req, res) {
2531
var parsedUrl = new URL(req.url, this.serverUrl)
2632
var instream = req

0 commit comments

Comments
 (0)