dd-trace 5.87.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 +225 -4
- package/package.json +9 -6
- package/packages/datadog-instrumentations/src/ai.js +54 -90
- package/packages/datadog-instrumentations/src/helpers/hook.js +17 -11
- 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/jest.js +76 -12
- package/packages/datadog-instrumentations/src/kafkajs.js +20 -17
- package/packages/datadog-instrumentations/src/playwright.js +1 -1
- package/packages/datadog-plugin-amqplib/src/consumer.js +14 -10
- package/packages/datadog-plugin-amqplib/src/producer.js +23 -19
- 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 +26 -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/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/azure_metadata.js +0 -2
- 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 -197
- package/packages/dd-trace/src/config/helper.js +43 -1
- package/packages/dd-trace/src/config/index.js +36 -14
- package/packages/dd-trace/src/config/supported-configurations.json +4115 -512
- 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/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/request.js +4 -4
- package/packages/dd-trace/src/llmobs/plugins/ai/index.js +5 -3
- 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 +15 -2
- package/packages/dd-trace/src/plugins/util/test.js +48 -0
- 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 +1 -1
- 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/scope/noop/scope.js +0 -21
|
@@ -46,8 +46,6 @@ module.exports = {
|
|
|
46
46
|
SCHEMA_TOPIC: 'schema.topic',
|
|
47
47
|
SCHEMA_OPERATION: 'schema.operation',
|
|
48
48
|
SCHEMA_NAME: 'schema.name',
|
|
49
|
-
GRPC_CLIENT_ERROR_STATUSES: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
|
50
|
-
GRPC_SERVER_ERROR_STATUSES: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16],
|
|
51
49
|
DYNAMODB_PTR_KIND: 'aws.dynamodb.item',
|
|
52
50
|
S3_PTR_KIND: 'aws.s3.object',
|
|
53
51
|
WEBSOCKET_PTR_KIND: 'websocket',
|
|
@@ -52,6 +52,12 @@ class Crashtracker {
|
|
|
52
52
|
#getConfig (config) {
|
|
53
53
|
const url = getAgentUrl(config)
|
|
54
54
|
|
|
55
|
+
// Out-of-process symbolication currently (crashtracker 27.0.0) works on
|
|
56
|
+
// Linux only, does not work on Mac.
|
|
57
|
+
const resolveMode = require('os').platform === 'linux'
|
|
58
|
+
? 'EnabledWithSymbolsInReceiver'
|
|
59
|
+
: 'EnabledWithInprocessSymbols'
|
|
60
|
+
|
|
55
61
|
return {
|
|
56
62
|
additional_files: [],
|
|
57
63
|
create_alt_stack: true,
|
|
@@ -67,9 +73,10 @@ class Crashtracker {
|
|
|
67
73
|
},
|
|
68
74
|
timeout_ms: 3000,
|
|
69
75
|
},
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
76
|
+
timeout: { secs: 5, nanos: 0 },
|
|
77
|
+
demangle_names: false,
|
|
78
|
+
signals: [],
|
|
79
|
+
resolve_frames: resolveMode,
|
|
73
80
|
}
|
|
74
81
|
}
|
|
75
82
|
|
|
@@ -26,17 +26,36 @@ function shaHash (checkpointString) {
|
|
|
26
26
|
* @param {string} env
|
|
27
27
|
* @param {string[]} edgeTags
|
|
28
28
|
* @param {Buffer} parentHash
|
|
29
|
+
* @param {bigint | null} propagationHashBigInt - Optional propagation hash for process/container tags
|
|
29
30
|
*/
|
|
30
|
-
function computeHash (service, env, edgeTags, parentHash) {
|
|
31
|
+
function computeHash (service, env, edgeTags, parentHash, propagationHashBigInt = null) {
|
|
31
32
|
edgeTags.sort()
|
|
32
33
|
const hashableEdgeTags = edgeTags.filter(item => item !== 'manual_checkpoint:true')
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
// Cache key includes parentHash to handle fan-in/fan-out scenarios where the same
|
|
36
|
+
// service+env+tags+propagationHash can have different parents. This ensures we cache
|
|
37
|
+
// the complete pathway context, not just the current node's identity.
|
|
38
|
+
const propagationPart = propagationHashBigInt ? `:${propagationHashBigInt.toString(16)}` : ''
|
|
39
|
+
const key = `${service}${env}${hashableEdgeTags.join('')}${parentHash}${propagationPart}`
|
|
40
|
+
|
|
35
41
|
let value = cache.get(key)
|
|
36
42
|
if (value) {
|
|
37
43
|
return value
|
|
38
44
|
}
|
|
39
|
-
|
|
45
|
+
|
|
46
|
+
// Key vs hashInput distinction:
|
|
47
|
+
// - 'key' (above) is used for caching and includes parentHash to differentiate pathways
|
|
48
|
+
// with the same node but different parents (e.g., multiple queues feeding one consumer)
|
|
49
|
+
// - 'hashInput' (below) excludes parentHash to compute only the current node's identity hash,
|
|
50
|
+
// which is then XORed with parentHash (line 54) to build the complete pathway hash
|
|
51
|
+
// This two-step approach (hash current node independently, then combine with parent) is
|
|
52
|
+
// required for proper pathway construction in the DSM protocol.
|
|
53
|
+
const baseString = `${service}${env}` + hashableEdgeTags.join('')
|
|
54
|
+
const hashInput = propagationHashBigInt
|
|
55
|
+
? `${baseString}:${propagationHashBigInt.toString(16)}`
|
|
56
|
+
: baseString
|
|
57
|
+
|
|
58
|
+
const currentHash = shaHash(hashInput)
|
|
40
59
|
const buf = Buffer.concat([currentHash, parentHash], 16)
|
|
41
60
|
value = shaHash(buf.toString())
|
|
42
61
|
cache.set(key, value)
|
|
@@ -6,6 +6,8 @@ const pkg = require('../../../../package.json')
|
|
|
6
6
|
const { LogCollapsingLowestDenseDDSketch } = require('../../../../vendor/dist/@datadog/sketches-js')
|
|
7
7
|
const { PATHWAY_HASH } = require('../../../../ext/tags')
|
|
8
8
|
const log = require('../log')
|
|
9
|
+
const processTags = require('../process-tags')
|
|
10
|
+
const propagationHash = require('../propagation-hash')
|
|
9
11
|
const { DsmPathwayCodec } = require('./pathway')
|
|
10
12
|
const { DataStreamsWriter } = require('./writer')
|
|
11
13
|
const { computePathwayHash } = require('./pathway')
|
|
@@ -162,6 +164,7 @@ class DataStreamsProcessor {
|
|
|
162
164
|
onInterval () {
|
|
163
165
|
const { Stats } = this._serializeBuckets()
|
|
164
166
|
if (Stats.length === 0) return
|
|
167
|
+
|
|
165
168
|
const payload = {
|
|
166
169
|
Env: this.env,
|
|
167
170
|
Service: this.service,
|
|
@@ -171,6 +174,12 @@ class DataStreamsProcessor {
|
|
|
171
174
|
Lang: 'javascript',
|
|
172
175
|
Tags: Object.entries(this.tags).map(([key, value]) => `${key}:${value}`),
|
|
173
176
|
}
|
|
177
|
+
|
|
178
|
+
// Add ProcessTags only if feature is enabled and process tags exist
|
|
179
|
+
if (propagationHash.isEnabled() && processTags.serialized) {
|
|
180
|
+
payload.ProcessTags = processTags.serialized.split(',')
|
|
181
|
+
}
|
|
182
|
+
|
|
174
183
|
this.writer.flush(payload)
|
|
175
184
|
}
|
|
176
185
|
|
|
@@ -234,7 +243,11 @@ class DataStreamsProcessor {
|
|
|
234
243
|
edgeTags
|
|
235
244
|
)
|
|
236
245
|
}
|
|
237
|
-
|
|
246
|
+
|
|
247
|
+
// Get propagation hash if enabled
|
|
248
|
+
const propagationHashValue = propagationHash.isEnabled() ? propagationHash.getHash() : null
|
|
249
|
+
|
|
250
|
+
const hash = computePathwayHash(this.service, this.env, edgeTags, parentHash, propagationHashValue)
|
|
238
251
|
const edgeLatencyNs = nowNs - edgeStartNs
|
|
239
252
|
const pathwayLatencyNs = nowNs - pathwayStartNs
|
|
240
253
|
const dataStreamsContext = {
|
|
@@ -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
|
}
|
|
@@ -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 }) => {
|