dd-trace 2.8.0 → 2.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. package/ci/init.js +22 -6
  2. package/ci/jest/env.js +19 -7
  3. package/ext/tags.d.ts +1 -0
  4. package/ext/tags.js +2 -1
  5. package/index.d.ts +4 -2
  6. package/package.json +5 -5
  7. package/packages/datadog-core/src/storage/async_resource.js +1 -1
  8. package/packages/datadog-instrumentations/index.js +1 -0
  9. package/packages/datadog-instrumentations/src/aws-sdk.js +5 -4
  10. package/packages/datadog-instrumentations/src/fastify.js +84 -92
  11. package/packages/datadog-instrumentations/src/graphql.js +352 -0
  12. package/packages/datadog-instrumentations/src/http/client.js +1 -1
  13. package/packages/datadog-instrumentations/src/jest.js +14 -14
  14. package/packages/datadog-instrumentations/src/koa.js +10 -7
  15. package/packages/datadog-instrumentations/src/tedious.js +1 -1
  16. package/packages/datadog-plugin-graphql/src/index.js +123 -412
  17. package/packages/datadog-plugin-graphql/src/resolve.js +125 -0
  18. package/packages/datadog-plugin-http/src/client.js +10 -7
  19. package/packages/datadog-plugin-http2/src/server.js +3 -1
  20. package/packages/datadog-plugin-jest/src/util.js +13 -1
  21. package/packages/datadog-plugin-mysql/src/index.js +18 -7
  22. package/packages/dd-trace/src/appsec/index.js +3 -3
  23. package/packages/dd-trace/src/config.js +1 -1
  24. package/packages/dd-trace/src/exporters/agent/writer.js +1 -1
  25. package/packages/dd-trace/src/plugin_manager.js +21 -0
  26. package/packages/dd-trace/src/plugins/util/test.js +3 -0
  27. package/packages/dd-trace/src/plugins/util/web.js +3 -1
  28. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -1
  29. package/packages/dd-trace/src/startup-log.js +1 -1
  30. package/packages/dd-trace/src/telemetry.js +1 -1
  31. package/scripts/install_plugin_modules.js +2 -4
  32. package/packages/dd-trace/lib/version.js +0 -1
@@ -1,260 +1,177 @@
1
1
  'use strict'
2
2
 
3
- const pick = require('lodash.pick')
4
- const log = require('../../dd-trace/src/log')
3
+ const Plugin = require('../../dd-trace/src/plugins/plugin')
4
+ const { storage } = require('../../datadog-core')
5
5
  const analyticsSampler = require('../../dd-trace/src/analytics_sampler')
6
-
7
- const contexts = new WeakMap()
8
- const documentSources = new WeakMap()
9
- const patchedTypes = new WeakSet()
10
- const patchedResolvers = new WeakSet()
6
+ const log = require('../../dd-trace/src/log')
7
+ const GraphQLResolvePlugin = require('./resolve')
11
8
 
12
9
  let tools
