diff --git a/.circleci/config.yml b/.circleci/config.yml
index c1f23caf8c2..b1f8f4b5470 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -31,6 +31,10 @@ jobs:
image: redis:4.0-alpine
- &mongo
image: mongo:3.6
+ - &elasticsearch
+ image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4
+ - &rabbitmq
+ image: rabbitmq:3.6-alpine
working_directory: ~/dd-trace-js
steps:
- checkout
@@ -54,6 +58,8 @@ jobs:
- *mysql
- *redis
- *mongo
+ - *elasticsearch
+ - *rabbitmq
build-node-6:
<<: *node-base
docker:
@@ -62,6 +68,8 @@ jobs:
- *mysql
- *redis
- *mongo
+ - *elasticsearch
+ - *rabbitmq
build-node-8:
<<: *node-base
docker:
@@ -70,6 +78,8 @@ jobs:
- *mysql
- *redis
- *mongo
+ - *elasticsearch
+ - *rabbitmq
build-node-latest:
<<: *node-base
docker:
@@ -78,6 +88,8 @@ jobs:
- *mysql
- *redis
- *mongo
+ - *elasticsearch
+ - *rabbitmq
workflows:
version: 2
diff --git a/LICENSE-3rdparty.csv b/LICENSE-3rdparty.csv
index 4146a8b1b04..d32a20de1ab 100644
--- a/LICENSE-3rdparty.csv
+++ b/LICENSE-3rdparty.csv
@@ -4,6 +4,7 @@ require,cls-hooked,BSD-2-Clause,Copyright 2013-2016 Forrest L Norvell
require,continuation-local-storage,BSD-2-Clause,Copyright 2013-2016 Forrest L Norvell
require,int64-buffer,MIT,Copyright 2015-2016 Yusuke Kawasaki
require,koalas,MIT,Copyright 2013-2017 Brian Woodward
+require,lodash.kebabcase,MIT,Copyright JS Foundation and other contributors
require,methods,MIT,Copyright 2013-2014 TJ Holowaychuk 2013-2014 TJ Holowaychuk
require,msgpack-lite,MIT,Copyright 2015 Yusuke Kawasaki
require,opentracing,MIT,Copyright 2016 Resonance Labs Inc
@@ -16,11 +17,13 @@ require,require-in-the-middle,MIT,Copyright 2016-2018 Thomas Watson Steen
require,safe-buffer,MIT,Copyright Feross Aboukhadijeh
require,shimmer,BSD-2-Clause,Copyright Forrest L Norvell
require,url-parse,MIT,Copyright 2015 Unshift.io Arnout Kazemier the Contributors
+dev,amqplib,MIT,Copyright 2013-2014 Michael Bridgen
dev,axios,MIT,Copyright 2014-present Matt Zabriskie
dev,benchmark,MIT,Copyright 2010-2016 Mathias Bynens Robert Kieffer John-David Dalton
dev,bluebird,MIT,Copyright 2013-2018 Petka Antonov
dev,body-parser,MIT,Copyright 2014 Jonathan Ong 2014-2015 Douglas Christopher Wilson
dev,chai,MIT,Copyright 2017 Chai.js Assertion Library
+dev,elasticsearch,Apache-2.0,Copyright 2013 Elasticsearch BV
dev,eslint,MIT,Copyright JS Foundation and other contributors https://js.foundation
dev,eslint-config-standard,MIT,Copyright Feross Aboukhadijeh
dev,eslint-plugin-import,MIT,Copyright 2015 Ben Mosher
@@ -29,6 +32,7 @@ dev,eslint-plugin-promise,ISC,jden and other contributors
dev,eslint-plugin-standard,MIT,Copyright 2015 Jamund Ferguson
dev,express,MIT,Copyright 2009-2014 TJ Holowaychuk 2013-2014 Roman Shtylman 2014-2015 Douglas Christopher Wilson
dev,get-port,MIT,Copyright Sindre Sorhus
+dev,graphql,MIT,Copyright 2015-present Facebook Inc.
dev,gulp,MIT,Copyright 2013-2017 Blaine Bublitz Eric Schoffstall and other contributors
dev,gulp-jsdoc3,Apache-2.0,Copyright Marc Udoff
dev,jsdoc,Apache-2.0,Copyright 2011-present Michael Mathews and the contributors to JSDoc
diff --git a/docker-compose.yml b/docker-compose.yml
index f71f2e4a494..3c23fdc0c45 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -21,3 +21,11 @@ services:
image: mongo:3.6
ports:
- "127.0.0.1:27017:27017"
+ elasticsearch:
+ image: docker.elastic.co/elasticsearch/elasticsearch-oss:6.2.4
+ ports:
+ - "127.0.0.1:9200:9200"
+ rabbitmq:
+ image: rabbitmq:3.6-alpine
+ ports:
+ - "127.0.0.1:5672:5672"
diff --git a/docs/API.md b/docs/API.md
index 5a9ed492d90..d1de9e49da1 100644
--- a/docs/API.md
+++ b/docs/API.md
@@ -101,6 +101,65 @@ tracer.use('pg', {
Each integration also has its own list of default tags. These tags get automatically added to the span created by the integration.
+
amqplib
+
+
+
+| Tag | Description |
+|------------------|-----------------------------------------------------------|
+| out.host | The host of the AMQP server. |
+| out.port | The port of the AMQP server. |
+| span.kind | Set to either `producer` or `consumer` where it applies. |
+| amqp.queue | The queue targeted by the command (when available). |
+| amqp.exchange | The exchange targeted by the command (when available). |
+| amqp.routingKey | The routing key targeted by the command (when available). |
+| amqp.consumerTag | The consumer tag (when available). |
+| amqp.source | The source exchange of the binding (when available). |
+| amqp.destination | The destination exchange of the binding (when available). |
+
+Configuration Options
+
+| Option | Default | Description |
+|------------------|---------------------------|----------------------------------------|
+| service | *Service name of the app* | The service name for this integration. |
+
+Known Limitations
+
+When consuming messages, the current span will be immediately finished. This means that if any asynchronous operation is started in the message handler callback, its duration will be excluded from the span duration.
+
+For example:
+
+```js
+channel.consume('queue', msg => {
+ setTimeout(() => {
+ // The message span will not include the 1 second from this operation.
+ }, 1000)
+}, {}, () => {})
+```
+
+This limitation doesn't apply to other commands. We are working on improving this behavior in a future version.
+
+elasticsearch
+
+
+
+| Tag | Description |
+|----------------------|-------------------------------------------------------|
+| db.type | Always set to `elasticsearch`. |
+| out.host | The host of the Elasticsearch server. |
+| out.port | The port of the Elasticsearch server. |
+| span.kind | Always set to `client`. |
+| elasticsearch.method | The underlying HTTP request verb. |
+| elasticsearch.url | The underlying HTTP request URL path. |
+| elasticsearch.body | The body of the query. |
+| elasticsearch.params | The parameters of the query. |
+
+Configuration Options
+
+| Option | Default | Description |
+|------------------|------------------|----------------------------------------|
+| service | elasticsearch | The service name for this integration. |
+
express
@@ -117,6 +176,38 @@ Each integration also has its own list of default tags. These tags get automatic
|------------------|---------------------------|----------------------------------------|
| service | *Service name of the app* | The service name for this integration. |
+graphql
+
+The `graphql` integration uses the operation name as the span resource name. If no operation name is set, the resource name will always be just `query` or `mutation`.
+
+For example:
+
+```graphql
+# good, the resource name will be `query HelloWorld`
+query HelloWorld {
+ hello
+ world
+}
+
+# bad, the resource name will be `query`
+{
+ hello
+ world
+}
+```
+
+
+
+| Tag | Description |
+|------------------|-----------------------------------------------------------|
+| graphql.document | The original GraphQL document. |
+
+Configuration Options
+
+| Option | Default | Description |
+|---------|--------------------------------------------------|----------------------------------------|
+| service | *Service name of the app suffixed with -graphql* | The service name for this integration. |
+
http / https
@@ -233,7 +324,7 @@ Options can be configured as a parameter to the [init()](https://datadog.github.
| tags | | {} | Set global tags that should be applied to all spans. |
| sampleRate | | 1 | Percentage of spans to sample as a float between 0 and 1. |
| flushInterval | | 2000 | Interval in milliseconds at which the tracer will submit traces to the agent. |
-| experimental | | {} | Experimental features can be enabled all at once using boolean `true` or individually using key/value pairs. Available experimental features: `asyncHooks`. |
+| experimental | | {} | Experimental features can be enabled all at once using boolean `true` or individually using key/value pairs. There are currently no experimental features available. |
| plugins | | true | Whether or not to enable automatic instrumentation of external libraries using the built-in plugins. |
Custom Logging
diff --git a/lib/version.js b/lib/version.js
index 40a89829a2c..a7ac55811ef 100644
--- a/lib/version.js
+++ b/lib/version.js
@@ -1 +1 @@
-module.exports = '0.2.1'
+module.exports = '0.3.0'
diff --git a/package.json b/package.json
index c35e050ffc6..b9ae3ccadc1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "dd-trace",
- "version": "0.2.1",
+ "version": "0.3.0",
"description": "Datadog APM tracing client for JavaScript (experimental)",
"main": "index.js",
"scripts": {
@@ -29,7 +29,7 @@
},
"homepage": "https://github.com/DataDog/dd-trace-js#readme",
"engines": {
- "node": ">=4"
+ "node": ">=4.7"
},
"dependencies": {
"cls-bluebird": "^2.1.0",
@@ -37,6 +37,7 @@
"continuation-local-storage": "^3.2.1",
"int64-buffer": "^0.1.9",
"koalas": "^1.0.2",
+ "lodash.kebabcase": "^4.1.1",
"methods": "^1.1.2",
"msgpack-lite": "^0.1.26",
"opentracing": "0.14.1",
@@ -52,11 +53,13 @@
"url-parse": "^1.2.0"
},
"devDependencies": {
+ "amqplib": "^0.5.2",
"axios": "^0.18.0",
"benchmark": "^2.1.4",
"bluebird": "^3.5.1",
"body-parser": "^1.18.2",
"chai": "^4.1.2",
+ "elasticsearch": "^15.0.0",
"eslint": "^4.15.0",
"eslint-config-standard": "^11.0.0-beta.0",
"eslint-plugin-import": "^2.8.0",
@@ -65,6 +68,7 @@
"eslint-plugin-standard": "^3.0.1",
"express": "^4.16.2",
"get-port": "^3.2.0",
+ "graphql": "^0.13.2",
"gulp": "^3.9.1",
"gulp-jsdoc3": "^2.0.0",
"jsdoc": "^3.5.5",
diff --git a/scripts/helpers/color.js b/scripts/helpers/color.js
new file mode 100644
index 00000000000..1b9d019f6a0
--- /dev/null
+++ b/scripts/helpers/color.js
@@ -0,0 +1,8 @@
+'use strict'
+
+// https://en.wikipedia.org/wiki/ANSI_escape_code#Colors
+module.exports = {
+ GRAY: '\\033[1;90m',
+ CYAN: '\\033[1;36m',
+ NONE: '\\033[0m'
+}
diff --git a/scripts/helpers/exec.js b/scripts/helpers/exec.js
new file mode 100644
index 00000000000..36778cdaebe
--- /dev/null
+++ b/scripts/helpers/exec.js
@@ -0,0 +1,13 @@
+'use strict'
+
+const execSync = require('child_process').execSync
+const color = require('./color')
+
+function exec (command, options) {
+ options = Object.assign({ stdio: [0, 1, 2] }, options)
+
+ execSync(`echo "${color.GRAY}$ ${command}${color.NONE}"`, options)
+ execSync(command, options)
+}
+
+module.exports = exec
diff --git a/scripts/helpers/title.js b/scripts/helpers/title.js
new file mode 100644
index 00000000000..2d752550175
--- /dev/null
+++ b/scripts/helpers/title.js
@@ -0,0 +1,15 @@
+'use strict'
+
+const exec = require('child_process').execSync
+const color = require('./color')
+
+function title (str) {
+ const options = { stdio: [0, 1, 2] }
+ const line = ''.padStart(str.length, '=')
+
+ exec(`echo "${color.CYAN}${line}${color.NONE}"`, options)
+ exec(`echo "${color.CYAN}${str}${color.NONE}"`, options)
+ exec(`echo "${color.CYAN}${line}${color.NONE}"`, options)
+}
+
+module.exports = title
diff --git a/scripts/publish_docs.js b/scripts/publish_docs.js
new file mode 100644
index 00000000000..ce7d85f20eb
--- /dev/null
+++ b/scripts/publish_docs.js
@@ -0,0 +1,26 @@
+'use strict'
+
+const fs = require('fs')
+const exec = require('./helpers/exec')
+const title = require('./helpers/title')
+
+title(`Publishing API documentation to GitHub Pages`)
+
+const msg = process.argv[2]
+
+if (!msg) {
+ throw new Error('Please provide a reason for the change. Example: node scripts/publish_docs.js "fix typo"')
+}
+
+if (fs.existsSync('yarn.lock')) {
+ exec('yarn')
+} else {
+ exec('npm install')
+}
+
+exec('rm -rf ./out')
+exec('git clone -b gh-pages --single-branch git@github.com:DataDog/dd-trace-js.git out')
+exec('npm run jsdoc')
+exec('git add -A', { cwd: './out' })
+exec(`git commit -m "${msg}"`, { cwd: './out' })
+exec('git push', { cwd: './out' })
diff --git a/scripts/release.js b/scripts/release.js
index 21d89cbc038..bb5931649f5 100644
--- a/scripts/release.js
+++ b/scripts/release.js
@@ -1,8 +1,14 @@
'use strict'
-const exec = require('child_process').execSync
+const exec = require('./helpers/exec')
+const title = require('./helpers/title')
+
+title(`Publishing package to the npm registry`)
+
+const pkg = require('../package.json')
exec('npm whoami')
exec('git checkout master')
exec('git pull')
exec('npm publish')
+exec(`node scripts/publish_docs.js "v${pkg.version}"`)
diff --git a/scripts/version.js b/scripts/version.js
index 9513922dcf2..cb80990b87c 100644
--- a/scripts/version.js
+++ b/scripts/version.js
@@ -3,7 +3,10 @@
const path = require('path')
const fs = require('fs')
const semver = require('semver')
-const exec = require('child_process').execSync
+const exec = require('./helpers/exec')
+const title = require('./helpers/title')
+
+title('Pulling latest changes from master')
exec(`git checkout master`)
exec(`git pull`)
@@ -12,6 +15,8 @@ const pkg = require('../package.json')
const increment = getIncrement()
const version = semver.inc(pkg.version, increment)
+title(`Bumping version to v${version} in a new branch`)
+
pkg.version = version
exec(`git checkout -b v${version}`)
diff --git a/src/config.js b/src/config.js
index 06ea59603e8..e627fff5814 100644
--- a/src/config.js
+++ b/src/config.js
@@ -19,6 +19,9 @@ class Config {
const flushInterval = coalesce(parseInt(options.flushInterval, 10), 2000)
const plugins = coalesce(options.plugins, true)
+ // Temporary safety net. Do not disable without contacting support.
+ const asyncHooks = coalesce(options.asyncHooks, true)
+
this.enabled = String(enabled) === 'true'
this.debug = String(debug) === 'true'
this.service = service
@@ -30,18 +33,8 @@ class Config {
this.sampleRate = sampleRate
this.logger = options.logger
this.plugins = !!plugins
- this.experimental = {
- asyncHooks: isFlagEnabled(options.experimental, 'asyncHooks')
- }
+ this.asyncHooks = !!asyncHooks
}
}
-function isFlagEnabled (obj, prop) {
- return obj === true || (isObject(obj) && !!obj[prop])
-}
-
-function isObject (value) {
- return typeof value === 'object' && value !== null
-}
-
module.exports = Config
diff --git a/src/platform/node/context/index.js b/src/platform/node/context/index.js
index 8453751db40..a66f9dcf4d5 100644
--- a/src/platform/node/context/index.js
+++ b/src/platform/node/context/index.js
@@ -1,9 +1,9 @@
'use strict'
-module.exports = config => {
+module.exports = function () {
let namespace
- if (config.experimental.asyncHooks) {
+ if (this._config.asyncHooks) {
namespace = require('./cls_hooked')
} else {
namespace = require('./cls')
diff --git a/src/platform/node/index.js b/src/platform/node/index.js
index dfd1dfbfe04..bf7e2ea1c11 100644
--- a/src/platform/node/index.js
+++ b/src/platform/node/index.js
@@ -10,9 +10,13 @@ const context = require('./context')
const msgpack = require('./msgpack')
module.exports = {
+ _config: {},
name: () => 'nodejs',
version: () => process.version,
engine: () => process.jsEngine || 'v8',
+ configure (config) {
+ this._config = config
+ },
id,
now,
env,
diff --git a/src/plugins/amqplib.js b/src/plugins/amqplib.js
new file mode 100644
index 00000000000..59e0ebf327d
--- /dev/null
+++ b/src/plugins/amqplib.js
@@ -0,0 +1,166 @@
+'use strict'
+
+const shimmer = require('shimmer')
+const kebabCase = require('lodash.kebabcase')
+
+let methods = {}
+
+function createWrapSendOrEnqueue (tracer, config) {
+ return function wrapSendOrEnqueue (sendOrEnqueue) {
+ return function sendOrEnqueueWithTrace (method, fields, reply) {
+ return sendOrEnqueue.call(this, method, fields, tracer.bind(reply))
+ }
+ }
+}
+
+function createWrapSendImmediately (tracer, config) {
+ return function wrapSendImmediately (sendImmediately) {
+ return function sendImmediatelyWithTrace (method, fields) {
+ return sendWithTrace(sendImmediately, this, arguments, tracer, config, methods[method], fields)
+ }
+ }
+}
+
+function createWrapSendMessage (tracer, config) {
+ return function wrapSendMessage (sendMessage) {
+ return function sendMessageWithTrace (fields) {
+ return sendWithTrace(sendMessage, this, arguments, tracer, config, 'basic.publish', fields)
+ }
+ }
+}
+
+function createWrapDispatchMessage (tracer, config) {
+ return function wrapDispatchMessage (dispatchMessage) {
+ return function dispatchMessageWithTrace (fields, message) {
+ let returnValue
+
+ tracer.trace('amqp.command', span => {
+ addTags(this, config, span, 'basic.deliver', fields)
+
+ try {
+ returnValue = dispatchMessage.apply(this, arguments)
+ } catch (e) {
+ throw addError(span, e)
+ } finally {
+ // Do not use this without contacting support first
+ if (config.consumerAutoFinish !== false) {
+ span.finish()
+ }
+ }
+ })
+
+ return returnValue
+ }
+ }
+}
+
+function sendWithTrace (send, channel, args, tracer, config, method, fields) {
+ let span
+
+ tracer.trace('amqp.command', child => {
+ span = child
+ })
+
+ addTags(channel, config, span, method, fields)
+
+ try {
+ return send.apply(channel, args)
+ } catch (e) {
+ throw addError(span, e)
+ } finally {
+ span.finish()
+ }
+}
+
+function isCamelCase (str) {
+ return /([A-Z][a-z0-9]+)+/.test(str)
+}
+
+function getResourceName (method, fields) {
+ return [
+ method,
+ fields.exchange,
+ fields.routingKey,
+ fields.queue,
+ fields.source,
+ fields.destination
+ ].filter(val => val).join(' ')
+}
+
+function addError (span, error) {
+ span.addTags({
+ 'error.type': error.name,
+ 'error.msg': error.message,
+ 'error.stack': error.stack
+ })
+
+ return error
+}
+
+function addTags (channel, config, span, method, fields) {
+ const fieldNames = [
+ 'queue',
+ 'exchange',
+ 'routingKey',
+ 'consumerTag',
+ 'source',
+ 'destination'
+ ]
+
+ span.addTags({
+ 'service.name': config.service || 'amqp',
+ 'resource.name': getResourceName(method, fields),
+ 'span.type': 'worker',
+ 'out.host': channel.connection.stream._host,
+ 'out.port': channel.connection.stream.remotePort
+ })
+
+ switch (method) {
+ case 'basic.publish':
+ span.setTag('span.kind', 'producer')
+ break
+ case 'basic.consume':
+ case 'basic.get':
+ case 'basic.deliver':
+ span.setTag('span.kind', 'consumer')
+ break
+ }
+
+ fieldNames.forEach(field => {
+ fields[field] !== undefined && span.setTag(`amqp.${field}`, fields[field])
+ })
+}
+
+module.exports = [
+ {
+ name: 'amqplib',
+ file: 'lib/defs.js',
+ versions: ['0.5.x'],
+ patch (defs, tracer, config) {
+ methods = Object.keys(defs)
+ .filter(key => Number.isInteger(defs[key]))
+ .filter(key => isCamelCase(key))
+ .reduce((acc, key) => Object.assign(acc, { [defs[key]]: kebabCase(key).replace('-', '.') }), {})
+ },
+ unpatch (defs) {
+ methods = {}
+ }
+ },
+ {
+ name: 'amqplib',
+ file: 'lib/channel.js',
+ versions: ['0.5.x'],
+ patch (channel, tracer, config) {
+ shimmer.wrap(channel.Channel.prototype, 'sendOrEnqueue', createWrapSendOrEnqueue(tracer, config))
+ shimmer.wrap(channel.Channel.prototype, 'sendImmediately', createWrapSendImmediately(tracer, config))
+ shimmer.wrap(channel.Channel.prototype, 'sendMessage', createWrapSendMessage(tracer, config))
+ shimmer.wrap(channel.BaseChannel.prototype, 'dispatchMessage', createWrapDispatchMessage(tracer, config))
+ },
+ unpatch (channel) {
+ shimmer.unwrap(channel.Channel.prototype, 'sendOrEnqueue')
+ shimmer.unwrap(channel.Channel.prototype, 'sendImmediately')
+ shimmer.unwrap(channel.Channel.prototype, 'sendMessage')
+ shimmer.unwrap(channel.BaseChannel.prototype, 'dispatchMessage')
+ }
+ }
+]
diff --git a/src/plugins/elasticsearch.js b/src/plugins/elasticsearch.js
new file mode 100644
index 00000000000..a7e891bb764
--- /dev/null
+++ b/src/plugins/elasticsearch.js
@@ -0,0 +1,130 @@
+'use strict'
+
+const Tags = require('opentracing').Tags
+const shimmer = require('shimmer')
+
+function createWrapRequest (tracer, config) {
+ return function wrapRequest (request) {
+ return function requestWithTrace (params, cb) {
+ let returnValue
+
+ tracer._context.run(() => {
+ let defer
+
+ if (typeof cb === 'function') {
+ cb = tracer.bind(cb)
+ } else {
+ defer = this.defer()
+
+ cb = tracer.bind((err, parsedBody, status) => {
+ if (err) {
+ err.body = parsedBody
+ err.status = status
+ defer.reject(err)
+ } else {
+ defer.resolve(parsedBody)
+ }
+ })
+ }
+
+ tracer.trace('elasticsearch.query', {
+ tags: {
+ [Tags.SPAN_KIND]: Tags.SPAN_KIND_RPC_CLIENT,
+ [Tags.DB_TYPE]: 'elasticsearch'
+ }
+ }, span => {
+ span.addTags({
+ 'service.name': config.service || 'elasticsearch',
+ 'resource.name': `${params.method} ${quantizePath(params.path)}`,
+ 'span.type': 'db',
+ 'elasticsearch.url': params.path,
+ 'elasticsearch.method': params.method,
+ 'elasticsearch.params': JSON.stringify(params.query)
+ })
+
+ if (JSON.stringify(params.body)) {
+ span.setTag('elasticsearch.body', JSON.stringify(params.body))
+ }
+
+ if (!defer) {
+ returnValue = request.call(this, params, wrapCallback(tracer, span, cb))
+ } else {
+ const ret = request.call(this, params, wrapCallback(tracer, span, cb))
+
+ returnValue = defer.promise
+ returnValue.abort = ret.abort
+ }
+ })
+ })
+
+ return returnValue
+ }
+ }
+}
+
+function createWrapSelect (tracer, config) {
+ return function wrapSelect (select) {
+ return function selectWithTrace (cb) {
+ const span = tracer.currentSpan()
+
+ return select.call(this, function (_, conn) {
+ if (conn && conn.host) {
+ span.addTags({
+ 'out.host': conn.host.host,
+ 'out.port': conn.host.port
+ })
+ }
+
+ return cb.apply(null, arguments)
+ })
+ }
+ }
+}
+
+function wrapCallback (tracer, span, done) {
+ return function (err) {
+ finish(span, err)
+ done.apply(null, arguments)
+ }
+}
+
+function finish (span, err) {
+ if (err) {
+ span.addTags({
+ 'error.type': err.name,
+ 'error.msg': err.message,
+ 'error.stack': err.stack
+ })
+ }
+
+ span.finish()
+}
+
+function quantizePath (path) {
+ return path.replace(/[0-9]+/g, '?')
+}
+
+module.exports = [
+ {
+ name: 'elasticsearch',
+ file: 'src/lib/connection_pool.js',
+ versions: ['15.x'],
+ patch (ConnectionPool, tracer, config) {
+ shimmer.wrap(ConnectionPool.prototype, 'select', createWrapSelect(tracer, config))
+ },
+ unpatch (ConnectionPool) {
+ shimmer.unwrap(ConnectionPool.prototype, 'select')
+ }
+ },
+ {
+ name: 'elasticsearch',
+ file: 'src/lib/transport.js',
+ versions: ['15.x'],
+ patch (Transport, tracer, config) {
+ shimmer.wrap(Transport.prototype, 'request', createWrapRequest(tracer, config))
+ },
+ unpatch (Transport) {
+ shimmer.unwrap(Transport.prototype, 'request')
+ }
+ }
+]
diff --git a/src/plugins/graphql.js b/src/plugins/graphql.js
new file mode 100644
index 00000000000..09efc0bf186
--- /dev/null
+++ b/src/plugins/graphql.js
@@ -0,0 +1,241 @@
+'use strict'
+
+const shimmer = require('shimmer')
+const platform = require('../platform')
+
+function createWrapGraphql (tracer, config, defaultFieldResolver) {
+ return function wrapGraphql (graphql) {
+ return function graphqlWithTrace () {
+ const source = arguments[1] || arguments[0].source
+ const contextValue = arguments[3] || arguments[0].contextValue || {}
+
+ if (arguments.length === 1) {
+ arguments[0].contextValue = contextValue
+ } else {
+ arguments[3] = contextValue
+ arguments.length = Math.max(arguments.length, 4)
+ }
+
+ Object.defineProperties(contextValue, {
+ _datadog_operation: { value: {} },
+ _datadog_fields: { value: {} },
+ _datadog_source: { value: source }
+ })
+
+ return graphql.apply(this, arguments)
+ }
+ }
+}
+
+function createWrapExecute (tracer, config, defaultFieldResolver) {
+ return function wrapExecute (execute) {
+ return function executeWithTrace () {
+ const schema = arguments[0]
+ const contextValue = arguments[3]
+ const fieldResolver = arguments[6] || defaultFieldResolver
+
+ arguments[6] = wrapResolve(fieldResolver, tracer, config)
+ arguments[3] = contextValue
+
+ if (!schema._datadog_patched) {
+ wrapFields(schema._queryType._fields, tracer, config, [])
+ schema._datadog_patched = true
+ }
+
+ return call(execute, this, arguments, defer(tracer), () => finishOperation(contextValue))
+ }
+ }
+}
+
+function wrapFields (fields, tracer, config) {
+ Object.keys(fields).forEach(key => {
+ const field = fields[key]
+
+ if (typeof field.resolve === 'function') {
+ field.resolve = wrapResolve(field.resolve, tracer, config)
+ }
+
+ if (field.type && field.type._fields) {
+ wrapFields(field.type._fields, tracer, config)
+ }
+ })
+}
+
+function wrapResolve (resolve, tracer, config) {
+ return function resolveWithTrace (source, args, contextValue, info) {
+ const path = getPath(info.path)
+ const fieldParent = getFieldParent(tracer, config, contextValue, info, path)
+ const childOf = createSpan('graphql.field', tracer, config, fieldParent, path)
+ const deferred = defer(tracer)
+
+ let result
+
+ contextValue._datadog_fields[path] = {
+ span: childOf,
+ parent: fieldParent
+ }
+
+ tracer.trace('graphql.resolve', { childOf }, span => {
+ addTags(span, tracer, config, path)
+
+ result = call(resolve, this, arguments, deferred, err => finish(span, contextValue, path, err))
+ })
+
+ return result
+ }
+}
+
+function call (fn, thisContext, args, deferred, callback) {
+ try {
+ let result = fn.apply(thisContext, args)
+
+ if (result && typeof result.then === 'function') {
+ result = result
+ .then(value => {
+ callback(null, value)
+ deferred.resolve(value)
+ return deferred.promise
+ })
+ .catch(err => {
+ callback(err)
+ deferred.reject(err)
+ return deferred.promise
+ })
+ } else {
+ callback(null, result)
+ }
+
+ return result
+ } catch (e) {
+ callback(e)
+ throw e
+ }
+}
+
+function defer (tracer) {
+ const deferred = {}
+
+ deferred.promise = new Promise((resolve, reject) => {
+ deferred.resolve = tracer.bind(resolve)
+ deferred.reject = tracer.bind(reject)
+ })
+
+ return deferred
+}
+
+function getFieldParent (tracer, config, contextValue, info, path) {
+ if (!contextValue._datadog_operation.span) {
+ contextValue._datadog_operation.span = createOperationSpan(tracer, config, contextValue, info)
+ }
+
+ if (path.length === 1) {
+ return contextValue._datadog_operation.span
+ }
+
+ return contextValue._datadog_fields[path.slice(0, -1).join('.')].span
+}
+
+function createOperationSpan (tracer, config, contextValue, info) {
+ const type = info.operation.operation
+ const name = info.operation.name && info.operation.name.value
+
+ let span
+
+ tracer.trace(`graphql.${info.operation.operation}`, parent => {
+ span = parent
+ span.addTags({
+ 'service.name': getService(tracer, config),
+ 'resource.name': [type, name].filter(val => val).join(' '),
+ 'span.type': 'custom',
+ 'graphql.document': contextValue._datadog_source
+ })
+ })
+
+ return span
+}
+
+function createSpan (name, tracer, config, childOf, path) {
+ let span
+
+ tracer.trace(name, { childOf }, parent => {
+ span = parent
+ addTags(span, tracer, config, path)
+ })
+
+ return span
+}
+
+function addTags (span, tracer, config, path) {
+ span.addTags({
+ 'service.name': getService(tracer, config),
+ 'resource.name': path.join('.'),
+ 'span.type': 'custom'
+ })
+}
+
+function finish (span, contextValue, path, error) {
+ addError(span, error)
+
+ span.finish()
+
+ for (let i = path.length - 2; i >= 0; i--) {
+ contextValue._datadog_fields[path[i]].finishTime = platform.now()
+ }
+}
+
+function finishOperation (contextValue) {
+ for (const key in contextValue._datadog_fields) {
+ contextValue._datadog_fields[key].span.finish(contextValue._datadog_fields[key].finishTime)
+ }
+
+ contextValue._datadog_operation.span.finish()
+}
+
+function getService (tracer, config) {
+ return config.service || `${tracer._service}-graphql`
+}
+
+function getPath (path) {
+ if (path.prev) {
+ return getPath(path.prev).concat(path.key)
+ } else {
+ return [path.key]
+ }
+}
+
+function addError (span, error) {
+ if (error) {
+ span.addTags({
+ 'error.type': error.name,
+ 'error.msg': error.message,
+ 'error.stack': error.stack
+ })
+ }
+
+ return error
+}
+
+module.exports = [
+ {
+ name: 'graphql',
+ file: 'graphql.js',
+ versions: ['0.13.x'],
+ patch (graphql, tracer, config) {
+ shimmer.wrap(graphql, 'graphql', createWrapGraphql(tracer, config))
+ },
+ unpatch (graphql) {
+ shimmer.unwrap(graphql, 'graphql')
+ }
+ },
+ {
+ name: 'graphql',
+ file: 'execution/execute.js',
+ versions: ['0.13.x'],
+ patch (execute, tracer, config) {
+ shimmer.wrap(execute, 'execute', createWrapExecute(tracer, config, execute.defaultFieldResolver))
+ },
+ unpatch (execute) {
+ shimmer.unwrap(execute, 'execute')
+ }
+ }
+]
diff --git a/src/plugins/redis.js b/src/plugins/redis.js
index 87c071a4bc6..bf559560e18 100644
--- a/src/plugins/redis.js
+++ b/src/plugins/redis.js
@@ -83,7 +83,7 @@ function unpatch (redis) {
module.exports = {
name: 'redis',
- versions: ['>=2.6'],
+ versions: ['^2.6'],
patch,
unpatch
}
diff --git a/src/proxy.js b/src/proxy.js
index bc3f2f4a9ad..80a6137320a 100644
--- a/src/proxy.js
+++ b/src/proxy.js
@@ -35,7 +35,6 @@ class Tracer extends BaseTracer {
* will submit traces to the agent.
* @param {Object|boolean} [options.experimental={}] Experimental features can be enabled all at once
* using boolean `true` or individually using key/value pairs.
- * @param {boolean} [options.experimental.asyncHooks=false] Whether to use Node's experimental async hooks.
* @param {boolean} [options.plugins=true] Whether to load all built-in plugins.
* @returns {Tracer} Self
*/
@@ -45,6 +44,8 @@ class Tracer extends BaseTracer {
const config = new Config(options)
+ platform.configure(config)
+
this._tracer = new DatadogTracer(config)
this._instrumenter.patch(config)
}
diff --git a/test/.eslintrc.json b/test/.eslintrc.json
index 2d58d58ac6a..45220b03263 100644
--- a/test/.eslintrc.json
+++ b/test/.eslintrc.json
@@ -9,7 +9,8 @@
"expect": true,
"sinon": true,
"proxyquire": true,
- "nock": true
+ "nock": true,
+ "wrapIt": true
},
"rules": {
"no-unused-expressions": 0
diff --git a/test/config.spec.js b/test/config.spec.js
index d76ef0e7287..f90cea04f62 100644
--- a/test/config.spec.js
+++ b/test/config.spec.js
@@ -106,28 +106,6 @@ describe('Config', () => {
expect(config).to.have.property('env', 'development')
})
- it('should support global experimental flag', () => {
- const config = new Config({
- experimental: true
- })
-
- expect(config).to.have.deep.property('experimental', {
- asyncHooks: true
- })
- })
-
- it('should support experimental asyncHooks flag', () => {
- const config = new Config({
- experimental: {
- asyncHooks: true
- }
- })
-
- expect(config).to.have.deep.property('experimental', {
- asyncHooks: true
- })
- })
-
it('should sanitize the sample rate to be between 0 and 1', () => {
expect(new Config({ sampleRate: -1 })).to.have.property('sampleRate', 0)
expect(new Config({ sampleRate: 2 })).to.have.property('sampleRate', 1)
diff --git a/test/platform/node/index.spec.js b/test/platform/node/index.spec.js
index d2725003f98..b187cebd7a5 100644
--- a/test/platform/node/index.spec.js
+++ b/test/platform/node/index.spec.js
@@ -4,6 +4,8 @@ const EventEmitter = require('events')
const Buffer = require('safe-buffer').Buffer
const semver = require('semver')
+wrapIt()
+
describe('Platform', () => {
describe('Node', () => {
let platform
@@ -321,7 +323,6 @@ describe('Platform', () => {
})
describe('context', () => {
- let context
let namespace
let clsBluebird
let config
@@ -329,7 +330,6 @@ describe('Platform', () => {
beforeEach(() => {
require('cls-bluebird')
clsBluebird = sinon.spy(require.cache[require.resolve('cls-bluebird')], 'exports')
- context = require('../../../src/platform/node/context')
})
afterEach(() => {
@@ -338,23 +338,21 @@ describe('Platform', () => {
describe('continuation-local-storage', () => {
beforeEach(() => {
- config = { experimental: { asyncHooks: false } }
- namespace = context(config)
+ platform.configure({ asyncHooks: false })
+ namespace = platform.context()
})
testContext('../../../src/platform/node/context/cls')
})
- if (semver.gte(semver.valid(process.version), '8.2.0')) {
- describe('cls-hooked', () => {
- beforeEach(() => {
- config = { experimental: { asyncHooks: true } }
- namespace = context(config)
- })
-
- testContext('../../../src/platform/node/context/cls_hooked')
+ describe('cls-hooked', () => {
+ beforeEach(() => {
+ platform.configure({ asyncHooks: true })
+ namespace = platform.context(config)
})
- }
+
+ testContext('../../../src/platform/node/context/cls_hooked')
+ })
function testContext (modulePath) {
it('should use the correct implementation from the experimental flag', () => {
@@ -456,7 +454,7 @@ describe('Platform', () => {
})
it('should only patch bluebird once', () => {
- context(config)
+ platform.context()
expect(clsBluebird).to.not.have.been.called
})
diff --git a/test/plugins/agent.js b/test/plugins/agent.js
index 66a444cc6d7..bf79eb46029 100644
--- a/test/plugins/agent.js
+++ b/test/plugins/agent.js
@@ -11,6 +11,7 @@ let agent = null
let listener = null
let tracer = null
let handlers = []
+let promise
let skip = []
module.exports = {
@@ -60,17 +61,24 @@ module.exports = {
})
},
- use (callback) {
- return new Promise((resolve, reject) => {
- handlers.push(function () {
- try {
- callback.apply(null, arguments)
- resolve()
- } catch (e) {
- reject(e)
- }
- })
- })
+ use (callback, count) {
+ count = count || 1
+ promise = Promise.reject(new Error('No request was expected.'))
+
+ for (let i = 0; i < count; i++) {
+ promise = promise.catch(() => new Promise((resolve, reject) => {
+ handlers.push(function () {
+ try {
+ callback.apply(null, arguments)
+ resolve()
+ } catch (e) {
+ reject(e)
+ }
+ })
+ }))
+ }
+
+ return promise
},
skip (count) {
diff --git a/test/plugins/amqplib.spec.js b/test/plugins/amqplib.spec.js
new file mode 100644
index 00000000000..cabcbf720eb
--- /dev/null
+++ b/test/plugins/amqplib.spec.js
@@ -0,0 +1,275 @@
+'use strict'
+
+const agent = require('./agent')
+
+wrapIt()
+
+describe('Plugin', () => {
+ let plugin
+ let context
+ let connection
+ let channel
+
+ describe('amqplib', () => {
+ beforeEach(() => {
+ plugin = require('../../src/plugins/amqplib')
+ context = require('../../src/platform').context({ experimental: { asyncHooks: false } })
+ })
+
+ afterEach(() => {
+ agent.close()
+ connection.close()
+ })
+
+ describe('without configuration', () => {
+ describe('when using a callback', () => {
+ beforeEach(done => {
+ agent.load(plugin, 'amqplib')
+ .then(() => {
+ require('amqplib/callback_api')
+ .connect((err, conn) => {
+ connection = conn
+
+ if (err != null) {
+ return done(err)
+ }
+
+ conn.createChannel((err, ch) => {
+ channel = ch
+ done(err)
+ })
+ })
+ })
+ .catch(done)
+ })
+
+ describe('when sending commands', () => {
+ it('should do automatic instrumentation for immediate commands', done => {
+ agent
+ .use(traces => {
+ const span = traces[0][0]
+
+ expect(span).to.have.property('name', 'amqp.command')
+ expect(span).to.have.property('service', 'amqp')
+ expect(span).to.have.property('resource', 'queue.declare test')
+ expect(span).to.have.property('type', 'worker')
+ expect(span.meta).to.have.property('out.host', 'localhost')
+ expect(span.meta).to.have.property('out.port', '5672')
+ }, 2)
+ .then(done)
+ .catch(done)
+
+ channel.assertQueue('test', {}, () => {})
+ })
+
+ it('should do automatic instrumentation for queued commands', done => {
+ agent
+ .use(traces => {
+ const span = traces[0][0]
+
+ expect(span).to.have.property('name', 'amqp.command')
+ expect(span).to.have.property('service', 'amqp')
+ expect(span).to.have.property('resource', 'queue.delete test')
+ expect(span).to.have.property('type', 'worker')
+ expect(span.meta).to.have.property('out.host', 'localhost')
+ expect(span.meta).to.have.property('out.port', '5672')
+ }, 3)
+ .then(done)
+ .catch(done)
+
+ channel.assertQueue('test', {}, () => {})
+ channel.deleteQueue('test', () => {})
+ })
+
+ it('should handle errors', done => {
+ let error
+
+ agent
+ .use(traces => {
+ const span = traces[0][0]
+
+ expect(span).to.have.property('error', 1)
+ expect(span.meta).to.have.property('error.type', error.name)
+ expect(span.meta).to.have.property('error.msg', error.message)
+ expect(span.meta).to.have.property('error.stack', error.stack)
+ }, 2)
+ .then(done)
+ .catch(done)
+
+ try {
+ channel.deleteQueue(null, () => {})
+ } catch (e) {
+ error = e
+ }
+ })
+ })
+
+ describe('when publishing messages', () => {
+ it('should do automatic instrumentation', done => {
+ agent
+ .use(traces => {
+ const span = traces[0][0]
+
+ expect(span).to.have.property('name', 'amqp.command')
+ expect(span).to.have.property('service', 'amqp')
+ expect(span).to.have.property('resource', 'basic.publish exchange routingKey')
+ expect(span).to.have.property('type', 'worker')
+ expect(span.meta).to.have.property('out.host', 'localhost')
+ expect(span.meta).to.have.property('out.port', '5672')
+ expect(span.meta).to.have.property('span.kind', 'producer')
+ expect(span.meta).to.have.property('amqp.routingKey', 'routingKey')
+ }, 3)
+ .then(done)
+ .catch(done)
+
+ channel.assertExchange('exchange', 'direct', {}, () => {})
+ channel.publish('exchange', 'routingKey', Buffer.from('content'))
+ })
+
+ it('should handle errors', done => {
+ let error
+
+ agent
+ .use(traces => {
+ const span = traces[0][0]
+
+ expect(span).to.have.property('error', 1)
+ expect(span.meta).to.have.property('error.type', error.name)
+ expect(span.meta).to.have.property('error.msg', error.message)
+ expect(span.meta).to.have.property('error.stack', error.stack)
+ }, 2)
+ .then(done)
+ .catch(done)
+
+ try {
+ channel.sendToQueue('test', 'invalid')
+ } catch (e) {
+ error = e
+ }
+ })
+ })
+
+ describe('when consuming messages', () => {
+ it('should do automatic instrumentation', done => {
+ let consumerTag
+ let queue
+
+ agent
+ .use(traces => {
+ const span = traces[0][0]
+
+ expect(span).to.have.property('name', 'amqp.command')
+ expect(span).to.have.property('service', 'amqp')
+ expect(span).to.have.property('resource', `basic.deliver ${queue}`)
+ expect(span).to.have.property('type', 'worker')
+ expect(span.meta).to.have.property('out.host', 'localhost')
+ expect(span.meta).to.have.property('out.port', '5672')
+ expect(span.meta).to.have.property('span.kind', 'consumer')
+ expect(span.meta).to.have.property('amqp.consumerTag', consumerTag)
+ }, 5)
+ .then(done)
+ .catch(done)
+
+ channel.assertQueue('', {}, (err, ok) => {
+ if (err) return done(err)
+
+ queue = ok.queue
+
+ channel.sendToQueue(ok.queue, Buffer.from('content'))
+ channel.consume(ok.queue, () => {}, {}, (err, ok) => {
+ if (err) return done(err)
+ consumerTag = ok.consumerTag
+ })
+ })
+ })
+
+ it('should run the command callback in the parent context', done => {
+ context.run(() => {
+ context.set('foo', 'bar')
+
+ channel.assertQueue('', {}, (err, ok) => {
+ if (err) return done(err)
+
+ channel.consume(ok.queue, () => {}, {}, () => {
+ expect(context.get('current')).to.be.undefined
+ expect(context.get('foo')).to.equal('bar')
+ done()
+ })
+ })
+ })
+ })
+
+ it('should run the delivery callback in the current context', done => {
+ channel.assertQueue('', {}, (err, ok) => {
+ if (err) return done(err)
+
+ channel.sendToQueue(ok.queue, Buffer.from('content'))
+ channel.consume(ok.queue, () => {
+ expect(context.get('current')).to.not.be.undefined
+ done()
+ }, {}, err => err && done(err))
+ })
+ })
+ })
+ })
+
+ describe('when using a promise', () => {
+ beforeEach(() => {
+ return agent.load(plugin, 'amqplib')
+ .then(() => require('amqplib').connect())
+ .then(conn => (connection = conn))
+ .then(conn => conn.createChannel())
+ .then(ch => (channel = ch))
+ })
+
+ it('should run the callback in the parent context', done => {
+ context.run(() => {
+ context.set('foo', 'bar')
+
+ channel.assertQueue('test', {})
+ .then(() => {
+ expect(context.get('current')).to.be.undefined
+ expect(context.get('foo')).to.equal('bar')
+ done()
+ })
+ .catch(done)
+ })
+ })
+ })
+ })
+
+ describe('with configuration', () => {
+ beforeEach(done => {
+ agent.load(plugin, 'amqplib', { service: 'test' })
+ .then(() => {
+ require('amqplib/callback_api')
+ .connect((err, conn) => {
+ connection = conn
+
+ if (err !== null) {
+ return done(err)
+ }
+
+ conn.createChannel((err, ch) => {
+ channel = ch
+ done(err)
+ })
+ })
+ })
+ .catch(done)
+ })
+
+ it('should be configured with the correct values', done => {
+ agent
+ .use(traces => {
+ expect(traces[0][0]).to.have.property('service', 'test')
+ expect(traces[0][0]).to.have.property('resource', 'queue.declare test')
+ }, 2)
+ .then(done)
+ .catch(done)
+
+ channel.assertQueue('test', {}, () => {})
+ })
+ })
+ })
+})
diff --git a/test/plugins/elasticsearch.spec.js b/test/plugins/elasticsearch.spec.js
new file mode 100644
index 00000000000..74835a04753
--- /dev/null
+++ b/test/plugins/elasticsearch.spec.js
@@ -0,0 +1,252 @@
+'use strict'
+
+const agent = require('./agent')
+
+wrapIt()
+
+describe('Plugin', () => {
+ let plugin
+ let elasticsearch
+ let tracer
+
+ describe('elasticsearch', () => {
+ beforeEach(() => {
+ elasticsearch = require('elasticsearch')
+ plugin = require('../../src/plugins/elasticsearch')
+ tracer = require('../..')
+ })
+
+ afterEach(() => {
+ agent.close()
+ })
+
+ describe('without configuration', () => {
+ let client
+
+ beforeEach(() => {
+ client = new elasticsearch.Client({
+ host: 'localhost:9200'
+ })
+
+ return agent.load(plugin, 'elasticsearch')
+ })
+
+ it('should work without any living connection', done => {
+ agent
+ .use(traces => {
+ expect(traces[0][0].meta).to.not.have.property('out.host')
+ expect(traces[0][0].meta).to.not.have.property('out.port')
+ })
+ .then(done)
+ .catch(done)
+
+ client = new elasticsearch.Client({
+ hosts: [],
+ log: 'error'
+ })
+
+ client.ping(() => {})
+ })
+
+ it('should sanitize the resource name', done => {
+ agent
+ .use(traces => {
+ expect(traces[0][0]).to.have.property('resource', 'POST /logstash-?.?.?/_search')
+ })
+ .then(done)
+ .catch(done)
+
+ client.search({ index: 'logstash-2000.01.01' }, () => {})
+ })
+
+ it('should set the correct tags', done => {
+ agent
+ .use(traces => {
+ expect(traces[0][0].meta).to.have.property('db.type', 'elasticsearch')
+ expect(traces[0][0].meta).to.have.property('out.host', 'localhost')
+ expect(traces[0][0].meta).to.have.property('out.port', '9200')
+ expect(traces[0][0].meta).to.have.property('span.kind', 'client')
+ expect(traces[0][0].meta).to.have.property('elasticsearch.method', 'POST')
+ expect(traces[0][0].meta).to.have.property('elasticsearch.url', '/docs/_search')
+ expect(traces[0][0].meta).to.have.property('elasticsearch.body', '{"query":{"match_all":{}}}')
+ expect(traces[0][0].meta).to.have.property('elasticsearch.params', '{"sort":"name","size":100}')
+ })
+ .then(done)
+ .catch(done)
+
+ client.search({
+ index: 'docs',
+ sort: 'name',
+ size: 100,
+ body: {
+ query: {
+ match_all: {}
+ }
+ }
+ }, () => {})
+ })
+
+ it('should skip tags for unavailable fields', done => {
+ agent
+ .use(traces => {
+ expect(traces[0][0].meta).to.not.have.property('elasticsearch.body')
+ })
+ .then(done)
+ .catch(done)
+
+ client.ping(err => err && done(err))
+ })
+
+ describe('when using a callback', () => {
+ it('should do automatic instrumentation', done => {
+ agent
+ .use(traces => {
+ expect(traces[0][0]).to.have.property('service', 'elasticsearch')
+ expect(traces[0][0]).to.have.property('resource', 'HEAD /')
+ expect(traces[0][0]).to.have.property('type', 'db')
+ })
+ .then(done)
+ .catch(done)
+
+ client.ping(err => err && done(err))
+ })
+
+ it('should propagate context', done => {
+ agent
+ .use(traces => {
+ expect(traces[0][0]).to.have.property('parent_id')
+ expect(traces[0][0].parent_id).to.not.be.null
+ })
+ .then(done)
+ .catch(done)
+
+ tracer.trace('test', span => {
+ client.ping(() => span.finish())
+ })
+ })
+
+ it('should run the callback in the parent context', done => {
+ client.ping(error => {
+ expect(tracer.currentSpan()).to.be.null
+ done(error)
+ })
+ })
+
+ it('should handle errors', done => {
+ let error
+
+ agent
+ .use(traces => {
+ expect(traces[0][0].meta).to.have.property('error.type', error.name)
+ expect(traces[0][0].meta).to.have.property('error.msg', error.message)
+ expect(traces[0][0].meta).to.have.property('error.stack', error.stack)
+ })
+ .then(done)
+ .catch(done)
+
+ client.search({ index: 'invalid' }, err => {
+ error = err
+ })
+ })
+
+ it('should support aborting the query', () => {
+ expect(() => {
+ client.ping(() => {}).abort()
+ }).not.to.throw()
+ })
+ })
+
+ describe('when using a promise', () => {
+ it('should do automatic instrumentation', done => {
+ agent
+ .use(traces => {
+ expect(traces[0][0]).to.have.property('service', 'elasticsearch')
+ expect(traces[0][0]).to.have.property('resource', 'HEAD /')
+ expect(traces[0][0]).to.have.property('type', 'db')
+ })
+ .then(done)
+ .catch(done)
+
+ client.ping().catch(done)
+ })
+
+ it('should propagate context', done => {
+ agent
+ .use(traces => {
+ expect(traces[0][0]).to.have.property('parent_id')
+ expect(traces[0][0].parent_id).to.not.be.null
+ })
+ .then(done)
+ .catch(done)
+
+ tracer.trace('test', span => {
+ client.ping()
+ .then(() => span.finish())
+ .catch(done)
+ })
+ })
+
+ it('should run resolved promises in the parent context', () => {
+ return client.ping()
+ .then(() => {
+ expect(tracer.currentSpan()).to.be.null
+ })
+ })
+
+ it('should run rejected promises in the parent context', done => {
+ client.search({ index: 'invalid' })
+ .catch(() => {
+ expect(tracer.currentSpan()).to.be.null
+ done()
+ })
+ })
+
+ it('should handle errors', done => {
+ let error
+
+ agent.use(traces => {
+ expect(traces[0][0].meta).to.have.property('error.type', error.name)
+ expect(traces[0][0].meta).to.have.property('error.msg', error.message)
+ expect(traces[0][0].meta).to.have.property('error.stack', error.stack)
+ })
+ .then(done)
+ .catch(done)
+
+ client.search({ index: 'invalid' })
+ .catch(err => {
+ error = err
+ })
+ })
+
+ it('should support aborting the query', () => {
+ expect(() => {
+ client.ping().abort()
+ }).not.to.throw()
+ })
+ })
+ })
+
+ describe('with configuration', () => {
+ let client
+
+ beforeEach(() => {
+ client = new elasticsearch.Client({
+ host: 'localhost:9200'
+ })
+
+ return agent.load(plugin, 'elasticsearch', { service: 'test' })
+ })
+
+ it('should be configured with the correct values', done => {
+ agent
+ .use(traces => {
+ expect(traces[0][0]).to.have.property('service', 'test')
+ })
+ .then(done)
+ .catch(done)
+
+ client.ping(err => err && done(err))
+ })
+ })
+ })
+})
diff --git a/test/plugins/express.spec.js b/test/plugins/express.spec.js
index 02908e6181b..8ddf22e2bca 100644
--- a/test/plugins/express.spec.js
+++ b/test/plugins/express.spec.js
@@ -4,6 +4,8 @@ const axios = require('axios')
const getPort = require('get-port')
const agent = require('./agent')
+wrapIt()
+
describe('Plugin', () => {
let plugin
let context
@@ -14,7 +16,7 @@ describe('Plugin', () => {
beforeEach(() => {
plugin = require('../../src/plugins/express')
express = require('express')
- context = require('../../src/platform').context({ experimental: { asyncHooks: false } })
+ context = require('../../src/platform').context()
})
afterEach(() => {
diff --git a/test/plugins/graphql.spec.js b/test/plugins/graphql.spec.js
new file mode 100644
index 00000000000..b3c04edabb9
--- /dev/null
+++ b/test/plugins/graphql.spec.js
@@ -0,0 +1,377 @@
+'use strict'
+
+const agent = require('./agent')
+
+wrapIt()
+
+describe('Plugin', () => {
+ let plugin
+ let context
+ let graphql
+ let schema
+ let sort
+
+ describe('graphql', () => {
+ beforeEach(() => {
+ plugin = require('../../src/plugins/graphql')
+ graphql = require('graphql')
+ context = require('../../src/platform').context()
+
+ schema = new graphql.GraphQLSchema({
+ query: new graphql.GraphQLObjectType({
+ name: 'RootQueryType',
+ fields: {
+ hello: {
+ type: graphql.GraphQLString,
+ args: {
+ name: {
+ type: graphql.GraphQLString
+ }
+ },
+ resolve (obj, args) {
+ return args.name
+ }
+ },
+ human: {
+ type: new graphql.GraphQLObjectType({
+ name: 'Human',
+ fields: {
+ name: {
+ type: graphql.GraphQLString,
+ resolve (obj, args) {
+ return obj
+ }
+ }
+ }
+ }),
+ resolve (obj, args) {
+ return Promise.resolve('test')
+ }
+ }
+ }
+ })
+ })
+
+ sort = spans => spans.sort((a, b) => a.start.toString() > b.start.toString() ? 1 : -1)
+ })
+
+ afterEach(() => {
+ agent.close()
+ })
+
+ describe('without configuration', () => {
+ beforeEach(() => {
+ return agent.load(plugin, 'graphql')
+ })
+
+ it('should instrument operations', done => {
+ const source = `query MyQuery { hello(name: "world") }`
+
+ agent
+ .use(traces => {
+ const spans = sort(traces[0])
+
+ expect(spans).to.have.length(3)
+ expect(spans[0]).to.have.property('service', 'test-graphql')
+ expect(spans[0]).to.have.property('name', 'graphql.query')
+ expect(spans[0]).to.have.property('resource', 'query MyQuery')
+ expect(spans[0].meta).to.have.property('graphql.document', source)
+ })
+ .then(done)
+ .catch(done)
+
+ graphql.graphql(schema, source).catch(done)
+ })
+
+ it('should instrument fields', done => {
+ const source = `{ hello(name: "world") }`
+
+ agent
+ .use(traces => {
+ const spans = sort(traces[0])
+
+ expect(spans).to.have.length(3)
+ expect(spans[1]).to.have.property('service', 'test-graphql')
+ expect(spans[1]).to.have.property('name', 'graphql.field')
+ expect(spans[1]).to.have.property('resource', 'hello')
+ })
+ .then(done)
+ .catch(done)
+
+ graphql.graphql(schema, source).catch(done)
+ })
+
+ it('should instrument schema resolvers', done => {
+ const source = `{ hello(name: "world") }`
+
+ agent
+ .use(traces => {
+ const spans = sort(traces[0])
+
+ expect(spans).to.have.length(3)
+ expect(spans[2]).to.have.property('service', 'test-graphql')
+ expect(spans[2]).to.have.property('name', 'graphql.resolve')
+ expect(spans[2]).to.have.property('resource', 'hello')
+ })
+ .then(done)
+ .catch(done)
+
+ graphql.graphql(schema, source).catch(done)
+ })
+
+ it('should instrument nested field resolvers', done => {
+ const source = `{ human { name } }`
+
+ agent
+ .use(traces => {
+ const spans = sort(traces[0])
+
+ expect(spans).to.have.length(5)
+
+ const query = spans[0]
+ const humanField = spans[1]
+ const humanResolve = spans[2]
+ const humanNameField = spans[3]
+ const humanNameResolve = spans[4]
+
+ expect(query).to.have.property('name', 'graphql.query')
+ expect(query).to.have.property('resource', 'query')
+
+ expect(humanField).to.have.property('name', 'graphql.field')
+ expect(humanField).to.have.property('resource', 'human')
+ expect(humanField.parent_id.toString()).to.equal(query.span_id.toString())
+
+ expect(humanResolve).to.have.property('name', 'graphql.resolve')
+ expect(humanResolve).to.have.property('resource', 'human')
+ expect(humanResolve.parent_id.toString()).to.equal(humanField.span_id.toString())
+
+ expect(humanNameField).to.have.property('name', 'graphql.field')
+ expect(humanNameField).to.have.property('resource', 'human.name')
+ expect(humanNameField.parent_id.toString()).to.equal(humanField.span_id.toString())
+
+ expect(humanNameResolve).to.have.property('name', 'graphql.resolve')
+ expect(humanNameResolve).to.have.property('resource', 'human.name')
+ expect(humanNameResolve.parent_id.toString()).to.equal(humanNameField.span_id.toString())
+ })
+ .then(done)
+ .catch(done)
+
+ graphql.graphql(schema, source).catch(done)
+ })
+
+ it('should instrument the default field resolver', done => {
+ const schema = graphql.buildSchema(`
+ type Query {
+ hello: String
+ }
+ `)
+
+ const source = `{ hello }`
+
+ agent
+ .use(traces => {
+ const spans = sort(traces[0])
+
+ expect(spans).to.have.length(3)
+ expect(spans[2]).to.have.property('resource', 'hello')
+ })
+ .then(done)
+ .catch(done)
+
+ graphql.graphql(schema, source, { hello: 'world' }).catch(done)
+ })
+
+ it('should instrument a custom field resolver', done => {
+ const schema = graphql.buildSchema(`
+ type Query {
+ hello: String
+ }
+ `)
+
+ const source = `{ hello }`
+
+ const rootValue = { hello: 'world' }
+
+ const fieldResolver = (source, args, contextValue, info) => {
+ return source[info.fieldName]
+ }
+
+ agent
+ .use(traces => {
+ const spans = sort(traces[0])
+
+ expect(spans).to.have.length(3)
+ expect(spans[2]).to.have.property('resource', 'hello')
+ })
+ .then(done)
+ .catch(done)
+
+ graphql.graphql({ schema, source, rootValue, fieldResolver }).catch(done)
+ })
+
+ it('should not instrument schema resolvers multiple times', done => {
+ const source = `{ hello(name: "world") }`
+
+ agent.use(() => { // skip first call
+ agent
+ .use(traces => {
+ const spans = sort(traces[0])
+
+ expect(spans).to.have.length(3)
+ })
+ .then(done)
+ .catch(done)
+
+ graphql.graphql(schema, source).catch(done)
+ })
+
+ graphql.graphql(schema, source).catch(done)
+ })
+
+ it('should run the field resolver in the trace context', done => {
+ const schema = graphql.buildSchema(`
+ type Query {
+ hello: String
+ }
+ `)
+
+ const source = `{ hello }`
+
+ const rootValue = { hello: 'world' }
+
+ const fieldResolver = (source, args, contextValue, info) => {
+ expect(context.get('current')).to.not.be.undefined
+ done()
+ return source[info.fieldName]
+ }
+
+ graphql.graphql({ schema, source, rootValue, fieldResolver }).catch(done)
+ })
+
+ it('should run resolvers in the current context', done => {
+ const schema = graphql.buildSchema(`
+ type Query {
+ hello: String
+ }
+ `)
+
+ const source = `{ hello }`
+
+ const rootValue = {
+ hello () {
+ expect(context.get('current')).to.not.be.undefined
+ done()
+ }
+ }
+
+ graphql.graphql({ schema, source, rootValue }).catch(done)
+ })
+
+ it('should run returned promise in the parent context', () => {
+ const schema = graphql.buildSchema(`
+ type Query {
+ hello: String
+ }
+ `)
+
+ const source = `{ hello }`
+
+ const rootValue = {
+ hello () {
+ return Promise.resolve('test')
+ }
+ }
+
+ return graphql.graphql({ schema, source, rootValue })
+ .then(value => {
+ expect(value).to.have.nested.property('data.hello', 'test')
+ expect(context.get('current')).to.be.undefined
+ })
+ })
+
+ it('should handle exceptions', done => {
+ const error = new Error('test')
+
+ const schema = graphql.buildSchema(`
+ type Query {
+ hello: String
+ }
+ `)
+
+ const source = `{ hello }`
+
+ const fieldResolver = (source, args, contextValue, info) => {
+ throw error
+ }
+
+ agent
+ .use(traces => {
+ const spans = sort(traces[0])
+
+ expect(spans).to.have.length(3)
+ expect(spans[2]).to.have.property('error', 1)
+ expect(spans[2].meta).to.have.property('error.type', error.name)
+ expect(spans[2].meta).to.have.property('error.msg', error.message)
+ expect(spans[2].meta).to.have.property('error.stack', error.stack)
+ })
+ .then(done)
+ .catch(done)
+
+ graphql.graphql({ schema, source, fieldResolver }).catch(done)
+ })
+
+ it('should handle rejected promises', done => {
+ const error = new Error('test')
+
+ const schema = graphql.buildSchema(`
+ type Query {
+ hello: String
+ }
+ `)
+
+ const source = `{ hello }`
+
+ const fieldResolver = (source, args, contextValue, info) => {
+ return Promise.reject(error)
+ }
+
+ agent
+ .use(traces => {
+ const spans = sort(traces[0])
+
+ expect(spans).to.have.length(3)
+ expect(spans[2]).to.have.property('error', 1)
+ expect(spans[2].meta).to.have.property('error.type', error.name)
+ expect(spans[2].meta).to.have.property('error.msg', error.message)
+ expect(spans[2].meta).to.have.property('error.stack', error.stack)
+ })
+ .then(done)
+ .catch(done)
+
+ graphql.graphql({ schema, source, fieldResolver }).catch(done)
+ })
+ })
+
+ describe('with configuration', () => {
+ beforeEach(() => {
+ return agent.load(plugin, 'graphql', { service: 'test' })
+ })
+
+ it('should be configured with the correct values', done => {
+ const source = `{ hello(name: "world") }`
+
+ agent
+ .use(traces => {
+ const spans = sort(traces[0])
+
+ expect(spans).to.have.length(3)
+ expect(spans[2]).to.have.property('service', 'test')
+ })
+ .then(done)
+ .catch(done)
+
+ graphql.graphql(schema, source).catch(done)
+ })
+ })
+ })
+})
diff --git a/test/plugins/http.spec.js b/test/plugins/http.spec.js
index f74fa8e51cb..bd4adee1f6c 100644
--- a/test/plugins/http.spec.js
+++ b/test/plugins/http.spec.js
@@ -3,6 +3,8 @@
const getPort = require('get-port')
const agent = require('./agent')
+wrapIt()
+
describe('Plugin', () => {
let plugin
let express
diff --git a/test/plugins/mongodb-core.spec.js b/test/plugins/mongodb-core.spec.js
index 109b65c4d74..e5d8b5fc499 100644
--- a/test/plugins/mongodb-core.spec.js
+++ b/test/plugins/mongodb-core.spec.js
@@ -2,6 +2,8 @@
const agent = require('./agent')
+wrapIt()
+
describe('Plugin', () => {
let plugin
let mongo
@@ -44,7 +46,7 @@ describe('Plugin', () => {
mongo = require('mongodb-core')
plugin = require('../../src/plugins/mongodb-core')
platform = require('../../src/platform')
- context = platform.context({ experimental: { asyncHooks: false } })
+ context = platform.context()
collection = platform.id().toString()
diff --git a/test/plugins/mysql.spec.js b/test/plugins/mysql.spec.js
index 6375df7f107..664c1f53e18 100644
--- a/test/plugins/mysql.spec.js
+++ b/test/plugins/mysql.spec.js
@@ -2,6 +2,8 @@
const agent = require('./agent')
+wrapIt()
+
describe('Plugin', () => {
let plugin
let mysql
@@ -11,7 +13,7 @@ describe('Plugin', () => {
beforeEach(() => {
mysql = require('mysql')
plugin = require('../../src/plugins/mysql')
- context = require('../../src/platform').context({ experimental: { asyncHooks: false } })
+ context = require('../../src/platform').context()
})
afterEach(() => {
@@ -115,7 +117,6 @@ describe('Plugin', () => {
connection.query('INVALID', (err, results, fields) => {
error = err
- expect(error).to.be.an('error')
})
})
diff --git a/test/plugins/mysql2.spec.js b/test/plugins/mysql2.spec.js
index 5b63307b3a9..e16ac49a3a9 100644
--- a/test/plugins/mysql2.spec.js
+++ b/test/plugins/mysql2.spec.js
@@ -2,6 +2,8 @@
const agent = require('./agent')
+wrapIt()
+
describe('Plugin', () => {
let plugin
let mysql2
@@ -10,7 +12,7 @@ describe('Plugin', () => {
describe('mysql2', () => {
beforeEach(() => {
plugin = require('../../src/plugins/mysql2')
- context = require('../../src/platform').context({ experimental: { asyncHooks: false } })
+ context = require('../../src/platform').context()
})
afterEach(() => {
@@ -116,7 +118,6 @@ describe('Plugin', () => {
connection.query('INVALID', (err, results, fields) => {
error = err
- expect(error).to.be.an('error')
})
})
diff --git a/test/plugins/pg.spec.js b/test/plugins/pg.spec.js
index 5b23e1fec31..b173dfe241d 100644
--- a/test/plugins/pg.spec.js
+++ b/test/plugins/pg.spec.js
@@ -2,6 +2,8 @@
const agent = require('./agent')
+wrapIt()
+
describe('Plugin', () => {
let plugin
let pg
diff --git a/test/plugins/redis.spec.js b/test/plugins/redis.spec.js
index 5a2deebba18..dfed4ff87f7 100644
--- a/test/plugins/redis.spec.js
+++ b/test/plugins/redis.spec.js
@@ -2,6 +2,8 @@
const agent = require('./agent')
+wrapIt()
+
describe('Plugin', () => {
let plugin
let redis
@@ -12,7 +14,7 @@ describe('Plugin', () => {
beforeEach(() => {
redis = require('redis')
plugin = require('../../src/plugins/redis')
- context = require('../../src/platform').context({ experimental: { asyncHooks: false } })
+ context = require('../../src/platform').context()
})
afterEach(() => {
diff --git a/test/setup.js b/test/setup.js
index 358b436b8ef..bba60ea7183 100644
--- a/test/setup.js
+++ b/test/setup.js
@@ -10,6 +10,8 @@ const pg = require('pg')
const mysql = require('mysql')
const redis = require('redis')
const mongo = require('mongodb-core')
+const elasticsearch = require('elasticsearch')
+const amqplib = require('amqplib/callback_api')
const platform = require('../src/platform')
const node = require('../src/platform/node')
@@ -27,6 +29,7 @@ global.sinon = sinon
global.expect = chai.expect
global.proxyquire = proxyquire
global.nock = nock
+global.wrapIt = wrapIt
platform.use(node)
@@ -41,7 +44,9 @@ function waitForServices () {
waitForPostgres(),
waitForMysql(),
waitForRedis(),
- waitForMongo()
+ waitForMongo(),
+ waitForElasticsearch(),
+ waitForRabbitMQ()
])
}
@@ -144,3 +149,80 @@ function waitForMongo () {
})
})
}
+
+function waitForElasticsearch () {
+ return new Promise((resolve, reject) => {
+ const operation = retry.operation(retryOptions)
+
+ operation.attempt(currentAttempt => {
+ const client = new elasticsearch.Client({
+ host: 'localhost:9200'
+ })
+
+ client.ping((err) => {
+ if (operation.retry(err)) return
+ if (err) reject(err)
+
+ resolve()
+ })
+ })
+ })
+}
+
+function waitForRabbitMQ () {
+ return new Promise((resolve, reject) => {
+ const operation = retry.operation(retryOptions)
+
+ operation.attempt(currentAttempt => {
+ amqplib
+ .connect((err, conn) => {
+ if (operation.retry(err)) return
+ if (err) reject(err)
+
+ conn.close(() => resolve())
+ })
+ })
+ })
+}
+
+function wrapIt () {
+ const it = global.it
+
+ global.it = function (title, fn) {
+ if (fn.length > 0) {
+ return it.call(this, title, function (done) {
+ const context = platform.context()
+
+ arguments[0] = context.bind(done)
+
+ return fn.apply(this, arguments)
+ })
+ } else {
+ return it.call(this, title, function () {
+ const context = platform.context()
+ const defer = {}
+
+ defer.promise = new Promise((resolve, reject) => {
+ defer.resolve = context.bind(resolve)
+ defer.reject = context.bind(reject)
+ })
+
+ const result = fn.apply(this, arguments)
+
+ if (result && result.then) {
+ return result
+ .then(function () {
+ defer.resolve.apply(defer, arguments)
+ return defer.promise
+ })
+ .catch(function () {
+ defer.reject.apply(defer, arguments)
+ return defer.promise
+ })
+ }
+
+ return result
+ })
+ }
+ }
+}
diff --git a/test/tracer.spec.js b/test/tracer.spec.js
index a2df00be573..80fee86cb96 100644
--- a/test/tracer.spec.js
+++ b/test/tracer.spec.js
@@ -1,10 +1,13 @@
'use strict'
+const EventEmitter = require('events')
const Span = require('../src/opentracing/span')
const SpanContext = require('../src/opentracing/span_context')
const Config = require('../src/config')
const platform = require('../src/platform')
+wrapIt()
+
describe('Tracer', () => {
let Tracer
let tracer
@@ -16,8 +19,8 @@ describe('Tracer', () => {
beforeEach(() => {
config = new Config({ service: 'service' })
context = platform.context(config)
- sinon.stub(context, 'bind')
- sinon.stub(context, 'bindEmitter')
+ sinon.spy(context, 'bind')
+ sinon.spy(context, 'bindEmitter')
instrumenter = {
use: sinon.spy(),
@@ -28,6 +31,8 @@ describe('Tracer', () => {
Tracer = proxyquire('../src/tracer', {
'./instrumenter': Instrumenter
})
+
+ tracer = new Tracer(config)
})
afterEach(() => {
@@ -37,8 +42,6 @@ describe('Tracer', () => {
describe('trace', () => {
it('should run the callback with the new span', done => {
- tracer = new Tracer(config)
-
tracer.trace('name', current => {
expect(current).to.be.instanceof(Span)
done()
@@ -46,8 +49,6 @@ describe('Tracer', () => {
})
it('should use the parent context', done => {
- tracer = new Tracer(config)
-
tracer.trace('parent', parent => {
tracer.trace('child', child => {
expect(child.context()).to.have.property('parentId', parent.context().spanId)
@@ -57,8 +58,6 @@ describe('Tracer', () => {
})
it('should support explicitly creating a root span', done => {
- tracer = new Tracer(config)
-
tracer.trace('parent', parent => {
tracer.trace('child', { childOf: null }, child => {
expect(child.context()).to.have.property('parentId', null)
@@ -68,8 +67,6 @@ describe('Tracer', () => {
})
it('should set default tags', done => {
- tracer = new Tracer(config)
-
tracer.trace('name', current => {
expect(current._tags).to.have.property('service.name', 'service')
expect(current._tags).to.have.property('resource.name', 'name')
@@ -79,8 +76,6 @@ describe('Tracer', () => {
})
it('should support service option', done => {
- tracer = new Tracer(config)
-
tracer.trace('name', { service: 'test' }, current => {
expect(current._tags).to.have.property('service.name', 'test')
done()
@@ -88,8 +83,6 @@ describe('Tracer', () => {
})
it('should support resource option', done => {
- tracer = new Tracer(config)
-
tracer.trace('name', { resource: 'test' }, current => {
expect(current._tags).to.have.property('resource.name', 'test')
done()
@@ -97,8 +90,6 @@ describe('Tracer', () => {
})
it('should support type option', done => {
- tracer = new Tracer(config)
-
tracer.trace('name', { type: 'test' }, current => {
expect(current._tags).to.have.property('span.type', 'test')
done()
@@ -110,8 +101,6 @@ describe('Tracer', () => {
'foo': 'bar'
}
- tracer = new Tracer(config)
-
tracer.trace('name', { tags }, current => {
expect(current._tags).to.have.property('foo', 'bar')
done()
@@ -124,8 +113,6 @@ describe('Tracer', () => {
spanId: 5678
})
- tracer = new Tracer(config)
-
tracer.trace('name', { childOf }, current => {
expect(current.context().traceId).to.equal(childOf.traceId)
expect(current.context().parentId).to.equal(childOf.spanId)
@@ -136,8 +123,6 @@ describe('Tracer', () => {
describe('currentSpan', () => {
it('should return the current span', done => {
- tracer = new Tracer(config)
-
tracer.trace('name', current => {
expect(tracer.currentSpan()).to.equal(current)
done()
@@ -145,8 +130,6 @@ describe('Tracer', () => {
})
it('should return null when there is no current span', () => {
- tracer = new Tracer(config)
-
expect(tracer.currentSpan()).to.be.null
})
})
@@ -155,8 +138,6 @@ describe('Tracer', () => {
it('should bind a function to the context', done => {
const callback = () => {}
- tracer = new Tracer(config)
-
tracer.trace('name', current => {
tracer.bind(callback)
expect(context.bind).to.have.been.calledWith(callback)
@@ -166,11 +147,13 @@ describe('Tracer', () => {
})
describe('bindEmitter', () => {
- it('should bind an emitter to the context', done => {
- const emitter = 'emitter'
+ let emitter
- tracer = new Tracer(config)
+ beforeEach(() => {
+ emitter = new EventEmitter()
+ })
+ it('should bind an emitter to the context', done => {
tracer.trace('name', current => {
tracer.bindEmitter(emitter)
expect(context.bindEmitter).to.have.been.calledWith(emitter)