dd-trace 2.0.0 → 2.2.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 (31) hide show
  1. package/index.d.ts +22 -0
  2. package/package.json +2 -2
  3. package/packages/datadog-instrumentations/index.js +6 -3
  4. package/packages/datadog-instrumentations/src/couchbase.js +143 -0
  5. package/packages/datadog-instrumentations/src/elasticsearch.js +74 -0
  6. package/packages/datadog-instrumentations/src/mysql.js +2 -2
  7. package/packages/datadog-instrumentations/src/mysql2.js +76 -0
  8. package/packages/datadog-instrumentations/src/q.js +9 -1
  9. package/packages/datadog-plugin-aws-sdk/src/helpers.js +4 -3
  10. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +48 -0
  11. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +56 -6
  12. package/packages/datadog-plugin-aws-sdk/src/services/sns.js +33 -6
  13. package/packages/datadog-plugin-couchbase/src/index.js +51 -148
  14. package/packages/datadog-plugin-elasticsearch/src/index.js +41 -82
  15. package/packages/datadog-plugin-fastify/src/fastify.js +22 -1
  16. package/packages/datadog-plugin-mysql/src/index.js +4 -4
  17. package/packages/datadog-plugin-mysql2/src/index.js +5 -88
  18. package/packages/datadog-plugin-next/src/index.js +10 -6
  19. package/packages/datadog-plugin-winston/src/index.js +33 -12
  20. package/packages/dd-trace/lib/version.js +1 -1
  21. package/packages/dd-trace/src/appsec/index.js +4 -0
  22. package/packages/dd-trace/src/appsec/recommended.json +5708 -1
  23. package/packages/dd-trace/src/profiling/config.js +5 -1
  24. package/packages/dd-trace/src/profiling/profiler.js +15 -6
  25. package/packages/dd-trace/src/profiling/profilers/cpu.js +1 -1
  26. package/packages/dd-trace/src/profiling/profilers/heap.js +3 -2
  27. package/packages/dd-trace/src/tagger.js +14 -21
  28. package/packages/dd-trace/src/tracer.js +1 -1
  29. package/scripts/publish_docs.js +1 -1
  30. package/NOTICE +0 -4
  31. package/packages/dd-trace/src/profiling/mapper.js +0 -91
package/index.d.ts CHANGED
@@ -406,6 +406,28 @@ export declare interface TracerOptions {
406
406
  * @default true
407
407
  */
408
408
  orphanable?: boolean
409
+
410
+ /**
411
+ * Configuration of the AppSec protection. Can be a boolean as an alias to `appsec.enabled`.
412
+ */
413
+ appsec?: boolean | {
414
+ /**
415
+ * Whether to enable AppSec.
416
+ * @default false
417
+ */
418
+ enabled?: boolean,
419
+
420
+ /**
421
+ * Specifies a path to a custom rules file.
422
+ */
423
+ rules?: string,
424
+
425
+ /**
426
+ * Controls the maximum amount of traces sampled by AppSec attacks, per second.
427
+ * @default 100
428
+ */
429
+ rateLimit?: number
430
+ };
409
431
  }
410
432
 
411
433
  /** @hidden */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dd-trace",
3
- "version": "2.0.0",
3
+ "version": "2.2.0",
4
4
  "description": "Datadog APM tracing client for JavaScript",
5
5
  "main": "index.js",
6
6
  "typings": "index.d.ts",
@@ -61,7 +61,7 @@
61
61
  "node": ">=12"
62
62
  },