10
+ class GraphQLPlugin extends Plugin {
11
+ static get name () {
12
+ return 'graphql'
13
+ }
13
14
 
14
- function createWrapExecute (tracer, config, defaultFieldResolver) {
15
- return function wrapExecute (execute) {
16
- return function executeWithTrace () {
17
- const args = normalizeArgs(arguments, tracer, config, defaultFieldResolver)
18
- const schema = args.schema
19
- const document = args.document
20
- const source = documentSources.get(document)
21
- const contextValue = args.contextValue
22
- const operation = getOperation(document, args.operationName)
23
-
24
- if (contexts.has(contextValue)) {
25
- return execute.apply(this, arguments)
26
- }
27
-
28
- if (schema) {
29
- wrapFields(schema._queryType, tracer, config)
30
- wrapFields(schema._mutationType, tracer, config)
31
- }
32
-
33
- const span = startExecutionSpan(tracer, config, operation, args)
34
- const context = { source, span, fields: {} }
15
+ constructor (...args) {
16
+ super(...args)
35
17
 
36
- contexts.set(contextValue, context)
18
+ /** Execute Subs */
37
19
 
38
- return call(execute, span, this, arguments, (err, res) => {
39
- finishResolvers(context)
20
+ // resolve plugin separated for disabling purposes
21
+ this.resolve = new GraphQLResolvePlugin(...args)
40
22
 
41
- setError(span, err || (res && res.errors && res.errors[0]))
42
- config.hooks.execute(span, args, res)
43
- finish(span)
44
- })
45
- }
46
- }
47
- }
23
+ this.addSub('apm:graphql:execute:start', ({ operation, args, docSource }) => {
24
+ const store = storage.getStore()
25
+ const span = startSpan('execute', this.config, this.tracer, store)
48
26
 
49
- function createWrapParse (tracer, config) {
50
- return function wrapParse (parse) {
51
- return function parseWithTrace (source) {
52
- const span = startSpan(tracer, config, 'parse')
27
+ // process add tags
28
+ addExecutionTags(span, this.config, operation, args.document)
29
+ addDocumentTags(span, args.document, this.config, docSource)
30
+ addVariableTags(this.config, span, args.variableValues)
53
31
 
54
- analyticsSampler.sample(span, config.measured, true)
32
+ analyticsSampler.sample(span, this.config.measured, true)
55
33
 
56
- let document
57
- try {
58
- document = parse.apply(this, arguments)
59
- const operation = getOperation(document)
34
+ this.enter(span, store)
35
+ })
60
36
 
61
- if (!operation) return document // skip schema parsing
37
+ this.addSub('apm:graphql:execute:error', this.addError)
62
38
 
63
- if (source) {
64
- documentSources.set(document, source.body || source)
65
- }
39
+ this.addSub('apm:graphql:execute:finish', ({ res, args }) => {
40
+ const span = storage.getStore().span
41
+ this.config.hooks.execute(span, args, res)
42
+ span.finish()
43
+ })
66
44
 
67
- addDocumentTags(span, document, config)
45
+ /** Parser Subs */
68
46
 
69
- return document
70
- } catch (e) {
71
- setError(span, e)
72
- throw e
73
- } finally {
74
- config.hooks.parse(span, source, document)
75
- finish(span)
76
- }
77
- }
78
- }
79
- }
47
+ this.addSub('apm:graphql:parser:start', () => {
48
+ const store = storage.getStore()
49
+ const span = startSpan('parse', this.config, this.tracer, store)
80
50
 
81
- function createWrapValidate (tracer, config) {
82
- return function wrapValidate (validate) {
83
- return function validateWithTrace (schema, document, rules, typeInfo) {
84
- const span = startSpan(tracer, config, 'validate')
51
+ analyticsSampler.sample(span, this.config.measured, true)
52
+ this.enter(span, store)
53
+ })
85
54
 
86
- analyticsSampler.sample(span, config.measured, true)
55
+ this.addSub('apm:graphql:parser:finish', ({ source, document, docSource }) => {
56
+ const span = storage.getStore().span
87
57
 
88
- // skip for schema stitching nested validation
89
- if (document && document.loc) {
90
- addDocumentTags(span, document, config)
58
+ const tags = {}
59
+ if (this.config.source && document) {
60
+ tags['graphql.source'] = docSource
91
61
  }
92
62
 
93
- let errors
94
- try {
95
- errors = validate.apply(this, arguments)
63
+ span.addTags(tags)
96
64
 
97
- setError(span, errors && errors[0])
65
+ this.config.hooks.parse(span, source, document)
98
66
 
99
- return errors
100
- } catch (e) {
101
- setError(span, e)
102
- throw e
103
- } finally {
104
- config.hooks.validate(span, document, errors)
105
- finish(span)
106
- }
107
- }
108
- }
109
- }
67
+ span.finish()
68
+ })
110
69
 
111
- function wrapFields (type, tracer, config) {
112
- if (!type || !type._fields || patchedTypes.has(type)) {
113
- return
114
- }
70
+ this.addSub('apm:graphql:parser:error', this.addError)
115
71
 
116
- patchedTypes.add(type)
72
+ /** Validate Subs */
117
73
 
118
- Object.keys(type._fields).forEach(key => {
119
- const field = type._fields[key]
74
+ this.addSub('apm:graphql:validate:start', ({ docSource, document }) => {
75
+ const store = storage.getStore()
76
+ const span = startSpan('validate', this.config, this.tracer, store)
120
77
 
121
- wrapFieldResolve(field, tracer, config)
122
- wrapFieldType(field, tracer, config)
123
- })
124
- }
78
+ analyticsSampler.sample(span, this.config.measured, true)
125
79
 
126
- function wrapFieldResolve (field, tracer, config) {
127
- if (!field || !field.resolve) return
80
+ if (document && document.loc) {
81
+ const tags = {}
82
+ if (this.config.source && document) {
83
+ tags['graphql.source'] = docSource
84
+ }
128
85
 
129
- field.resolve = wrapResolve(field.resolve, tracer, config)
130
- }
86
+ span.addTags(tags)
87
+ }
131
88
 
132
- function wrapFieldType (field, tracer, config) {
133
- if (!field || !field.type) return
89
+ this.enter(span, store)
90
+ })
134
91
 
135
- let unwrappedType = field.type
92
+ this.addSub('apm:graphql:validate:finish', ({ document, errors }) => {
93
+ const span = storage.getStore().span
94
+ this.config.hooks.validate(span, document, errors)
95
+ span.finish()
96
+ })
136
97
 
137
- while (unwrappedType.ofType) {
138
- unwrappedType = unwrappedType.ofType
98
+ this.addSub('apm:graphql:validate:error', this.addError)
139
99
  }
140
100
 
141
- wrapFields(unwrappedType, tracer, config)
142
- }
143
-
144
- function wrapResolve (resolve, tracer, config) {
145
- if (typeof resolve !== 'function' || patchedResolvers.has(resolve)) return resolve
101
+ configure (config) {
102
+ const validated = validateConfig(config)
146
103
 
147
- const responsePathAsArray = config.collapse
148
- ? withCollapse(pathToArray)
149
- : pathToArray
104
+ // this will disable resolve subscribers if `config.depth` is set to 0
105
+ const resolveConfig = validated.depth === 0 ? false : validated
106
+ this.resolve.configure(resolveConfig)
150
107
 
151
- function resolveWithTrace (source, args, contextValue, info) {
152
- const context = contexts.get(contextValue)
153
-
154
- if (!context) return resolve.apply(this, arguments)
155
-
156
- const path = responsePathAsArray(info && info.path)
157
-
158
- if (config.depth >= 0) {
159
- const depth = path.filter(item => typeof item === 'string').length
160
-
161
- if (config.depth < depth) {
162
- const parent = getParentField(tracer, context, path)
163
-
164
- return call(resolve, parent.span, this, arguments)
165
- }
166
- }
167
-
168
- const field = assertField(tracer, config, context, info, path)
169
-
170
- return call(resolve, field.span, this, arguments, err => updateField(field, err))
108
+ return super.configure(validated)
171
109
  }
172
-
173
- patchedResolvers.add(resolveWithTrace)
174
-
175
- return resolveWithTrace
176
110
  }
177
111
 
178
- function call (fn, span, thisArg, args, callback) {
179
- const scope = span.tracer().scope()
180
-
181
- callback = callback || (() => {})
112
+ // config validator helpers
182
113
 
183
- try {
184
- const result = scope.activate(span, () => fn.apply(thisArg, args))
185
-
186
- if (result && typeof result.then === 'function') {
187
- result.then(
188
- res => callback(null, res),
189
- err => callback(err)
190
- )
191
- } else {
192
- callback(null, result)
193
- }
194
-
195
- return result
196
- } catch (e) {
197
- callback(e)
198
- throw e
199
- }
114
+ function validateConfig (config) {
115
+ return Object.assign({}, config, {
116
+ depth: getDepth(config),
117
+ variables: getVariablesFilter(config),
118
+ collapse: config.collapse === undefined || !!config.collapse,
119
+ hooks: getHooks(config)
120
+ })
200
121
  }
201
122
 
202
- function getParentField (tracer, context, path) {
203
- for (let i = path.length - 1; i > 0; i--) {
204
- const field = getField(context, path.slice(0, i))
205
-
206
- if (field) {
207
- return field
208
- }
209
- }
210
-
211
- return {
212
- span: context.span
123
+ function getDepth (config) {
124
+ if (typeof config.depth === 'number') {
125
+ return config.depth
126
+ } else if (config.hasOwnProperty('depth')) {
127
+ log.error('Expected `depth` to be a integer.')
213
128
  }
129
+ return -1
214
130
  }
215
131
 
216
- function getField (context, path) {
217
- return context.fields[path.join('.')]
132
+ function getVariablesFilter (config) {
133
+ if (typeof config.variables === 'function') {
134
+ return config.variables
135
+ } else if (config.variables instanceof Array) {
136
+ return variables => pick(variables, config.variables)
137
+ } else if (config.hasOwnProperty('variables')) {
138
+ log.error('Expected `variables` to be an array or function.')
139
+ }
140
+ return null
218
141
  }
219
142
 
220
- function normalizeArgs (args, tracer, config, defaultFieldResolver) {
221
- if (args.length !== 1) return normalizePositional(args, tracer, config, defaultFieldResolver)
222
-
223
- args[0].contextValue = args[0].contextValue || {}
224
- args[0].fieldResolver = wrapResolve(args[0].fieldResolver || defaultFieldResolver, tracer, config)
225
-
226
- return args[0]
227
- }
143
+ function getHooks (config) {
144
+ const noop = () => { }
145
+ const execute = (config.hooks && config.hooks.execute) || noop
146
+ const parse = (config.hooks && config.hooks.parse) || noop
147
+ const validate = (config.hooks && config.hooks.validate) || noop
228
148
 
229
- function normalizePositional (args, tracer, config, defaultFieldResolver) {
230
- args[3] = args[3] || {} // contextValue
231
- args[6] = wrapResolve(args[6] || defaultFieldResolver, tracer, config) // fieldResolver
232
- args.length = Math.max(args.length, 7)
233
-
234
- return {
235
- schema: args[0],
236
- document: args[1],
237
- rootValue: args[2],
238
- contextValue: args[3],
239
- variableValues: args[4],
240
- operationName: args[5],
241
- fieldResolver: args[6]
242
- }
149
+ return { execute, parse, validate }
243
150
  }
244
151
 
245
- function startExecutionSpan (tracer, config, operation, args) {
246
- const span = startSpan(tracer, config, 'execute')
152
+ // non-lodash pick
247
153
 
248
- addExecutionTags(span, config, operation, args.document, args.operationName)
249
- addDocumentTags(span, args.document, config)
250
- addVariableTags(tracer, config, span, args.variableValues)
154
+ function pick (obj, selectors) {
155
+ return Object.fromEntries(Object.entries(obj).filter(([key]) => selectors.includes(key)))
156
+ }
251
157
 
252
- analyticsSampler.sample(span, config.measured, true)
158
+ // span-related
253
159
 
254
- return span
160
+ function startSpan (name, conf, tracer, store, options) {
161
+ const service = conf.service || tracer._service
162
+ const childOf = store ? store.span : store
163
+ options = options || {}
164
+ return tracer.startSpan(`graphql.${name}`, {
165
+ childOf: options.childOf || childOf,
166
+ startTime: options.startTime,
167
+ tags: {
168
+ 'service.name': service,
169
+ 'span.type': 'graphql'
170
+ }
171
+ })
255
172
  }
256
173
 
257
- function addExecutionTags (span, config, operation, document, operationName) {
174
+ function addExecutionTags (span, config, operation, document) {
258
175
  const type = operation && operation.operation
259
176
  const name = operation && operation.name && operation.name.value
260
177
  const tags = {
@@ -272,17 +189,17 @@ function addExecutionTags (span, config, operation, document, operationName) {
272
189
  span.addTags(tags)
273
190
  }
274
191
 
275
- function addDocumentTags (span, document, config) {
192
+ function addDocumentTags (span, document, config, docSource) {
276
193
  const tags = {}
277
194
 
278
195
  if (config.source && document) {
279
- tags['graphql.source'] = documentSources.get(document)
196
+ tags['graphql.source'] = docSource
280
197
  }
281
198
 
282
199
  span.addTags(tags)
283
200
  }
284
201
 
285
- function addVariableTags (tracer, config, span, variableValues) {
202
+ function addVariableTags (config, span, variableValues) {
286
203
  const tags = {}
287
204
 
288
205
  if (variableValues && config.variables) {
@@ -295,155 +212,6 @@ function addVariableTags (tracer, config, span, variableValues) {
295
212
  span.addTags(tags)
296
213
  }
297
214
 
298
- function startSpan (tracer, config, name, options) {
299
- options = options || {}
300
-
301
- return tracer.startSpan(`graphql.${name}`, {
302
- childOf: options.childOf || tracer.scope().active(),
303
- startTime: options.startTime,
304
- tags: {
305
- 'service.name': getService(tracer, config),
306
- 'span.type': 'graphql'
307
- }
308
- })
309
- }
310
-
311
- function startResolveSpan (tracer, config, childOf, path, info, { source }) {
312
- const span = startSpan(tracer, config, 'resolve', { childOf })
313
- const document = source
314
- const fieldNode = info.fieldNodes.find(fieldNode => fieldNode.kind === 'Field')
315
-
316
- analyticsSampler.sample(span, config.measured)
317
-
318
- span.addTags({
319
- 'resource.name': `${info.fieldName}:${info.returnType}`,
320
- 'graphql.field.name': info.fieldName,
321
- 'graphql.field.path': path.join('.'),
322
- 'graphql.field.type': info.returnType.name
323
- })
324
-
325
- if (fieldNode) {
326
- if (config.source && document && fieldNode.loc) {
327
- span.setTag('graphql.source', document.substring(fieldNode.loc.start, fieldNode.loc.end))
328
- }
329
-
330
- if (config.variables && fieldNode.arguments) {
331
- const variables = config.variables(info.variableValues)
332
-
333
- fieldNode.arguments
334
- .filter(arg => arg.value && arg.value.kind === 'Variable')
335
- .filter(arg => arg.value.name && variables[arg.value.name.value])
336
- .map(arg => arg.value.name.value)
337
- .forEach(name => {
338
- span.setTag(`graphql.variables.${name}`, variables[name])
339
- })
340
- }
341
- }
342
-
343
- return span
344
- }
345
-
346
- function setError (span, error) {
347
- if (error) {
348
- span.setTag('error', error)
349
- }
350
- }
351
-
352
- function finish (span, finishTime) {
353
- span.finish(finishTime)
354
- }
355
-
356
- function finishResolvers ({ fields }) {
357
- Object.keys(fields).reverse().forEach(key => {
358
- const field = fields[key]
359
-
360
- setError(field.span, field.error)
361
- finish(field.span, field.finishTime)
362
- })
363
- }
364
-
365
- function updateField (field, error) {
366
- // TODO: update this to also work with no-op spans without a hack
367
- field.finishTime = field.span._getTime ? field.span._getTime() : 0
368
- field.error = field.error || error
369
- }
370
-
371
- function withCollapse (responsePathAsArray) {
372
- return function () {
373
- return responsePathAsArray.apply(this, arguments)
374
- .map(segment => typeof segment === 'number' ? '*' : segment)
375
- }
376
- }
377
-
378
- function assertField (tracer, config, context, info, path) {
379
- const pathString = path.join('.')
380
- const fields = context.fields
381
-
382
- let field = fields[pathString]
383
-
384
- if (!field) {
385
- const parent = getParentField(tracer, context, path)
386
-
387
- field = fields[pathString] = {
388
- parent,
389
- span: startResolveSpan(tracer, config, parent.span, path, info, context),
390
- error: null
391
- }
392
- }
393
-
394
- return field
395
- }
396
-
397
- function getService (tracer, config) {
398
- return config.service || tracer._service
399
- }
400
-
401
- function getOperation (document, operationName) {
402
- if (!document || !Array.isArray(document.definitions)) {
403
- return
404
- }
405
-
406
- const definitions = document.definitions.filter(def => def)
407
- const types = ['query', 'mutation', 'subscription']
408
-
409
- if (operationName) {
410
- return definitions
411
- .filter(def => types.indexOf(def.operation) !== -1)
412
- .find(def => operationName === (def.name && def.name.value))
413
- } else {
414
- return definitions.find(def => types.indexOf(def.operation) !== -1)
415
- }
416
- }
417
-
418
- function validateConfig (config) {
419
- return Object.assign({}, config, {
420
- depth: getDepth(config),
421
- variables: getVariablesFilter(config),
422
- collapse: config.collapse === undefined || !!config.collapse,
423
- hooks: getHooks(config)
424
- })
425
- }
426
-
427
- function getDepth (config) {
428
- if (typeof config.depth === 'number') {
429
- return config.depth
430
- } else if (config.hasOwnProperty('depth')) {
431
- log.error('Expected `depth` to be a integer.')
432
- }
433
- return -1
434
- }
435
-
436
- function getVariablesFilter (config) {
437
- if (typeof config.variables === 'function') {
438
- return config.variables
439
- } else if (config.variables instanceof Array) {
440
- return variables => pick(variables, config.variables)
441
- } else if (config.hasOwnProperty('variables')) {
442
- log.error('Expected `variables` to be an array or function.')
443
- }
444
- return null
445
- }
446
-
447
215
  function getSignature (document, operationName, operationType, calculate) {
448
216
  if (calculate !== false && tools !== false) {
449
217
  try {
@@ -463,61 +231,4 @@ function getSignature (document, operationName, operationType, calculate) {
463
231
  return [operationType, operationName].filter(val => val).join(' ')
464
232
  }
465
233
 
466
- function pathToArray (path) {
467
- const flattened = []
468
- let curr = path
469
- while (curr) {
470
- flattened.push(curr.key)
471
- curr = curr.prev
472
- }
473
- return flattened.reverse()
474
- }
475
-
476
- function getHooks (config) {
477
- const noop = () => {}
478
- const execute = (config.hooks && config.hooks.execute) || noop
479
- const parse = (config.hooks && config.hooks.parse) || noop
480
- const validate = (config.hooks && config.hooks.validate) || noop
481
-
482
- return { execute, parse, validate }
483
- }
484
-
485
- module.exports = [
486
- {
487
- name: 'graphql',
488
- file: 'execution/execute.js',
489
- versions: ['>=0.10'],
490
- patch (execute, tracer, config) {
491
- this.wrap(execute, 'execute', createWrapExecute(
492
- tracer,
493
- validateConfig(config),
494
- execute.defaultFieldResolver
495
- ))
496
- },
497
- unpatch (execute) {
498
- this.unwrap(execute, 'execute')
499
- }
500
- },
501
- {
502
- name: 'graphql',
503
- file: 'language/parser.js',
504
- versions: ['>=0.10'],
505
- patch (parser, tracer, config) {
506
- this.wrap(parser, 'parse', createWrapParse(tracer, validateConfig(config)))
507
- },
508
- unpatch (parser) {
509
- this.unwrap(parser, 'parse')
510
- }
511
- },
512
- {
513
- name: 'graphql',
514
- file: 'validation/validate.js',
515
- versions: ['>=0.10'],
516
- patch (validate, tracer, config) {
517
- this.wrap(validate, 'validate', createWrapValidate(tracer, validateConfig(config)))
518
- },
519
- unpatch (validate) {
520
- this.unwrap(validate, 'validate')
521
- }
522
- }
523
- ]
234
+ module.exports = GraphQLPlugin