Skip to content

Commit 5ec35f3

Browse files
committed
refactor(*): use async_hooks for Node.js 8.1.0 and above
Fixes elastic#75
1 parent eee12ef commit 5ec35f3

File tree

9 files changed

+250
-3
lines changed

9 files changed

+250
-3
lines changed

.travis.yml

+8
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,14 @@ jobs:
3535
# TEST STAGE #
3636
###########################################
3737

38+
# Async Hooks
39+
-
40+
node_js: '9'
41+
env: ELASTIC_APM_FF_ASYNC_HOOKS=1
42+
-
43+
node_js: '8'
44+
env: ELASTIC_APM_FF_ASYNC_HOOKS=1
45+
3846
# Docs
3947
-
4048
script: npm run docs

docs/compatibility.asciidoc

+2-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ Previous versions of Node.js are not supported.
77
Some versions of Node.js contain bugs or issues that limit our ability to instrument them correctly.
88
The following versions of Node.js are known to not be fully instrumented:
99

10-
- v7.10.x - Recommended solution: Upgrade to v8.0.0 or higher to get full support
10+
- v7.10.x - Recommended solution: Upgrade to v8.1.0 or higher to get full support
11+
- v8.0.0 - Recommended solution: Upgrade to v8.1.0 or higher to get full support
1112

1213
[float]
1314
[[compatibility-frameworks]]

lib/config.js

+3
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ var DEFAULTS = {
3434
errorOnAbortedRequests: false,
3535
abortedErrorThreshold: 25000,
3636
instrument: true,
37+
ff_asyncHooks: false,
3738
ff_captureFrame: false,
3839
sourceContext: true
3940
}
@@ -57,6 +58,7 @@ var ENV_TABLE = {
5758
instrument: 'ELASTIC_APM_INSTRUMENT',
5859
flushInterval: 'ELASTIC_APM_FLUSH_INTERVAL',
5960
maxQueueSize: 'ELASTIC_APM_MAX_QUEUE_SIZE',
61+
ff_asyncHooks: 'ELASTIC_APM_FF_ASYNC_HOOKS',
6062
ff_captureFrame: 'ELASTIC_APM_FF_CAPTURE_FRAME',
6163
sourceContext: 'ELASTIC_APM_SOURCE_CONTEXT'
6264
}
@@ -70,6 +72,7 @@ var BOOL_OPTS = [
7072
'logBody',
7173
'errorOnAbortedRequests',
7274
'instrument',
75+
'ff_asyncHooks',
7376
'ff_captureFrame',
7477
'sourceContext'
7578
]

lib/instrumentation/async-hooks.js

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
'use strict'
2+
3+
const asyncHooks = require('async_hooks')
4+
5+
module.exports = function (ins) {
6+
const asyncHook = asyncHooks.createHook({init, before, after, destroy})
7+
const initState = new Map()
8+
const beforeState = new Map()
9+
10+
asyncHook.enable()
11+
12+
function init (asyncId, type, triggerAsyncId, resource) {
13+
// We don't care about the TIMERWRAP, as it will only init once for each
14+
// timer that shares the timeout value. Instead we rely on the Timeout
15+
// type, which will init for each scheduled timer.
16+
if (type === 'TIMERWRAP') return
17+
18+
initState.set(asyncId, ins.currentTransaction)
19+
}
20+
21+
function before (asyncId) {
22+
if (!initState.has(asyncId)) return // in case type === TIMERWRAP
23+
beforeState.set(asyncId, ins.currentTransaction)
24+
ins.currentTransaction = initState.get(asyncId)
25+
}
26+
27+
function after (asyncId) {
28+
if (!initState.has(asyncId)) return // in case type === TIMERWRAP
29+
ins.currentTransaction = beforeState.get(asyncId)
30+
}
31+
32+
function destroy (asyncId) {
33+
if (!initState.has(asyncId)) return // in case type === TIMERWRAP
34+
initState.delete(asyncId)
35+
beforeState.delete(asyncId)
36+
}
37+
}

lib/instrumentation/index.js

+6-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
var fs = require('fs')
44
var path = require('path')
5+
var semver = require('semver')
56
var hook = require('require-in-the-middle')
67
var Transaction = require('./transaction')
78
var Queue = require('./queue')
@@ -40,7 +41,11 @@ Instrumentation.prototype.start = function () {
4041
})
4142
})
4243

43-
require('./patch-async')(this)
44+
if (this._agent._conf.ff_asyncHooks && semver.gte(process.version, '8.1.0')) {
45+
require('./async-hooks')(this)
46+
} else {
47+
require('./patch-async')(this)
48+
}
4449

