dd-trace
Version:
Datadog APM tracing client for JavaScript
286 lines (228 loc) • 8.64 kB
JavaScript
'use strict'
const { errorMonitor } = require('events')
const {
channel,
addHook,
AsyncResource
} = require('./helpers/instrument')
const shimmer = require('../../datadog-shimmer')
function findCallbackIndex (args, lowerbound = 2) {
for (let i = args.length - 1; i >= lowerbound; i--) {
if (typeof args[i] === 'function') return i
}
return -1
}
// handles n1ql and string queries
function getQueryResource (q) {
return q && (typeof q === 'string' ? q : q.statement)
}
function wrapAllNames (names, action) {
names.forEach(name => action(name))
}
// semver >=2 <3
function wrapMaybeInvoke (_maybeInvoke) {
const wrapped = function (fn, args) {
if (!Array.isArray(args)) return _maybeInvoke.apply(this, arguments)
const callbackIndex = args.length - 1
const callback = args[callbackIndex]
if (typeof callback === 'function') {
args[callbackIndex] = AsyncResource.bind(callback)
}
return _maybeInvoke.apply(this, arguments)
}
return wrapped
}
function wrapQuery (query) {
const wrapped = function (q, params, callback) {
if (typeof arguments[arguments.length - 1] === 'function') {
arguments[arguments.length - 1] = AsyncResource.bind(arguments[arguments.length - 1])
}
return query.apply(this, arguments)
}
return wrapped
}
function wrap (prefix, fn) {
const startCh = channel(prefix + ':start')
const finishCh = channel(prefix + ':finish')
const errorCh = channel(prefix + ':error')
const wrapped = function () {
if (!startCh.hasSubscribers) {
return fn.apply(this, arguments)
}
const callbackIndex = findCallbackIndex(arguments)
if (callbackIndex < 0) return fn.apply(this, arguments)
const callbackResource = new AsyncResource('bound-anonymous-fn')
const asyncResource = new AsyncResource('bound-anonymous-fn')
return asyncResource.runInAsyncScope(() => {
const cb = callbackResource.bind(arguments[callbackIndex])
startCh.publish({ bucket: { name: this.name || this._name }, seedNodes: this._dd_hosts })
arguments[callbackIndex] = shimmer.wrapFunction(cb, cb => asyncResource.bind(function (error, result) {
if (error) {
errorCh.publish(error)
}
finishCh.publish(result)
return cb.apply(this, arguments)
}))
try {
return fn.apply(this, arguments)
} catch (error) {
error.stack // trigger getting the stack at the original throwing point
errorCh.publish(error)
throw error
}
})
}
return wrapped
}
// semver >=3
function wrapCBandPromise (fn, name, startData, thisArg, args) {
const startCh = channel(`apm:couchbase:${name}:start`)
const finishCh = channel(`apm:couchbase:${name}:finish`)
const errorCh = channel(`apm:couchbase:${name}:error`)
if (!startCh.hasSubscribers) return fn.apply(thisArg, args)
const asyncResource = new AsyncResource('bound-anonymous-fn')
const callbackResource = new AsyncResource('bound-anonymous-fn')
return asyncResource.runInAsyncScope(() => {
startCh.publish(startData)
try {
const cbIndex = findCallbackIndex(args, 1)
if (cbIndex >= 0) {
// v3 offers callback or promises event handling
// NOTE: this does not work with v3.2.0-3.2.1 cluster.query, as there is a bug in the couchbase source code
const cb = callbackResource.bind(args[cbIndex])
args[cbIndex] = shimmer.wrapFunction(cb, cb => asyncResource.bind(function (error, result) {
if (error) {
errorCh.publish(error)
}
finishCh.publish({ result })
return cb.apply(thisArg, arguments)
}))
}
const res = fn.apply(thisArg, args)
// semver >=3 will always return promise by default
res.then(
asyncResource.bind((result) => finishCh.publish({ result })),
asyncResource.bind((err) => errorCh.publish(err)))
return res
} catch (e) {
e.stack
errorCh.publish(e)
throw e
}
})
}
function wrapWithName (name) {
return function (operation) {
return function () { // no arguments used by us
return wrapCBandPromise(operation, name, {
collection: { name: this._name || '_default' },
bucket: { name: this._scope._bucket._name },
seedNodes: this._dd_connStr
}, this, arguments)
}
}
}
function wrapV3Query (query) {
return function (q) {
const resource = getQueryResource(q)
return wrapCBandPromise(query, 'query', { resource, seedNodes: this._connStr }, this, arguments)
}
}
// semver >=2 <3
addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^2.6.12'] }, Bucket => {
const startCh = channel('apm:couchbase:query:start')
const finishCh = channel('apm:couchbase:query:finish')
const errorCh = channel('apm:couchbase:query:error')
shimmer.wrap(Bucket.prototype, '_maybeInvoke', maybeInvoke => wrapMaybeInvoke(maybeInvoke))
shimmer.wrap(Bucket.prototype, 'query', query => wrapQuery(query))
shimmer.wrap(Bucket.prototype, '_n1qlReq', _n1qlReq => function (host, q, adhoc, emitter) {
if (!startCh.hasSubscribers) {
return _n1qlReq.apply(this, arguments)
}
if (!emitter || !emitter.once) return _n1qlReq.apply(this, arguments)
const n1qlQuery = getQueryResource(q)
const asyncResource = new AsyncResource('bound-anonymous-fn')
return asyncResource.runInAsyncScope(() => {
startCh.publish({ resource: n1qlQuery, bucket: { name: this.name || this._name }, seedNodes: this._dd_hosts })
emitter.once('rows', asyncResource.bind(() => {
finishCh.publish()
}))
emitter.once(errorMonitor, asyncResource.bind((error) => {
errorCh.publish(error)
finishCh.publish()
}))
try {
return _n1qlReq.apply(this, arguments)
} catch (err) {
err.stack // trigger getting the stack at the original throwing point
errorCh.publish(err)
throw err
}
})
})
wrapAllNames(['upsert', 'insert', 'replace', 'append', 'prepend'], name => {
shimmer.wrap(Bucket.prototype, name, fn => wrap(`apm:couchbase:${name}`, fn))
})
return Bucket
})
addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^2.6.12'] }, Cluster => {
shimmer.wrap(Cluster.prototype, '_maybeInvoke', maybeInvoke => wrapMaybeInvoke(maybeInvoke))
shimmer.wrap(Cluster.prototype, 'query', query => wrapQuery(query))
shimmer.wrap(Cluster.prototype, 'openBucket', openBucket => {
return function () {
const bucket = openBucket.apply(this, arguments)
const hosts = this.dsnObj.hosts
bucket._dd_hosts = hosts.map(hostAndPort => hostAndPort.join(':')).join(',')
return bucket
}
})
return Cluster
})
// semver >=3 <3.2.0
addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^3.0.7', '^3.1.3'] }, Bucket => {
shimmer.wrap(Bucket.prototype, 'collection', getCollection => {
return function () {
const collection = getCollection.apply(this, arguments)
const connStr = this._cluster._connStr
collection._dd_connStr = connStr
return collection
}
})
return Bucket
})
addHook({ name: 'couchbase', file: 'lib/collection.js', versions: ['^3.0.7', '^3.1.3'] }, Collection => {
wrapAllNames(['upsert', 'insert', 'replace'], name => {
shimmer.wrap(Collection.prototype, name, wrapWithName(name))
})
return Collection
})
addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^3.0.7', '^3.1.3'] }, Cluster => {
shimmer.wrap(Cluster.prototype, 'query', wrapV3Query)
return Cluster
})
// semver >=3.2.2
// NOTE: <3.2.2 segfaults on cluster.close() https://issues.couchbase.com/browse/JSCBC-936
addHook({ name: 'couchbase', file: 'dist/collection.js', versions: ['>=3.2.2'] }, collection => {
const Collection = collection.Collection
wrapAllNames(['upsert', 'insert', 'replace'], name => {
shimmer.wrap(Collection.prototype, name, wrapWithName(name))
})
return collection
})
addHook({ name: 'couchbase', file: 'dist/bucket.js', versions: ['>=3.2.2'] }, bucket => {
const Bucket = bucket.Bucket
shimmer.wrap(Bucket.prototype, 'collection', getCollection => {
return function () {
const collection = getCollection.apply(this, arguments)
const connStr = this._cluster._connStr
collection._dd_connStr = connStr
return collection
}
})
return bucket
})
addHook({ name: 'couchbase', file: 'dist/cluster.js', versions: ['>=3.2.2'] }, (cluster) => {
const Cluster = cluster.Cluster
shimmer.wrap(Cluster.prototype, 'query', wrapV3Query)
return cluster
})