UNPKG

signalfx-tracing

Version:

Provides auto-instrumentation for JavaScript libraries and frameworks

531 lines (430 loc) 14.3 kB
'use strict' const pick = require('lodash.pick') const semver = require('semver') const platform = require('../platform') const log = require('../log') const analyticsSampler = require('../analytics_sampler') let tools function createWrapExecute (tracer, config, defaultFieldResolver) { return function wrapExecute (execute) { return function executeWithTrace () { const args = normalizeArgs(arguments) const schema = args.schema const document = args.document const source = document && document._datadog_source const fieldResolver = args.fieldResolver || defaultFieldResolver const contextValue = args.contextValue = args.contextValue || {} const operation = getOperation(document, args.operationName) if (contextValue._datadog_graphql) { return execute.apply(this, arguments) } args.fieldResolver = wrapResolve(fieldResolver, tracer, config) if (schema) { wrapFields(schema._queryType, tracer, config) wrapFields(schema._mutationType, tracer, config) } const span = startExecutionSpan(tracer, config, operation, args) Object.defineProperty(contextValue, '_datadog_graphql', { value: { source, span, fields: {} } }) return call(execute, span, this, [args], (err, res) => { finishResolvers(contextValue) finish(err || (res && res.errors && res.errors[0]), span) }) } } } function createWrapParse (tracer, config) { return function wrapParse (parse) { return function parseWithTrace (source) { const span = startSpan(tracer, config, 'parse') analyticsSampler.sample(span, config.analytics) try { const document = parse.apply(this, arguments) const operation = getOperation(document) if (!operation) return document // skip schema parsing if (operation.operation) { let operationValue = `${operation.operation}` if (operation.name && operation.name.value) { operationValue = `.${operationValue}.${operation.name.value}` } else if (operation.selectionSet && operation.selectionSet.selections) { if (operation.selectionSet.selections.length > 0) { if (operation.selectionSet.selections[0].name && operation.selectionSet.selections[0].name.value) { operationValue = `.${operationValue}.${operation.selectionSet.selections[0].name.value}` } } } span.setOperationName(`graphql.parse${operationValue}`) } Object.defineProperties(document, { _datadog_source: { value: source.body || source } }) addDocumentTags(span, document) finish(null, span) return document } catch (e) { finish(e, span) throw e } } } } function createWrapValidate (tracer, config) { return function wrapValidate (validate) { return function validateWithTrace (schema, document, rules, typeInfo) { const startTime = platform.now() let error try { const errors = validate.apply(this, arguments) error = errors[0] return errors } catch (e) { throw error = e } finally { // skip schema stitching nested validation if (error || document.loc) { const span = startSpan(tracer, config, 'validate', { startTime }) if (document) { if (document.definitions[0]) { const docs = document.definitions[0] if (docs.operation) { let operationValue = `.${docs.operation}` if (docs.name && docs.name.value) { operationValue = `${operationValue}.${docs.name.value}` } else if (docs.selectionSet && docs.selectionSet.selections) { if (docs.selectionSet.selections.length > 1) { if (docs.selectionSet.selections[0].name && docs.selectionSet.selections[0].name.value) { operationValue = `${operationValue}.${docs.selectionSet.selections[0].name.value}` } } } span.setOperationName(`graphql.validate${operationValue}`) } } } addDocumentTags(span, document) analyticsSampler.sample(span, config.analytics) finish(error, span) } } } } } function wrapFields (type, tracer, config) { if (!type || !type._fields || type._datadog_patched) { return } type._datadog_patched = true Object.keys(type._fields).forEach(key => { const field = type._fields[key] if (typeof field.resolve === 'function') { field.resolve = wrapResolve(field.resolve, tracer, config) } let unwrappedType = field.type while (unwrappedType.ofType) { unwrappedType = unwrappedType.ofType } wrapFields(unwrappedType, tracer, config) }) } function wrapResolve (resolve, tracer, config) { if (resolve._datadog_patched || typeof resolve !== 'function') return resolve const responsePathAsArray = config.collapse ? withCollapse(pathToArray) : pathToArray function resolveWithTrace (source, args, contextValue, info) { const path = responsePathAsArray(info.path) const depth = path.filter(item => typeof item === 'string').length if (config.depth >= 0 && config.depth < depth) { const parent = getParentField(tracer, contextValue, path) return call(resolve, parent.span, this, arguments) } const field = assertField(tracer, config, contextValue, info, path) return call(resolve, field.span, this, arguments, err => updateField(field, err)) } resolveWithTrace._datadog_patched = true return resolveWithTrace } function call (fn, span, thisArg, args, callback) { const scope = span.tracer().scope() callback = callback || (() => {}) try { const result = scope.activate(span, () => fn.apply(thisArg, args)) if (result && typeof result.then === 'function') { result.then( res => callback(null, res), err => callback(err) ) } else { callback(null, result) } return result } catch (e) { callback(e) throw e } } function getParentField (tracer, contextValue, path) { for (let i = path.length - 1; i > 0; i--) { const field = getField(contextValue, path.slice(0, i)) if (field) { return field } } return { span: contextValue._datadog_graphql.span } } function getField (contextValue, path) { return contextValue._datadog_graphql.fields[path.join('.')] } function normalizeArgs (args) { if (args.length === 1) { return args[0] } return { schema: args[0], document: args[1], rootValue: args[2], contextValue: args[3], variableValues: args[4], operationName: args[5], fieldResolver: args[6] } } function startExecutionSpan (tracer, config, operation, args) { const span = startSpan(tracer, config, 'execute') let operationValue = '' if (operation && operation.name) { operationValue = `.${operation.operation}.${operation.name.value}` } span.setOperationName(`graphql.execute${operationValue}`) addExecutionTags(span, config, operation, args.document, args.operationName) addDocumentTags(span, args.document) addVariableTags(tracer, config, span, args.variableValues) analyticsSampler.sample(span, config.analytics, true) return span } function addExecutionTags (span, config, operation, document, operationName) { const type = operation && operation.operation const name = operation && operation.name && operation.name.value const tags = { 'resource.name': `${span.context()._name}`, 'graphql.operation.signature': getSignature(document, name, type, config.signature) } if (type) { tags['graphql.operation.type'] = type } if (name) { tags['graphql.operation.name'] = name } span.addTags(tags) } function addDocumentTags (span, document) { const tags = {} if (document && document._datadog_source) { tags['graphql.source'] = document._datadog_source } span.addTags(tags) } function addVariableTags (tracer, config, span, variableValues) { const tags = {} if (variableValues && config.variables) { const variables = config.variables(variableValues) for (const param in variables) { tags[`graphql.variables.${param}`] = variables[param] } } span.addTags(tags) } function startSpan (tracer, config, name, options) { options = options || {} return tracer.startSpan(`graphql.${name}`, { childOf: options.childOf || tracer.scope().active(), startTime: options.startTime, tags: { 'service.name': getService(tracer, config), 'component': 'graphql' } }) } function startResolveSpan (tracer, config, childOf, path, info, contextValue) { const span = startSpan(tracer, config, 'resolve', { childOf }) const document = contextValue._datadog_graphql.source const fieldNode = info.fieldNodes.find(fieldNode => fieldNode.kind === 'Field') span.addTags({ 'resource.name': `${span.context()._name}`, 'graphql.field.info': `${info.fieldName}:${info.returnType}`, 'graphql.field.name': info.fieldName, 'graphql.field.path': path.join('.'), 'graphql.field.type': info.returnType.name }) if (fieldNode) { if (document) { span.setTag('graphql.source', document.substring(fieldNode.loc.start, fieldNode.loc.end)) } if (config.variables) { const variables = config.variables(info.variableValues) fieldNode.arguments .filter(arg => arg.value && arg.value.kind === 'Variable') .filter(arg => arg.value.name && variables[arg.value.name.value]) .map(arg => arg.value.name.value) .forEach(name => { span.setTag(`graphql.variables.${name}`, variables[name]) }) } } analyticsSampler.sample(span, config.analytics) return span } function finish (error, span, finishTime) { if (error) { span.addTags({ 'sfx.error.kind': error.name, 'sfx.error.message': error.message, 'sfx.error.stack': error.stack }) } span.finish(finishTime) } function finishResolvers (contextValue) { const fields = contextValue._datadog_graphql.fields Object.keys(fields).reverse().forEach(key => { const field = fields[key] finish(field.error, field.span, field.finishTime) }) } function updateField (field, error) { field.finishTime = platform.now() field.error = field.error || error } function withCollapse (responsePathAsArray) { return function () { return responsePathAsArray.apply(this, arguments) .map(segment => typeof segment === 'number' ? '*' : segment) } } function assertField (tracer, config, contextValue, info, path) { const pathString = path.join('.') const fields = contextValue._datadog_graphql.fields let field = fields[pathString] if (!field) { const parent = getParentField(tracer, contextValue, path) field = fields[pathString] = { parent, span: startResolveSpan(tracer, config, parent.span, path, info, contextValue), error: null } } return field } function getService (tracer, config) { return config.service || `${tracer._service}-graphql` } function getOperation (document, operationName) { if (!document || !Array.isArray(document.definitions)) { return } const types = ['query', 'mutation', 'subscription'] if (operationName) { return document.definitions .filter(def => types.indexOf(def.operation) !== -1) .find(def => operationName === (def.name && def.name.value)) } else { return document.definitions.find(def => types.indexOf(def.operation) !== -1) } } function validateConfig (config) { return Object.assign({}, config, { depth: getDepth(config), variables: getVariablesFilter(config), collapse: config.collapse === undefined || !!config.collapse }) } function getDepth (config) { if (typeof config.depth === 'number') { return config.depth } else if (config.hasOwnProperty('depth')) { log.error('Expected `depth` to be an integer.') } return -1 } function getVariablesFilter (config) { if (typeof config.variables === 'function') { return config.variables } else if (config.variables instanceof Array) { return variables => pick(variables, config.variables) } else if (config.hasOwnProperty('variables')) { log.error('Expected `variables` to be an array or function.') } return null } function getSignature (document, operationName, operationType, calculate) { if (calculate !== false && tools !== false) { try { try { tools = tools || require('./graphql/tools') } catch (e) { tools = false throw e } return tools.defaultEngineReportingSignature(document, operationName) } catch (e) { // safety net } } return [operationType, operationName].filter(val => val).join(' ') } function pathToArray (path) { const flattened = [] let curr = path while (curr) { flattened.push(curr.key) curr = curr.prev } return flattened.reverse() } const versions = semver.intersects('<10', process.version) ? ['>=0.10 <15'] : ['>=0.10'] module.exports = [ { name: 'graphql', file: 'execution/execute.js', versions: versions, patch (execute, tracer, config) { this.wrap(execute, 'execute', createWrapExecute( tracer, validateConfig(config), execute.defaultFieldResolver )) }, unpatch (execute) { this.unwrap(execute, 'execute') } }, { name: 'graphql', file: 'language/parser.js', versions: versions, patch (parser, tracer, config) { this.wrap(parser, 'parse', createWrapParse(tracer, validateConfig(config))) }, unpatch (parser) { this.unwrap(parser, 'parse') } }, { name: 'graphql', file: 'validation/validate.js', versions: versions, patch (validate, tracer, config) { this.wrap(validate, 'validate', createWrapValidate(tracer, validateConfig(config))) }, unpatch (validate) { this.unwrap(validate, 'validate') } } ]