dd-trace 5.86.0 → 5.88.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/LICENSE-3rdparty.csv +60 -32
- package/ext/exporters.d.ts +1 -0
- package/ext/exporters.js +1 -0
- package/index.d.ts +243 -7
- package/package.json +9 -6
- package/packages/datadog-instrumentations/src/ai.js +54 -90
- package/packages/datadog-instrumentations/src/cucumber.js +14 -0
- package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
- package/packages/datadog-instrumentations/src/helpers/hooks.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +55 -14
- package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +15 -13
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +103 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.js +108 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +2 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/transformer.js +21 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +138 -12
- package/packages/datadog-instrumentations/src/http/client.js +119 -1
- package/packages/datadog-instrumentations/src/jest.js +179 -15
- package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
- package/packages/datadog-instrumentations/src/mocha/utils.js +6 -0
- package/packages/datadog-instrumentations/src/mysql2.js +131 -64
- package/packages/datadog-instrumentations/src/playwright.js +9 -1
- package/packages/datadog-instrumentations/src/stripe.js +92 -0
- package/packages/datadog-instrumentations/src/vitest.js +11 -0
- package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
- package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
- package/packages/datadog-plugin-azure-functions/src/index.js +53 -37
- package/packages/datadog-plugin-bullmq/src/consumer.js +33 -11
- package/packages/datadog-plugin-bullmq/src/producer.js +60 -31
- package/packages/datadog-plugin-cucumber/src/index.js +9 -6
- package/packages/datadog-plugin-cypress/src/cypress-plugin.js +33 -0
- package/packages/datadog-plugin-cypress/src/support.js +48 -8
- package/packages/datadog-plugin-jest/src/index.js +12 -2
- package/packages/datadog-plugin-jest/src/util.js +2 -1
- package/packages/datadog-plugin-kafkajs/src/consumer.js +22 -12
- package/packages/datadog-plugin-kafkajs/src/producer.js +33 -22
- package/packages/datadog-plugin-mocha/src/index.js +9 -6
- package/packages/datadog-plugin-playwright/src/index.js +10 -6
- package/packages/datadog-plugin-vitest/src/index.js +13 -8
- package/packages/dd-trace/src/appsec/addresses.js +11 -0
- package/packages/dd-trace/src/appsec/channels.js +5 -1
- package/packages/dd-trace/src/appsec/downstream_requests.js +302 -0
- package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/ssrf-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/unvalidated-redirect-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/analyzers/vulnerability-analyzer.js +4 -5
- package/packages/dd-trace/src/appsec/iast/path-line.js +36 -25
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/command-sensitive-analyzer.js +1 -1
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-handler.js +3 -4
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +3 -2
- package/packages/dd-trace/src/appsec/index.js +103 -0
- package/packages/dd-trace/src/appsec/rasp/ssrf.js +66 -4
- package/packages/dd-trace/src/azure_metadata.js +0 -2
- package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +14 -1
- package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +1 -1
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +2 -0
- package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +1 -1
- package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -1
- package/packages/dd-trace/src/ci-visibility/requests/request.js +236 -0
- package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +1 -1
- package/packages/dd-trace/src/config/defaults.js +148 -195
- package/packages/dd-trace/src/config/helper.js +43 -1
- package/packages/dd-trace/src/config/index.js +42 -14
- package/packages/dd-trace/src/config/supported-configurations.json +4115 -510
- package/packages/dd-trace/src/constants.js +0 -2
- package/packages/dd-trace/src/crashtracking/crashtracker.js +10 -3
- package/packages/dd-trace/src/datastreams/pathway.js +22 -3
- package/packages/dd-trace/src/datastreams/processor.js +14 -1
- package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +47 -2
- package/packages/dd-trace/src/debugger/devtools_client/index.js +75 -23
- package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +23 -1
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +3 -3
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +168 -36
- package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +18 -0
- package/packages/dd-trace/src/encode/agentless-json.js +141 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/agent/writer.js +22 -8
- package/packages/dd-trace/src/exporters/agentless/index.js +89 -0
- package/packages/dd-trace/src/exporters/agentless/writer.js +184 -0
- package/packages/dd-trace/src/exporters/common/agents.js +1 -1
- package/packages/dd-trace/src/exporters/common/request.js +4 -4
- package/packages/dd-trace/src/llmobs/constants/writers.js +1 -1
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
- package/packages/dd-trace/src/llmobs/sdk.js +34 -5
- package/packages/dd-trace/src/opentelemetry/context_manager.js +19 -46
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +3 -4
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +3 -5
- package/packages/dd-trace/src/opentracing/span.js +6 -4
- package/packages/dd-trace/src/plugins/ci_plugin.js +57 -5
- package/packages/dd-trace/src/plugins/database.js +57 -45
- package/packages/dd-trace/src/plugins/outbound.js +27 -2
- package/packages/dd-trace/src/plugins/tracing.js +39 -4
- package/packages/dd-trace/src/plugins/util/inferred_proxy.js +7 -0
- package/packages/dd-trace/src/plugins/util/test.js +48 -0
- package/packages/dd-trace/src/plugins/util/web.js +8 -7
- package/packages/dd-trace/src/profiling/exporter_cli.js +1 -0
- package/packages/dd-trace/src/propagation-hash/index.js +145 -0
- package/packages/dd-trace/src/proxy.js +4 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
- package/packages/dd-trace/src/startup-log.js +3 -3
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/bullmq.json +0 -106
- package/packages/dd-trace/src/appsec/iast/analyzers/hardcoded-secrets-rules.js +0 -741
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-regex.js +0 -11
- package/packages/dd-trace/src/plugins/util/serverless.js +0 -8
- package/packages/dd-trace/src/scope/noop/scope.js +0 -21
|
@@ -6,6 +6,24 @@ const { normalizeName, REDACTED_IDENTIFIERS } = require('./redaction')
|
|
|
6
6
|
|
|
7
7
|
module.exports = {
|
|
8
8
|
processRawState: processProperties,
|
|
9
|
+
processRemoteObject,
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A RemoteObject with collected properties attached.
|
|
14
|
+
*
|
|
15
|
+
* @typedef {import('inspector').Runtime.RemoteObject & { properties?: object[] }} RemoteObjectWithProperties
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Process a RemoteObject into the snapshot format.
|
|
20
|
+
*
|
|
21
|
+
* @param {RemoteObjectWithProperties} remoteObject
|
|
22
|
+
* @param {number} maxLength - Maximum string length
|
|
23
|
+
* @returns {object} The processed value in snapshot format
|
|
24
|
+
*/
|
|
25
|
+
function processRemoteObject (remoteObject, maxLength) {
|
|
26
|
+
return getPropertyValueRaw({ value: remoteObject }, maxLength)
|
|
9
27
|
}
|
|
10
28
|
|
|
11
29
|
// Matches classes in source code, no matter how it's written:
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../log')
|
|
4
|
+
const { truncateSpan, normalizeSpan } = require('./tags-processors')
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Formats a span for JSON encoding.
|
|
8
|
+
* @param {object} span - The span to format
|
|
9
|
+
* @returns {object} The formatted span
|
|
10
|
+
*/
|
|
11
|
+
function formatSpan (span) {
|
|
12
|
+
span = normalizeSpan(truncateSpan(span, false))
|
|
13
|
+
|
|
14
|
+
if (span.span_events) {
|
|
15
|
+
span.meta.events = JSON.stringify(span.span_events)
|
|
16
|
+
delete span.span_events
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
return span
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Converts a span to JSON-serializable format.
|
|
24
|
+
* IDs are converted to lowercase hex strings. Start time is converted from
|
|
25
|
+
* nanoseconds to seconds for the intake format.
|
|
26
|
+
* @param {object} span - The formatted span
|
|
27
|
+
* @returns {object} JSON-serializable span object
|
|
28
|
+
*/
|
|
29
|
+
function spanToJSON (span) {
|
|
30
|
+
const result = {
|
|
31
|
+
trace_id: span.trace_id.toString(16).toLowerCase(),
|
|
32
|
+
span_id: span.span_id.toString(16).toLowerCase(),
|
|
33
|
+
parent_id: span.parent_id.toString(16).toLowerCase(),
|
|
34
|
+
name: span.name,
|
|
35
|
+
resource: span.resource,
|
|
36
|
+
service: span.service,
|
|
37
|
+
error: span.error,
|
|
38
|
+
start: Math.floor(span.start / 1e9),
|
|
39
|
+
duration: span.duration,
|
|
40
|
+
meta: span.meta,
|
|
41
|
+
metrics: span.metrics,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (span.type) {
|
|
45
|
+
result.type = span.type
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (span.meta_struct) {
|
|
49
|
+
result.meta_struct = span.meta_struct
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (span.links && span.links.length > 0) {
|
|
53
|
+
result.links = span.links
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return result
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* JSON encoder for agentless span intake.
|
|
61
|
+
* Encodes a single trace as JSON with the payload format: {"spans": [...]}
|
|
62
|
+
*
|
|
63
|
+
* This encoder handles one trace at a time since each trace must be sent as a
|
|
64
|
+
* separate request to the intake. -- bengl
|
|
65
|
+
*/
|
|
66
|
+
class AgentlessJSONEncoder {
|
|
67
|
+
constructor () {
|
|
68
|
+
this._reset()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Returns the number of spans encoded.
|
|
73
|
+
* @returns {number}
|
|
74
|
+
*/
|
|
75
|
+
count () {
|
|
76
|
+
return this._spanCount
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Encodes a trace (array of spans) into the buffer.
|
|
81
|
+
* @param {object[]} trace - Array of spans to encode
|
|
82
|
+
*/
|
|
83
|
+
encode (trace) {
|
|
84
|
+
for (const span of trace) {
|
|
85
|
+
try {
|
|
86
|
+
const formattedSpan = formatSpan(span)
|
|
87
|
+
const jsonSpan = spanToJSON(formattedSpan)
|
|
88
|
+
|
|
89
|
+
this._spans.push(jsonSpan)
|
|
90
|
+
this._spanCount++
|
|
91
|
+
} catch (err) {
|
|
92
|
+
log.error(
|
|
93
|
+
'Failed to encode span (name: %s, service: %s). Span will be dropped. Error: %s\n%s',
|
|
94
|
+
span?.name || 'unknown',
|
|
95
|
+
span?.service || 'unknown',
|
|
96
|
+
err.message,
|
|
97
|
+
err.stack
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates the JSON payload for the encoded trace.
|
|
105
|
+
* @returns {Buffer} JSON payload as a buffer, or empty buffer if no spans
|
|
106
|
+
*/
|
|
107
|
+
makePayload () {
|
|
108
|
+
if (this._spans.length === 0) {
|
|
109
|
+
this._reset()
|
|
110
|
+
return Buffer.alloc(0)
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
const payload = JSON.stringify({ spans: this._spans })
|
|
115
|
+
this._reset()
|
|
116
|
+
return Buffer.from(payload, 'utf8')
|
|
117
|
+
} catch (err) {
|
|
118
|
+
log.error(
|
|
119
|
+
'Failed to encode trace as JSON (%d spans). Trace will be dropped. Error: %s',
|
|
120
|
+
this._spans.length,
|
|
121
|
+
err.message
|
|
122
|
+
)
|
|
123
|
+
this._reset()
|
|
124
|
+
return Buffer.alloc(0)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Resets the encoder state.
|
|
130
|
+
*/
|
|
131
|
+
reset () {
|
|
132
|
+
this._reset()
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_reset () {
|
|
136
|
+
this._spans = []
|
|
137
|
+
this._spanCount = 0
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
module.exports = { AgentlessJSONEncoder }
|
|
@@ -11,6 +11,8 @@ module.exports = function getExporter (name) {
|
|
|
11
11
|
return require('./exporters/log')
|
|
12
12
|
case exporters.AGENT:
|
|
13
13
|
return require('./exporters/agent')
|
|
14
|
+
case exporters.AGENTLESS:
|
|
15
|
+
return require('./exporters/agentless')
|
|
14
16
|
case exporters.DATADOG:
|
|
15
17
|
return require('./ci-visibility/exporters/agentless')
|
|
16
18
|
case exporters.AGENT_PROXY:
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { inspect } = require('util')
|
|
4
|
-
|
|
5
3
|
const request = require('../common/request')
|
|
6
4
|
const { startupLog } = require('../../startup-log')
|
|
7
5
|
const runtimeMetrics = require('../../runtime_metrics')
|
|
8
6
|
const log = require('../../log')
|
|
9
7
|
const tracerVersion = require('../../../../../package.json').version
|
|
10
8
|
const BaseWriter = require('../common/writer')
|
|
9
|
+
const propagationHash = require('../../propagation-hash')
|
|
11
10
|
|
|
12
11
|
const METRIC_PREFIX = 'datadog.tracer.node.exporter.agent'
|
|
13
12
|
|
|
@@ -29,10 +28,7 @@ class AgentWriter extends BaseWriter {
|
|
|
29
28
|
runtimeMetrics.increment(`${METRIC_PREFIX}.requests`, true)
|
|
30
29
|
|
|
31
30
|
const { _headers, _lookup, _protocolVersion, _url } = this
|
|
32
|
-
makeRequest(_protocolVersion, data, count, _url, _headers, _lookup, (err, res, status) => {
|
|
33
|
-
// Note that logging will only happen once, regardless of how many times this is called.
|
|
34
|
-
startupLog(status !== 404 && status !== 200 ? { status, message: err?.message ?? inspect(err) } : undefined)
|
|
35
|
-
|
|
31
|
+
makeRequest(_protocolVersion, data, count, _url, _headers, _lookup, true, (err, res, status, headers) => {
|
|
36
32
|
if (status) {
|
|
37
33
|
runtimeMetrics.increment(`${METRIC_PREFIX}.responses`, true)
|
|
38
34
|
runtimeMetrics.increment(`${METRIC_PREFIX}.responses.by.status`, `status:${status}`, true)
|
|
@@ -53,6 +49,16 @@ class AgentWriter extends BaseWriter {
|
|
|
53
49
|
|
|
54
50
|
log.debug('Response from the agent: %s', res)
|
|
55
51
|
|
|
52
|
+
// Capture container tags hash from agent response headers
|
|
53
|
+
// The hash is sent by the agent only when Datadog-Container-ID is present in the request
|
|
54
|
+
// (Datadog-Container-ID is automatically injected by docker.inject() in exporters/common/request.js)
|
|
55
|
+
if (headers) {
|
|
56
|
+
const containerTagsHash = headers['Datadog-Container-Tags-Hash']
|
|
57
|
+
if (containerTagsHash) {
|
|
58
|
+
propagationHash.updateContainerTagsHash(containerTagsHash)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
56
62
|
try {
|
|
57
63
|
this._prioritySampler.update(JSON.parse(res).rate_by_service)
|
|
58
64
|
} catch (e) {
|
|
@@ -72,7 +78,7 @@ function getEncoder (protocolVersion) {
|
|
|
72
78
|
: require('../../encode/0.4').AgentEncoder
|
|
73
79
|
}
|
|
74
80
|
|
|
75
|
-
function makeRequest (version, data, count, url, headers, lookup, cb) {
|
|
81
|
+
function makeRequest (version, data, count, url, headers, lookup, needsStartupLog, cb) {
|
|
76
82
|
const options = {
|
|
77
83
|
path: `/v${version}/traces`,
|
|
78
84
|
method: 'PUT',
|
|
@@ -91,7 +97,15 @@ function makeRequest (version, data, count, url, headers, lookup, cb) {
|
|
|
91
97
|
|
|
92
98
|
log.debug('Request to the agent: %j', options)
|
|
93
99
|
|
|
94
|
-
request(data, options,
|
|
100
|
+
request(data, options, (err, res, status, headers) => {
|
|
101
|
+
if (needsStartupLog) {
|
|
102
|
+
// Note that logging will only happen once, regardless of how many times this is called.
|
|
103
|
+
startupLog({
|
|
104
|
+
agentError: status !== 404 && status !== 200 ? err : undefined,
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
cb(err, res, status, headers)
|
|
108
|
+
})
|
|
95
109
|
}
|
|
96
110
|
|
|
97
111
|
module.exports = AgentWriter
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { URL } = require('node:url')
|
|
4
|
+
|
|
5
|
+
const log = require('../../log')
|
|
6
|
+
const Writer = require('./writer')
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Agentless exporter for APM span intake.
|
|
10
|
+
* Sends spans directly to the Datadog intake without requiring a local agent.
|
|
11
|
+
*
|
|
12
|
+
* Each trace is sent immediately as a separate request. The intake only accepts one trace
|
|
13
|
+
* per request - requests with spans from different traces return HTTP 200 but silently
|
|
14
|
+
* drop all spans. By flushing immediately after each export (which contains one trace),
|
|
15
|
+
* we avoid this limitation entirely. -- bengl
|
|
16
|
+
*/
|
|
17
|
+
class AgentlessExporter {
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} config - Configuration object
|
|
20
|
+
* @param {string} [config.site='datadoghq.com'] - The Datadog site
|
|
21
|
+
* @param {string} [config.url] - Override intake URL
|
|
22
|
+
*/
|
|
23
|
+
constructor (config) {
|
|
24
|
+
this._config = config
|
|
25
|
+
const { site = 'datadoghq.com', url } = config
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
this._url = url ? new URL(url) : new URL(`https://public-trace-http-intake.logs.${site}`)
|
|
29
|
+
} catch (err) {
|
|
30
|
+
log.error(
|
|
31
|
+
'Invalid URL configuration for agentless exporter. url=%s, site=%s. Error: %s',
|
|
32
|
+
url || 'not set',
|
|
33
|
+
site,
|
|
34
|
+
err.message
|
|
35
|
+
)
|
|
36
|
+
this._url = null
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this._writer = new Writer({
|
|
40
|
+
url: this._url,
|
|
41
|
+
site,
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const ddTrace = globalThis[Symbol.for('dd-trace')]
|
|
45
|
+
if (ddTrace?.beforeExitHandlers) {
|
|
46
|
+
ddTrace.beforeExitHandlers.add(this.flush.bind(this))
|
|
47
|
+
} else {
|
|
48
|
+
log.error('dd-trace global not properly initialized. beforeExit handler not registered for agentless exporter.')
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Sets the intake URL.
|
|
54
|
+
* @param {string} urlString - The new intake URL
|
|
55
|
+
* @returns {boolean} True if URL was set successfully
|
|
56
|
+
*/
|
|
57
|
+
setUrl (urlString) {
|
|
58
|
+
try {
|
|
59
|
+
const url = new URL(urlString)
|
|
60
|
+
this._url = url
|
|
61
|
+
this._writer.setUrl(url)
|
|
62
|
+
return true
|
|
63
|
+
} catch {
|
|
64
|
+
log.error('Invalid URL for agentless exporter: %s. Using previous URL: %s', urlString, this._url?.href || 'none')
|
|
65
|
+
return false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Exports a trace to the intake. Flushes immediately since each trace must be
|
|
71
|
+
* sent as a separate request.
|
|
72
|
+
* @param {object[]} spans - Array of spans (all from the same trace)
|
|
73
|
+
*/
|
|
74
|
+
export (spans) {
|
|
75
|
+
this._writer.append(spans)
|
|
76
|
+
this._writer.flush()
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Flushes any pending spans. With immediate flush per trace, this is mainly
|
|
81
|
+
* used for the beforeExit handler to ensure nothing is left unsent.
|
|
82
|
+
* @param {Function} [done] - Callback when flush is complete
|
|
83
|
+
*/
|
|
84
|
+
flush (done = () => {}) {
|
|
85
|
+
this._writer.flush(done)
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = AgentlessExporter
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const { getValueFromEnvSources } = require('../../config/helper')
|
|
4
|
+
const log = require('../../log')
|
|
5
|
+
const request = require('../common/request')
|
|
6
|
+
const tracerVersion = require('../../../../../package.json').version
|
|
7
|
+
|
|
8
|
+
const BaseWriter = require('../common/writer')
|
|
9
|
+
const { AgentlessJSONEncoder } = require('../../encode/agentless-json')
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Writer for agentless APM span intake.
|
|
13
|
+
* Sends spans directly to the Datadog intake endpoint without an agent.
|
|
14
|
+
*/
|
|
15
|
+
class AgentlessWriter extends BaseWriter {
|
|
16
|
+
#apiKeyMissing = false
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {object} options - Writer options
|
|
20
|
+
* @param {URL} [options.url] - The intake URL. If not provided, constructed from site.
|
|
21
|
+
* @param {string} [options.site='datadoghq.com'] - The Datadog site
|
|
22
|
+
*/
|
|
23
|
+
constructor ({ url, site = 'datadoghq.com' }) {
|
|
24
|
+
super({ url })
|
|
25
|
+
this._encoder = new AgentlessJSONEncoder()
|
|
26
|
+
|
|
27
|
+
if (!url) {
|
|
28
|
+
try {
|
|
29
|
+
this._url = new URL(`https://public-trace-http-intake.logs.${site}`)
|
|
30
|
+
} catch (err) {
|
|
31
|
+
log.error(
|
|
32
|
+
'Invalid site value for agentless intake: %s. Cannot construct URL. Error: %s',
|
|
33
|
+
site,
|
|
34
|
+
err.message
|
|
35
|
+
)
|
|
36
|
+
this._url = null
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (!getValueFromEnvSources('DD_API_KEY')) {
|
|
41
|
+
this.#apiKeyMissing = true
|
|
42
|
+
log.error('DD_API_KEY is required for agentless span intake. Set DD_API_KEY. Spans will not be sent.')
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Flushes the current trace. Since we flush after each trace, this sends
|
|
48
|
+
* a single request.
|
|
49
|
+
* @param {Function} [done] - Callback when send completes
|
|
50
|
+
*/
|
|
51
|
+
flush (done = () => {}) {
|
|
52
|
+
if (!request.writable) {
|
|
53
|
+
this._encoder.reset()
|
|
54
|
+
done()
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const count = this._encoder.count()
|
|
59
|
+
|
|
60
|
+
if (count === 0) {
|
|
61
|
+
done()
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const payload = this._encoder.makePayload()
|
|
66
|
+
|
|
67
|
+
if (payload.length === 0) {
|
|
68
|
+
log.debug('Skipping send of empty payload')
|
|
69
|
+
done()
|
|
70
|
+
return
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this._sendPayload(payload, count, done)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Sends the encoded payload to the intake endpoint.
|
|
78
|
+
* @param {Buffer} data - The encoded JSON payload
|
|
79
|
+
* @param {number} count - Number of spans in the payload
|
|
80
|
+
* @param {Function} done - Callback when complete
|
|
81
|
+
*/
|
|
82
|
+
_sendPayload (data, count, done) {
|
|
83
|
+
if (!data || data.length === 0) {
|
|
84
|
+
log.debug('Skipping send of empty payload')
|
|
85
|
+
done()
|
|
86
|
+
return
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (!this._url) {
|
|
90
|
+
log.debug('Skipping send due to invalid URL configuration')
|
|
91
|
+
done()
|
|
92
|
+
return
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const apiKey = getValueFromEnvSources('DD_API_KEY')
|
|
96
|
+
if (!apiKey) {
|
|
97
|
+
if (!this.#apiKeyMissing) {
|
|
98
|
+
this.#apiKeyMissing = true
|
|
99
|
+
log.error('DD_API_KEY is required for agentless span intake. Set DD_API_KEY. Spans will not be sent.')
|
|
100
|
+
}
|
|
101
|
+
log.debug('Dropping %d span(s) due to missing DD_API_KEY', count)
|
|
102
|
+
done()
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
this.#apiKeyMissing = false
|
|
106
|
+
|
|
107
|
+
const options = {
|
|
108
|
+
path: '/v1/input',
|
|
109
|
+
method: 'POST',
|
|
110
|
+
headers: {
|
|
111
|
+
'Content-Type': 'application/json',
|
|
112
|
+
'dd-api-key': apiKey,
|
|
113
|
+
'Datadog-Meta-Lang': 'nodejs',
|
|
114
|
+
'Datadog-Meta-Lang-Version': process.version,
|
|
115
|
+
'Datadog-Meta-Lang-Interpreter': process.versions.bun ? 'JavaScriptCore' : 'v8',
|
|
116
|
+
'Datadog-Meta-Tracer-Version': tracerVersion,
|
|
117
|
+
},
|
|
118
|
+
timeout: 15_000,
|
|
119
|
+
url: this._url,
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
log.debug('Request to the agentless intake: %j', options)
|
|
123
|
+
|
|
124
|
+
request(data, options, (err, res, statusCode) => {
|
|
125
|
+
if (err) {
|
|
126
|
+
this._logRequestError(err, statusCode, count)
|
|
127
|
+
done()
|
|
128
|
+
return
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
log.debug('Response from the agentless intake: %s', res)
|
|
132
|
+
done()
|
|
133
|
+
})
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Logs request errors with status-specific guidance.
|
|
138
|
+
* @param {Error} err - The error object
|
|
139
|
+
* @param {number} statusCode - HTTP status code (if available)
|
|
140
|
+
* @param {number} count - Number of spans that were being sent
|
|
141
|
+
*/
|
|
142
|
+
_logRequestError (err, statusCode, count) {
|
|
143
|
+
if (statusCode === 401 || statusCode === 403) {
|
|
144
|
+
log.error(
|
|
145
|
+
'Authentication failed sending %d span(s) (status %s). Verify DD_API_KEY is valid.',
|
|
146
|
+
count,
|
|
147
|
+
statusCode
|
|
148
|
+
)
|
|
149
|
+
} else if (statusCode === 404) {
|
|
150
|
+
log.error(
|
|
151
|
+
'Span intake endpoint not found (status %s). Verify DD_SITE is correctly configured. %d span(s) dropped.',
|
|
152
|
+
statusCode,
|
|
153
|
+
count
|
|
154
|
+
)
|
|
155
|
+
} else if (statusCode === 429) {
|
|
156
|
+
log.error(
|
|
157
|
+
'Rate limited by span intake (status 429). %d span(s) dropped.',
|
|
158
|
+
count
|
|
159
|
+
)
|
|
160
|
+
} else if (statusCode >= 500) {
|
|
161
|
+
log.error(
|
|
162
|
+
'Span intake server error (status %s). %d span(s) dropped. This may be transient.',
|
|
163
|
+
statusCode,
|
|
164
|
+
count
|
|
165
|
+
)
|
|
166
|
+
} else if (statusCode) {
|
|
167
|
+
log.error(
|
|
168
|
+
'Error sending agentless payload (status %s): %s. %d span(s) dropped.',
|
|
169
|
+
statusCode,
|
|
170
|
+
err.message,
|
|
171
|
+
count
|
|
172
|
+
)
|
|
173
|
+
} else {
|
|
174
|
+
log.error(
|
|
175
|
+
'Network error sending %d span(s) to %s: %s',
|
|
176
|
+
count,
|
|
177
|
+
this._url?.hostname || 'unknown',
|
|
178
|
+
err.message
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module.exports = AgentlessWriter
|
|
@@ -88,13 +88,13 @@ function request (data, options, callback) {
|
|
|
88
88
|
zlib.gunzip(buffer, (err, result) => {
|
|
89
89
|
if (err) {
|
|
90
90
|
log.error('Could not gunzip response: %s', err.message)
|
|
91
|
-
callback(null, '', res.statusCode)
|
|
91
|
+
callback(null, '', res.statusCode, res.headers)
|
|
92
92
|
} else {
|
|
93
|
-
callback(null, result.toString(), res.statusCode)
|
|
93
|
+
callback(null, result.toString(), res.statusCode, res.headers)
|
|
94
94
|
}
|
|
95
95
|
})
|
|
96
96
|
} else {
|
|
97
|
-
callback(null, buffer.toString(), res.statusCode)
|
|
97
|
+
callback(null, buffer.toString(), res.statusCode, res.headers)
|
|
98
98
|
}
|
|
99
99
|
} else {
|
|
100
100
|
let errorMessage = ''
|
|
@@ -115,7 +115,7 @@ function request (data, options, callback) {
|
|
|
115
115
|
const error = new log.NoTransmitError(errorMessage)
|
|
116
116
|
error.status = res.statusCode
|
|
117
117
|
|
|
118
|
-
callback(error, null, res.statusCode)
|
|
118
|
+
callback(error, null, res.statusCode, res.headers)
|
|
119
119
|
}
|
|
120
120
|
})
|
|
121
121
|
}
|
|
@@ -10,7 +10,7 @@ module.exports = {
|
|
|
10
10
|
|
|
11
11
|
EVALUATIONS_INTAKE: 'api',
|
|
12
12
|
EVALUATIONS_EVENT_TYPE: 'evaluation_metric',
|
|
13
|
-
EVALUATIONS_ENDPOINT: '/api/intake/llm-obs/
|
|
13
|
+
EVALUATIONS_ENDPOINT: '/api/intake/llm-obs/v2/eval-metric',
|
|
14
14
|
|
|
15
15
|
EVP_PAYLOAD_SIZE_LIMIT: 5 << 20, // 5MB (actual limit is 5.1MB)
|
|
16
16
|
EVP_EVENT_SIZE_LIMIT: (1 << 20) - 1024, // 999KB (actual limit is 1MB)
|
|
@@ -4,7 +4,7 @@ const { channel } = require('dc-polyfill')
|
|
|
4
4
|
const BaseLLMObsPlugin = require('../base')
|
|
5
5
|
const { getModelProvider } = require('../../../../../datadog-plugin-ai/src/utils')
|
|
6
6
|
|
|
7
|
-
const toolCreationCh = channel('
|
|
7
|
+
const toolCreationCh = channel('tracing:orchestrion:ai:tool:start')
|
|
8
8
|
const setAttributesCh = channel('dd-trace:vercel-ai:span:setAttributes')
|
|
9
9
|
|
|
10
10
|
const { MODEL_NAME, MODEL_PROVIDER, NAME } = require('../../constants/tags')
|
|
@@ -94,8 +94,10 @@ class VercelAILLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
94
94
|
|
|
95
95
|
this.#toolCallIdsToName = {}
|
|
96
96
|
this.#availableTools = new Set()
|
|
97
|
-
toolCreationCh.subscribe(
|
|
98
|
-
|
|
97
|
+
toolCreationCh.subscribe(ctx => {
|
|
98
|
+
const toolArgs = ctx.arguments
|
|
99
|
+
const tool = toolArgs[0] ?? {}
|
|
100
|
+
this.#availableTools.add(tool)
|
|
99
101
|
})
|
|
100
102
|
|
|
101
103
|
setAttributesCh.subscribe(({ ctx, attributes }) => {
|
|
@@ -359,15 +359,15 @@ class LLMObs extends NoopLLMObs {
|
|
|
359
359
|
throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
|
|
360
360
|
}
|
|
361
361
|
|
|
362
|
-
const { label, value, tags } = options
|
|
362
|
+
const { label, value, tags, reasoning, assessment, metadata } = options
|
|
363
363
|
const metricType = options.metricType?.toLowerCase()
|
|
364
364
|
if (!label) {
|
|
365
365
|
err = 'invalid_metric_label'
|
|
366
366
|
throw new Error('label must be the specified name of the evaluation metric')
|
|
367
367
|
}
|
|
368
|
-
if (!metricType || !['categorical', 'score', 'boolean'].includes(metricType)) {
|
|
368
|
+
if (!metricType || !['categorical', 'score', 'boolean', 'json'].includes(metricType)) {
|
|
369
369
|
err = 'invalid_metric_type'
|
|
370
|
-
throw new Error('metricType must be one of "categorical" or "
|
|
370
|
+
throw new Error('metricType must be one of "categorical", "score", "boolean" or "json"')
|
|
371
371
|
}
|
|
372
372
|
if (metricType === 'categorical' && typeof value !== 'string') {
|
|
373
373
|
err = 'invalid_metric_value'
|
|
@@ -381,6 +381,22 @@ class LLMObs extends NoopLLMObs {
|
|
|
381
381
|
err = 'invalid_metric_value'
|
|
382
382
|
throw new Error('value must be a boolean for a boolean metric')
|
|
383
383
|
}
|
|
384
|
+
if (metricType === 'json' && !(typeof value === 'object' && value != null && !Array.isArray(value))) {
|
|
385
|
+
err = 'invalid_metric_value'
|
|
386
|
+
throw new Error('value must be a JSON object for a json metric')
|
|
387
|
+
}
|
|
388
|
+
if (assessment != null && assessment !== 'pass' && assessment !== 'fail') {
|
|
389
|
+
err = 'invalid_assessment'
|
|
390
|
+
throw new Error('assessment must be pass or fail')
|
|
391
|
+
}
|
|
392
|
+
if (reasoning != null && typeof reasoning !== 'string') {
|
|
393
|
+
err = 'invalid_reasoning'
|
|
394
|
+
throw new Error('reasoning must be a string')
|
|
395
|
+
}
|
|
396
|
+
if (metadata != null && (typeof metadata !== 'object' || Array.isArray(metadata))) {
|
|
397
|
+
err = 'invalid_metadata'
|
|
398
|
+
throw new Error('metadata must be a JSON object')
|
|
399
|
+
}
|
|
384
400
|
|
|
385
401
|
const evaluationTags = {
|
|
386
402
|
'ddtrace.version': tracerVersion,
|
|
@@ -412,8 +428,12 @@ class LLMObs extends NoopLLMObs {
|
|
|
412
428
|
}
|
|
413
429
|
|
|
414
430
|
const payload = {
|
|
415
|
-
|
|
416
|
-
|
|
431
|
+
join_on: {
|
|
432
|
+
span: {
|
|
433
|
+
span_id: spanId,
|
|
434
|
+
trace_id: traceId,
|
|
435
|
+
},
|
|
436
|
+
},
|
|
417
437
|
label,
|
|
418
438
|
metric_type: metricType,
|
|
419
439
|
ml_app: mlApp,
|
|
@@ -421,6 +441,15 @@ class LLMObs extends NoopLLMObs {
|
|
|
421
441
|
timestamp_ms: timestampMs,
|
|
422
442
|
tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`),
|
|
423
443
|
}
|
|
444
|
+
if (reasoning != null) {
|
|
445
|
+
payload.reasoning = reasoning
|
|
446
|
+
}
|
|
447
|
+
if (metadata != null) {
|
|
448
|
+
payload.metadata = metadata
|
|
449
|
+
}
|
|
450
|
+
if (assessment != null) {
|
|
451
|
+
payload.assessment = assessment
|
|
452
|
+
}
|
|
424
453
|
const currentStore = storage.getStore()
|
|
425
454
|
const routing = currentStore?.routingContext
|
|
426
455
|
evalMetricAppendCh.publish({ payload, routing })
|