UNPKG

dd-trace

Version:

Datadog APM tracing client for JavaScript

136 lines (116 loc) 5.67 kB
'use strict' const TracingPlugin = require('../../dd-trace/src/plugins/tracing') const { extractErrorIntoSpanEvent, getCachedRequestOperation, isApolloHealthCheckSource } = require('./utils') /** * @typedef {object} GraphQLRequestStore * @property {import('../../dd-trace/src/opentracing/span')} [span] * @property {import('../../dd-trace/src/opentracing/span')} [graphqlRequestSpan] * @property {string} [graphqlRequestOperationName] * @property {unknown} [graphqlRequestSource] */ /** * @typedef {object} GraphQLRequestContext * @property {unknown[]} [arguments] * @property {GraphQLRequestStore} [currentStore] * @property {GraphQLRequestStore} [parentStore] * @property {boolean} [ddSkipped] * @property {{ errors?: import('graphql').GraphQLError[] }} [result] */ // Top-level GraphQL request span for drivers that funnel every operation // through a single entry point but parse/validate/execute internally (mercurius // today). It parents the `graphql.parse`/`graphql.validate`/`graphql.execute` // (or JIT) sub-spans and carries the request text + operation name/type, which // otherwise have no home when the query is JIT-compiled and `graphql.execute` // never fires. // // The entry boundary only hands us the raw `source` (string or pre-parsed AST) // and `operationName`; the parsed document — and therefore the precise // operation signature — is only known once mercurius parses internally. On the // cold path the `validate` sub-plugin refines the resource/operation tags onto // this span via `ctx.currentStore.graphqlRequestSpan` once the document is // available, so we never re-parse on the hot path. On the JIT warm path no // sub-span fires, so we recover the same tags from the cache the cold path // populated, keyed by source + operationName. class GraphQLRequestPlugin extends TracingPlugin { static id = 'graphql' static operation = 'request' static type = 'graphql' static kind = 'server' static prefix = 'tracing:orchestrion:mercurius:apm:graphql:request' /** * @param {GraphQLRequestContext} ctx */ bindStart (ctx) { // fastifyGraphQl(source, context, variables, operationName) const source = ctx.arguments?.[0] if (isApolloHealthCheckSource(source)) { ctx.ddSkipped = true return ctx.currentStore } const operationName = ctx.arguments?.[3] // `source` is the request text on the common path, but mercurius also // accepts a pre-parsed document AST; only a string is the query text, and // `graphql.source` carries only the text form. const docSource = typeof source === 'string' ? source : undefined // Warm (JIT-compiled) path: execute never fires, so recover the operation // signature/type the cold path cached, keyed by source + operationName — // by query text for a string, by document identity for a pre-parsed AST. // Empty on the cold path — validate hasn't refined yet — where the request // span is refined from the parsed document instead. const cached = getCachedRequestOperation(source, operationName) const span = this.startSpan(this.operationName({ id: 'request' }), { service: this.config.service || this.serviceName(), // The cached signature is the precise resource; otherwise provisional and // refined by the validate sub-plugin once the document is parsed. // `operationName` is the best name at the boundary; falls back to the // operation signature once validate sees the document. resource: cached?.signature || operationName || undefined, kind: this.constructor.kind, type: this.constructor.type, meta: { 'graphql.operation.type': cached?.type, 'graphql.operation.name': cached?.name || operationName, 'graphql.source': this.config.source ? docSource : undefined, }, }, ctx) // Hand the span, the requested operation name, and the raw source to the // validate sub-plugin running inside this store so it can refine the // resource + operation tags from the parsed document (validate is the first // boundary that has it) and cache them keyed by the source the request // boundary saw. The raw source is the cache key — validate sees mercurius's // internally parsed document, not the caller's source, and for a pre-parsed // AST the two are different objects. ctx.currentStore.graphqlRequestSpan = span ctx.currentStore.graphqlRequestOperationName = operationName ctx.currentStore.graphqlRequestSource = source return ctx.currentStore } /** * @param {GraphQLRequestContext} ctx */ asyncEnd (ctx) { if (ctx.ddSkipped) return ctx.parentStore /* istanbul ignore next: currentStore is populated for the request lifecycle; activeSpan is base-plugin fallback. */ const span = ctx?.currentStore?.span || this.activeSpan /* istanbul ignore if: startSpan always populates currentStore for the request lifecycle. */ if (!span) return ctx.parentStore const result = ctx.result if (result?.errors?.length) { span.setTag('error', result.errors[0]) for (const error of result.errors) { extractErrorIntoSpanEvent(this.config, span, error) } } span.finish() return ctx.parentStore } error (ctx) { /* istanbul ignore next: currentStore is populated for request errors; activeSpan is base-plugin fallback. */ const span = ctx?.currentStore?.span || this.activeSpan /* istanbul ignore else: errors are only routed after the request span has started. */ if (span && ctx?.error) { span.setTag('error', ctx.error) } } } module.exports = GraphQLRequestPlugin