Skip to content

Commit b55231e

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

File tree

6 files changed

+171
-2
lines changed

6 files changed

+171
-2
lines changed

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/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 (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
@@ -102,6 +102,7 @@
102102
"pg": "^7.1.0",
103103
"redis": "^2.6.3",
104104
"restify": "^4.3.0",
105+
"send": "^0.16.1",
105106
"standard": "^10.0.2",
106107
"tape": "^4.8.0",
107108
"test-all-versions": "^3.1.1",

test/instrumentation/async-hooks.js

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
'use strict'
2+
3+
var agent = require('../..').start({
4+
appName: 'test',
5+
captureExceptions: false
6+
})
7+
var ins = agent._instrumentation
8+
9+
var test = require('tape')
10+
11+
test('setTimeout', function (t) {
12+
t.plan(2)
13+
twice(function () {
14+
var trans = agent.startTransaction()
15+
setTimeout(function () {
16+
t.equal(ins.currentTransaction, trans)
17+
}, 50)
18+
})
19+
})
20+
21+
test('setInterval', function (t) {
22+
t.plan(2)
23+
twice(function () {
24+
var trans = agent.startTransaction()
25+
var timer = setInterval(function () {
26+
clearInterval(timer)
27+
t.equal(ins.currentTransaction, trans)
28+
}, 50)
29+
})
30+
})
31+
32+
test('setImmediate', function (t) {
33+
t.plan(2)
34+
twice(function () {
35+
var trans = agent.startTransaction()
36+
setImmediate(function () {
37+
t.equal(ins.currentTransaction, trans)
38+
})
39+
})
40+
})
41+
42+
test('process.nextTick', function (t) {
43+
t.plan(2)
44+
twice(function () {
45+
var trans = agent.startTransaction()
46+
process.nextTick(function () {
47+
t.equal(ins.currentTransaction, trans)
48+
})
49+
})
50+
})
51+
52+
function twice (fn) {
53+
setImmediate(fn)
54+
setImmediate(fn)
55+
}
+70
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
'use strict'
2+
3+
var agent = require('../..').start({
4+
appName: 'test',
5+
captureExceptions: false
6+
})
7+
8+
var http = require('http')
9+
var send = require('send')
10+
var test = require('tape')
11+
12+
// run it 5 times in case of false positives due to race conditions
13+
times(5, function (n, done) {
14+
test('https://github.com/elastic/apm-agent-nodejs/issues/75 ' + n, function (t) {
15+
resetAgent(function (endpoint, headers, data, cb) {
16+
t.equal(data.transactions.length, 2, 'should create transactions')
17+
data.transactions.forEach(function (trans) {
18+
t.equal(trans.traces.length, 1, 'transaction should have one trace')
19+
t.equal(trans.traces[0].name, trans.id, 'trace should belong to transaction')
20+
})
21+
server.close()
22+
t.end()
23+
done()
24+
})
25+
26+
var server = http.createServer(function (req, res) {
27+
var trace = agent.buildTrace()
28+
trace.start(agent._instrumentation.currentTransaction.id)
29+
setTimeout(function () {
30+
trace.end()
31+
send(req, __filename).pipe(res)
32+
}, 50)
33+
})
34+
35+
var requestNo = 0
36+
37+
server.listen(function () {
38+
request()
39+
request()
40+
})
41+
42+
function request () {
43+
var port = server.address().port
44+
http.get('http://localhost:' + port, function (res) {
45+
res.on('end', function () {
46+
if (++requestNo === 2) {
47+
agent._instrumentation._queue._flush()
48+
}
49+
})
50+
res.resume()
51+
})
52+
}
53+
})
54+
})
55+
56+
function times (max, fn) {
57+
var n = 0
58+
run()
59+
function run () {
60+
if (++n > max) return
61+
fn(n, run)
62+
}
63+
}
64+
65+
function resetAgent (cb) {
66+
agent._instrumentation.currentTransaction = null
67+
agent._instrumentation._queue._clear()
68+
agent._httpClient = { request: cb || function () {} }
69+
agent.captureError = function (err) { throw err }
70+
}

0 commit comments

Comments
 (0)