dd-trace 5.87.0 → 5.89.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/ext/tags.js +2 -0
- package/index.d.ts +234 -4
- package/package.json +18 -11
- 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/index.js +27 -110
- 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/orchestrion/compiler.js +74 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/index.js +43 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/matcher.js +49 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/orchestrion/transformer.js +121 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/{transforms.js → orchestrion/transforms.js} +143 -17
- package/packages/datadog-instrumentations/src/jest.js +176 -54
- 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 +62 -5
- package/packages/datadog-plugin-cypress/src/source-map-utils.js +297 -0
- package/packages/datadog-plugin-cypress/src/support.js +52 -9
- 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/aiguard/sdk.js +5 -1
- 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 +3 -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 +38 -14
- package/packages/dd-trace/src/config/supported-configurations.json +4125 -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/checkpointer.js +13 -0
- package/packages/dd-trace/src/datastreams/index.js +3 -0
- package/packages/dd-trace/src/datastreams/manager.js +9 -0
- package/packages/dd-trace/src/datastreams/pathway.js +22 -3
- package/packages/dd-trace/src/datastreams/processor.js +140 -4
- package/packages/dd-trace/src/encode/agentless-json.js +155 -0
- package/packages/dd-trace/src/exporter.js +2 -0
- package/packages/dd-trace/src/exporters/agent/writer.js +21 -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/pkg.js +1 -1
- 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 +6 -1
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +1 -1
- package/packages/dd-trace/src/startup-log.js +53 -19
- package/vendor/dist/@datadog/sketches-js/index.js +1 -1
- package/vendor/dist/@datadog/source-map/index.js +1 -1
- package/vendor/dist/@isaacs/ttlcache/index.js +1 -1
- package/vendor/dist/@opentelemetry/core/index.js +1 -1
- package/vendor/dist/@opentelemetry/resources/index.js +1 -1
- package/vendor/dist/astring/index.js +1 -1
- package/vendor/dist/crypto-randomuuid/index.js +1 -1
- package/vendor/dist/escape-string-regexp/index.js +1 -1
- package/vendor/dist/esquery/index.js +1 -1
- package/vendor/dist/ignore/index.js +1 -1
- package/vendor/dist/istanbul-lib-coverage/index.js +1 -1
- package/vendor/dist/jest-docblock/index.js +1 -1
- package/vendor/dist/jsonpath-plus/index.js +1 -1
- package/vendor/dist/limiter/index.js +1 -1
- package/vendor/dist/lodash.sortby/index.js +1 -1
- package/vendor/dist/lru-cache/index.js +1 -1
- package/vendor/dist/meriyah/index.js +1 -1
- package/vendor/dist/module-details-from-path/index.js +1 -1
- package/vendor/dist/mutexify/promise/index.js +1 -1
- package/vendor/dist/opentracing/index.js +1 -1
- package/vendor/dist/path-to-regexp/index.js +1 -1
- package/vendor/dist/pprof-format/index.js +1 -1
- package/vendor/dist/protobufjs/index.js +1 -1
- package/vendor/dist/protobufjs/minimal/index.js +1 -1
- package/vendor/dist/retry/index.js +1 -1
- package/vendor/dist/rfdc/index.js +1 -1
- package/vendor/dist/semifies/index.js +1 -1
- package/vendor/dist/shell-quote/index.js +1 -1
- package/vendor/dist/source-map/index.js +1 -1
- package/vendor/dist/source-map/lib/util/index.js +1 -1
- package/vendor/dist/tlhunter-sorted-set/index.js +1 -1
- package/vendor/dist/ttl-set/index.js +1 -1
- package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +0 -33
- 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
|
|
|
@@ -60,6 +60,19 @@ class DataStreamsCheckpointer {
|
|
|
60
60
|
|
|
61
61
|
return ctx
|
|
62
62
|
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Records a transaction ID at a named checkpoint without pathway propagation.
|
|
66
|
+
* Tags the active span (or the provided span) with the transaction ID and checkpoint name.
|
|
67
|
+
* @param {string} transactionId - The transaction identifier to track.
|
|
68
|
+
* @param {string} checkpointName - The logical checkpoint name.
|
|
69
|
+
* @param {object|null} [span=null] - Span to tag. Defaults to the currently active span.
|
|
70
|
+
*/
|
|
71
|
+
trackTransaction (transactionId, checkpointName, span = null) {
|
|
72
|
+
if (!this.config.dsmEnabled) return
|
|
73
|
+
const activeSpan = span ?? this.tracer.scope().active()
|
|
74
|
+
this.dsmProcessor.trackTransaction(transactionId, checkpointName, activeSpan)
|
|
75
|
+
}
|
|
63
76
|
}
|
|
64
77
|
|
|
65
78
|
module.exports = {
|
|
@@ -71,6 +71,7 @@ const DsmPathwayCodec = lazyClass(() => require('./pathway').DsmPathwayCodec, []
|
|
|
71
71
|
const DataStreamsCheckpointer = lazyClass(() => require('./checkpointer').DataStreamsCheckpointer, [
|
|
72
72
|
'setProduceCheckpoint',
|
|
73
73
|
'setConsumeCheckpoint',
|
|
74
|
+
'trackTransaction',
|
|
74
75
|
])
|
|
75
76
|
|
|
76
77
|
/**
|
|
@@ -79,6 +80,7 @@ const DataStreamsCheckpointer = lazyClass(() => require('./checkpointer').DataSt
|
|
|
79
80
|
const DataStreamsManager = lazyClass(() => require('./manager').DataStreamsManager, [
|
|
80
81
|
'setCheckpoint',
|
|
81
82
|
'decodeDataStreamsContext',
|
|
83
|
+
'trackTransaction',
|
|
82
84
|
])
|
|
83
85
|
|
|
84
86
|
// TODO: Are all those methods actually public?
|
|
@@ -92,6 +94,7 @@ const DataStreamsProcessor = lazyClass(() => require('./processor').DataStreamsP
|
|
|
92
94
|
'setCheckpoint',
|
|
93
95
|
'recordOffset',
|
|
94
96
|
'setOffset',
|
|
97
|
+
'trackTransaction',
|
|
95
98
|
'setUrl',
|
|
96
99
|
'trySampleSchema',
|
|
97
100
|
'canSampleSchema',
|
|
@@ -22,6 +22,15 @@ class DataStreamsManager {
|
|
|
22
22
|
DataStreamsContext.setDataStreamsContext(ctx)
|
|
23
23
|
return ctx
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @param {string} transactionId
|
|
28
|
+
* @param {string} checkpointName
|
|
29
|
+
* @param {object|null} [span=null]
|
|
30
|
+
*/
|
|
31
|
+
trackTransaction (transactionId, checkpointName, span = null) {
|
|
32
|
+
this._dataStreamsProcessor.trackTransaction(transactionId, checkpointName, span)
|
|
33
|
+
}
|
|
25
34
|
}
|
|
26
35
|
|
|
27
36
|
module.exports = { DataStreamsManager }
|
|
@@ -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)
|
|
@@ -4,8 +4,10 @@ const os = require('os')
|
|
|
4
4
|
const pkg = require('../../../../package.json')
|
|
5
5
|
|
|
6
6
|
const { LogCollapsingLowestDenseDDSketch } = require('../../../../vendor/dist/@datadog/sketches-js')
|
|
7
|
-
const { PATHWAY_HASH } = require('../../../../ext/tags')
|
|
7
|
+
const { PATHWAY_HASH, DSM_TRANSACTION_ID, DSM_TRANSACTION_CHECKPOINT } = 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')
|
|
@@ -67,10 +69,66 @@ class Backlog {
|
|
|
67
69
|
}
|
|
68
70
|
}
|
|
69
71
|
|
|
72
|
+
/**
|
|
73
|
+
* Maps checkpoint name strings to single-byte IDs (1–254).
|
|
74
|
+
* ID 0 is reserved; 254 is the maximum number of unique names.
|
|
75
|
+
* Scope is per-processor so IDs are stable across bucket boundaries within a process lifetime.
|
|
76
|
+
*/
|
|
77
|
+
class CheckpointRegistry {
|
|
78
|
+
/** @type {Map<string, number>} */
|
|
79
|
+
#nameToId = new Map()
|
|
80
|
+
#nextId = 1
|
|
81
|
+
/** @type {Buffer[]} Pre-built [id uint8][nameLen uint8][name bytes] entries, one per registered name. */
|
|
82
|
+
#entryBuffers = []
|
|
83
|
+
/** @type {Buffer | null} Cached concat of #entryBuffers; reset when a new name is added. */
|
|
84
|
+
#encodedKeysCache = null
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Returns the byte ID for the given checkpoint name, assigning one if not seen before.
|
|
88
|
+
* Returns undefined when registry is full (254 entries exhausted).
|
|
89
|
+
* @param {string} name
|
|
90
|
+
* @returns {number | undefined}
|
|
91
|
+
*/
|
|
92
|
+
getId (name) {
|
|
93
|
+
const existing = this.#nameToId.get(name)
|
|
94
|
+
if (existing !== undefined) return existing
|
|
95
|
+
if (this.#nextId > 254) return
|
|
96
|
+
const id = this.#nextId++
|
|
97
|
+
this.#nameToId.set(name, id)
|
|
98
|
+
// Build the wire entry now with a bounded write so long names never materialise
|
|
99
|
+
// their full UTF-8 encoding — buf.write() stops at the supplied byte limit.
|
|
100
|
+
const nameBuf = Buffer.allocUnsafe(255)
|
|
101
|
+
const nameByteLen = nameBuf.write(name, 0, 255, 'utf8')
|
|
102
|
+
const entry = Buffer.allocUnsafe(2 + nameByteLen)
|
|
103
|
+
entry.writeUInt8(id, 0)
|
|
104
|
+
entry.writeUInt8(nameByteLen, 1)
|
|
105
|
+
nameBuf.copy(entry, 2, 0, nameByteLen)
|
|
106
|
+
this.#entryBuffers.push(entry)
|
|
107
|
+
this.#encodedKeysCache = null
|
|
108
|
+
return id
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Returns a Buffer encoding all registered names as [id uint8][nameLen uint8][name bytes].
|
|
113
|
+
* Names are truncated to 255 UTF-8 bytes.
|
|
114
|
+
* Result is cached and only recomputed when new names are registered.
|
|
115
|
+
* @returns {Buffer}
|
|
116
|
+
*/
|
|
117
|
+
get encodedKeys () {
|
|
118
|
+
if (this.#encodedKeysCache !== null) return this.#encodedKeysCache
|
|
119
|
+
this.#encodedKeysCache = this.#entryBuffers.length > 0
|
|
120
|
+
? Buffer.concat(this.#entryBuffers)
|
|
121
|
+
: Buffer.alloc(0)
|
|
122
|
+
return this.#encodedKeysCache
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
70
126
|
class StatsBucket {
|
|
71
127
|
constructor () {
|
|
72
128
|
this._checkpoints = new Map()
|
|
73
129
|
this._backlogs = new Map()
|
|
130
|
+
/** @type {Buffer[]} Accumulated transaction byte chunks, concatenated lazily. */
|
|
131
|
+
this._transactionChunks = []
|
|
74
132
|
}
|
|
75
133
|
|
|
76
134
|
get checkpoints () {
|
|
@@ -81,6 +139,16 @@ class StatsBucket {
|
|
|
81
139
|
return this._backlogs
|
|
82
140
|
}
|
|
83
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Returns the concatenated transaction bytes, or null if no transactions have been added.
|
|
144
|
+
* Concatenation is deferred to read time to avoid O(N²) copies during accumulation.
|
|
145
|
+
* @returns {Buffer | null}
|
|
146
|
+
*/
|
|
147
|
+
get transactions () {
|
|
148
|
+
if (this._transactionChunks.length === 0) return null
|
|
149
|
+
return Buffer.concat(this._transactionChunks)
|
|
150
|
+
}
|
|
151
|
+
|
|
84
152
|
forCheckpoint ({ hash, parentHash, edgeTags }) {
|
|
85
153
|
let checkpoint = this._checkpoints.get(hash)
|
|
86
154
|
if (!checkpoint) {
|
|
@@ -91,6 +159,14 @@ class StatsBucket {
|
|
|
91
159
|
return checkpoint
|
|
92
160
|
}
|
|
93
161
|
|
|
162
|
+
/**
|
|
163
|
+
* Appends pre-encoded transaction bytes to this bucket.
|
|
164
|
+
* @param {Buffer} bytes
|
|
165
|
+
*/
|
|
166
|
+
addTransaction (bytes) {
|
|
167
|
+
this._transactionChunks.push(bytes)
|
|
168
|
+
}
|
|
169
|
+
|
|
94
170
|
/**
|
|
95
171
|
* Conditionally add a backlog to the bucket. If there is currently an offset
|
|
96
172
|
* matching the backlog's tags, overwrite the offset IFF the backlog's offset
|
|
@@ -151,6 +227,7 @@ class DataStreamsProcessor {
|
|
|
151
227
|
this.sequence = 0
|
|
152
228
|
this.flushInterval = flushInterval
|
|
153
229
|
this._schemaSamplers = {}
|
|
230
|
+
this._checkpointRegistry = new CheckpointRegistry()
|
|
154
231
|
|
|
155
232
|
if (this.enabled) {
|
|
156
233
|
this.timer = setInterval(this.onInterval.bind(this), flushInterval)
|
|
@@ -162,6 +239,7 @@ class DataStreamsProcessor {
|
|
|
162
239
|
onInterval () {
|
|
163
240
|
const { Stats } = this._serializeBuckets()
|
|
164
241
|
if (Stats.length === 0) return
|
|
242
|
+
|
|
165
243
|
const payload = {
|
|
166
244
|
Env: this.env,
|
|
167
245
|
Service: this.service,
|
|
@@ -171,6 +249,12 @@ class DataStreamsProcessor {
|
|
|
171
249
|
Lang: 'javascript',
|
|
172
250
|
Tags: Object.entries(this.tags).map(([key, value]) => `${key}:${value}`),
|
|
173
251
|
}
|
|
252
|
+
|
|
253
|
+
// Add ProcessTags only if feature is enabled and process tags exist
|
|
254
|
+
if (propagationHash.isEnabled() && processTags.serialized) {
|
|
255
|
+
payload.ProcessTags = processTags.serialized.split(',')
|
|
256
|
+
}
|
|
257
|
+
|
|
174
258
|
this.writer.flush(payload)
|
|
175
259
|
}
|
|
176
260
|
|
|
@@ -234,7 +318,11 @@ class DataStreamsProcessor {
|
|
|
234
318
|
edgeTags
|
|
235
319
|
)
|
|
236
320
|
}
|
|
237
|
-
|
|
321
|
+
|
|
322
|
+
// Get propagation hash if enabled
|
|
323
|
+
const propagationHashValue = propagationHash.isEnabled() ? propagationHash.getHash() : null
|
|
324
|
+
|
|
325
|
+
const hash = computePathwayHash(this.service, this.env, edgeTags, parentHash, propagationHashValue)
|
|
238
326
|
const edgeLatencyNs = nowNs - edgeStartNs
|
|
239
327
|
const pathwayLatencyNs = nowNs - pathwayStartNs
|
|
240
328
|
const dataStreamsContext = {
|
|
@@ -281,9 +369,47 @@ class DataStreamsProcessor {
|
|
|
281
369
|
this.recordOffset(backlogData)
|
|
282
370
|
}
|
|
283
371
|
|
|
372
|
+
/**
|
|
373
|
+
* Records a transaction ID at a named checkpoint using the binary wire format shared with Go/Java tracers.
|
|
374
|
+
*
|
|
375
|
+
* Wire format per entry: [checkpointId uint8][timestamp int64 big-endian 8 bytes][idLen uint8][id bytes]
|
|
376
|
+
*
|
|
377
|
+
* @param {string} transactionId - Truncated to 255 UTF-8 bytes.
|
|
378
|
+
* @param {string} checkpointName - Mapped to a stable 1-byte ID; silently dropped if registry full.
|
|
379
|
+
* @param {import('../opentelemetry/span').Span|null} [span=null] - Active span to tag with DSM transaction metadata.
|
|
380
|
+
*/
|
|
381
|
+
trackTransaction (transactionId, checkpointName, span = null) {
|
|
382
|
+
if (!this.enabled) {
|
|
383
|
+
log.warn('trackTransaction called but DD_DATA_STREAMS_ENABLED is not set. Transaction will not be tracked.')
|
|
384
|
+
return
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
const checkpointId = this._checkpointRegistry.getId(checkpointName)
|
|
388
|
+
if (checkpointId === undefined) return
|
|
389
|
+
|
|
390
|
+
const idBytes = Buffer.from(transactionId, 'utf8').subarray(0, 255)
|
|
391
|
+
// Multiply as BigInt to avoid precision loss past MAX_SAFE_INTEGER
|
|
392
|
+
const timestampNs = BigInt(Date.now()) * 1_000_000n
|
|
393
|
+
|
|
394
|
+
const entry = Buffer.alloc(1 + 8 + 1 + idBytes.length)
|
|
395
|
+
entry.writeUInt8(checkpointId, 0)
|
|
396
|
+
entry.writeBigInt64BE(timestampNs, 1)
|
|
397
|
+
entry.writeUInt8(idBytes.length, 9)
|
|
398
|
+
idBytes.copy(entry, 10)
|
|
399
|
+
|
|
400
|
+
// Number() cast is safe here: 10s bucket granularity tolerates ~0.5ns precision loss
|
|
401
|
+
this.bucketFromTimestamp(Number(timestampNs)).addTransaction(entry)
|
|
402
|
+
|
|
403
|
+
if (span) {
|
|
404
|
+
span.setTag(DSM_TRANSACTION_ID, transactionId)
|
|
405
|
+
span.setTag(DSM_TRANSACTION_CHECKPOINT, checkpointName)
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
284
409
|
_serializeBuckets () {
|
|
285
410
|
// TimeBuckets
|
|
286
411
|
const serializedBuckets = []
|
|
412
|
+
const registrySnapshot = this._checkpointRegistry.encodedKeys
|
|
287
413
|
|
|
288
414
|
for (const [timeNs, bucket] of this.buckets.entries()) {
|
|
289
415
|
const points = []
|
|
@@ -298,12 +424,21 @@ class DataStreamsProcessor {
|
|
|
298
424
|
for (const backlog of bucket._backlogs.values()) {
|
|
299
425
|
backlogs.push(backlog.encode())
|
|
300
426
|
}
|
|
301
|
-
|
|
427
|
+
|
|
428
|
+
const serializedBucket = {
|
|
302
429
|
Start: BigInt(timeNs),
|
|
303
430
|
Duration: BigInt(this.bucketSizeNs),
|
|
304
431
|
Stats: points,
|
|
305
432
|
Backlogs: backlogs,
|
|
306
|
-
}
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const transactions = bucket.transactions
|
|
436
|
+
if (transactions !== null) {
|
|
437
|
+
serializedBucket.Transactions = transactions
|
|
438
|
+
serializedBucket.TransactionCheckpointIds = registrySnapshot
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
serializedBuckets.push(serializedBucket)
|
|
307
442
|
}
|
|
308
443
|
|
|
309
444
|
this.buckets.clear()
|
|
@@ -345,6 +480,7 @@ class DataStreamsProcessor {
|
|
|
345
480
|
}
|
|
346
481
|
|
|
347
482
|
module.exports = {
|
|
483
|
+
CheckpointRegistry,
|
|
348
484
|
DataStreamsProcessor,
|
|
349
485
|
StatsPoint,
|
|
350
486
|
StatsBucket,
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
'use strict'
|
|
2
|
+
|
|
3
|
+
const log = require('../log')
|
|
4
|
+
const { TOP_LEVEL_KEY } = require('../constants')
|
|
5
|
+
const { truncateSpan, normalizeSpan } = require('./tags-processors')
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Formats a span for JSON encoding.
|
|
9
|
+
* @param {object} span - The span to format
|
|
10
|
+
* @param {boolean} isFirstSpan - Whether this is the first span in the trace
|
|
11
|
+
* @returns {object} The formatted span
|
|
12
|
+
*/
|
|
13
|
+
function formatSpan (span, isFirstSpan) {
|
|
14
|
+
span = normalizeSpan(truncateSpan(span, false))
|
|
15
|
+
|
|
16
|
+
if (span.span_events) {
|
|
17
|
+
span.meta.events = JSON.stringify(span.span_events)
|
|
18
|
+
delete span.span_events
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (isFirstSpan) {
|
|
22
|
+
span.meta['_dd.compute_stats'] = '1'
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (span.parent_id?.toString(10) === '0') {
|
|
26
|
+
span.metrics._trace_root = 1
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (span.metrics[TOP_LEVEL_KEY]) {
|
|
30
|
+
span.metrics._top_level = 1
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return span
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Converts a span to JSON-serializable format.
|
|
38
|
+
* IDs are converted to lowercase hex strings. Start time is converted from
|
|
39
|
+
* nanoseconds to seconds for the intake format.
|
|
40
|
+
* @param {object} span - The formatted span
|
|
41
|
+
* @returns {object} JSON-serializable span object
|
|
42
|
+
*/
|
|
43
|
+
function spanToJSON (span) {
|
|
44
|
+
const result = {
|
|
45
|
+
trace_id: span.trace_id.toString(16).toLowerCase(),
|
|
46
|
+
span_id: span.span_id.toString(16).toLowerCase(),
|
|
47
|
+
parent_id: span.parent_id.toString(16).toLowerCase(),
|
|
48
|
+
name: span.name,
|
|
49
|
+
resource: span.resource,
|
|
50
|
+
service: span.service,
|
|
51
|
+
error: span.error,
|
|
52
|
+
start: Math.floor(span.start / 1e9),
|
|
53
|
+
duration: span.duration,
|
|
54
|
+
meta: span.meta,
|
|
55
|
+
metrics: span.metrics,
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (span.type) {
|
|
59
|
+
result.type = span.type
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (span.meta_struct) {
|
|
63
|
+
result.meta_struct = span.meta_struct
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (span.links && span.links.length > 0) {
|
|
67
|
+
result.links = span.links
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* JSON encoder for agentless span intake.
|
|
75
|
+
* Encodes a single trace as JSON with the payload format: {"spans": [...]}
|
|
76
|
+
*
|
|
77
|
+
* This encoder handles one trace at a time since each trace must be sent as a
|
|
78
|
+
* separate request to the intake. -- bengl
|
|
79
|
+
*/
|
|
80
|
+
class AgentlessJSONEncoder {
|
|
81
|
+
constructor () {
|
|
82
|
+
this._reset()
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Returns the number of spans encoded.
|
|
87
|
+
* @returns {number}
|
|
88
|
+
*/
|
|
89
|
+
count () {
|
|
90
|
+
return this._spanCount
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Encodes a trace (array of spans) into the buffer.
|
|
95
|
+
* @param {object[]} trace - Array of spans to encode
|
|
96
|
+
*/
|
|
97
|
+
encode (trace) {
|
|
98
|
+
for (const span of trace) {
|
|
99
|
+
try {
|
|
100
|
+
const formattedSpan = formatSpan(span, this._spanCount === 0)
|
|
101
|
+
const jsonSpan = spanToJSON(formattedSpan)
|
|
102
|
+
|
|
103
|
+
this._spans.push(jsonSpan)
|
|
104
|
+
this._spanCount++
|
|
105
|
+
} catch (err) {
|
|
106
|
+
log.error(
|
|
107
|
+
'Failed to encode span (name: %s, service: %s). Span will be dropped. Error: %s\n%s',
|
|
108
|
+
span?.name || 'unknown',
|
|
109
|
+
span?.service || 'unknown',
|
|
110
|
+
err.message,
|
|
111
|
+
err.stack
|
|
112
|
+
)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Creates the JSON payload for the encoded trace.
|
|
119
|
+
* @returns {Buffer} JSON payload as a buffer, or empty buffer if no spans
|
|
120
|
+
*/
|
|
121
|
+
makePayload () {
|
|
122
|
+
if (this._spans.length === 0) {
|
|
123
|
+
this._reset()
|
|
124
|
+
return Buffer.alloc(0)
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const payload = JSON.stringify({ spans: this._spans })
|
|
129
|
+
this._reset()
|
|
130
|
+
return Buffer.from(payload, 'utf8')
|
|
131
|
+
} catch (err) {
|
|
132
|
+
log.error(
|
|
133
|
+
'Failed to encode trace as JSON (%d spans). Trace will be dropped. Error: %s',
|
|
134
|
+
this._spans.length,
|
|
135
|
+
err.message
|
|
136
|
+
)
|
|
137
|
+
this._reset()
|
|
138
|
+
return Buffer.alloc(0)
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resets the encoder state.
|
|
144
|
+
*/
|
|
145
|
+
reset () {
|
|
146
|
+
this._reset()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
_reset () {
|
|
150
|
+
this._spans = []
|
|
151
|
+
this._spanCount = 0
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
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,13 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { inspect } = require('util')
|
|
4
|
-
|
|
3
|
+
const { inspect } = require('node:util')
|
|
5
4
|
const request = require('../common/request')
|
|
6
|
-
const {
|
|
5
|
+
const { logIntegrations, logAgentError } = require('../../startup-log')
|
|
7
6
|
const runtimeMetrics = require('../../runtime_metrics')
|
|
8
7
|
const log = require('../../log')
|
|
9
8
|
const tracerVersion = require('../../../../../package.json').version
|
|
10
9
|
const BaseWriter = require('../common/writer')
|
|
10
|
+
const propagationHash = require('../../propagation-hash')
|
|
11
11
|
|
|
12
12
|
const METRIC_PREFIX = 'datadog.tracer.node.exporter.agent'
|
|
13
13
|
|
|
@@ -29,10 +29,7 @@ class AgentWriter extends BaseWriter {
|
|
|
29
29
|
runtimeMetrics.increment(`${METRIC_PREFIX}.requests`, true)
|
|
30
30
|
|
|
31
31
|
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
|
-
|
|
32
|
+
makeRequest(_protocolVersion, data, count, _url, _headers, _lookup, (err, res, status, headers) => {
|
|
36
33
|
if (status) {
|
|
37
34
|
runtimeMetrics.increment(`${METRIC_PREFIX}.responses`, true)
|
|
38
35
|
runtimeMetrics.increment(`${METRIC_PREFIX}.responses.by.status`, `status:${status}`, true)
|
|
@@ -53,6 +50,16 @@ class AgentWriter extends BaseWriter {
|
|
|
53
50
|
|
|
54
51
|
log.debug('Response from the agent: %s', res)
|
|
55
52
|
|
|
53
|
+
// Capture container tags hash from agent response headers
|
|
54
|
+
// The hash is sent by the agent only when Datadog-Container-ID is present in the request
|
|
55
|
+
// (Datadog-Container-ID is automatically injected by docker.inject() in exporters/common/request.js)
|
|
56
|
+
if (headers) {
|
|
57
|
+
const containerTagsHash = headers['Datadog-Container-Tags-Hash']
|
|
58
|
+
if (containerTagsHash) {
|
|
59
|
+
propagationHash.updateContainerTagsHash(containerTagsHash)
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
56
63
|
try {
|
|
57
64
|
this._prioritySampler.update(JSON.parse(res).rate_by_service)
|
|
58
65
|
} catch (e) {
|
|
@@ -91,7 +98,13 @@ function makeRequest (version, data, count, url, headers, lookup, cb) {
|
|
|
91
98
|
|
|
92
99
|
log.debug('Request to the agent: %j', options)
|
|
93
100
|
|
|
94
|
-
request(data, options,
|
|
101
|
+
request(data, options, (err, res, status, headers) => {
|
|
102
|
+
logIntegrations()
|
|
103
|
+
if (status !== 404 && status !== 200 && err) {
|
|
104
|
+
logAgentError({ status, message: err.message ?? inspect(err) })
|
|
105
|
+
}
|
|
106
|
+
cb(err, res, status, headers)
|
|
107
|
+
})
|
|
95
108
|
}
|
|
96
109
|
|
|
97
110
|
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
|