Skip to content

Commit fb90c01

Browse files
BridgeARlonglho
andauthored
feat: add middleware enter/exit/finish instrumentations to hono (#7198)
Co-authored-by: Long Ho <longlho@users.noreply.github.com>
1 parent b642331 commit fb90c01

2 files changed

Lines changed: 197 additions & 10 deletions

File tree

packages/datadog-instrumentations/src/hono.js

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ const {
99
const routeChannel = channel('apm:hono:request:route')
1010
const handleChannel = channel('apm:hono:request:handle')
1111
const errorChannel = channel('apm:hono:request:error')
12+
const nextChannel = channel('apm:hono:middleware:next')
13+
const enterChannel = channel('apm:hono:middleware:enter')
14+
const exitChannel = channel('apm:hono:middleware:exit')
15+
const finishChannel = channel('apm:hono:middleware:finish')
1216

1317
function wrapFetch (fetch) {
1418
return function (request, env, executionCtx) {
@@ -34,21 +38,62 @@ function wrapCompose (compose) {
3438

3539
const instrumentedMiddlewares = middlewares.map(h => {
3640
const [[fn, meta], params] = h
37-
38-
const instrumentedFn = (...args) => {
39-
const context = args[0]
40-
routeChannel.publish({
41-
req: context.env.incoming,
42-
route: meta?.path
43-
})
44-
return fn(...args)
45-
}
46-
return [[instrumentedFn, meta], params]
41+
return [[wrapMiddleware(fn, meta?.path), meta], params]
4742
})
4843
return compose.call(this, instrumentedMiddlewares, instrumentedOnError, onNotFound)
4944
}
5045
}
5146

47+
function wrapNext (req, route, next) {
48+
return shimmer.wrapFunction(
49+
next,
50+
(next) =>
51+
function () {
52+
nextChannel.publish({ req, route })
53+
54+
return next.apply(this, arguments)
55+
}
56+
)
57+
}
58+
59+
function wrapMiddleware (middleware, route) {
60+
const name = middleware.name
61+
return shimmer.wrapFunction(
62+
middleware,
63+
(middleware) =>
64+
function (context, next) {
65+
const req = context.env.incoming
66+
routeChannel.publish({ req, route })
67+
enterChannel.publish({ req, name, route })
68+
if (typeof next === 'function') {
69+
arguments[1] = wrapNext(req, route, next)
70+
}
71+
try {
72+
const result = middleware.apply(this, arguments)
73+
if (result && typeof result.then === 'function') {
74+
return result.then(
75+
(result) => {
76+
finishChannel.publish({ req })
77+
return result
78+
},
79+
(error) => {
80+
errorChannel.publish({ req, error })
81+
throw error
82+
}
83+
)
84+
}
85+
finishChannel.publish({ req })
86+
return result
87+
} catch (error) {
88+
errorChannel.publish({ req, error })
89+
throw error
90+
} finally {
91+
exitChannel.publish({ req, route })
92+
}
93+
}
94+
)
95+
}
96+
5297
addHook({
5398
name: 'hono',
5499
versions: ['>=4'],
Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
'use strict'
2+
3+
const assert = require('assert/strict')
4+
5+
const axios = require('axios')
6+
const dc = require('dc-polyfill')
7+
const { describe, it, beforeEach, before, after } = require('mocha')
8+
const sinon = require('sinon')
9+
10+
const agent = require('../../dd-trace/test/plugins/agent')
11+
const { withVersions } = require('../../dd-trace/test/setup/mocha')
12+
13+
withVersions('hono', 'hono', version => {
14+
describe('hono instrumentation', () => {
15+
let routeChannelCb, handleChannelCb, errorChannelCb, nextChannelCb
16+
let enterChannelCb, exitChannelCb, finishChannelCb
17+
let port, server, middlewareCalled
18+
19+
const routeChannel = dc.channel('apm:hono:request:route')
20+
const handleChannel = dc.channel('apm:hono:request:handle')
21+
const errorChannel = dc.channel('apm:hono:request:error')
22+
const nextChannel = dc.channel('apm:hono:middleware:next')
23+
const enterChannel = dc.channel('apm:hono:middleware:enter')
24+
const exitChannel = dc.channel('apm:hono:middleware:exit')
25+
const finishChannel = dc.channel('apm:hono:middleware:finish')
26+
27+
before(() => {
28+
return agent.load('hono', { client: false })
29+
})
30+
31+
before((done) => {
32+
const { Hono } = require(`../../../versions/hono@${version}`).get()
33+
const { serve } = require('../../../versions/@hono/node-server').get()
34+
const app = new Hono()
35+
36+
// Add a middleware
37+
app.use(async function named (_c, next) {
38+
middlewareCalled()
39+
await next()
40+
})
41+
42+
// Add a route
43+
app.get('/test', (c) => {
44+
return c.text('OK')
45+
})
46+
47+
// Add an error route
48+
app.get('/error', (_c) => {
49+
throw new Error('test error')
50+
})
51+
52+
server = serve({ port: 0, fetch: app.fetch }, (info) => {
53+
port = info.port
54+
done()
55+
})
56+
})
57+
58+
beforeEach(() => {
59+
routeChannelCb = sinon.stub()
60+
handleChannelCb = sinon.stub()
61+
errorChannelCb = sinon.stub()
62+
nextChannelCb = sinon.stub()
63+
enterChannelCb = sinon.stub()
64+
exitChannelCb = sinon.stub()
65+
finishChannelCb = sinon.stub()
66+
middlewareCalled = sinon.stub()
67+
68+
routeChannel.subscribe(routeChannelCb)
69+
handleChannel.subscribe(handleChannelCb)
70+
errorChannel.subscribe(errorChannelCb)
71+
nextChannel.subscribe(nextChannelCb)
72+
enterChannel.subscribe(enterChannelCb)
73+
exitChannel.subscribe(exitChannelCb)
74+
finishChannel.subscribe(finishChannelCb)
75+
})
76+
77+
afterEach(() => {
78+
routeChannel.unsubscribe(routeChannelCb)
79+
handleChannel.unsubscribe(handleChannelCb)
80+
errorChannel.unsubscribe(errorChannelCb)
81+
nextChannel.unsubscribe(nextChannelCb)
82+
enterChannel.unsubscribe(enterChannelCb)
83+
exitChannel.unsubscribe(exitChannelCb)
84+
finishChannel.unsubscribe(finishChannelCb)
85+
})
86+
87+
after(() => {
88+
server.close()
89+
return agent.close({ ritmReset: false })
90+
})
91+
92+
it('should publish to handleChannel on request', async () => {
93+
const res = await axios.get(`http://localhost:${port}/test`)
94+
95+
assert.strictEqual(res.data, 'OK')
96+
sinon.assert.called(handleChannelCb)
97+
})
98+
99+
it('should publish to middleware channels', async () => {
100+
const res = await axios.get(`http://localhost:${port}/test`)
101+
102+
sinon.assert.called(routeChannelCb)
103+
sinon.assert.calledOnce(middlewareCalled)
104+
105+
assert.strictEqual(res.data, 'OK')
106+
sinon.assert.called(enterChannelCb)
107+
let callArgs = enterChannelCb.firstCall.args[0]
108+
assert.deepStrictEqual(Object.keys(callArgs), ['req', 'name', 'route'])
109+
assert.strictEqual(callArgs.req.url, '/test')
110+
assert.strictEqual(callArgs.name, 'named')
111+
112+
sinon.assert.called(nextChannelCb)
113+
callArgs = nextChannelCb.firstCall.args[0]
114+
assert.deepStrictEqual(Object.keys(callArgs), ['req', 'route'])
115+
assert.strictEqual(callArgs.req.url, '/test')
116+
117+
sinon.assert.called(exitChannelCb)
118+
callArgs = exitChannelCb.firstCall.args[0]
119+
assert.deepStrictEqual(Object.keys(callArgs), ['req', 'route'])
120+
assert.strictEqual(callArgs.req.url, '/test')
121+
122+
sinon.assert.called(finishChannelCb)
123+
callArgs = finishChannelCb.firstCall.args[0]
124+
assert.deepStrictEqual(Object.keys(callArgs), ['req'])
125+
assert.strictEqual(callArgs.req.url, '/test')
126+
})
127+
128+
it('should publish to errorChannel when middleware throws', async () => {
129+
try {
130+
await axios.get(`http://localhost:${port}/error`)
131+
} catch (e) {
132+
// Expected to fail
133+
}
134+
135+
sinon.assert.called(errorChannelCb)
136+
const callArgs = errorChannelCb.firstCall.args[0]
137+
assert.deepStrictEqual(Object.keys(callArgs), ['req', 'error'])
138+
assert.strictEqual(callArgs.req.url, '/error')
139+
assert.strictEqual(callArgs.error.message, 'test error')
140+
})
141+
})
142+
})

0 commit comments

Comments
 (0)