Skip to content

Commit

Permalink
add koa integration (#288)
Browse files Browse the repository at this point in the history
  • Loading branch information
rochdev authored Sep 21, 2018
1 parent 80ab467 commit 98334d2
Show file tree
Hide file tree
Showing 9 changed files with 358 additions and 10 deletions.
19 changes: 19 additions & 0 deletions docs/API.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,25 @@ query HelloWorld {
|------------------|------------------|----------------------------------------|
| service | redis | The service name for this integration. |

<h3 id="koa">koa</h3>

<h5 id="koa-tags">Tags</h5>

| Tag | Description |
|------------------|-----------------------------------------------------------|
| http.url | The complete URL of the request. |
| http.method | The HTTP method of the request. |
| http.status_code | The HTTP status code of the response. |
| http.headers.* | A recorded HTTP header. |

<h5 id="koa-config">Configuration Options</h5>

| Option | Default | Description |
|------------------|---------------------------|----------------------------------------|
| service | *Service name of the app* | The service name for this integration. |
| validateStatus | `code => code < 500` | Callback function to determine if there was an error. It should take a status code as its only parameter and return `true` for success or `false` for errors. |
| headers | `[]` | An array of headers to include in the span metadata. |

<h3 id="mongodb-core">mongodb-core</h3>

<h5 id="mongodb-core-tags">Tags</h5>
Expand Down
20 changes: 12 additions & 8 deletions scripts/install_plugin_modules.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const crypto = require('crypto')
const semver = require('semver')
const exec = require('./helpers/exec')
const plugins = requireDir('../src/plugins')
const externals = require('../test/plugins/externals')

const workspaces = new Set()

Expand All @@ -19,14 +20,17 @@ function run () {
}

function assertVersions () {
Object.keys(plugins).filter(key => key !== 'index').forEach(key => {
[].concat(plugins[key]).forEach(instrumentation => {
[].concat(instrumentation.versions).forEach(version => {
if (version) {
assertModules(instrumentation.name, version)
assertModules(instrumentation.name, semver.coerce(version).version)
}
})
const internals = Object.keys(plugins)
.filter(key => key !== 'index')
.map(key => plugins[key])
.reduce((prev, next) => prev.concat(next), [])

internals.concat(externals).forEach(instrumentation => {
[].concat(instrumentation.versions).forEach(version => {
if (version) {
assertModules(instrumentation.name, version)
assertModules(instrumentation.name, semver.coerce(version).version)
}
})
})
}
Expand Down
1 change: 1 addition & 0 deletions src/plugins/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module.exports = {
'hapi': require('./hapi'),
'http': require('./http'),
'ioredis': require('./ioredis'),
'koa': require('./koa'),
'mongodb-core': require('./mongodb-core'),
'mysql': require('./mysql'),
'mysql2': require('./mysql2'),
Expand Down
57 changes: 57 additions & 0 deletions src/plugins/koa.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
'use strict'

const web = require('./util/web')

function createWrapUse (tracer, config) {
config = web.normalizeConfig(config)

function ddTrace (ctx, next) {
if (web.active(ctx.req)) return next()

web.instrument(tracer, config, ctx.req, ctx.res, 'koa.request')

return next()
.then(() => extractRoute(ctx))
.catch(e => {
extractRoute(ctx)
return Promise.reject(e)
})
}

return function wrapUse (use) {
return function useWithTrace (fn) {
if (!this._datadog_trace_patched) {
this._datadog_trace_patched = true
use.call(this, ddTrace)
}

return use.call(this, function (ctx, next) {
web.reactivate(ctx.req)
return fn.apply(this, arguments)
})
}
}
}

function extractRoute (ctx) {
if (ctx.matched) {
ctx.matched
.filter(layer => layer.methods.length > 0)
.forEach(layer => {
web.enterRoute(ctx.req, layer.path)
})
}
}

module.exports = [
{
name: 'koa',
versions: ['2.x'],
patch (Koa, tracer, config) {
this.wrap(Koa.prototype, 'use', createWrapUse(tracer, config))
},
unpatch (Koa) {
this.unwrap(Koa.prototype, 'use')
}
}
]
2 changes: 1 addition & 1 deletion src/plugins/util/web.js
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ const web = {

// Return the active span. For now, this is always the request span.
active (req) {
return req._datadog.span
return req._datadog ? req._datadog.span : null
}
}

Expand Down
6 changes: 6 additions & 0 deletions test/plugins/externals.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
[
{
"name": "koa-router",
"versions": ["7.x"]
}
]
256 changes: 256 additions & 0 deletions test/plugins/koa.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,256 @@
'use strict'

const axios = require('axios')
const getPort = require('get-port')
const agent = require('./agent')
const plugin = require('../../src/plugins/koa')

wrapIt()

describe('Plugin', () => {
let tracer
let Koa
let appListener

describe('express', () => {
withVersions(plugin, 'koa', version => {
let port

beforeEach(() => {
tracer = require('../..')
})

afterEach(done => {
appListener.close(() => done())
})

afterEach(() => {
return agent.close()
})

describe('without configuration', () => {
beforeEach(() => {
return agent.load(plugin, 'koa')
})

beforeEach(() => {
Koa = require(`./versions/koa@${version}`).get()
return getPort().then(newPort => {
port = newPort
})
})

it('should do automatic instrumentation on middleware', done => {
const app = new Koa()

app.use((ctx) => {
ctx.body = ''
})

agent
.use(traces => {
expect(traces[0][0]).to.have.property('name', 'koa.request')
expect(traces[0][0]).to.have.property('service', 'test')
expect(traces[0][0]).to.have.property('type', 'http')
expect(traces[0][0]).to.have.property('resource', 'GET')
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`)
expect(traces[0][0].meta).to.have.property('http.method', 'GET')
expect(traces[0][0].meta).to.have.property('http.status_code', '200')
})
.then(done)
.catch(done)

appListener = app.listen(port, 'localhost', () => {
axios
.get(`http://localhost:${port}/user`)
.catch(done)
})
})

it('should run middleware in the request scope', done => {
if (process.env.DD_CONTEXT_PROPAGATION === 'false') return done()

const app = new Koa()

app.use((ctx, next) => {
ctx.body = ''

expect(tracer.scopeManager().active()).to.not.be.null

return next()
.then(() => {
expect(tracer.scopeManager().active()).to.not.be.null
done()
})
.catch(done)
})

appListener = app.listen(port, 'localhost', () => {
axios
.get(`http://localhost:${port}/app/user/123`)
.catch(done)
})
})

it('should reactivate the request span in middleware scopes', done => {
if (process.env.DD_CONTEXT_PROPAGATION === 'false') return done()

const app = new Koa()

let span

app.use((ctx, next) => {
span = tracer.scopeManager().active().span()
tracer.scopeManager().activate({})
return next()
})

app.use((ctx) => {
const scope = tracer.scopeManager().active()

ctx.body = ''

try {
expect(scope).to.not.be.null
expect(scope.span()).to.equal(span)
done()
} catch (e) {
done(e)
}
})

getPort().then(port => {
appListener = app.listen(port, 'localhost', () => {
axios.get(`http://localhost:${port}/user`)
.catch(done)
})
})
})

withVersions(plugin, 'koa-router', routerVersion => {
let Router

beforeEach(() => {
Router = require(`./versions/koa-router@${routerVersion}`).get()
})

it('should do automatic instrumentation on routers', done => {
const app = new Koa()
const router = new Router()

router.get('/user/:id', (ctx, next) => {
ctx.body = ''
})

app
.use(router.routes())
.use(router.allowedMethods())

agent
.use(traces => {
expect(traces[0][0]).to.have.property('resource', 'GET /user/:id')
expect(traces[0][0].meta).to.have.property('http.url', `http://localhost:${port}/user/123`)
})
.then(done)
.catch(done)

appListener = app.listen(port, 'localhost', (e) => {
axios
.get(`http://localhost:${port}/user/123`)
.catch(done)
})
})

it('should support nested routers', done => {
const app = new Koa()
const forums = new Router()
const posts = new Router()

posts.get('/:pid', (ctx, next) => {
ctx.body = ''
})

forums.use('/forums/:fid/posts', posts.routes(), posts.allowedMethods())

app.use(forums.routes())

agent
.use(traces => {
expect(traces[0][0]).to.have.property('resource', 'GET /forums/:fid/posts/:pid')
expect(traces[0][0].meta).to.have.property('http.url', `http://localhost:${port}/forums/123/posts/456`)
})
.then(done)
.catch(done)

appListener = app.listen(port, 'localhost', () => {
axios
.get(`http://localhost:${port}/forums/123/posts/456`)
.catch(done)
})
})

it('should support a router prefix', done => {
const app = new Koa()
const router = new Router({
prefix: '/user'
})

router.get('/:id', (ctx, next) => {
ctx.body = ''
})

app
.use(router.routes())
.use(router.allowedMethods())

agent
.use(traces => {
expect(traces[0][0]).to.have.property('resource', 'GET /user/:id')
expect(traces[0][0].meta).to.have.property('http.url', `http://localhost:${port}/user/123`)
})
.then(done)
.catch(done)

appListener = app.listen(port, 'localhost', () => {
axios
.get(`http://localhost:${port}/user/123`)
.catch(done)
})
})

it('should handle errors', done => {
const app = new Koa()
const router = new Router({
prefix: '/user'
})

router.get('/:id', (ctx, next) => {
throw new Error()
})

app.silent = true
app
.use(router.routes())
.use(router.allowedMethods())

agent
.use(traces => {
expect(traces[0][0]).to.have.property('resource', 'GET /user/:id')
expect(traces[0][0].meta).to.have.property('http.url', `http://localhost:${port}/user/123`)
expect(traces[0][0].error).to.equal(1)
})
.then(done)
.catch(done)

appListener = app.listen(port, 'localhost', () => {
axios
.get(`http://localhost:${port}/user/123`)
.catch(() => {})
})
})
})
})
})
})
})
Loading

0 comments on commit 98334d2

Please sign in to comment.