UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

179 lines (134 loc) 4.8 kB
'use strict' const dc = require('dc-polyfill') const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const collapsedPathSym = Symbol('collapsedPaths') class GraphQLResolvePlugin extends TracingPlugin { static id = 'graphql' static operation = 'resolve' start (fieldCtx) { const { info, rootCtx, args, path: pathAsArray, pathString } = fieldCtx const path = getPath(this.config, pathAsArray) // we need to get the parent span to the field if it exists for correct span parenting // of nested fields const parentField = getParentField(rootCtx, pathString) const childOf = parentField?.ctx?.currentStore?.span fieldCtx.parent = parentField if (!shouldInstrument(this.config, path)) return const computedPathString = path.join('.') if (this.config.collapse) { if (rootCtx.fields[computedPathString]) return if (!rootCtx[collapsedPathSym]) { rootCtx[collapsedPathSym] = Object.create(null) } else if (rootCtx[collapsedPathSym][computedPathString]) { return } rootCtx[collapsedPathSym][computedPathString] = true } const document = rootCtx.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}`, childOf, type: 'graphql', meta: { 'graphql.field.name': info.fieldName, 'graphql.field.path': computedPathString, 'graphql.field.type': info.returnType.name, 'graphql.source': source, }, }, fieldCtx) if (fieldNode && this.config.variables && fieldNode.arguments) { const variables = this.config.variables(info.variableValues) for (const arg of fieldNode.arguments) { if (arg.value?.name && arg.value.kind === 'Variable' && variables[arg.value.name.value]) { const name = arg.value.name.value span.setTag(`graphql.variables.${name}`, variables[name]) } } } if (this.resolverStartCh.hasSubscribers) { this.resolverStartCh.publish({ ctx: rootCtx, resolverInfo: getResolverInfo(info, args) }) } return fieldCtx.currentStore } constructor (...args) { super(...args) this.addTraceSub('updateField', (ctx) => { const { field, error, path: pathAsArray } = ctx const path = getPath(this.config, pathAsArray) if (!shouldInstrument(this.config, path)) return const span = ctx?.currentStore?.span || this.activeSpan field.finishTime = span._getTime ? span._getTime() : 0 field.error = field.error || error }) 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 (ctx) { const { finishTime } = ctx const span = ctx?.currentStore?.span || this.activeSpan span.finish(finishTime) return ctx.parentStore } } // 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 (config, pathAsArray) { return config.collapse ? withCollapse(pathAsArray) : pathAsArray } function withCollapse (pathAsArray) { return pathAsArray.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 } function getParentField (parentCtx, pathToString) { let current = pathToString while (current) { const lastJoin = current.lastIndexOf('.') if (lastJoin === -1) break current = current.slice(0, lastJoin) const field = parentCtx.fields[current] if (field) return field } return null } module.exports = GraphQLResolvePlugin