diff --git a/index.d.ts b/index.d.ts index 940ca6a06d..11c5b54cc3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -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; @@ -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. diff --git a/packages/datadog-instrumentations/src/helpers/hooks.js b/packages/datadog-instrumentations/src/helpers/hooks.js index 21bdf21298..179a11f3a7 100644 --- a/packages/datadog-instrumentations/src/helpers/hooks.js +++ b/packages/datadog-instrumentations/src/helpers/hooks.js @@ -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'), diff --git a/packages/datadog-instrumentations/src/hono.js b/packages/datadog-instrumentations/src/hono.js new file mode 100644 index 0000000000..c938345b6e --- /dev/null +++ b/packages/datadog-instrumentations/src/hono.js @@ -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) +}) diff --git a/packages/datadog-plugin-hono/src/index.js b/packages/datadog-plugin-hono/src/index.js new file mode 100644 index 0000000000..c6959f85c5 --- /dev/null +++ b/packages/datadog-plugin-hono/src/index.js @@ -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 diff --git a/packages/datadog-plugin-hono/test/index.spec.js b/packages/datadog-plugin-hono/test/index.spec.js new file mode 100644 index 0000000000..fe8176828c --- /dev/null +++ b/packages/datadog-plugin-hono/test/index.spec.js @@ -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) + }) + }) + }) + }) +}) diff --git a/packages/datadog-plugin-hono/test/integration-test/client.spec.js b/packages/datadog-plugin-hono/test/integration-test/client.spec.js new file mode 100644 index 0000000000..51fb954e31 --- /dev/null +++ b/packages/datadog-plugin-hono/test/integration-test/client.spec.js @@ -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) + }) +}) diff --git a/packages/datadog-plugin-hono/test/integration-test/server.mjs b/packages/datadog-plugin-hono/test/integration-test/server.mjs new file mode 100644 index 0000000000..eb392d7e54 --- /dev/null +++ b/packages/datadog-plugin-hono/test/integration-test/server.mjs @@ -0,0 +1,17 @@ +import 'dd-trace/init.js' +import {Hono} from 'hono'; +import {serve} from '@hono/node-server'; + +const app = new Hono() + +app.get('/', (c) => { + return c.text('hello, world\n') +}) + + +serve({ + fetch: app.fetch, +}, (i) => { + const port = i.port; + process.send({ port }) +}); diff --git a/packages/dd-trace/src/plugins/index.js b/packages/dd-trace/src/plugins/index.js index 80c3240153..c6bfd90ccd 100644 --- a/packages/dd-trace/src/plugins/index.js +++ b/packages/dd-trace/src/plugins/index.js @@ -39,6 +39,7 @@ module.exports = { get graphql () { return require('../../../datadog-plugin-graphql/src') }, get grpc () { return require('../../../datadog-plugin-grpc/src') }, get hapi () { return require('../../../datadog-plugin-hapi/src') }, + get hono () { return require('../../../datadog-plugin-hono/src') }, get http () { return require('../../../datadog-plugin-http/src') }, get http2 () { return require('../../../datadog-plugin-http2/src') }, get https () { return require('../../../datadog-plugin-http/src') }, diff --git a/packages/dd-trace/test/plugins/externals.json b/packages/dd-trace/test/plugins/externals.json index 5b00aa6061..511970f728 100644 --- a/packages/dd-trace/test/plugins/externals.json +++ b/packages/dd-trace/test/plugins/externals.json @@ -229,6 +229,12 @@ "versions": ["9.1.4"] } ], + "hono": [ + { + "name": "@hono/node-server", + "versions": ["1.13.2"] + } + ], "jest": [ { "name": "jest",