dd-trace 4.2.0 → 4.3.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 (49) hide show
  1. package/index.d.ts +7 -0
  2. package/package.json +5 -5
  3. package/packages/datadog-instrumentations/src/cookie.js +21 -0
  4. package/packages/datadog-instrumentations/src/fetch.js +48 -0
  5. package/packages/datadog-instrumentations/src/grpc/server.js +1 -1
  6. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  7. package/packages/datadog-instrumentations/src/helpers/register.js +10 -0
  8. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +18 -0
  9. package/packages/datadog-plugin-fetch/src/index.js +36 -0
  10. package/packages/datadog-plugin-http/src/client.js +24 -8
  11. package/packages/datadog-plugin-mysql/src/index.js +2 -11
  12. package/packages/datadog-plugin-tedious/src/index.js +2 -2
  13. package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +3 -0
  14. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +52 -0
  15. package/packages/dd-trace/src/appsec/iast/analyzers/insecure-cookie-analyzer.js +3 -22
  16. package/packages/dd-trace/src/appsec/iast/analyzers/no-httponly-cookie-analyzer.js +12 -0
  17. package/packages/dd-trace/src/appsec/iast/analyzers/no-samesite-cookie-analyzer.js +12 -0
  18. package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +7 -3
  19. package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +3 -3
  20. package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +48 -0
  21. package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +3 -3
  22. package/packages/dd-trace/src/appsec/iast/index.js +9 -2
  23. package/packages/dd-trace/src/appsec/iast/path-line.js +13 -0
  24. package/packages/dd-trace/src/appsec/iast/tags.js +6 -0
  25. package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +2 -1
  26. package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +13 -4
  27. package/packages/dd-trace/src/appsec/iast/taint-tracking/origin-types.js +5 -1
  28. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +24 -4
  29. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -1
  30. package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +3 -0
  31. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +7 -1
  32. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -3
  33. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +5 -2
  34. package/packages/dd-trace/src/config.js +13 -0
  35. package/packages/dd-trace/src/external-logger/src/index.js +126 -0
  36. package/packages/dd-trace/src/external-logger/test/index.spec.js +147 -0
  37. package/packages/dd-trace/src/lambda/handler.js +3 -15
  38. package/packages/dd-trace/src/noop/proxy.js +4 -0
  39. package/packages/dd-trace/src/opentelemetry/context_manager.js +1 -1
  40. package/packages/dd-trace/src/plugin_manager.js +10 -7
  41. package/packages/dd-trace/src/plugins/database.js +7 -3
  42. package/packages/dd-trace/src/plugins/plugin.js +3 -1
  43. package/packages/dd-trace/src/plugins/util/exec.js +2 -2
  44. package/packages/dd-trace/src/plugins/util/git.js +51 -24
  45. package/packages/dd-trace/src/profiling/config.js +2 -0
  46. package/packages/dd-trace/src/profiling/profiler.js +13 -4
  47. package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +24 -1
  48. package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +18 -1
  49. package/packages/dd-trace/src/util.js +1 -1
@@ -0,0 +1,6 @@
1
+ 'use strict'
2
+
3
+ module.exports = {
4
+ IAST_ENABLED_TAG_KEY: '_dd.iast.enabled',
5
+ IAST_JSON_TAG_KEY: '_dd.iast.json'
6
+ }
@@ -23,5 +23,6 @@ module.exports = {
23
23
  },
24
24
  setMaxTransactions: setMaxTransactions,
25
25
  createTransaction: createTransaction,
26
- removeTransaction: removeTransaction
26
+ removeTransaction: removeTransaction,
27
+ taintTrackingPlugin
27
28
  }
@@ -30,14 +30,14 @@ function newTaintedString (iastContext, string, name, type) {
30
30
  return result
31
31
  }
32
32
 
