diff --git a/packages/datadog-instrumentations/src/graphql.js b/packages/datadog-instrumentations/src/graphql.js index 7b3c5e17c9e..30f3c8cc137 100644 --- a/packages/datadog-instrumentations/src/graphql.js +++ b/packages/datadog-instrumentations/src/graphql.js @@ -2,8 +2,7 @@ const { addHook, - channel, - AsyncResource + channel } = require('./helpers/instrument') const shimmer = require('../../datadog-shimmer') @@ -89,29 +88,28 @@ function wrapParse (parse) { return parse.apply(this, arguments) } - const asyncResource = new AsyncResource('bound-anonymous-fn') - - return asyncResource.runInAsyncScope(() => { - parseStartCh.publish() - let document + const ctx = { source } + return parseStartCh.runStores(ctx, () => { try { - document = parse.apply(this, arguments) - const operation = getOperation(document) + ctx.document = parse.apply(this, arguments) + const operation = getOperation(ctx.document) - if (!operation) return document + if (!operation) return ctx.document if (source) { - documentSources.set(document, source.body || source) + documentSources.set(ctx.document, source.body || source) } + ctx.docSource = documentSources.get(ctx.document) - return document + return ctx.document } catch (err) { err.stack - parseErrorCh.publish(err) + ctx.error = err + parseErrorCh.publish(ctx) throw err } finally { - parseFinishCh.publish({ source, document, docSource: documentSources.get(document) }) + parseFinishCh.publish(ctx) } }) } @@ -123,25 +121,25 @@ function wrapValidate (validate) { return validate.apply(this, arguments) } - const asyncResource = new AsyncResource('bound-anonymous-fn') - - return asyncResource.runInAsyncScope(() => { - validateStartCh.publish({ docSource: documentSources.get(document), document }) - + const ctx = { docSource: documentSources.get(document), document } + return validateStartCh.runStores(ctx, () => { let errors try { errors = validate.apply(this, arguments) if (errors && errors[0]) { - validateErrorCh.publish(errors && errors[0]) + ctx.error = errors && errors[0] + validateErrorCh.publish(ctx) } return errors } catch (err) { err.stack - validateErrorCh.publish(err) + ctx.error = err + validateErrorCh.publish(ctx) throw err } finally { - validateFinishCh.publish({ document, errors }) + ctx.errors = errors + validateFinishCh.publish(ctx) } }) } @@ -155,15 +153,22 @@ function wrapExecute (execute) { return exe.apply(this, arguments) } - const asyncResource = new AsyncResource('bound-anonymous-fn') - return asyncResource.runInAsyncScope(() => { - const args = normalizeArgs(arguments, defaultFieldResolver) - const schema = args.schema - const document = args.document - const source = documentSources.get(document) - const contextValue = args.contextValue - const operation = getOperation(document, args.operationName) - + const args = normalizeArgs(arguments, defaultFieldResolver) + const schema = args.schema + const document = args.document + const source = documentSources.get(document) + const contextValue = args.contextValue + const operation = getOperation(document, args.operationName) + + const ctx = { + operation, + args, + docSource: documentSources.get(document), + source, + fields: {}, + abortController: new AbortController() + } + return startExecuteCh.runStores(ctx, () => { if (contexts.has(contextValue)) { return exe.apply(this, arguments) } @@ -173,26 +178,20 @@ function wrapExecute (execute) { wrapFields(schema._mutationType) } - startExecuteCh.publish({ - operation, - args, - docSource: documentSources.get(document) - }) - - const context = { source, asyncResource, fields: {}, abortController: new AbortController() } - - contexts.set(contextValue, context) + contexts.set(contextValue, ctx) - return callInAsyncScope(exe, asyncResource, this, arguments, context.abortController, (err, res) => { - if (finishResolveCh.hasSubscribers) finishResolvers(context) + return callInAsyncScope(exe, this, arguments, ctx.abortController, (err, res) => { + if (finishResolveCh.hasSubscribers) finishResolvers(ctx) const error = err || (res && res.errors && res.errors[0]) if (error) { - executeErrorCh.publish(error) + ctx.error = error + executeErrorCh.publish(ctx) } - finishExecuteCh.publish({ res, args, context }) + ctx.res = res + finishExecuteCh.publish(ctx) }) }) } @@ -205,14 +204,17 @@ function wrapResolve (resolve) { function resolveAsync (source, args, contextValue, info) { if (!startResolveCh.hasSubscribers) return resolve.apply(this, arguments) - const context = contexts.get(contextValue) + const ctx = contexts.get(contextValue) - if (!context) return resolve.apply(this, arguments) + if (!ctx) return resolve.apply(this, arguments) - const field = assertField(context, info, args) + const field = assertField(ctx, info, args) - return callInAsyncScope(resolve, field.asyncResource, this, arguments, context.abortController, (err) => { - updateFieldCh.publish({ field, info, err }) + return callInAsyncScope(resolve, this, arguments, ctx.abortController, (err) => { + field.ctx.error = err + field.ctx.info = info + field.ctx.field = field + updateFieldCh.publish(field.ctx) }) } @@ -221,32 +223,30 @@ function wrapResolve (resolve) { return resolveAsync } -function callInAsyncScope (fn, aR, thisArg, args, abortController, cb) { +function callInAsyncScope (fn, thisArg, args, abortController, cb) { cb = cb || (() => {}) - return aR.runInAsyncScope(() => { - if (abortController?.signal.aborted) { - cb(null, null) - throw new AbortError('Aborted') - } + if (abortController?.signal.aborted) { + cb(null, null) + throw new AbortError('Aborted') + } - try { - const result = fn.apply(thisArg, args) - if (result && typeof result.then === 'function') { - // bind callback to this scope - result.then( - aR.bind(res => cb(null, res)), - aR.bind(err => cb(err)) - ) - } else { - cb(null, result) - } - return result - } catch (err) { - cb(err) - throw err + try { + const result = fn.apply(thisArg, args) + if (result && typeof result.then === 'function') { + // bind callback to this scope + result.then( + res => cb(null, res), + err => cb(err) + ) + } else { + cb(null, result) } - }) + return result + } catch (err) { + cb(err) + throw err + } } function pathToArray (path) { @@ -259,59 +259,26 @@ function pathToArray (path) { return flattened.reverse() } -function assertField (context, info, args) { +function assertField (parentCtx, info, args) { const pathInfo = info && info.path const path = pathToArray(pathInfo) const pathString = path.join('.') - const fields = context.fields + const fields = parentCtx.fields let field = fields[pathString] if (!field) { - const parent = getParentField(context, path) - - // we want to spawn the new span off of the parent, not a new async resource - parent.asyncResource.runInAsyncScope(() => { - /* this child resource will run a branched scope off of the parent resource, which - accesses the parent span from the storage unit in its own scope */ - const childResource = new AsyncResource('bound-anonymous-fn') - - childResource.runInAsyncScope(() => { - startResolveCh.publish({ - info, - context, - args - }) - }) - - field = fields[pathString] = { - parent, - asyncResource: childResource, - error: null - } - }) - } - - return field -} - -function getParentField (context, path) { - for (let i = path.length - 1; i > 0; i--) { - const field = getField(context, path.slice(0, i)) - if (field) { - return field + const fieldCtx = { info, ctx: parentCtx, args } + startResolveCh.publish(fieldCtx) + field = fields[pathString] = { + error: null, + ctx: fieldCtx } } - return { - asyncResource: context.asyncResource - } -} - -function getField (context, path) { - return context.fields[path.join('.')] + return field } function wrapFields (type) { @@ -349,13 +316,13 @@ function wrapFieldType (field) { function finishResolvers ({ fields }) { Object.keys(fields).reverse().forEach(key => { const field = fields[key] - const asyncResource = field.asyncResource - asyncResource.runInAsyncScope(() => { - if (field.error) { - resolveErrorCh.publish(field.error) - } - finishResolveCh.publish(field.finishTime) - }) + field.ctx.finishTime = field.finishTime + field.ctx.field = field + if (field.error) { + field.ctx.error = field.error + resolveErrorCh.publish(field.ctx) + } + finishResolveCh.publish(field.ctx) }) } diff --git a/packages/datadog-plugin-graphql/src/execute.js b/packages/datadog-plugin-graphql/src/execute.js index 638e6e9ca47..3831f7d58dc 100644 --- a/packages/datadog-plugin-graphql/src/execute.js +++ b/packages/datadog-plugin-graphql/src/execute.js @@ -11,7 +11,9 @@ class GraphQLExecutePlugin extends TracingPlugin { static get type () { return 'graphql' } static get kind () { return 'server' } - start ({ operation, args, docSource }) { + bindStart (ctx) { + const { operation, args, docSource } = ctx + const type = operation && operation.operation const name = operation && operation.name && operation.name.value const document = args.document @@ -27,20 +29,25 @@ class GraphQLExecutePlugin extends TracingPlugin { 'graphql.operation.name': name, 'graphql.source': source } - }) + }, ctx) addVariableTags(this.config, span, args.variableValues) + + return ctx.currentStore } - finish ({ res, args }) { - const span = this.activeSpan + finish (ctx) { + const { res, args } = ctx + const span = ctx?.currentStore?.span || this.activeSpan this.config.hooks.execute(span, args, res) if (res?.errors) { for (const err of res.errors) { extractErrorIntoSpanEvent(this._tracerConfig, span, err) } } - super.finish() + super.finish(ctx) + + return ctx.parentStore } } diff --git a/packages/datadog-plugin-graphql/src/parse.js b/packages/datadog-plugin-graphql/src/parse.js index 12669af7a10..1df8f3eab68 100644 --- a/packages/datadog-plugin-graphql/src/parse.js +++ b/packages/datadog-plugin-graphql/src/parse.js @@ -6,16 +6,19 @@ class GraphQLParsePlugin extends TracingPlugin { static get id () { return 'graphql' } static get operation () { return 'parser' } - start () { + bindStart (ctx) { this.startSpan('graphql.parse', { service: this.config.service, type: 'graphql', meta: {} - }) + }, ctx) + + return ctx.currentStore } - finish ({ source, document, docSource }) { - const span = this.activeSpan + finish (ctx) { + const { source, document, docSource } = ctx + const span = ctx?.currentStore?.span || this.activeSpan if (this.config.source && document) { span.setTag('graphql.source', docSource) @@ -23,7 +26,9 @@ class GraphQLParsePlugin extends TracingPlugin { this.config.hooks.parse(span, source, document) - super.finish() + super.finish(ctx) + + return ctx.parentStore } } diff --git a/packages/datadog-plugin-graphql/src/resolve.js b/packages/datadog-plugin-graphql/src/resolve.js index c3f3c13d6c7..c22b34f37c4 100644 --- a/packages/datadog-plugin-graphql/src/resolve.js +++ b/packages/datadog-plugin-graphql/src/resolve.js @@ -9,25 +9,34 @@ class GraphQLResolvePlugin extends TracingPlugin { static get id () { return 'graphql' } static get operation () { return 'resolve' } - start ({ info, context, args }) { + start (fieldCtx) { + const { info, ctx: parentCtx, args } = fieldCtx + const path = getPath(info, this.config) + // we need to get the parent span to the field if it exists for correct span parenting + // of nested fields + const parentField = getParentField(parentCtx, pathToArray(info && info.path)) + const childOf = parentField?.ctx?.currentStore?.span + + fieldCtx.parent = parentField + if (!shouldInstrument(this.config, path)) return const computedPathString = path.join('.') if (this.config.collapse) { - if (context.fields[computedPathString]) return + if (parentCtx.fields[computedPathString]) return - if (!context[collapsedPathSym]) { - context[collapsedPathSym] = {} - } else if (context[collapsedPathSym][computedPathString]) { + if (!parentCtx[collapsedPathSym]) { + parentCtx[collapsedPathSym] = {} + } else if (parentCtx[collapsedPathSym][computedPathString]) { return } - context[collapsedPathSym][computedPathString] = true + parentCtx[collapsedPathSym][computedPathString] = true } - const document = context.source + const document = parentCtx.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) @@ -35,6 +44,7 @@ class GraphQLResolvePlugin extends TracingPlugin { const span = this.startSpan('graphql.resolve', { service: this.config.service, resource: `${info.fieldName}:${info.returnType}`, + childOf, type: 'graphql', meta: { 'graphql.field.name': info.fieldName, @@ -42,7 +52,7 @@ class GraphQLResolvePlugin extends TracingPlugin { 'graphql.field.type': info.returnType.name, 'graphql.source': source } - }) + }, fieldCtx) if (fieldNode && this.config.variables && fieldNode.arguments) { const variables = this.config.variables(info.variableValues) @@ -56,21 +66,25 @@ class GraphQLResolvePlugin extends TracingPlugin { } if (this.resolverStartCh.hasSubscribers) { - this.resolverStartCh.publish({ context, resolverInfo: getResolverInfo(info, args) }) + this.resolverStartCh.publish({ ctx: parentCtx, resolverInfo: getResolverInfo(info, args) }) } + + return fieldCtx.currentStore } constructor (...args) { super(...args) - this.addTraceSub('updateField', ({ field, info, err }) => { + this.addTraceSub('updateField', (ctx) => { + const { field, info, error } = ctx + const path = getPath(info, this.config) if (!shouldInstrument(this.config, path)) return - const span = this.activeSpan + const span = ctx?.currentStore?.span || this.activeSpan field.finishTime = span._getTime ? span._getTime() : 0 - field.error = field.error || err + field.error = field.error || error }) this.resolverStartCh = dc.channel('datadog:graphql:resolver:start') @@ -81,8 +95,13 @@ class GraphQLResolvePlugin extends TracingPlugin { super.configure(config.depth === 0 ? false : config) } - finish (finishTime) { - this.activeSpan.finish(finishTime) + finish (ctx) { + const { finishTime } = ctx + + const span = ctx?.currentStore?.span || this.activeSpan + span.finish(finishTime) + + return ctx.parentStore } } @@ -154,4 +173,19 @@ function getResolverInfo (info, args) { return resolverInfo } +function getParentField (parentCtx, path) { + for (let i = path.length - 1; i > 0; i--) { + const field = getField(parentCtx, path.slice(0, i)) + if (field) { + return field + } + } + + return null +} + +function getField (parentCtx, path) { + return parentCtx.fields[path.join('.')] +} + module.exports = GraphQLResolvePlugin diff --git a/packages/datadog-plugin-graphql/src/validate.js b/packages/datadog-plugin-graphql/src/validate.js index 2ed05179b31..bfe3ac9f389 100644 --- a/packages/datadog-plugin-graphql/src/validate.js +++ b/packages/datadog-plugin-graphql/src/validate.js @@ -7,7 +7,8 @@ class GraphQLValidatePlugin extends TracingPlugin { static get id () { return 'graphql' } static get operation () { return 'validate' } - start ({ docSource, document }) { + bindStart (ctx) { + const { docSource, document } = ctx const source = this.config.source && document && docSource this.startSpan('graphql.validate', { @@ -16,18 +17,23 @@ class GraphQLValidatePlugin extends TracingPlugin { meta: { 'graphql.source': source } - }) + }, ctx) + + return ctx.currentStore } - finish ({ document, errors }) { - const span = this.activeSpan + finish (ctx) { + const { document, errors } = ctx + const span = ctx?.currentStore?.span || this.activeSpan this.config.hooks.validate(span, document, errors) if (errors) { for (const err of errors) { extractErrorIntoSpanEvent(this._tracerConfig, span, err) } } - super.finish() + super.finish(ctx) + + return ctx.parentStore } }