63
63
  "dependencies": {
64
- "@datadog/native-appsec": "^0.8.0",
64
+ "@datadog/native-appsec": "^0.8.1",
65
65
  "@datadog/native-metrics": "^1.1.0",
66
66
  "@datadog/pprof": "^0.3.0",
67
67
  "@datadog/sketches-js": "^1.0.4",
@@ -1,10 +1,13 @@
1
1
  'use strict'
2
2
 
3
+ require('./src/bluebird')
4
+ require('./src/couchbase')
3
5
  require('./src/dns')
6
+ require('./src/elasticsearch')
4
7
  require('./src/memcached')
5
8
  require('./src/mysql')
6
- require('./src/bluebird')
7
- require('./src/when')
9
+ require('./src/mysql2')
8
10
  require('./src/promise')
9
- require('./src/q')
10
11
  require('./src/promise-js')
12
+ require('./src/q')
13
+ require('./src/when')
@@ -0,0 +1,143 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ channel,
5
+ addHook,
6
+ AsyncResource
7
+ } = require('./helpers/instrument')
8
+ const shimmer = require('../../datadog-shimmer')
9
+
10
+ addHook({ name: 'couchbase', file: 'lib/bucket.js', versions: ['^2.6.5'] }, Bucket => {
11
+ const startCh = channel('apm:couchbase:query:start')
12
+ const asyncEndCh = channel('apm:couchbase:query:async-end')
13
+ const endCh = channel('apm:couchbase:query:end')
14
+ const errorCh = channel('apm:couchbase:query:error')
15
+
16
+ Bucket.prototype._maybeInvoke = wrapMaybeInvoke(Bucket.prototype._maybeInvoke)
17
+ Bucket.prototype.query = wrapQuery(Bucket.prototype.query)
18
+
19
+ shimmer.wrap(Bucket.prototype, '_n1qlReq', _n1qlReq => function (host, q, adhoc, emitter) {
20
+ if (!startCh.hasSubscribers) {
21
+ return _n1qlReq.apply(this, arguments)
22
+ }
23
+
24
+ if (!emitter || !emitter.once) return _n1qlReq.apply(this, arguments)
25
+
26
+ const n1qlQuery = q && q.statement
27
+
28
+ startCh.publish({ resource: n1qlQuery, bucket: this })
29
+
30
+ emitter.once('rows', AsyncResource.bind(() => {
31
+ asyncEndCh.publish(undefined)
32
+ }))
33
+
34
+ emitter.once('error', AsyncResource.bind((error) => {
35
+ errorCh.publish(error)
36
+ asyncEndCh.publish(undefined)
37
+ }))
38
+
39
+ try {
40
+ return _n1qlReq.apply(this, arguments)
41
+ } catch (err) {
42
+ err.stack // trigger getting the stack at the original throwing point
43
+ errorCh.publish(err)
44
+
45
+ throw err
46
+ } finally {
47
+ endCh.publish(undefined)
48
+ }
49
+ })
50
+
51
+ Bucket.prototype.upsert = wrap('apm:couchbase:upsert', Bucket.prototype.upsert)
52
+ Bucket.prototype.insert = wrap('apm:couchbase:insert', Bucket.prototype.insert)
53
+ Bucket.prototype.replace = wrap('apm:couchbase:replace', Bucket.prototype.replace)
54
+ Bucket.prototype.append = wrap('apm:couchbase:append', Bucket.prototype.append)
55
+ Bucket.prototype.prepend = wrap('apm:couchbase:prepend', Bucket.prototype.prepend)
56
+
57
+ return Bucket
58
+ })
59
+
60
+ addHook({ name: 'couchbase', file: 'lib/cluster.js', versions: ['^2.6.5'] }, Cluster => {
61
+ Cluster.prototype._maybeInvoke = wrapMaybeInvoke(Cluster.prototype._maybeInvoke)
62
+ Cluster.prototype.query = wrapQuery(Cluster.prototype.query)
63
+
64
+ return Cluster
65
+ })
66
+
67
+ function findCallbackIndex (args) {
68
+ for (let i = args.length - 1; i >= 2; i--) {
69
+ if (typeof args[i] === 'function') return i
70
+ }
71
+ return -1
72
+ }
73
+
74
+ function wrapMaybeInvoke (_maybeInvoke) {
75
+ const wrapped = function (fn, args) {
76
+ if (!Array.isArray(args)) return _maybeInvoke.apply(this, arguments)
77
+
78
+ const callbackIndex = args.length - 1
79
+ const callback = args[callbackIndex]
80
+
81
+ if (callback instanceof Function) {
82
+ args[callbackIndex] = AsyncResource.bind(callback)
83
+ }
84
+
85
+ return _maybeInvoke.apply(this, arguments)
86
+ }
87
+ return shimmer.wrap(_maybeInvoke, wrapped)
88
+ }
89
+
90
+ function wrapQuery (query) {
91
+ const wrapped = function (q, params, callback) {
92
+ callback = AsyncResource.bind(arguments[arguments.length - 1])
93
+
94
+ if (typeof callback === 'function') {
95
+ arguments[arguments.length - 1] = callback
96
+ }
97
+
98
+ const res = query.apply(this, arguments)
99
+ return res
100
+ }
101
+ return shimmer.wrap(query, wrapped)
102
+ }
103
+
104
+ function wrap (prefix, fn) {
105
+ const startCh = channel(prefix + ':start')
106
+ const endCh = channel(prefix + ':end')
107
+ const asyncEndCh = channel(prefix + ':async-end')
108
+ const errorCh = channel(prefix + ':error')
109
+
110
+ const wrapped = function (key, value, options, callback) {
111
+ if (!startCh.hasSubscribers) {
112
+ return fn.apply(this, arguments)
113
+ }
114
+
115
+ const callbackIndex = findCallbackIndex(arguments)
116
+
117
+ if (callbackIndex < 0) return fn.apply(this, arguments)
118
+
119
+ const cb = arguments[callbackIndex]
120
+
121
+ startCh.publish({ bucket: this })
122
+
123
+ arguments[callbackIndex] = function (error, result) {
124
+ if (error) {
125
+ errorCh.publish(error)
126
+ }
127
+ asyncEndCh.publish(result)
128
+ return cb.apply(this, arguments)
129
+ }
130
+
131
+ try {
132
+ return fn.apply(this, arguments)
133
+ } catch (error) {
134
+ error.stack // trigger getting the stack at the original throwing point
135
+ errorCh.publish(error)
136
+
137
+ throw error
138
+ } finally {
139
+ endCh.publish(undefined)
140
+ }
141
+ }
142
+ return shimmer.wrap(fn, wrapped)
143
+ }
@@ -0,0 +1,74 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ channel,
5
+ addHook,
6
+ AsyncResource
7
+ } = require('./helpers/instrument')
8
+ const shimmer = require('../../datadog-shimmer')
9
+
10
+ const startCh = channel('apm:elasticsearch:query:start')
11
+ const asyncEndCh = channel('apm:elasticsearch:query:async-end')
12
+ const endCh = channel('apm:elasticsearch:query:end')
13
+ const errorCh = channel('apm:elasticsearch:query:error')
14
+
15
+ addHook({ name: '@elastic/elasticsearch', file: 'lib/Transport.js', versions: ['>=5.6.16'] }, Transport => {
16
+ shimmer.wrap(Transport.prototype, 'request', wrapRequest)
17
+ return Transport
18
+ })
19
+
20
+ addHook({ name: 'elasticsearch', file: 'src/lib/transport.js', versions: ['>=10'] }, Transport => {
21
+ shimmer.wrap(Transport.prototype, 'request', wrapRequest)
22
+ return Transport
23
+ })
24
+
25
+ function wrapRequest (request) {
26
+ return function (params, options, cb) {
27
+ if (!startCh.hasSubscribers) {
28
+ return request.apply(this, arguments)
29
+ }
30
+
31
+ if (!params) return request.apply(this, arguments)
32
+
33
+ const asyncResource = new AsyncResource('bound-anonymous-fn')
34
+
35
+ startCh.publish({ params })
36
+
37
+ try {
38
+ const lastIndex = arguments.length - 1
39
+ cb = arguments[lastIndex]
40
+
41
+ if (typeof cb === 'function') {
42
+ cb = asyncResource.bind(cb)
43
+
44
+ arguments[lastIndex] = AsyncResource.bind(function (error) {
45
+ finish(params, error)
46
+ return cb.apply(null, arguments)
47
+ })
48
+ return request.apply(this, arguments)
49
+ } else {
50
+ const promise = request.apply(this, arguments)
51
+ if (promise && typeof promise.then === 'function') {
52
+ promise.then(() => finish(params), e => finish(params, e))
53
+ } else {
54
+ finish(params)
55
+ }
56
+ return promise
57
+ }
58
+ } catch (err) {
59
+ err.stack // trigger getting the stack at the original throwing point
60
+ errorCh.publish(err)
61
+
62
+ throw err
63
+ } finally {
64
+ endCh.publish(undefined)
65
+ }
66
+ }
67
+ }
68
+
69
+ function finish (params, error) {
70
+ if (error) {
71
+ errorCh.publish(error)
72
+ }
73
+ asyncEndCh.publish({ params })
74
+ }
@@ -20,9 +20,9 @@ addHook({ name: 'mysql', file: 'lib/Connection.js', versions: ['>=2'] }, Connect
20
20
  }
