Skip to content

Commit 1a3f87f

Browse files
authored
feat: Added instrumentation for Restify async handlers (#1910)
1 parent 7ed64bd commit 1a3f87f

File tree

7 files changed

+267
-46
lines changed

7 files changed

+267
-46
lines changed

lib/instrumentation/restify.js

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,12 +55,20 @@ module.exports = function initialize(agent, restify, moduleName, shim) {
5555
if (shim.isWrapped(middleware)) {
5656
return middleware
5757
}
58-
return shim.recordMiddleware(middleware, {
58+
const spec = {
5959
matchArity: true,
6060
route,
6161
req: shim.FIRST,
6262
next: shim.LAST
63-
})
63+
}
64+
65+
const wrappedMw = shim.recordMiddleware(middleware, spec)
66+
if (middleware.constructor.name === 'AsyncFunction') {
67+
return async function asyncShim() {
68+
return wrappedMw.apply(this, arguments)
69+
}
70+
}
71+
return wrappedMw
6472
}
6573
})
6674

@@ -70,11 +78,18 @@ module.exports = function initialize(agent, restify, moduleName, shim) {
7078
if (shim.isWrapped(middleware)) {
7179
return middleware
7280
}
73-
return shim.recordMiddleware(middleware, {
81+
const spec = {
7482
matchArity: true,
7583
req: shim.FIRST,
7684
next: shim.LAST
77-
})
85+
}
86+
const wrappedMw = shim.recordMiddleware(middleware, spec)
87+
if (middleware.constructor.name === 'AsyncFunction') {
88+
return async function asyncShim() {
89+
return wrappedMw.apply(this, arguments)
90+
}
91+
}
92+
return wrappedMw
7893
})
7994
}
8095
}

lib/util/logger.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -261,7 +261,7 @@ Logger.prototype.write = function write(level, args, extra) {
261261
data = stringify(entry) + '\n'
262262
} catch (err) {
263263
// eslint-disable-line no-unused-vars
264-
this.debug('Unabled to stringify log message')
264+
this.debug('Unable to stringify log message')
265265
}
266266