4550
debug('shimming Module._load function')
4651
hook(MODULES, function (exports, name, basedir) {

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
"test": "npm run check && node test/test.js",
1818
"posttest": "test/posttest.sh",
1919
"test-cli": "node test/script/cli.js",
20-
"test-deps": "dependency-check . && dependency-check . --unused --no-dev --entry lib/instrumentation/modules/*"
20+
"test-deps": "dependency-check . -i async_hooks && dependency-check . --unused --no-dev --entry lib/instrumentation/modules/*"
2121
},
2222
"directories": {
2323
"test": "test"
@@ -112,6 +112,7 @@
112112
"pg": "^7.1.0",
113113
"redis": "^2.6.3",
114114
"restify": "^4.3.0",
115+
"send": "^0.16.1",
115116
"standard": "^10.0.2",
116117
"tape": "^4.8.0",
117118
"test-all-versions": "^3.1.1",

test/agent.js

+1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ var optionFixtures = [
2626
['stackTraceLimit', 'STACK_TRACE_LIMIT', Infinity],
2727
['captureExceptions', 'CAPTURE_EXCEPTIONS', true],
2828
['instrument', 'INSTRUMENT', true],
29+
['ff_asyncHooks', 'FF_ASYNC_HOOKS', false],
2930
['ff_captureFrame', 'FF_CAPTURE_FRAME', false]
3031
]
3132

test/instrumentation/async-hooks.js

+120
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
'use strict'
2+
3+
var agent = require('../..').start({
4+
appName: 'test',
5+
captureExceptions: false,
6+
ff_asyncHooks: true
7+
})
8+
var ins = agent._instrumentation
9+
10+
var test = require('tape')
11+
var semver = require('semver')
12+
13+
test('setTimeout', function (t) {
14+
t.plan(2)
15+
twice(function () {
16+
var trans = agent.startTransaction()
17+
setTimeout(function () {
18+
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id)
19+
}, 50)
20+
})
21+
})
22+
23+
test('setInterval', function (t) {
24+
t.plan(2)
25+
twice(function () {
26+
var trans = agent.startTransaction()
27+
var timer = setInterval(function () {
28+
clearInterval(timer)
29+
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id)
30+
}, 50)
31+
})
32+
})
33+
34+
test('setImmediate', function (t) {
35+
t.plan(2)
36+
twice(function () {
37+
var trans = agent.startTransaction()
38+
setImmediate(function () {
39+
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id)
40+
})
41+
})
42+
})
43+
44+
test('process.nextTick', function (t) {
45+
t.plan(2)
46+
twice(function () {
47+
var trans = agent.startTransaction()
48+
process.nextTick(function () {
49+
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id)
50+
})
51+
})
52+
})
53+
54+
// We can't instrument ore-defined promises properly without async-hooks, so
55+
// lets not run these tests on versions of Node.js without async-hooks
56+
if (semver.gte(process.version, '8.1.0')) {
57+
test('pre-defined, pre-resolved shared promise', function (t) {
58+
t.plan(4)
59+
60+
var p = Promise.resolve('success')
61+
62+
twice(function () {
63+
var trans = agent.startTransaction()
64+
p.then(function (result) {
65+
t.equal(result, 'success')
66+
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id)
67+
})
68+
})
69+
})
70+
71+
test('pre-defined, pre-resolved non-shared promise', function (t) {
72+
t.plan(4)
73+
74+
twice(function () {
75+
var p = Promise.resolve('success')
76+
var trans = agent.startTransaction()
77+
p.then(function (result) {
78+
t.equal(result, 'success')
79+
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id)
80+
})
81+
})
82+
})
83+
84+
test('pre-defined, post-resolved promise', function (t) {
85+
t.plan(4)
86+
twice(function () {
87+
var p = new Promise(function (resolve) {
88+
setTimeout(function () {
89+
resolve('success')
90+
}, 20)
91+
})
92+
var trans = agent.startTransaction()
93+
p.then(function (result) {
94+
t.equal(result, 'success')
95+
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id)
96+
})
97+
})
98+
})
99+
}
100+
101+
test('post-defined, post-resolved promise', function (t) {
102+
t.plan(4)
103+
twice(function () {
104+
var trans = agent.startTransaction()
105+
var p = new Promise(function (resolve) {
106+
setTimeout(function () {
107+
resolve('success')
108+
}, 20)
109+
})
110+
p.then(function (result) {
111+
t.equal(result, 'success')
112+
t.equal(ins.currentTransaction && ins.currentTransaction.id, trans.id)
113+
})
114+
})
115+
})
116+
117+
function twice (fn) {
118+
setImmediate(fn)
119+
setImmediate(fn)
120+
}
+71
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
'use strict'
2+
3+
var agent = require('../..').start({
4+
appName: 'test',
5+
captureExceptions: false,
6+
ff_asyncHooks: true
7+
})
8+
9+
var http = require('http')
10+
var send = require('send')
11+
var test = require('tape')
12+
13+
// run it 5 times in case of false positives due to race conditions
14+
times(5, function (n, done) {
15+
test('https://github.com/elastic/apm-agent-nodejs/issues/75 ' + n, function (t) {
16+
resetAgent(function (endpoint, headers, data, cb) {
17+
t.equal(data.transactions.length, 2, 'should create transactions')
18+
data.transactions.forEach(function (trans) {
19+
t.equal(trans.traces.length, 1, 'transaction should have one trace')
20+
t.equal(trans.traces[0].name, trans.id, 'trace should belong to transaction')
21+
})
22+
server.close()
23+
t.end()
24+
done()
25+
})
26+
27+
var server = http.createServer(function (req, res) {
28+
var trace = agent.buildTrace()
29+
trace.start(agent._instrumentation.currentTransaction.id)
30+
setTimeout(function () {
31+
trace.end()
32+
send(req, __filename).pipe(res)
33+
}, 50)
34+
})
35+
36+
var requestNo = 0
37+
38+
server.listen(function () {
39+
request()
40+
request()
41+
})
42+
43+
function request () {
44+
var port = server.address().port
45+
http.get('http://localhost:' + port, function (res) {
46+
res.on('end', function () {
47+
if (++requestNo === 2) {
48+
agent._instrumentation._queue._flush()
49+
}
50+
})
51+
res.resume()
52+
})
53+
}
54+
})
55+
})
56+
57+
function times (max, fn) {
58+
var n = 0
59+
run()
60+
function run () {
61+
if (++n > max) return
62+
fn(n, run)
63+
}
64+
}
65+
66+
function resetAgent (cb) {
67+
agent._instrumentation.currentTransaction = null
68+
agent._instrumentation._queue._clear()
69+
agent._httpClient = { request: cb || function () {} }
70+
agent.captureError = function (err) { throw err }
71+
}

0 commit comments

Comments
 (0)