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.
- package/index.d.ts +7 -0
- package/package.json +5 -5
- package/packages/datadog-instrumentations/src/cookie.js +21 -0
- package/packages/datadog-instrumentations/src/fetch.js +48 -0
- package/packages/datadog-instrumentations/src/grpc/server.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
- package/packages/datadog-instrumentations/src/helpers/register.js +10 -0
- package/packages/datadog-instrumentations/src/otel-sdk-trace.js +18 -0
- package/packages/datadog-plugin-fetch/src/index.js +36 -0
- package/packages/datadog-plugin-http/src/client.js +24 -8
- package/packages/datadog-plugin-mysql/src/index.js +2 -11
- package/packages/datadog-plugin-tedious/src/index.js +2 -2
- package/packages/dd-trace/src/appsec/iast/analyzers/analyzers.js +3 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +52 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/insecure-cookie-analyzer.js +3 -22
- package/packages/dd-trace/src/appsec/iast/analyzers/no-httponly-cookie-analyzer.js +12 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/no-samesite-cookie-analyzer.js +12 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/set-cookies-header-interceptor.js +7 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/sql-injection-analyzer.js +3 -3
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +48 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +3 -3
- package/packages/dd-trace/src/appsec/iast/index.js +9 -2
- package/packages/dd-trace/src/appsec/iast/path-line.js +13 -0
- package/packages/dd-trace/src/appsec/iast/tags.js +6 -0
- package/packages/dd-trace/src/appsec/iast/taint-tracking/index.js +2 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/operations.js +13 -4
- package/packages/dd-trace/src/appsec/iast/taint-tracking/origin-types.js +5 -1
- package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +24 -4
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities.js +3 -0
- package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +7 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +4 -3
- package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +5 -2
- package/packages/dd-trace/src/config.js +13 -0
- package/packages/dd-trace/src/external-logger/src/index.js +126 -0
- package/packages/dd-trace/src/external-logger/test/index.spec.js +147 -0
- package/packages/dd-trace/src/lambda/handler.js +3 -15
- package/packages/dd-trace/src/noop/proxy.js +4 -0
- package/packages/dd-trace/src/opentelemetry/context_manager.js +1 -1
- package/packages/dd-trace/src/plugin_manager.js +10 -7
- package/packages/dd-trace/src/plugins/database.js +7 -3
- package/packages/dd-trace/src/plugins/plugin.js +3 -1
- package/packages/dd-trace/src/plugins/util/exec.js +2 -2
- package/packages/dd-trace/src/plugins/util/git.js +51 -24
- package/packages/dd-trace/src/profiling/config.js +2 -0
- package/packages/dd-trace/src/profiling/profiler.js +13 -4
- package/packages/dd-trace/src/service-naming/schemas/v0/storage.js +24 -1
- package/packages/dd-trace/src/service-naming/schemas/v1/storage.js +18 -1
- package/packages/dd-trace/src/util.js +1 -1
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
86
|
+
const context = extractContext(args)
|
|
87
87
|
|
|
88
|
-
|
|
89
|
-
|
|
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
|
}
|
|
@@ -26,8 +26,13 @@ const disabledPlugins = new Set(
|
|
|
26
26
|
const pluginClasses = {}
|
|
27
27
|
|
|
28
28
|
loadChannel.subscribe(({ name }) => {
|
|
29
|
-
|
|
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
|
-
|
|
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
|
-
|
|
48
|
+
|
|
49
|
+
if (isPreparedStatement || mode === 'service') {
|
|
46
50
|
return `/*${servicePropagation}*/ ${query}`
|
|
47
|
-
} else if (
|
|
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.
|
|
5
|
+
return cp.execFileSync(cmd, flags, options).toString().replace(/(\r\n|\n|\r)/gm, '')
|
|
6
6
|
} catch (e) {
|
|
7
7
|
return ''
|
|
8
8
|
}
|