signalfx-tracing
Version:
Provides auto-instrumentation for JavaScript libraries and frameworks
531 lines (430 loc) • 14.3 kB
JavaScript
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')
}
}
]