267267
if (this.reading) {
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Copyright 2020 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
8+
const tap = require('tap')
9+
10+
const helper = require('../../../lib/agent_helper')
11+
const { assertMetrics } = require('../../../lib/metrics_helper')
12+
const { runTest } = require('./common')
13+
14+
const simulateAsyncWork = async () => {
15+
const delay = Math.floor(Math.random() * 100)
16+
await new Promise((resolve) => setTimeout(resolve, delay))
17+
return delay
18+
}
19+
20+
tap.test('Restify with async handlers should work the same as with sync', (t) => {
21+
t.autoend()
22+
23+
let agent = null
24+
let restify = null
25+
let server = null
26+
27+
t.beforeEach(() => {
28+
agent = helper.instrumentMockedAgent()
29+
30+
restify = require('restify')
31+
server = restify.createServer()
32+
})
33+
34+
t.afterEach(() => {
35+
return new Promise((resolve) => {
36+
helper.unloadAgent(agent)
37+
if (server) {
38+
server.close(resolve)
39+
} else {
40+
resolve()
41+
}
42+
})
43+
})
44+
45+
/* very similar synchronous tests are in transaction-naming */
46+
47+
t.test('transaction name for single async route', (t) => {
48+
t.plan(1)
49+
50+
server.get('/path1', async (req, res) => {
51+
res.send()
52+
})
53+
54+
runTest({ agent, server, t, endpoint: '/path1', expectedName: 'GET//path1' })
55+
})
56+
57+
t.test('transaction name for async route with sync middleware', (t) => {
58+
t.plan(1)
59+
60+
server.use((req, res, next) => {
61+
next()
62+
})
63+
server.get('/path1', async (req, res) => {
64+
res.send()
65+
})
66+
67+
runTest({ agent, server, t, endpoint: '/path1', expectedName: 'GET//path1' })
68+
})
69+
70+
t.test('transaction name for async route with async middleware', (t) => {
71+
t.plan(1)
72+
73+
server.use(async (req) => {
74+
req.test = await simulateAsyncWork()
75+
})
76+
server.get('/path1', async (req, res) => {
77+
res.send()
78+
})
79+
80+
runTest({ agent, server, t, endpoint: '/path1', expectedName: 'GET//path1' })
81+
})
82+
83+
t.test('transaction name for async route with multiple async middleware', (t) => {
84+
t.plan(4)
85+
86+
server.use(async (req) => {
87+
t.pass('should enter first `use` middleware')
88+
req.test = await simulateAsyncWork()
89+
})
90+
// eslint-disable-next-line no-unused-vars
91+
server.use(async (req) => {
92+
t.pass('should enter second `use` middleware')
93+
req.test2 = await simulateAsyncWork()
94+
})
95+
server.get('/path1', async (req, res) => {
96+
t.pass('should enter route handler')
97+
res.send()
98+
})
99+
100+
runTest({ agent, server, t, endpoint: '/path1', expectedName: 'GET//path1' })
101+
})
102+
})
103+
104+
tap.test('Restify metrics for async handlers', (t) => {
105+
t.autoend()
106+
107+
let agent = null
108+
let restify = null
109+
t.beforeEach(() => {
110+
agent = helper.instrumentMockedAgent()
111+
112+
restify = require('restify')
113+
})
114+
115+
t.afterEach(() => {
116+
helper.unloadAgent(agent)
117+
})
118+
119+
t.test('should generate middleware metrics for async handlers', (t) => {
120+
// Metrics for this transaction with the right name.
121+
const expectedMiddlewareMetrics = [
122+
[{ name: 'WebTransaction/Restify/GET//foo/:bar' }],
123+
[{ name: 'WebTransactionTotalTime/Restify/GET//foo/:bar' }],
124+
[{ name: 'Apdex/Restify/GET//foo/:bar' }],
125+
126+
// Unscoped middleware metrics.
127+
[{ name: 'Nodejs/Middleware/Restify/middleware//' }],
128+
[{ name: 'Nodejs/Middleware/Restify/middleware2//' }],
129+
[{ name: 'Nodejs/Middleware/Restify/handler//foo/:bar' }],
130+
131+
// Scoped middleware metrics.
132+
[
133+
{
134+
name: 'Nodejs/Middleware/Restify/middleware//',
135+
scope: 'WebTransaction/Restify/GET//foo/:bar'
136+
}
137+
],
138+
[
139+
{
140+
name: 'Nodejs/Middleware/Restify/middleware2//',
141+
scope: 'WebTransaction/Restify/GET//foo/:bar'
142+
}
143+
],
144+
[
145+
{
146+
name: 'Nodejs/Middleware/Restify/handler//foo/:bar',
147+
scope: 'WebTransaction/Restify/GET//foo/:bar'
148+
}
149+
]
150+
]
151+
152+
const server = restify.createServer()
153+
t.teardown(() => server.close())
154+
155+
server.use(async function middleware() {
156+
t.ok(agent.getTransaction(), 'should be in transaction context')
157+
})
158+
159+
server.use(async function middleware2() {
160+
t.ok(agent.getTransaction(), 'should be in transaction context')
161+
})
162+
163+
server.get('/foo/:bar', async function handler(req, res) {
164+
t.ok(agent.getTransaction(), 'should be in transaction context')
165+
res.send({ message: 'done' })
166+
})
167+
168+
server.listen(0, function () {
169+
const port = server.address().port
170+
const url = `http://localhost:${port}/foo/bar`
171+
172+
helper.makeGetRequest(url, function (error) {
173+
t.error(error)
174+
175+
assertMetrics(agent.metrics, expectedMiddlewareMetrics, false, false)
176+
t.end()
177+
})
178+
})
179+
})
180+
})

test/versioned/restify/restify-post-7/capture-params.tap.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ test('Restify capture params introspection', function (t) {
6161
port = server.address().port
6262
helper.makeGetRequest('http://localhost:' + port + '/test', function (error, res, body) {
6363
t.equal(res.statusCode, 200, 'nothing exploded')
64-
t.same(body, { status: 'ok' }, 'got expected respose')
64+
t.same(body, { status: 'ok' }, 'got expected response')
6565
t.end()
6666
})
6767
})
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/*
2+
* Copyright 2023 New Relic Corporation. All rights reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
6+
'use strict'
7+
const common = module.exports
8+
const helper = require('../../../lib/agent_helper')
9+
10+
/**
11+
* @param {object} cfg
12+
* @property {object} cfg.t
13+
* @property {string} cfg.endpoint
14+
* @property {string} [cfg.prefix='Restify']
15+
* @property {string} cfg.expectedName
16+
* @property {Function} [cfg.cb=t.end]
17+
* @property {object} [cfg.requestOpts=null]
18+
* @property {object} cfg.agent
19+
* @property {object} cfg.server
20+
*/
21+
common.runTest = function runTest(cfg) {
22+
const { t, endpoint, agent, prefix = 'Restify', requestOpts = null, server } = cfg
23+
let { expectedName } = cfg
24+
expectedName = `WebTransaction/${prefix}/${expectedName}`
25+
26+
agent.on('transactionFinished', (tx) => {
27+
t.equal(tx.name, expectedName, 'should have correct name')
28+
t.end()
29+
})
30+
31+
server.listen(() => {
32+
const port = server.address().port
33+
helper.makeGetRequest(`http://localhost:${port}${endpoint}`, requestOpts)
34+
})
35+
}

test/versioned/restify/restify-post-7/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"restify-errors": "6.1"
3333
},
3434
"files": [
35+
"async-handlers.tap.js",
3536
"capture-params.tap.js",
3637
"ignoring.tap.js",
3738
"restify.tap.js",

0 commit comments

Comments
 (0)