33
- function taintObject (iastContext, object, type) {
33
+ function taintObject (iastContext, object, type, keyTainting, keyType) {
34
34
  let result = object
35
35
  if (iastContext && iastContext[IAST_TRANSACTION_ID]) {
36
36
  const transactionId = iastContext[IAST_TRANSACTION_ID]
37
37
  const queue = [{ parent: null, property: null, value: object }]
38
38
  const visited = new WeakSet()
39
39
  while (queue.length > 0) {
40
- const { parent, property, value } = queue.pop()
40
+ const { parent, property, value, key } = queue.pop()
41
41
  if (value === null) {
42
42
  continue
43
43
  }
@@ -47,14 +47,23 @@ function taintObject (iastContext, object, type) {
47
47
  if (!parent) {
48
48
  result = tainted
49
49
  } else {
50
- parent[property] = tainted
50
+ if (keyTainting && key) {
51
+ const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
52
+ parent[taintedProperty] = tainted
53
+ } else {
54
+ parent[property] = tainted
55
+ }
51
56
  }
52
57
  } else if (typeof value === 'object' && !visited.has(value)) {
53
58
  visited.add(value)
54
59
  const keys = Object.keys(value)
55
60
  for (let i = 0; i < keys.length; i++) {
56
61
  const key = keys[i]
57
- queue.push({ parent: value, property: property ? `${property}.${key}` : key, value: value[key] })
62
+ queue.push({ parent: value, property: property ? `${property}.${key}` : key, value: value[key], key })
63
+ }
64
+ if (parent && keyTainting && key) {
65
+ const taintedProperty = TaintedUtils.newTaintedString(transactionId, key, property, keyType)
66
+ parent[taintedProperty] = value
58
67
  }
59
68
  }
60
69
  } catch (e) {
@@ -2,5 +2,9 @@
2
2
 
3
3
  module.exports = {
4
4
  HTTP_REQUEST_BODY: 'http.request.body',
5
- HTTP_REQUEST_PARAMETER: 'http.request.parameter'
5
+ HTTP_REQUEST_PARAMETER: 'http.request.parameter',
6
+ HTTP_REQUEST_COOKIE_VALUE: 'http.request.cookie.value',
7
+ HTTP_REQUEST_COOKIE_NAME: 'http.request.cookie.name',
8
+ HTTP_REQUEST_HEADER_NAME: 'http.request.header.name',
9
+ HTTP_REQUEST_HEADER_VALUE: 'http.request.header'
6
10
  }
@@ -3,8 +3,15 @@
3
3
  const Plugin = require('../../../plugins/plugin')
4
4
  const { getIastContext } = require('../iast-context')
5
5
  const { storage } = require('../../../../../datadog-core')
6
- const { HTTP_REQUEST_PARAMETER, HTTP_REQUEST_BODY } = require('./origin-types')
7
6
  const { taintObject } = require('./operations')
7
+ const {
8
+ HTTP_REQUEST_PARAMETER,
9
+ HTTP_REQUEST_BODY,
10
+ HTTP_REQUEST_COOKIE_VALUE,
11
+ HTTP_REQUEST_COOKIE_NAME,
12
+ HTTP_REQUEST_HEADER_VALUE,
13
+ HTTP_REQUEST_HEADER_NAME
14
+ } = require('./origin-types')
8
15
 
9
16
  class TaintTrackingPlugin extends Plugin {
10
17
  constructor () {
@@ -22,8 +29,8 @@ class TaintTrackingPlugin extends Plugin {
22
29
  )
23
30
  this.addSub(
24
31
  'datadog:qs:parse:finish',
25
- ({ qs }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, qs))
26
-
32
+ ({ qs }) => this._taintTrackingHandler(HTTP_REQUEST_PARAMETER, qs)
33
+ )
27
34
  this.addSub('apm:express:middleware:next', ({ req }) => {
28
35
  if (req && req.body && typeof req.body === 'object') {
29
36
  const iastContext = getIastContext(storage.getStore())
@@ -33,16 +40,29 @@ class TaintTrackingPlugin extends Plugin {
33
40
  }
34
41
  }
35
42
  })
43
+ this.addSub(
44
+ 'datadog:cookie:parse:finish',
45
+ ({ cookies }) => this._cookiesTaintTrackingHandler(cookies)
46
+ )
36
47
  }
37
48
 
38
49
  _taintTrackingHandler (type, target, property, iastContext = getIastContext(storage.getStore())) {
39
50
  if (!property) {
40
51
  taintObject(iastContext, target, type)
41
- } else {
52
+ } else if (target[property]) {
42
53
  target[property] = taintObject(iastContext, target[property], type)
43
54
  }
44
55
  }
45
56
 
57
+ _cookiesTaintTrackingHandler (target) {
58
+ const iastContext = getIastContext(storage.getStore())
59
+ taintObject(iastContext, target, HTTP_REQUEST_COOKIE_VALUE, true, HTTP_REQUEST_COOKIE_NAME)
60
+ }
61
+
62
+ taintHeaders (headers, iastContext) {
63
+ taintObject(iastContext, headers, HTTP_REQUEST_HEADER_VALUE, true, HTTP_REQUEST_HEADER_NAME)
64
+ }
65
+
46
66
  enable () {
47
67
  this.configure(true)
48
68
  }
@@ -23,7 +23,9 @@ class SensitiveHandler {
23
23
  this._sensitiveAnalyzers.set(vulnerabilities.COMMAND_INJECTION, new CommandSensitiveAnalyzer())
24
24
  this._sensitiveAnalyzers.set(vulnerabilities.LDAP_INJECTION, new LdapSensitiveAnalyzer())
25
25
  this._sensitiveAnalyzers.set(vulnerabilities.SQL_INJECTION, new SqlSensitiveAnalyzer())
26
- this._sensitiveAnalyzers.set(vulnerabilities.SSRF, new UrlSensitiveAnalyzer())
26
+ const urlSensitiveAnalyzer = new UrlSensitiveAnalyzer()
27
+ this._sensitiveAnalyzers.set(vulnerabilities.SSRF, urlSensitiveAnalyzer)
28
+ this._sensitiveAnalyzers.set(vulnerabilities.UNVALIDATED_REDIRECT, urlSensitiveAnalyzer)
27
29
  }
28
30
 
29
31
  isSensibleName (name) {
@@ -2,9 +2,12 @@ module.exports = {
2
2
  COMMAND_INJECTION: 'COMMAND_INJECTION',
3
3
  INSECURE_COOKIE: 'INSECURE_COOKIE',
4
4
  LDAP_INJECTION: 'LDAP_INJECTION',
5
+ NO_HTTPONLY_COOKIE: 'NO_HTTPONLY_COOKIE',
6
+ NO_SAMESITE_COOKIE: 'NO_SAMESITE_COOKIE',
5
7
  PATH_TRAVERSAL: 'PATH_TRAVERSAL',
6
8
  SQL_INJECTION: 'SQL_INJECTION',
7
9
  SSRF: 'SSRF',
10
+ UNVALIDATED_REDIRECT: 'UNVALIDATED_REDIRECT',
8
11
  WEAK_CIPHER: 'WEAK_CIPHER',
9
12
  WEAK_HASH: 'WEAK_HASH'
10
13
  }
@@ -1,8 +1,11 @@
1
+ 'use strict'
2
+
1
3
  const { MANUAL_KEEP } = require('../../../../../ext/tags')
2
4
  const LRU = require('lru-cache')
3
5
  const vulnerabilitiesFormatter = require('./vulnerabilities-formatter')
6
+ const { IAST_ENABLED_TAG_KEY, IAST_JSON_TAG_KEY } = require('./tags')
7
+
4
8
  const VULNERABILITIES_KEY = 'vulnerabilities'
5
- const IAST_JSON_TAG_KEY = '_dd.iast.json'
6
9
  const VULNERABILITY_HASHES_MAX_SIZE = 1000
7
10
  const VULNERABILITY_HASHES = new LRU({ max: VULNERABILITY_HASHES_MAX_SIZE })
8
11
  const RESET_VULNERABILITY_CACHE_INTERVAL = 60 * 60 * 1000 // 1 hour
@@ -39,6 +42,9 @@ function sendVulnerabilities (vulnerabilities, rootSpan) {
39
42
  vulnerabilities.forEach((vulnerability) => {
40
43
  vulnerability.location.spanId = span.context().toSpanId()
41
44
  })
45
+ span.addTags({
46
+ [IAST_ENABLED_TAG_KEY]: 1
47
+ })
42
48
  }
43
49
 
44
50
  if (span && span.addTags) {
@@ -120,7 +120,8 @@ class CiVisibilityExporter extends AgentInfoExporter {
120
120
  * CI Visibility Protocol, hence the this._canUseCiVisProtocol promise.
121
121
  */
122
122
  getItrConfiguration (testConfiguration, callback) {
123
- this.sendGitMetadata()
123
+ const { repositoryUrl } = testConfiguration
124
+ this.sendGitMetadata(repositoryUrl)
124
125
  if (!this.shouldRequestItrConfiguration()) {
125
126
  return callback(null, {})
126
127
  }
@@ -147,7 +148,7 @@ class CiVisibilityExporter extends AgentInfoExporter {
147
148
  })
148
149
  }
149
150
 
150
- sendGitMetadata () {
151
+ sendGitMetadata (repositoryUrl) {
151
152
  if (!this._config.isGitUploadEnabled) {
152
153
  return
153
154
  }
@@ -155,7 +156,7 @@ class CiVisibilityExporter extends AgentInfoExporter {
155
156
  if (!canUseCiVisProtocol) {
156
157
  return
157
158
  }
158
- sendGitMetadataRequest(this._getApiUrl(), !!this._isUsingEvpProxy, (err) => {
159
+ sendGitMetadataRequest(this._getApiUrl(), !!this._isUsingEvpProxy, repositoryUrl, (err) => {
159
160
  if (err) {
160
161
  log.error(`Error uploading git metadata: ${err.message}`)
161
162
  } else {
@@ -152,8 +152,11 @@ function uploadPackFile ({ url, isEvpProxy, packFileToUpload, repositoryUrl, hea
152
152
  /**
153
153
  * This function uploads git metadata to CI Visibility's backend.
154
154
  */
155
- function sendGitMetadata (url, isEvpProxy, callback) {
156
- const repositoryUrl = getRepositoryUrl()
155
+ function sendGitMetadata (url, isEvpProxy, configRepositoryUrl, callback) {
156
+ let repositoryUrl = configRepositoryUrl
157
+ if (!repositoryUrl) {
158
+ repositoryUrl = getRepositoryUrl()
159
+ }
157
160
 
158
161
  if (!repositoryUrl) {
159
162
  return callback(new Error('Repository URL is empty'))
@@ -204,6 +204,17 @@ class Config {
204
204
  false
205
205
  )
206
206
 
207
+ const DD_OPENAI_LOGS_ENABLED = coalesce(
208
+ options.openAiLogsEnabled,
209
+ process.env.DD_OPENAI_LOGS_ENABLED,
210
+ false
211
+ )
212
+
213
+ const DD_API_KEY = coalesce(
214
+ process.env.DATADOG_API_KEY,
215
+ process.env.DD_API_KEY
216
+ )
217
+
207
218
  const inAWSLambda = process.env.AWS_LAMBDA_FUNCTION_NAME !== undefined
208
219
 
209
220
  const isDeprecatedGCPFunction = process.env.FUNCTION_NAME !== undefined && process.env.GCP_PROJECT !== undefined
@@ -471,6 +482,8 @@ ken|consumer_?(?:id|key|secret)|sign(?:ed|ature)?|auth(?:entication|orization)?)
471
482
 
472
483
  this.tracing = !isFalse(DD_TRACING_ENABLED)
473
484
  this.dbmPropagationMode = DD_DBM_PROPAGATION_MODE
485
+ this.openAiLogsEnabled = DD_OPENAI_LOGS_ENABLED
486
+ this.apiKey = DD_API_KEY
474
487
  this.logInjection = isTrue(DD_LOGS_INJECTION)
475
488
  this.env = DD_ENV
476
489
  this.url = DD_CIVISIBILITY_AGENTLESS_URL ? new URL(DD_CIVISIBILITY_AGENTLESS_URL)
@@ -0,0 +1,126 @@
1
+ const tracerLogger = require('../../log')// path to require tracer logger
2
+
3
+ const https = require('https')
4
+
5
+ class ExternalLogger {
6
+ // Note: these attribute names match the corresponding entry in the JSON payload.
7
+ constructor ({
8
+ ddsource, hostname, service, apiKey, site = 'datadoghq.com', interval = 10000, timeout = 2000, limit = 1000
9
+ }) {
10
+ this.ddsource = ddsource
11
+ this.hostname = hostname
12
+ this.service = service
13
+ this.interval = interval
14
+ this.timeout = timeout
15
+ this.queue = []
16
+ this.limit = limit
17
+ this.endpoint = '/api/v2/logs'
18
+ this.site = site
19
+ this.intake = `http-intake.logs.${this.site}`
20
+ this.headers = {
21
+ 'DD-API-KEY': apiKey,
22
+ 'Content-Type': 'application/json'
23
+ }
24
+ this.timer = setInterval(() => {
25
+ this.flush()
26
+ }, this.interval).unref()
27
+
28
+ tracerLogger.debug(`started log writer to https://${this.intake}${this.endpoint}`)
29
+ }
30
+
31
+ static tagString (tags) {
32
+ const tagArray = []
33
+ for (const key in tags) {
34
+ tagArray.push(key + ':' + tags[key])
35
+ }
36
+ return tagArray.join(',')
37
+ }
38
+
39
+ // Parses and enqueues a log
40
+ log (log, span, tags) {
41
+ const logTags = ExternalLogger.tagString(tags)
42
+
43
+ if (span) {
44
+ log['dd.trace_id'] = String(span.trace_id)
45
+ log['dd.span_id'] = String(span.span_id)
46
+ }
47
+
48
+ const payload = {
49
+ ...log,
50
+ 'timestamp': Date.now(),
51
+ 'hostname': log.hostname || this.hostname,
52
+ 'ddsource': log.ddsource || this.ddsource,
53
+ 'service': log.service || this.service,
54
+ 'ddtags': logTags || undefined
55
+ }
56
+
57
+ this.enqueue(payload)
58
+ }
59
+
60
+ // Enqueues a raw, non-formatted log object
61
+ enqueue (log) {
62
+ if (this.queue.length >= this.limit) {
63
+ this.flush()
64
+ }
65
+ this.queue.push(log)
66
+ }
67
+
68
+ shutdown () {
69
+ clearInterval(this.timer)
70
+ this.flush()
71
+ }
72
+
73
+ // Flushes logs with optional callback for when the call is complete
74
+ flush (cb = () => {}) {
75
+ let logs
76
+ let numLogs
77
+ let encodedLogs
78
+
79
+ if (!this.queue.length) {
80
+ setImmediate(() => cb())
81
+ return
82
+ }
83
+
84
+ try {
85
+ logs = this.queue
86
+ this.queue = []
87
+
88
+ numLogs = logs.length
89
+ encodedLogs = JSON.stringify(logs)
90
+ } catch (error) {
91
+ tracerLogger.error(`failed to encode ${numLogs} logs`)
92
+ setImmediate(() => cb(error))
93
+ return
94
+ }
95
+
96
+ const options = {
97
+ hostname: this.intake,
98
+ port: 443,
99
+ path: this.endpoint,
100
+ method: 'POST',
101
+ headers: this.headers,
102
+ timeout: this.timeout
103
+ }
104
+
105
+ const req = https.request(options, (res) => {
106
+ tracerLogger.info(`statusCode: ${res.statusCode}`)
107
+ })
108
+ req.once('error', (e) => {
109
+ tracerLogger.error(`failed to send ${numLogs} log(s), with error ${e.message}`)
110
+ cb(e)
111
+ })
112
+ req.write(encodedLogs)
113
+ req.end()
114
+ req.once('response', (res) => {
115
+ if (res.statusCode >= 400) {
116
+ const error = new Error(`failed to send ${numLogs} logs, received response code ${res.statusCode}`)
117
+ tracerLogger.error(error.message)
118
+ cb(error)
119
+ return
120
+ }
121
+ cb()
122
+ })
123
+ }
124
+ }
125
+
126
+ module.exports = ExternalLogger
@@ -0,0 +1,147 @@
1
+ 'use strict'
2
+
3
+ require('../../../../dd-trace/test/setup/tap')
4
+ const proxyquire = require('proxyquire')
5
+ const { expect } = require('chai')
6
+ const nock = require('nock')
7
+
8
+ const tracerLogger = require('../../log')
9
+
10
+ describe('External Logger', () => {
11
+ let externalLogger
12
+ let interceptor
13
+ let errorLog
14
+
15
+ beforeEach(() => {
16
+ errorLog = sinon.spy(tracerLogger, 'error')
17
+
18
+ const ExternalLogger = proxyquire('../src', {
19
+ '../../log': {
20
+ error: errorLog
21
+ }
22
+ })
23
+
24
+ externalLogger = new ExternalLogger({
25
+ ddsource: 'logging_from_space',
26
+ hostname: 'mac_desktop',
27
+ apiKey: 'API_KEY_PLACEHOLDER',
28
+ interval: 10000,
29
+ timeout: 5000,
30
+ limit: 10
31
+ })
32
+ })
33
+
34
+ afterEach(() => {
35
+ interceptor.done()
36
+ errorLog.restore()
37
+ })
38
+
39
+ it('should properly encode the log message', (done) => {
40
+ let request
41
+ const currentTime = Date.now()
42
+
43
+ interceptor = nock('https://http-intake.logs.datadoghq.com:443')
44
+ .post('/api/v2/logs')
45
+ .reply((_uri, req, cb) => {
46
+ request = req
47
+ cb(null, [202, '{}', { 'Content-Type': 'application/json' }])
48
+ })
49
+
50
+ const span = {
51
+ service: 'openAi',
52
+ trace_id: '000001000',
53
+ span_id: '9999991999'
54
+ }
55
+ const tags = {
56
+ env: 'external_logger',
57
+ version: '1.2.3',
58
+ service: 'external'
59
+ }
60
+ externalLogger.log({
61
+ message: 'oh no, something is up',
62
+ custom: 'field',
63
+ attribute: 'funky',
64
+ service: 'outer_space',
65
+ level: 'info'
66
+ }, span, tags)
67
+
68
+ externalLogger.flush((err) => {
69
+ try {
70
+ expect(request[0]).to.have.property('message', 'oh no, something is up')
71
+ expect(request[0]).to.have.property('custom', 'field')
72
+ expect(request[0]).to.have.property('attribute', 'funky')
73
+ expect(request[0]).to.have.property('service', 'outer_space')
74
+ expect(request[0]).to.have.property('level', 'info')
75
+ expect(request[0]).to.have.property('dd.trace_id', '000001000')
76
+ expect(request[0]).to.have.property('dd.span_id', '9999991999')
77
+ expect(request[0].timestamp).to.be.greaterThanOrEqual(currentTime)
78
+ expect(request[0]).to.have.property('ddsource', 'logging_from_space')
79
+ expect(request[0]).to.have.property('ddtags', 'env:external_logger,version:1.2.3,service:external')
80
+ } catch (e) {
81
+ done(e)
82
+ return
83
+ }
84
+
85
+ done(err)
86
+ })
87
+ })
88
+
89
+ it('should empty the log queue when calling flush', (done) => {
90
+ interceptor = nock('https://http-intake.logs.datadoghq.com:443')
91
+ .post('/api/v2/logs')
92
+ .reply(202, {})
93
+
94
+ externalLogger.enqueue({})
95
+ expect(externalLogger.queue.length).to.equal(1)
96
+
97
+ externalLogger.flush((err) => {
98
+ expect(externalLogger.queue.length).to.equal(0)
99
+ done(err)
100
+ })
101
+ })
102
+
103
+ it('tracer logger should handle error response codes from Logs API', (done) => {
104
+ interceptor = nock('https://http-intake.logs.datadoghq.com:443')
105
+ .post('/api/v2/logs')
106
+ .reply(400, {})
107
+
108
+ externalLogger.enqueue({})
109
+ externalLogger.flush((err) => {
110
+ expect(err).to.be.an.instanceOf(Error)
111
+ expect(errorLog.getCall(0).args[0]).to.be.equal(
112
+ 'failed to send 1 logs, received response code 400'
113
+ )
114
+ done()
115
+ })
116
+ })
117
+
118
+ it('tracer logger should handle simulated network error', (done) => {
119
+ interceptor = nock('https://http-intake.logs.datadoghq.com:443')
120
+ .post('/api/v2/logs')
121
+ .replyWithError('missing API key')
122
+
123
+ externalLogger.enqueue({})
124
+ externalLogger.flush((err) => {
125
+ expect(err).to.be.an.instanceOf(Error)
126
+ expect(errorLog.getCall(0).args[0]).to.be.equal(
127
+ 'failed to send 1 log(s), with error missing API key'
128
+ )
129
+ done()
130
+ })
131
+ })
132
+
133
+ it('causes a flush when exceeding log queue limit', (done) => {
134
+ const flusher = sinon.stub(externalLogger, 'flush')
135
+
136
+ for (let i = 0; i < 10; i++) {
137
+ externalLogger.enqueue({})
138
+ }
139
+ expect(flusher).to.not.have.been.called
140
+
141
+ externalLogger.enqueue({})
142
+ expect(flusher).to.have.been.called
143
+
144
+ flusher.restore()
145
+ done()
146
+ })
147
+ })
@@ -83,21 +83,9 @@ function extractContext (args) {
83
83
  */
84
84
  exports.datadog = function datadog (lambdaHandler) {
85
85
  return (...args) => {
86
- const patched = lambdaHandler.apply(this, args)
86
+ const context = extractContext(args)
87
87
 
88
- try {
89
- const context = extractContext(args)
90
-
91
- checkTimeout(context)
92
-
93
- if (patched) {
94
- // clear the timeout as soon as a result is returned
95
- patched.then(_ => clearTimeout(__lambdaTimeout))
96
- }
97
- } catch (e) {
98
- log.debug('Error patching AWS Lambda handler. Timeout spans will not be generated.')
99
- }
100
-
101
- return patched
88
+ checkTimeout(context)
89
+ return lambdaHandler.apply(this, args).then((res) => { clearTimeout(__lambdaTimeout); return res })
102
90
  }
103
91
  }
@@ -75,6 +75,10 @@ class Tracer {
75
75
  this.appsec.setUser(user)
76
76
  return this
77
77
  }
78
+
79
+ get TracerProvider () {
80
+ return require('../opentelemetry/tracer_provider')
81
+ }
78
82
  }
79
83
 
80
84
  module.exports = Tracer
@@ -62,7 +62,7 @@ class ContextManager {
62
62
  bind (context, target) {
63
63
  const self = this
64
64
  return function (...args) {
65
- return self.with(context, target, this, args)
65
+ return self.with(context, target, this, ...args)
66
66
  }
67
67
  }
68
68
 
@@ -26,8 +26,13 @@ const disabledPlugins = new Set(
26
26
  const pluginClasses = {}
27
27
 
28
28
  loadChannel.subscribe(({ name }) => {
29
- const Plugin = plugins[name]
29
+ maybeEnable(plugins[name])
30
+ })
31
+
32
+ // Globals
33
+ maybeEnable(require('../../datadog-plugin-fetch/src'))
30
34
 
35
+ function maybeEnable (Plugin) {
31
36
  if (!Plugin || typeof Plugin !== 'function') return
32
37
  if (!pluginClasses[Plugin.id]) {
33
38
  const envName = `DD_TRACE_${Plugin.id.toUpperCase()}_ENABLED`
@@ -42,7 +47,7 @@ loadChannel.subscribe(({ name }) => {
42
47
  pluginClasses[Plugin.id] = Plugin
43
48
  }
44
49
  }
45
- })
50
+ }
46
51
 
47
52
  // TODO this must always be a singleton.
48
53
  module.exports = class PluginManager {
@@ -68,7 +73,7 @@ module.exports = class PluginManager {
68
73
 
69
74
  if (!Plugin) return
70
75
  if (!this._pluginsByName[name]) {
71
- this._pluginsByName[name] = new Plugin(this._tracer)
76
+ this._pluginsByName[name] = new Plugin(this._tracer, this._tracerConfig)
72
77
  }
73
78
  if (!this._tracerConfig) return // TODO: don't wait for tracer to be initialized
74
79
 
@@ -76,6 +81,7 @@ module.exports = class PluginManager {
76
81
  enabled: this._tracerConfig.plugins !== false
77
82
  }
78
83
 
84
+ // extracts predetermined configuration from tracer and combines it with plugin-specific config
79
85
  this._pluginsByName[name].configure({
80
86
  ...this._getSharedConfig(name),
81
87
  ...pluginConfig
@@ -127,8 +133,7 @@ module.exports = class PluginManager {
127
133
  serviceMapping,
128
134
  queryStringObfuscation,
129
135
  site,
130
- url,
131
- dbmPropagationMode
136
+ url
132
137
  } = this._tracerConfig
133
138
 
134
139
  const sharedConfig = {}
@@ -141,8 +146,6 @@ module.exports = class PluginManager {
141
146
  sharedConfig.queryStringObfuscation = queryStringObfuscation
142
147
  }
143
148
 
144
- sharedConfig.dbmPropagationMode = dbmPropagationMode
145
-
146
149
  if (serviceMapping && serviceMapping[name]) {
147
150
  sharedConfig.service = serviceMapping[name]
148
151
  }
@@ -38,13 +38,17 @@ class DatabasePlugin extends StoragePlugin {
38
38
  }
39
39
 
40
40
  injectDbmQuery (query, serviceName, isPreparedStatement = false) {
41
- if (this.config.dbmPropagationMode === 'disabled') {
41
+ const mode = this.config.dbmPropagationMode || this._tracerConfig.dbmPropagationMode
42
+
43
+ if (mode === 'disabled') {
42
44
  return query
43
45
  }
46
+
44
47
  const servicePropagation = this.createDBMPropagationCommentService(serviceName)
45
- if (isPreparedStatement || this.config.dbmPropagationMode === 'service') {
48
+
49
+ if (isPreparedStatement || mode === 'service') {
46
50
  return `/*${servicePropagation}*/ ${query}`
47
- } else if (this.config.dbmPropagationMode === 'full') {
51
+ } else if (mode === 'full') {
48
52
  this.activeSpan.setTag('_dd.dbm_trace_injected', 'true')
49
53
  const traceparent = this.activeSpan._spanContext.toTraceparent()
50
54
  return `/*${servicePropagation},traceparent='${traceparent}'*/ ${query}`
@@ -26,10 +26,12 @@ class Subscription {
26
26
  }
27
27
 
28
28
  module.exports = class Plugin {
29
- constructor (tracer) {
29
+ constructor (tracer, tracerConfig) {
30
30
  this._subscriptions = []
31
31
  this._enabled = false
32
32
  this._tracer = tracer
33
+ this.config = {} // plugin-specific configuration, unset until .configure() is called
34
+ this._tracerConfig = tracerConfig // global tracer configuration
33
35
  }
34
36
 
35
37
  get tracer () {
@@ -1,8 +1,8 @@
1
1
  const cp = require('child_process')
2
2
 
3
- const sanitizedExec = (cmd, options = {}) => {
3
+ const sanitizedExec = (cmd, flags, options = { stdio: 'pipe' }) => {
4
4
  try {
5
- return cp.execSync(cmd, options).toString().replace(/(\r\n|\n|\r)/gm, '')
5
+ return cp.execFileSync(cmd, flags, options).toString().replace(/(\r\n|\n|\r)/gm, '')
6
6
  } catch (e) {
7
7
  return ''
8
8
  }