21
21
 
22
22
  const sql = arguments[0].sql ? arguments[0].sql : arguments[0]
23
- const startArgs = [sql, this.config]
23
+ const conf = this.config
24
24
 
25
- startCh.publish(startArgs)
25
+ startCh.publish({ sql, conf })
26
26
 
27
27
  try {
28
28
  const res = query.apply(this, arguments)
@@ -0,0 +1,76 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ channel,
5
+ addHook,
6
+ AsyncResource
7
+ } = require('./helpers/instrument')
8
+ const shimmer = require('../../datadog-shimmer')
9
+
10
+ addHook({ name: 'mysql2', file: 'lib/connection.js', versions: ['>=1'] }, Connection => {
11
+ const startCh = channel('apm:mysql2:query:start')
12
+ const asyncEndCh = channel('apm:mysql2:query:async-end')
13
+ const endCh = channel('apm:mysql2:query:end')
14
+ const errorCh = channel('apm:mysql2:query:error')
15
+
16
+ shimmer.wrap(Connection.prototype, 'addCommand', addCommand => function (cmd) {
17
+ if (!startCh.hasSubscribers) return addCommand.apply(this, arguments)
18
+
19
+ const asyncResource = new AsyncResource('bound-anonymous-fn')
20
+ const name = cmd && cmd.constructor && cmd.constructor.name
21
+ const isCommand = typeof cmd.execute === 'function'
22
+ const isQuery = isCommand && (name === 'Execute' || name === 'Query')
23
+
24
+ // TODO: consider supporting all commands and not just queries
25
+ cmd.execute = isQuery
26
+ ? wrapExecute(cmd, cmd.execute, asyncResource, this.config)
27
+ : bindExecute(cmd, cmd.execute, asyncResource)
28
+
29
+ return asyncResource.bind(addCommand, this).apply(this, arguments)
30
+ })
31
+
32
+ return Connection
33
+
34
+ function bindExecute (cmd, execute, asyncResource) {
35
+ return asyncResource.bind(function executeWithTrace (packet, connection) {
36
+ if (this.onResult) {
37
+ this.onResult = asyncResource.bind(this.onResult)
38
+ }
39
+
40
+ return execute.apply(this, arguments)
41
+ }, cmd)
42
+ }
43
+
44
+ function wrapExecute (cmd, execute, asyncResource, config) {
45
+ return asyncResource.bind(function executeWithTrace (packet, connection) {
46
+ const sql = cmd.statement ? cmd.statement.query : cmd.sql
47
+
48
+ startCh.publish({ sql, conf: config })
49
+
50
+ if (this.onResult) {
51
+ const onResult = asyncResource.bind(this.onResult)
52
+
53
+ this.onResult = AsyncResource.bind(function (error) {
54
+ if (error) {
55
+ errorCh.publish(error)
56
+ }
57
+ asyncEndCh.publish(undefined)
58
+ onResult.apply(this, arguments)
59
+ }, 'bound-anonymous-fn', this)
60
+ } else {
61
+ this.on('error', AsyncResource.bind(error => errorCh.publish(error)))
62
+ this.on('end', AsyncResource.bind(() => asyncEndCh.publish(undefined)))
63
+ }
64
+
65
+ this.execute = execute
66
+
67
+ try {
68
+ return execute.apply(this, arguments)
69
+ } catch (err) {
70
+ errorCh.publish(err)
71
+ } finally {
72
+ endCh.publish(undefined)
73
+ }
74
+ }, cmd)
75
+ }
76
+ })
@@ -6,8 +6,16 @@ const shimmer = require('../../datadog-shimmer')
6
6
 
