Skip to content
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.

Commit 9188e17

Browse files
committedDec 13, 2017
refactor(*): use async_hooks for Node.js 8.1.0 and above
Fixes elastic#75
1 parent d832d51 commit 9188e17

File tree

10 files changed

+216
-2
lines changed

10 files changed

+216
-2
lines changed
 

‎.travis.yml

+8
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,14 @@ jobs:
3131

3232
include:
3333

34+
# Async Hooks build stage
35+
- stage: async-hooks
36+
node_js: '9'
37+
env: ELASTIC_APM_FF_ASYNC_HOOKS=1
38+
-
39+
node_js: '8'
40+
env: ELASTIC_APM_FF_ASYNC_HOOKS=1
41+
3442
# Coverage
3543
- stage: 'coverage'
3644
script:

‎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/agent.js

+1
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ Agent.prototype._config = function (opts) {
8080
this._ignoreUrlRegExp = opts.ignoreUrlRegExp
8181
this._ignoreUserAgentStr = opts.ignoreUserAgentStr
8282
this._ignoreUserAgentRegExp = opts.ignoreUserAgentRegExp
83+
this.ff_asyncHooks = opts.ff_asyncHooks
8384
this.ff_captureFrame = opts.ff_captureFrame
8485
this.sourceContext = opts.sourceContext
8586

‎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.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

+1
Original file line numberDiff line numberDiff line change
@@ -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

+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
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+
12+
test('setTimeout', function (t) {
13+
t.plan(2)
14+
twice(function () {
15+
var trans = agent.startTransaction()
16+
setTimeout(function () {
17+
t.equal(ins.currentTransaction, trans)
18+
}, 50)
19+
})
20+
})
21+
22+
test('setInterval', function (t) {
23+
t.plan(2)
24+
twice(function () {
25+
var trans = agent.startTransaction()
26+
var timer = setInterval(function () {
27+
clearInterval(timer)
28+
t.equal(ins.currentTransaction, trans)
29+
}, 50)
30+
})
31+
})
32+
33+
test('setImmediate', function (t) {
34+
t.plan(2)
35+
twice(function () {
36+
var trans = agent.startTransaction()
37+
setImmediate(function () {
38+
t.equal(ins.currentTransaction, trans)
39+
})
40+
})
41+
})
42+
43+
test('process.nextTick', function (t) {
44+
t.plan(2)
45+
twice(function () {
46+
var trans = agent.startTransaction()
47+
process.nextTick(function () {
48+
t.equal(ins.currentTransaction, trans)
49+
})
50+
})
51+
})
52+
53+
test('pre-resolved promise', function (t) {
54+
t.plan(4)
55+
56+
var p = Promise.resolve('success')
57+
58+
twice(function () {
59+
var trans = agent.startTransaction()
60+
p.then(function (result) {
61+
t.equal(result, 'success')
62+
t.equal(ins.currentTransaction, trans)
63+
})
64+
})
65+
})
66+
67+
test('resolve promise', function (t) {
68+
t.plan(4)
69+
twice(function () {
70+
var p = new Promise(function (resolve) {
71+
setTimeout(function () {
72+
resolve('success')
73+
}, 20)
74+
})
75+
var trans = agent.startTransaction()
76+
p.then(function (result) {
77+
t.equal(result, 'success')
78+
t.equal(ins.currentTransaction, trans)
79+
})
80+
})
81+
})
82+
83+
function twice (fn) {
84+
setImmediate(fn)
85+
setImmediate(fn)
86+
}
+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)
Please sign in to comment.