dd-trace
Version:
Datadog APM tracing client for JavaScript
158 lines (124 loc) • 4.24 kB
JavaScript
const TracingPlugin = require('../../dd-trace/src/plugins/tracing')
const dc = require('dc-polyfill')
const collapsedPathSym = Symbol('collapsedPaths')
class GraphQLResolvePlugin extends TracingPlugin {
static get id () { return 'graphql' }
static get operation () { return 'resolve' }
start ({ info, context, args }) {
const path = getPath(info, this.config)
if (!shouldInstrument(this.config, path)) return
const computedPathString = path.join('.')
if (this.config.collapse) {
if (context.fields[computedPathString]) return
if (!context[collapsedPathSym]) {
context[collapsedPathSym] = {}
} else if (context[collapsedPathSym][computedPathString]) {
return
}
context[collapsedPathSym][computedPathString] = true
}
const document = context.source
const fieldNode = info.fieldNodes.find(fieldNode => fieldNode.kind === 'Field')
const loc = this.config.source && document && fieldNode && fieldNode.loc
const source = loc && document.slice(loc.start, loc.end)
const span = this.startSpan('graphql.resolve', {
service: this.config.service,
resource: `${info.fieldName}:${info.returnType}`,
type: 'graphql',
meta: {
'graphql.field.name': info.fieldName,
'graphql.field.path': computedPathString,
'graphql.field.type': info.returnType.name,
'graphql.source': source
}
})
if (fieldNode && this.config.variables && fieldNode.arguments) {
const variables = this.config.variables(info.variableValues)
fieldNode.arguments
.filter(arg => arg.value?.name && arg.value.kind === 'Variable' && variables[arg.value.name.value])
.forEach(arg => {
const name = arg.value.name.value
span.setTag(`graphql.variables.${name}`, variables[name])
})
}
if (this.resolverStartCh.hasSubscribers) {
this.resolverStartCh.publish({ context, resolverInfo: getResolverInfo(info, args) })
}
}
constructor (...args) {
super(...args)
this.addTraceSub('updateField', ({ field, info, err }) => {
const path = getPath(info, this.config)
if (!shouldInstrument(this.config, path)) return
const span = this.activeSpan
field.finishTime = span._getTime ? span._getTime() : 0
field.error = field.error || err
})
this.resolverStartCh = dc.channel('datadog:graphql:resolver:start')
}
configure (config) {
// this will disable resolve subscribers if `config.depth` is set to 0
super.configure(config.depth === 0 ? false : config)
}
finish (finishTime) {
this.activeSpan.finish(finishTime)
}
}
// helpers
function shouldInstrument (config, path) {
let depth = 0
for (const item of path) {
if (typeof item === 'string') {
depth += 1
}
}
return config.depth < 0 || config.depth >= depth
}
function getPath (info, config) {
const responsePathAsArray = config.collapse
? withCollapse(pathToArray)
: pathToArray
return responsePathAsArray(info && info.path)
}
function pathToArray (path) {
const flattened = []
let curr = path
while (curr) {
flattened.push(curr.key)
curr = curr.prev
}
return flattened.reverse()
}
function withCollapse (responsePathAsArray) {
return function () {
return responsePathAsArray.apply(this, arguments)
.map(segment => typeof segment === 'number' ? '*' : segment)
}
}
function getResolverInfo (info, args) {
let resolverInfo = null
const resolverVars = {}
if (args) {
Object.assign(resolverVars, args)
}
let hasResolvers = false
const directives = info.fieldNodes?.[0]?.directives
if (Array.isArray(directives)) {
for (const directive of directives) {
const argList = {}
for (const argument of directive.arguments) {
argList[argument.name.value] = argument.value.value
}
if (directive.arguments.length > 0) {
hasResolvers = true
resolverVars[directive.name.value] = argList
}
}
}
if (hasResolvers || args && Object.keys(resolverVars).length) {
resolverInfo = { [info.fieldName]: resolverVars }
}
return resolverInfo
}
module.exports = GraphQLResolvePlugin