7
7
  addHook({
8
8
  name: 'q',
9
- versions: ['>=1']
9
+ versions: ['1']
10
10
  }, Q => {
11
11
  shimmer.wrap(Q.makePromise.prototype, 'then', wrapThen)
12
12
  return Q
13
13
  })
14
+
15
+ addHook({
16
+ name: 'q',
17
+ versions: ['>=2']
18
+ }, Q => {
19
+ shimmer.wrap(Q.Promise.prototype, 'then', wrapThen)
20
+ return Q
21
+ })
@@ -10,7 +10,8 @@ const services = {
10
10
  s3: getService(require('./services/s3')),
11
11
  redshift: getService(require('./services/redshift')),
12
12
  sns: getService(require('./services/sns')),
13
- sqs: getService(require('./services/sqs'))
13
+ sqs: getService(require('./services/sqs')),
14
+ eventbridge: getService(require('./services/eventbridge'))
14
15
  }
15
16
 
16
17
  function getService (Service) {
@@ -78,8 +79,8 @@ const helpers = {
78
79
  requestInject (span, request, serviceName, tracer) {
79
80
  if (!span) return
80
81
 
81
- const inject = services[serviceName] && services[serviceName].requestInject
82
- if (inject) inject(span, request, tracer)
82
+ const service = services[serviceName] && services[serviceName]
83
+ if (service && service.requestInject) service.requestInject(span, request, tracer)
83
84
  },
84
85
 
85
86
  wrapCb (cb, serviceName, tags, request, tracer, childOf) {
@@ -0,0 +1,48 @@
1
+ 'use strict'
2
+ const log = require('../../../dd-trace/src/log')
3
+ class EventBridge {
4
+ generateTags (params, operation, response) {
5
+ if (!params || !params.source) return {}
6
+
7
+ return {
8
+ 'resource.name': `${operation} ${params.source}`,
9
+ 'aws.eventbridge.source': params.source
10
+ }
11
+ }
12
+
13
+ /**
14
+ * requestInject
15
+ * @param {*} span
16
+ * @param {*} request
17
+ * @param {*} tracer
18
+ *
19
+ * Docs: https://docs.aws.amazon.com/eventbridge/latest/APIReference/API_PutEventsRequestEntry.html
20
+ * We cannot use the traceHeader field as that's reserved for X-Ray.
21
+ * Detail must be a valid JSON string
22
+ * Max size per event is 256kb (https://docs.aws.amazon.com/eventbridge/latest/userguide/eb-putevent-size.html)
23
+ */
24
+ requestInject (span, request, tracer) {
25
+ const operation = request.operation
26
+ if (operation === 'putEvents' &&
27
+ request.params &&
28
+ request.params.Entries &&
29
+ request.params.Entries.length > 0 &&
30
+ request.params.Entries[0].Detail) {
31
+ try {
32
+ const details = JSON.parse(request.params.Entries[0].Detail)
33
+ details._datadog = {}
34
+ tracer.inject(span, 'text_map', details._datadog)
35
+ const finalData = JSON.stringify(details)
36
+ const byteSize = Buffer.byteLength(finalData)
37
+ if (byteSize >= (1024 * 256)) {
38
+ log.info('Payload size too large to pass context')
39
+ return
40
+ }
41
+ request.params.Entries[0].Detail = finalData
42
+ } catch (e) {
43
+ log.error(e)
44
+ }
45
+ }
46
+ }
47
+ }
48
+ module.exports = EventBridge
@@ -1,15 +1,65 @@
1
1
  'use strict'
2
-
2
+ const log = require('../../../dd-trace/src/log')
3
3
  class Kinesis {
4
4
  generateTags (params, operation, response) {
5
- const tags = {}
6
-
7
- if (!params || !params.StreamName) return tags
5
+ if (!params || !params.StreamName) return {}
8
6
 
9
- return Object.assign(tags, {
7
+ return {
10
8
  'resource.name': `${operation} ${params.StreamName}`,
11
9
  'aws.kinesis.stream_name': params.StreamName
12
- })
10
+ }
11
+ }
12
+
13
+ // AWS-SDK will b64 kinesis payloads
14
+ // or will accept an already b64 encoded payload
15
+ // This method handles both
16
+ _tryParse (body) {
17
+ try {
18
+ return JSON.parse(body)
19
+ } catch (e) {
20
+ log.info('Not JSON string. Trying Base64 encoded JSON string')
21
+ }
22
+ try {
23
+ return JSON.parse(Buffer.from(body, 'base64').toString('ascii'), true)
24
+ } catch (e) {
25
+ return null
26
+ }
27
+ }
28
+
29
+ requestInject (span, request, tracer) {
30
+ const operation = request.operation
31
+ if (operation === 'putRecord' || operation === 'putRecords') {
32
+ if (!request.params) {
33
+ return
34
+ }
35
+
36
+ const traceData = {}
37
+ tracer.inject(span, 'text_map', traceData)
38
+ let injectPath
39
+ if (request.params.Records && request.params.Records.length > 0) {
40
+ injectPath = request.params.Records[0]
41
+ } else if (request.params.Data) {
42
+ injectPath = request.params
43
+ } else {
44
+ log.error('No valid payload passed, unable to pass trace context')
45
+ return
46
+ }
47
+ const parsedData = this._tryParse(injectPath.Data)
48
+ if (parsedData) {
49
+ parsedData._datadog = traceData
50
+ const finalData = JSON.stringify(parsedData)
51
+ const byteSize = Buffer.byteLength(finalData, 'ascii')
52
+ // Kinesis max payload size is 1MB
53
+ // So we must ensure adding DD context won't go over that (512b is an estimate)
54
+ if (byteSize >= 1048576) {
55
+ log.info('Payload size too large to pass context')
56
+ return
57
+ }
58
+ injectPath.Data = finalData
59
+ } else {
60
+ log.error('Unable to parse payload, unable to pass trace context')
61
+ }
62
+ }
13
63
  }
14
64
  }
15
65
 
@@ -1,21 +1,48 @@
1
1
  'use strict'
2
+ const log = require('../../../dd-trace/src/log')
2
3
 
3
4
  class Sns {
4
5
  generateTags (params, operation, response) {
5
- const tags = {}
6
+ if (!params) return {}
6
7
 
7
- if (!params) return tags
8
+ if (!params.TopicArn && !(response.data && response.data.TopicArn)) return {}
8
9
 
9
- if (!params.TopicArn && !(response.data && response.data.TopicArn)) return tags
10
-
11
- return Object.assign(tags, {
10
+ return {
12
11
  'resource.name': `${operation} ${params.TopicArn || response.data.TopicArn}`,
13
12
  'aws.sns.topic_arn': params.TopicArn || response.data.TopicArn
14
- })
13
+ }
15
14
 
16
15
  // TODO: should arn be sanitized or quantized in some way here,
17
16
  // for example if it contains a phone number?
18
17
  }
18
+
19
+ requestInject (span, request, tracer) {
20
+ const operation = request.operation
21
+ if (operation === 'publish' || operation === 'publishBatch') {
22
+ if (!request.params) {
23
+ request.params = {}
24
+ }
25
+ let injectPath
26
+ if (request.params.PublishBatchRequestEntries && request.params.PublishBatchRequestEntries.length > 0) {
27
+ injectPath = request.params.PublishBatchRequestEntries[0]
28
+ } else if (request.params.Message) {
29
+ injectPath = request.params
30
+ }
31
+ if (!injectPath.MessageAttributes) {
32
+ injectPath.MessageAttributes = {}
33
+ }
34
+ if (Object.keys(injectPath.MessageAttributes).length >= 10) { // SNS quota
35
+ log.info('Message attributes full, skipping trace context injection')
36
+ return
37
+ }
38
+ const ddInfo = {}
39
+ tracer.inject(span, 'text_map', ddInfo)
40
+ injectPath.MessageAttributes._datadog = {
41
+ DataType: 'String',
42
+ StringValue: JSON.stringify(ddInfo)
43
+ }
44
+ }
45
+ }
19
46
  }
20
47
 
21
48
  module.exports = Sns