Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hono Plugin #4812

Draft
wants to merge 3 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ interface Plugins {
"graphql": tracer.plugins.graphql;
"grpc": tracer.plugins.grpc;
"hapi": tracer.plugins.hapi;
"hono": tracer.plugins.hono;
"http": tracer.plugins.http;
"http2": tracer.plugins.http2;
"ioredis": tracer.plugins.ioredis;
Expand Down Expand Up @@ -1473,6 +1474,12 @@ declare namespace tracer {
*/
interface hapi extends HttpServer {}

/**
* This plugin automatically instruments the
* [hono](https://hono.dev/) module.
*/
interface hono extends HttpServer {}

/**
* This plugin automatically instruments the
* [http](https://nodejs.org/api/http.html) module.
Expand Down
1 change: 1 addition & 0 deletions packages/datadog-instrumentations/src/helpers/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ module.exports = {
graphql: () => require('../graphql'),
grpc: () => require('../grpc'),
hapi: () => require('../hapi'),
hono: () => require('../hono'),
http: () => require('../http'),
http2: () => require('../http2'),
https: () => require('../http'),
Expand Down
63 changes: 63 additions & 0 deletions packages/datadog-instrumentations/src/hono.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
'use strict'

const shimmer = require('../../datadog-shimmer')
const {
addHook,
channel
} = require('./helpers/instrument')

const routeChannel = channel('apm:hono:request:route')
const handleChannel = channel('apm:hono:request:handle')
const errorChannel = channel('apm:hono:request:error')

function wrapDispatch (dispatch) {
return function (request, executionCtx, env, method) {
handleChannel.publish({ req: env.incoming })
return dispatch.apply(this, arguments)
}
}

function wrapCompose (compose) {
return function (middleware, onError, onNotFound) {
const instrumentedOnError = (...args) => {
const [error, context] = args
const req = context.env.incoming
errorChannel.publish({ req, error })
return onError(...args)
}

const instrumentedMiddlewares = middleware.map(h => {
const [[fn, meta], params] = h

// TODO: handle middleware instrumentation
const instrumentedFn = (...args) => {
const context = args[0]
const req = context.env.incoming
const route = meta.path
routeChannel.publish({
req,
route
})
return fn(...args)
}
return [[instrumentedFn, meta], params]
})
return compose.apply(this, [instrumentedMiddlewares, instrumentedOnError, onNotFound])
}
}

addHook({
name: 'hono',
versions: ['>=4']
}, hono => {
shimmer.wrap(hono.Hono.prototype, 'dispatch', wrapDispatch)
return hono
})

addHook({
name: 'hono',
versions: ['>=4'],
file: 'dist/cjs/compose.js'
}, Compose => {
return shimmer.wrap(Compose, 'compose', wrapCompose)
})
28 changes: 28 additions & 0 deletions packages/datadog-plugin-hono/src/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
'use strict'

const RouterPlugin = require('../../datadog-plugin-router/src')
const web = require('../../dd-trace/src/plugins/util/web')

class HonoPlugin extends RouterPlugin {
static get id () {
return 'hono'
}

constructor (...args) {
super(...args)

this.addSub('apm:hono:request:handle', ({ req }) => {
this.setFramework(req, 'hono', this.config)
})

this.addSub('apm:hono:request:route', ({ req, route }) => {
web.setRoute(req, route)
})

this.addSub('apm:hono:request:error', ({ req, error }) => {
web.addError(req, error)
})
}
}

module.exports = HonoPlugin
218 changes: 218 additions & 0 deletions packages/datadog-plugin-hono/test/index.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
'use strict'

const axios = require('axios')
const agent = require('../../dd-trace/test/plugins/agent')
const {
ERROR_TYPE,
ERROR_MESSAGE,
ERROR_STACK
} = require('../../dd-trace/src/constants')

const sort = spans => spans.sort((a, b) => a.start.toString() >= b.start.toString() ? 1 : -1)

describe('Plugin', () => {
let tracer
let server
let app
let serve
let hono

describe('hono', () => {
withVersions('hono', 'hono', version => {
before(() => {
return agent.load(['hono', 'http'], [{}, { client: false }]).then(() => {
hono = require(`../../../versions/hono@${version}`).get()
})
})

after(() => {
return agent.close({ ritmReset: false })
})

beforeEach(() => {
tracer = require('../../dd-trace')
serve = require('../../../versions/@hono/node-server@1.13.2').get().serve

app = new hono.Hono()

app.use((c, next) => {
c.set('middleware', 'test')
return next()
})

app.get('/user/:id', (c) => {
return c.json({
id: c.req.param('id'),
middleware: c.get('middleware')
})
})
})

afterEach(() => {
server && server.close()
server = null
})

it('should do automatic instrumentation on routes', function (done) {
server = serve({
fetch: app.fetch,
port: 1
}, (i) => {
const port = i.port

agent
.use(traces => {
expect(traces[0][0]).to.have.property('name', 'hono.request')
expect(traces[0][0]).to.have.property('service', 'test')
expect(traces[0][0]).to.have.property('type', 'web')
expect(traces[0][0]).to.have.property('resource', 'GET /user/:id')
expect(traces[0][0].meta).to.have.property('span.kind', 'server')
expect(traces[0][0].meta).to.have.property('http.url', `http://localhost:${port}/user/123`)
expect(traces[0][0].meta).to.have.property('http.method', 'GET')
expect(traces[0][0].meta).to.have.property('http.status_code')
expect(traces[0][0].meta).to.have.property('component', 'hono')
expect(Number(traces[0][0].meta['http.status_code'])).to.be.within(200, 299)
})
.then(done)
.catch(done)

axios
.get(`http://localhost:${port}/user/123`)
.then(r => {
expect(r.data).to.deep.equal({
id: '123',
middleware: 'test'
})
})
.catch(done)
})
})

it('should do automatic instrumentation on nested routes', function (done) {
const books = new hono.Hono()

books.get('/:id', (c) => c.json({
id: c.req.param('id'),
name: 'test'
}))

app.route('/books', books)

server = serve({
fetch: app.fetch,
port: 1
}, (i) => {
const port = i.port

agent
.use(traces => {
expect(traces[0][0]).to.have.property('name', 'hono.request')
expect(traces[0][0]).to.have.property('service', 'test')
expect(traces[0][0]).to.have.property('type', 'web')
expect(traces[0][0]).to.have.property('resource', 'GET /books/:id')
expect(traces[0][0].meta).to.have.property('span.kind', 'server')
expect(traces[0][0].meta).to.have.property('http.url', `http://localhost:${port}/books/123`)
expect(traces[0][0].meta).to.have.property('http.method', 'GET')
expect(traces[0][0].meta).to.have.property('http.status_code')
expect(traces[0][0].meta).to.have.property('component', 'hono')
expect(Number(traces[0][0].meta['http.status_code'])).to.be.within(200, 299)
})
.then(done)
.catch(done)

axios
.get(`http://localhost:${port}/books/123`)
.then(r => {
expect(r.data).to.deep.equal({
id: '123',
name: 'test'
})
})
.catch(done)
})
})

it('should handle errors', function (done) {
const error = new Error('message')

app.get('/error', () => {
throw error
})

server = serve({
fetch: app.fetch,
port: 1
}, (i) => {
const port = i.port

agent
.use(traces => {
const spans = sort(traces[0])
expect(spans[0]).to.have.property('error', 1)
expect(spans[0].meta).to.have.property(ERROR_TYPE, error.name)
expect(spans[0].meta).to.have.property(ERROR_MESSAGE, error.message)
expect(spans[0].meta).to.have.property(ERROR_STACK, error.stack)
expect(spans[0].meta).to.have.property('http.status_code', '500')
expect(spans[0].meta).to.have.property('component', 'hono')
})
.then(done)
.catch(done)

axios
.get(`http://localhost:${port}/error`)
.catch(() => {
})
})
})

it('should have active scope within request', done => {
app.get('/request', (c) => {
expect(tracer.scope().active()).to.not.be.null
return c.text('test')
})

server = serve({
fetch: app.fetch,
port: 1
}, (i) => {
const port = i.port

axios
.get(`http://localhost:${port}/request`)
.then(r => {
expect(r.data).to.deep.equal('test')
done()
})
.catch(done)
})
})

it('should extract its parent span from the headers', done => {
server = serve({
fetch: app.fetch,
port: 1
}, (i) => {
const port = i.port

agent
.use(traces => {
expect(traces[0][0].trace_id.toString()).to.equal('1234')
expect(traces[0][0].parent_id.toString()).to.equal('5678')
})
.then(done)
.catch(done)

axios
.get(`http://localhost:${port}/user/123`, {
headers: {
'x-datadog-trace-id': '1234',
'x-datadog-parent-id': '5678',
'ot-baggage-foo': 'bar'
}
})
.catch(done)
})
})
})
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
'use strict'

const {
FakeAgent,
createSandbox,
curlAndAssertMessage,
spawnPluginIntegrationTestProc
} = require('../../../../integration-tests/helpers')
const { assert } = require('chai')

describe('esm', () => {
let agent
let proc
let sandbox

withVersions('hono', 'hono', version => {
before(async function () {
this.timeout(50000)
sandbox = await createSandbox([`'hono@${version}'`, '@hono/node-server@1.13.2'], false,
['./packages/datadog-plugin-hono/test/integration-test/*'])
})

after(async function () {
this.timeout(50000)
await sandbox.remove()
})

beforeEach(async () => {
agent = await new FakeAgent().start()
})

afterEach(async () => {
proc && proc.kill()
await agent.stop()
})

it('is instrumented', async () => {
proc = await spawnPluginIntegrationTestProc(sandbox.folder, 'server.mjs', agent.port)

return curlAndAssertMessage(agent, proc, ({ headers, payload }) => {
assert.propertyVal(headers, 'host', `127.0.0.1:${agent.port}`)
assert.isArray(payload)
assert.strictEqual(payload.length, 1)
assert.isArray(payload[0])
assert.strictEqual(payload[0].length, 4)
assert.propertyVal(payload[0][0], 'name', 'hono.request')
})
}).timeout(50000)
})
})
Loading
Loading