dd-trace 5.105.0 → 5.107.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.d.ts +20 -1
- package/package.json +5 -7
- package/packages/datadog-core/src/storage.js +47 -48
- package/packages/datadog-esbuild/index.js +6 -1
- package/packages/datadog-instrumentations/src/ai.js +12 -3
- package/packages/datadog-instrumentations/src/aws-sdk.js +3 -2
- package/packages/datadog-instrumentations/src/body-parser.js +5 -2
- package/packages/datadog-instrumentations/src/connect.js +3 -2
- package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
- package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +19 -0
- package/packages/datadog-instrumentations/src/cucumber.js +319 -152
- package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
- package/packages/datadog-instrumentations/src/express-session.js +12 -11
- package/packages/datadog-instrumentations/src/express.js +24 -20
- package/packages/datadog-instrumentations/src/fastify.js +18 -6
- package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
- package/packages/datadog-instrumentations/src/http/client.js +9 -12
- package/packages/datadog-instrumentations/src/http/server.js +30 -16
- package/packages/datadog-instrumentations/src/http2/client.js +15 -12
- package/packages/datadog-instrumentations/src/http2/server.js +15 -8
- package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
- package/packages/datadog-instrumentations/src/jest.js +143 -73
- package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
- package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
- package/packages/datadog-instrumentations/src/multer.js +3 -2
- package/packages/datadog-instrumentations/src/mysql2.js +34 -0
- package/packages/datadog-instrumentations/src/net.js +8 -6
- package/packages/datadog-instrumentations/src/openai.js +19 -7
- package/packages/datadog-instrumentations/src/pg.js +19 -0
- package/packages/datadog-instrumentations/src/router.js +12 -10
- package/packages/datadog-instrumentations/src/vitest.js +29 -4
- package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +1 -1
- package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +218 -4
- package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
- package/packages/datadog-plugin-cucumber/src/index.js +2 -0
- package/packages/datadog-plugin-cypress/src/support.js +31 -1
- package/packages/datadog-plugin-http/src/client.js +0 -3
- package/packages/datadog-plugin-http/src/server.js +11 -1
- package/packages/datadog-plugin-mocha/src/index.js +2 -0
- package/packages/datadog-plugin-pg/src/index.js +10 -0
- package/packages/dd-trace/src/aiguard/index.js +34 -15
- package/packages/dd-trace/src/aiguard/sdk.js +34 -3
- package/packages/dd-trace/src/aiguard/tags.js +6 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
- package/packages/dd-trace/src/config/defaults.js +14 -0
- package/packages/dd-trace/src/config/generated-config-types.d.ts +1 -1
- package/packages/dd-trace/src/config/helper.js +1 -0
- package/packages/dd-trace/src/config/index.js +5 -9
- package/packages/dd-trace/src/config/parsers.js +8 -0
- package/packages/dd-trace/src/config/supported-configurations.json +13 -6
- package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
- package/packages/dd-trace/src/datastreams/writer.js +1 -2
- package/packages/dd-trace/src/debugger/config.js +1 -1
- package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -2
- package/packages/dd-trace/src/debugger/index.js +1 -2
- package/packages/dd-trace/src/dogstatsd.js +2 -3
- package/packages/dd-trace/src/encode/0.4.js +49 -41
- package/packages/dd-trace/src/encode/agentless-json.js +5 -1
- package/packages/dd-trace/src/encode/tags-processors.js +14 -0
- package/packages/dd-trace/src/exporters/agent/index.js +1 -2
- package/packages/dd-trace/src/exporters/agentless/index.js +6 -10
- package/packages/dd-trace/src/exporters/common/buffering-exporter.js +1 -2
- package/packages/dd-trace/src/exporters/common/request.js +26 -0
- package/packages/dd-trace/src/exporters/span-stats/index.js +1 -2
- package/packages/dd-trace/src/id.js +15 -0
- package/packages/dd-trace/src/llmobs/plugins/ai/util.js +91 -5
- package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +43 -21
- package/packages/dd-trace/src/llmobs/plugins/genai/index.js +4 -0
- package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
- package/packages/dd-trace/src/llmobs/sdk.js +4 -1
- package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
- package/packages/dd-trace/src/llmobs/tagger.js +5 -3
- package/packages/dd-trace/src/llmobs/util.js +54 -0
- package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
- package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
- package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
- package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
- package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
- package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
- package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +25 -5
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +2 -10
- package/packages/dd-trace/src/opentracing/span.js +23 -18
- package/packages/dd-trace/src/opentracing/span_context.js +1 -3
- package/packages/dd-trace/src/opentracing/tracer.js +16 -12
- package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
- package/packages/dd-trace/src/priority_sampler.js +6 -5
- package/packages/dd-trace/src/profiling/config.js +11 -25
- package/packages/dd-trace/src/profiling/exporters/agent.js +11 -10
- package/packages/dd-trace/src/profiling/profiler.js +19 -9
- package/packages/dd-trace/src/profiling/profilers/wall.js +2 -3
- package/packages/dd-trace/src/proxy.js +13 -10
- package/packages/dd-trace/src/remote_config/index.js +1 -2
- package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
- package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
- package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
- package/packages/dd-trace/src/span_format.js +33 -25
- package/packages/dd-trace/src/span_stats.js +1 -1
- package/packages/dd-trace/src/startup-log.js +1 -2
- package/packages/dd-trace/src/telemetry/send-data.js +1 -1
- package/packages/dd-trace/src/tracer.js +1 -1
- package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
- package/vendor/dist/shell-quote/index.js +1 -1
- package/packages/dd-trace/src/agent/url.js +0 -28
- package/scripts/preinstall.js +0 -34
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { workerData: { config: parentConfig, parentThreadId, configPort } } = require('node:worker_threads')
|
|
4
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
5
4
|
const processTags = require('../../process-tags')
|
|
6
5
|
const log = require('./log')
|
|
7
6
|
|
|
@@ -21,6 +20,8 @@ configPort.on('messageerror', (err) =>
|
|
|
21
20
|
)
|
|
22
21
|
|
|
23
22
|
function updateConfig (updates) {
|
|
24
|
-
config.url
|
|
23
|
+
// The worker receives a serialized config (see ../config.js) where `url` is a string, so it is
|
|
24
|
+
// reconstructed into a URL here rather than read directly off a Config instance.
|
|
25
|
+
config.url = new URL(updates.url)
|
|
25
26
|
config.dynamicInstrumentation.captureTimeoutNs = BigInt(updates.dynamicInstrumentation.captureTimeoutMs) * 1_000_000n
|
|
26
27
|
}
|
|
@@ -6,7 +6,6 @@ const { join } = require('path')
|
|
|
6
6
|
const { Worker, MessageChannel, threadId: parentThreadId } = require('worker_threads')
|
|
7
7
|
const log = require('../log')
|
|
8
8
|
const { fetchAgentInfo } = require('../agent/info')
|
|
9
|
-
const { getAgentUrl } = require('../agent/url')
|
|
10
9
|
const getDebuggerConfig = require('./config')
|
|
11
10
|
const { DEBUGGER_DIAGNOSTICS_V1, DEBUGGER_INPUT_V2 } = require('./constants')
|
|
12
11
|
|
|
@@ -211,7 +210,7 @@ function cleanup (error) {
|
|
|
211
210
|
function detectDebuggerEndpoint (config, cb) {
|
|
212
211
|
log.debug('[debugger] Detecting available debugger endpoints...')
|
|
213
212
|
|
|
214
|
-
fetchAgentInfo(
|
|
213
|
+
fetchAgentInfo(config.url, (err, agentInfo) => {
|
|
215
214
|
if (err) {
|
|
216
215
|
log.warn('[debugger] Failed to query agent %s endpoint, falling back to %s',
|
|
217
216
|
DEBUGGER_INPUT_V2,
|
|
@@ -6,7 +6,6 @@ const isIP = require('net').isIP
|
|
|
6
6
|
const request = require('./exporters/common/request')
|
|
7
7
|
const log = require('./log')
|
|
8
8
|
const Histogram = require('./histogram')
|
|
9
|
-
const { getAgentUrl } = require('./agent/url')
|
|
10
9
|
const { entityId } = require('./exporters/common/docker')
|
|
11
10
|
|
|
12
11
|
const MAX_BUFFER_SIZE = 1024 // limit from the agent
|
|
@@ -191,8 +190,8 @@ class DogStatsDClient {
|
|
|
191
190
|
lookup: config.lookup,
|
|
192
191
|
}
|
|
193
192
|
|
|
194
|
-
if (config.url
|
|
195
|
-
clientConfig.metricsProxyUrl =
|
|
193
|
+
if (config.url) {
|
|
194
|
+
clientConfig.metricsProxyUrl = config.url
|
|
196
195
|
}
|
|
197
196
|
|
|
198
197
|
return clientConfig
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
const getConfig = require('../config')
|
|
4
4
|
const { MsgpackChunk } = require('../msgpack')
|
|
5
5
|
const log = require('../log')
|
|
6
|
-
const { normalizeSpan } = require('./tags-processors')
|
|
6
|
+
const { normalizeSpan, eventTimeNano } = require('./tags-processors')
|
|
7
7
|
|
|
8
8
|
const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
|
|
9
9
|
// Values longer than this byte threshold skip the `_stringMap` lookup and
|
|
@@ -114,8 +114,10 @@ const ATTR_PAYLOAD_BOOL_FALSE = Buffer.concat([ATTR_PREFIX_BOOL, Buffer.from([0x
|
|
|
114
114
|
function formatSpanWithLegacyEvents (span) {
|
|
115
115
|
span = normalizeSpan(span)
|
|
116
116
|
if (span.span_events) {
|
|
117
|
-
//
|
|
118
|
-
//
|
|
117
|
+
// Reads the raw `_events` array directly (no formatter pre-reshape) and
|
|
118
|
+
// serializes to the legacy meta.events JSON string. The serialization is
|
|
119
|
+
// still the main cost on the legacy path; the native span_events slot
|
|
120
|
+
// (`#encodeSpanEvents`) avoids it entirely.
|
|
119
121
|
span.meta.events = stringifySpanEvents(span.span_events)
|
|
120
122
|
// `= undefined` over `delete` to keep the span's hidden class — `delete`
|
|
121
123
|
// would push every event-bearing span into V8 dictionary mode.
|
|
@@ -125,14 +127,15 @@ function formatSpanWithLegacyEvents (span) {
|
|
|
125
127
|
}
|
|
126
128
|
|
|
127
129
|
/**
|
|
128
|
-
* Hand-written stringifier for `span.span_events`.
|
|
129
|
-
* `
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
133
|
-
*
|
|
130
|
+
* Hand-written stringifier for `span.span_events`. Events arrive in their raw
|
|
131
|
+
* `{ name, startTime, attributes? }` shape; `time_unix_nano` is derived per
|
|
132
|
+
* event via `eventTimeNano` and empty attribute objects are dropped, matching
|
|
133
|
+
* what the formatter used to precompute. Attribute values are pre-sanitized to
|
|
134
|
+
* primitives or arrays of primitives, so we skip everything `JSON.stringify`
|
|
135
|
+
* does for the generic case (toJSON probing, prototype-chain key iteration,
|
|
136
|
+
* replacer hooks).
|
|
134
137
|
*
|
|
135
|
-
* @param {Array<{ name:
|
|
138
|
+
* @param {Array<{ name: unknown, startTime: number, attributes?: object }>} spanEvents
|
|
136
139
|
* @returns {string}
|
|
137
140
|
*/
|
|
138
141
|
function stringifySpanEvents (spanEvents) {
|
|
@@ -140,17 +143,21 @@ function stringifySpanEvents (spanEvents) {
|
|
|
140
143
|
for (let index = 0; index < spanEvents.length; index++) {
|
|
141
144
|
if (index > 0) result += ','
|
|
142
145
|
const event = spanEvents[index]
|
|
146
|
+
// `_sanitizeEventAttributes` leaves `attributes` undefined when empty, so a
|
|
147
|
+
// present value always has entries — no emptiness probe here.
|
|
148
|
+
const attributes = event.attributes
|
|
143
149
|
// `addEvent` does not type-check `name`; defer the unusual cases to
|
|
144
|
-
// `JSON.stringify` so non-string names match the prior behaviour
|
|
145
|
-
//
|
|
150
|
+
// `JSON.stringify` so non-string names match the prior behaviour instead
|
|
151
|
+
// of throwing in `escapeJsonString`. Build the wire-shaped object so the
|
|
152
|
+
// emitted key stays `time_unix_nano`, not the raw `startTime`.
|
|
146
153
|
if (typeof event.name !== 'string') {
|
|
147
|
-
result += JSON.stringify(event)
|
|
154
|
+
result += JSON.stringify({ name: event.name, time_unix_nano: eventTimeNano(event), attributes })
|
|
148
155
|
continue
|
|
149
156
|
}
|
|
150
157
|
result += '{"name":' + escapeJsonString(event.name) +
|
|
151
|
-
',"time_unix_nano":' + jsonNumber(event
|
|
152
|
-
if (
|
|
153
|
-
result += ',"attributes":' + stringifyAttributes(
|
|
158
|
+
',"time_unix_nano":' + jsonNumber(eventTimeNano(event))
|
|
159
|
+
if (attributes) {
|
|
160
|
+
result += ',"attributes":' + stringifyAttributes(attributes)
|
|
154
161
|
}
|
|
155
162
|
result += '}'
|
|
156
163
|
}
|
|
@@ -241,8 +248,8 @@ class AgentEncoder {
|
|
|
241
248
|
this.#config = getConfig()
|
|
242
249
|
this.#debugEncoding = this.#config.DD_TRACE_ENCODING_DEBUG
|
|
243
250
|
// Pick the per-span formatter once so the hot loop pays no per-span
|
|
244
|
-
// config check. The native path
|
|
245
|
-
//
|
|
251
|
+
// config check. The native path keeps the raw `span_events` slot for
|
|
252
|
+
// `#encodeSpanEvents`; the legacy path serializes it into meta.events.
|
|
246
253
|
this.#formatSpan = this.#config.DD_TRACE_NATIVE_SPAN_EVENTS
|
|
247
254
|
? normalizeSpan
|
|
248
255
|
: formatSpanWithLegacyEvents
|
|
@@ -320,16 +327,18 @@ class AgentEncoder {
|
|
|
320
327
|
const resourceLen = resourceEntry.length
|
|
321
328
|
const serviceLen = serviceEntry.length
|
|
322
329
|
|
|
323
|
-
//
|
|
324
|
-
//
|
|
325
|
-
//
|
|
326
|
-
//
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
// each integer
|
|
330
|
-
|
|
331
|
-
const
|
|
332
|
-
|
|
330
|
+
// `error` is `0` or `1` on nearly every span, and `start` is a
|
|
331
|
+
// nanosecond timestamp ≥ 2³² (always a msgpack u64). Decide the fused
|
|
332
|
+
// error key+value up front (`KEY_ERROR_0` / `KEY_ERROR_1`, or `undefined`
|
|
333
|
+
// for the rare non-binary flag) so the tail fuses without re-deciding the
|
|
334
|
+
// error shape twice. The fused tail also needs `start` as a u64; when
|
|
335
|
+
// either misses (synthetic small `start`, non-binary error) the tail
|
|
336
|
+
// routes each integer through `writeIntOrFloat` for the shortest
|
|
337
|
+
// encoding.
|
|
338
|
+
const errorEntry = span.error === 0
|
|
339
|
+
? KEY_ERROR_0
|
|
340
|
+
: span.error === 1 ? KEY_ERROR_1 : undefined
|
|
341
|
+
const fuseTail = errorEntry !== undefined && span.start >= 0x1_00_00_00_00
|
|
333
342
|
|
|
334
343
|
let blockSize = 1 +
|
|
335
344
|
KEY_TRACE_ID_PREFIX.length + 8 +
|
|
@@ -340,7 +349,7 @@ class AgentEncoder {
|
|
|
340
349
|
KEY_SERVICE.length + serviceLen
|
|
341
350
|
if (typeEntry) blockSize += KEY_TYPE.length + typeEntry.length
|
|
342
351
|
if (fuseTail) {
|
|
343
|
-
blockSize +=
|
|
352
|
+
blockSize += errorEntry.length + KEY_START_PREFIX.length + 8 + KEY_DURATION.length
|
|
344
353
|
}
|
|
345
354
|
|
|
346
355
|
const blockOffset = bytes.length
|
|
@@ -377,8 +386,8 @@ class AgentEncoder {
|
|
|
377
386
|
cursor += serviceLen
|
|
378
387
|
|
|
379
388
|
if (fuseTail) {
|
|
380
|
-
target.set(
|
|
381
|
-
cursor +=
|
|
389
|
+
target.set(errorEntry, cursor)
|
|
390
|
+
cursor += errorEntry.length
|
|
382
391
|
|
|
383
392
|
target.set(KEY_START_PREFIX, cursor)
|
|
384
393
|
cursor += KEY_START_PREFIX.length
|
|
@@ -389,15 +398,14 @@ class AgentEncoder {
|
|
|
389
398
|
cursor += 8
|
|
390
399
|
|
|
391
400
|
target.set(KEY_DURATION, cursor)
|
|
401
|
+
} else if (errorEntry) {
|
|
402
|
+
bytes.set(errorEntry)
|
|
403
|
+
bytes.set(KEY_START)
|
|
404
|
+
bytes.writeIntOrFloat(span.start)
|
|
405
|
+
bytes.set(KEY_DURATION)
|
|
392
406
|
} else {
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
} else if (span.error === 1) {
|
|
396
|
-
bytes.set(KEY_ERROR_1)
|
|
397
|
-
} else {
|
|
398
|
-
bytes.set(KEY_ERROR)
|
|
399
|
-
bytes.writeIntOrFloat(span.error)
|
|
400
|
-
}
|
|
407
|
+
bytes.set(KEY_ERROR)
|
|
408
|
+
bytes.writeIntOrFloat(span.error)
|
|
401
409
|
bytes.set(KEY_START)
|
|
402
410
|
bytes.writeIntOrFloat(span.start)
|
|
403
411
|
bytes.set(KEY_DURATION)
|
|
@@ -763,7 +771,7 @@ class AgentEncoder {
|
|
|
763
771
|
* values — no `formatSpanEvents` pre-pass and no recursive generic walk.
|
|
764
772
|
*
|
|
765
773
|
* @param {MsgpackChunk} bytes
|
|
766
|
-
* @param {Array<{ name:
|
|
774
|
+
* @param {Array<{ name: unknown, startTime: number, attributes?: object }>} spanEvents
|
|
767
775
|
*/
|
|
768
776
|
#encodeSpanEvents (bytes, spanEvents) {
|
|
769
777
|
const offset = bytes.length
|
|
@@ -784,7 +792,7 @@ class AgentEncoder {
|
|
|
784
792
|
bytes.set(KEY_NAME)
|
|
785
793
|
this._encodeString(bytes, event.name)
|
|
786
794
|
bytes.set(KEY_EVENT_TIME)
|
|
787
|
-
bytes.writeFloat(event
|
|
795
|
+
bytes.writeFloat(eventTimeNano(event))
|
|
788
796
|
|
|
789
797
|
const attributes = event.attributes
|
|
790
798
|
if (attributes !== null && typeof attributes === 'object') {
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
const log = require('../log')
|
|
4
4
|
const { TOP_LEVEL_KEY } = require('../constants')
|
|
5
5
|
const { normalizeSpan } = require('./tags-processors')
|
|
6
|
+
const { stringifySpanEvents } = require('./0.4')
|
|
6
7
|
|
|
7
8
|
// Soft limit for estimated payload size. Triggers an early flush to stay under intake request size limits.
|
|
8
9
|
const SOFT_LIMIT = 8 * 1024 * 1024 // 8MB
|
|
@@ -20,7 +21,10 @@ function formatSpan (span, isFirstSpan) {
|
|
|
20
21
|
delete span.meta['_dd.p.tid']
|
|
21
22
|
|
|
22
23
|
if (span.span_events) {
|
|
23
|
-
|
|
24
|
+
// Events arrive raw (`{ name, startTime, attributes? }`); stringifySpanEvents
|
|
25
|
+
// derives `time_unix_nano` and drops empty attributes, matching the JSON the
|
|
26
|
+
// reshaped array used to produce.
|
|
27
|
+
span.meta.events = stringifySpanEvents(span.span_events)
|
|
24
28
|
delete span.span_events
|
|
25
29
|
}
|
|
26
30
|
|
|
@@ -46,6 +46,19 @@ function truncateSpanTestOpt (span) {
|
|
|
46
46
|
return span
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Convert a raw span event's `startTime` (milliseconds, sub-millisecond
|
|
51
|
+
* precision) to the wire `time_unix_nano`. Single source of truth for the
|
|
52
|
+
* formula so the four encoders that consume `span_events` stay in lockstep;
|
|
53
|
+
* the formatter no longer reshapes events, it hands the raw array through.
|
|
54
|
+
*
|
|
55
|
+
* @param {{ startTime: number }} event
|
|
56
|
+
* @returns {number}
|
|
57
|
+
*/
|
|
58
|
+
function eventTimeNano (event) {
|
|
59
|
+
return Math.round(event.startTime * 1e6)
|
|
60
|
+
}
|
|
61
|
+
|
|
49
62
|
function normalizeSpan (span) {
|
|
50
63
|
span.service = span.service || DEFAULT_SERVICE_NAME
|
|
51
64
|
if (span.service.length > MAX_SERVICE_LENGTH) {
|
|
@@ -69,6 +82,7 @@ module.exports = {
|
|
|
69
82
|
truncateSpan,
|
|
70
83
|
truncateSpanTestOpt,
|
|
71
84
|
normalizeSpan,
|
|
85
|
+
eventTimeNano,
|
|
72
86
|
MAX_META_KEY_LENGTH,
|
|
73
87
|
MAX_META_VALUE_LENGTH,
|
|
74
88
|
MAX_META_VALUE_LENGTH_TEST_OPTIMIZATION,
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
const { URL } = require('url')
|
|
4
4
|
const log = require('../../log')
|
|
5
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
6
5
|
const Writer = require('./writer')
|
|
7
6
|
|
|
8
7
|
class AgentExporter {
|
|
@@ -11,7 +10,7 @@ class AgentExporter {
|
|
|
11
10
|
constructor (config, prioritySampler) {
|
|
12
11
|
this._config = config
|
|
13
12
|
const { lookup, protocolVersion, stats = {}, apmTracingEnabled } = config
|
|
14
|
-
this._url =
|
|
13
|
+
this._url = config.url
|
|
15
14
|
|
|
16
15
|
const headers = {}
|
|
17
16
|
if (stats.enabled || apmTracingEnabled === false) {
|
|
@@ -18,25 +18,21 @@ class AgentlessExporter {
|
|
|
18
18
|
|
|
19
19
|
/**
|
|
20
20
|
* @param {object} config - Configuration object
|
|
21
|
-
* @param {string} [config.site] - The Datadog site. Defaults to 'datadoghq.com'.
|
|
22
|
-
* @param {string} [config.url] - Override intake URL
|
|
21
|
+
* @param {string} [config.site] - The Datadog site. Defaults to 'datadoghq.com'.
|
|
23
22
|
* @param {number} [config.flushInterval] - Batch flush interval in ms
|
|
24
23
|
* @param {string} [config.env] - Environment name
|
|
25
24
|
* @param {object} [config.tags] - Tags including runtime-id
|
|
26
25
|
*/
|
|
27
26
|
constructor (config) {
|
|
28
27
|
this._config = config
|
|
29
|
-
const
|
|
28
|
+
const site = config.site ?? 'datadoghq.com'
|
|
30
29
|
|
|
31
30
|
try {
|
|
32
|
-
|
|
31
|
+
// Agentless traffic carries the Datadog API key, so the intake is always the public https
|
|
32
|
+
// endpoint; never derive it from config.url (the agent's cleartext http) or the key leaks.
|
|
33
|
+
this._url = new URL(`https://public-trace-http-intake.logs.${site}`)
|
|
33
34
|
} catch (err) {
|
|
34
|
-
log.error(
|
|
35
|
-
'Invalid URL configuration for agentless exporter. url=%s, site=%s. Error: %s',
|
|
36
|
-
url || 'not set',
|
|
37
|
-
site,
|
|
38
|
-
err.message
|
|
39
|
-
)
|
|
35
|
+
log.error('Invalid site for agentless exporter. site=%s. Error: %s', site, err.message)
|
|
40
36
|
this._url = null
|
|
41
37
|
}
|
|
42
38
|
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
3
|
const { incrementCountMetric, TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION } = require('../../ci-visibility/telemetry')
|
|
4
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Base exporter that buffers traces until a writer is initialized.
|
|
@@ -14,7 +13,7 @@ class BufferingExporter {
|
|
|
14
13
|
|
|
15
14
|
constructor (tracerConfig) {
|
|
16
15
|
this._config = tracerConfig
|
|
17
|
-
this._url =
|
|
16
|
+
this._url = tracerConfig.url
|
|
18
17
|
}
|
|
19
18
|
|
|
20
19
|
export (trace) {
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const { Readable } = require('stream')
|
|
7
7
|
const http = require('http')
|
|
8
8
|
const https = require('https')
|
|
9
|
+
const net = require('net')
|
|
9
10
|
const zlib = require('zlib')
|
|
10
11
|
|
|
11
12
|
const { storage } = require('../../../../datadog-core')
|
|
@@ -45,6 +46,17 @@ function parseUrl (urlObjOrString) {
|
|
|
45
46
|
return url
|
|
46
47
|
}
|
|
47
48
|
|
|
49
|
+
/**
|
|
50
|
+
* @param {string} hostname Host as resolved by {@link parseUrl}; IPv6 is unbracketed (`::1`).
|
|
51
|
+
*/
|
|
52
|
+
function isLoopbackHost (hostname) {
|
|
53
|
+
// The 127.0.0.0/8 block is loopback, but only when the host is an actual IPv4 literal: a
|
|
54
|
+
// hostname like `127.evil.com` shares the prefix yet resolves anywhere, so net.isIPv4 gates it.
|
|
55
|
+
return hostname === 'localhost' ||
|
|
56
|
+
hostname === '::1' ||
|
|
57
|
+
(hostname.startsWith('127.') && net.isIPv4(hostname))
|
|
58
|
+
}
|
|
59
|
+
|
|
48
60
|
/**
|
|
49
61
|
* @param {Buffer|string|Readable|Array<Buffer|string>} data
|
|
50
62
|
* @param {object} options
|
|
@@ -67,6 +79,20 @@ function request (data, options, callback) {
|
|
|
67
79
|
}
|
|
68
80
|
}
|
|
69
81
|
|
|
82
|
+
// Never put the Datadog API key on a cleartext connection to a non-loopback host; that would
|
|
83
|
+
// expose it on the wire. Loopback (local agent, dev proxy, tests) is exempt. Strip the key
|
|
84
|
+
// rather than drop the request: the agent proxies telemetry with its own key, while an https
|
|
85
|
+
// intake URL is required to authenticate agentless traffic.
|
|
86
|
+
const hasApiKey = options.headers['dd-api-key'] !== undefined || options.headers['DD-API-KEY'] !== undefined
|
|
87
|
+
if (hasApiKey && options.protocol === 'http:' && !isLoopbackHost(options.hostname)) {
|
|
88
|
+
log.error(
|
|
89
|
+
'Not sending the Datadog API key over a non-TLS connection to %s. Configure an https intake URL.',
|
|
90
|
+
options.hostname
|
|
91
|
+
)
|
|
92
|
+
delete options.headers['dd-api-key']
|
|
93
|
+
delete options.headers['DD-API-KEY']
|
|
94
|
+
}
|
|
95
|
+
|
|
70
96
|
if (data instanceof Readable) {
|
|
71
97
|
const chunks = []
|
|
72
98
|
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const { getAgentUrl } = require('../../agent/url')
|
|
4
3
|
const { Writer } = require('./writer')
|
|
5
4
|
|
|
6
5
|
class SpanStatsExporter {
|
|
7
6
|
constructor (config) {
|
|
8
|
-
this._url =
|
|
7
|
+
this._url = config.url
|
|
9
8
|
this._writer = new Writer({ url: this._url })
|
|
10
9
|
}
|
|
11
10
|
|
|
@@ -78,6 +78,21 @@ class Identifier {
|
|
|
78
78
|
return this.toString()
|
|
79
79
|
}
|
|
80
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Returns the full hex trace ID. When this is a 64-bit identifier and `traceIdHigh`
|
|
83
|
+
* is provided, prepends it to form the 128-bit trace ID. Otherwise returns
|
|
84
|
+
* only this identifier's hex representation.
|
|
85
|
+
*
|
|
86
|
+
* @param {string | undefined} traceIdHigh - 16-char hex of the upper 64 bits, or undefined
|
|
87
|
+
* @returns {string}
|
|
88
|
+
*/
|
|
89
|
+
toTraceIdHex (traceIdHigh) {
|
|
90
|
+
if (traceIdHigh && this.#buffer.length <= 8) {
|
|
91
|
+
return traceIdHigh + this.toString(16)
|
|
92
|
+
}
|
|
93
|
+
return this.toString(16)
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
/**
|
|
82
97
|
* @param {Identifier} other
|
|
83
98
|
* @returns {boolean}
|
|
@@ -66,11 +66,22 @@ function getOperation (span) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
/**
|
|
69
|
-
* Get the LLM token usage from the span tags
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
69
|
+
* Get the LLM token usage from the span tags.
|
|
70
|
+
*
|
|
71
|
+
* Supports both AI SDK v4 (promptTokens/completionTokens) and v5+
|
|
72
|
+
* (inputTokens/outputTokens), and surfaces prompt-cache metrics for providers
|
|
73
|
+
* that report them. The AI SDK convention is that `inputTokens` already
|
|
74
|
+
* includes cached tokens, so cache reads are reported as a subset of input
|
|
75
|
+
* tokens rather than added on top.
|
|
76
|
+
*
|
|
77
|
+
* @param {SpanTags} tags
|
|
78
|
+
* @returns {{
|
|
79
|
+
* inputTokens?: number,
|
|
80
|
+
* outputTokens?: number,
|
|
81
|
+
* totalTokens?: number,
|
|
82
|
+
* cacheReadTokens?: number,
|
|
83
|
+
* cacheWriteTokens?: number
|
|
84
|
+
* }}
|
|
74
85
|
*/
|
|
75
86
|
function getUsage (tags) {
|
|
76
87
|
const usage = {}
|
|
@@ -87,9 +98,84 @@ function getUsage (tags) {
|
|
|
87
98
|
const totalTokens = tags['ai.usage.totalTokens'] ?? (inputTokens + outputTokens)
|
|
88
99
|
if (!Number.isNaN(totalTokens)) usage.totalTokens = totalTokens
|
|
89
100
|
|
|
101
|
+
// Prompt-cache metrics. AI SDK v6 standardizes cache READ tokens via
|
|
102
|
+
// `ai.usage.cachedInputTokens`; cache WRITE tokens (and earlier AI SDK
|
|
103
|
+
// versions / providers that don't fill `cachedInputTokens`) are only
|
|
104
|
+
// available through provider-specific `ai.response.providerMetadata`.
|
|
105
|
+
// Skip zero values: the AI SDK sets `cachedInputTokens=0` on every span
|
|
106
|
+
// regardless of provider, so emitting it would add noise to spans that
|
|
107
|
+
// don't actually use prompt caching (e.g. OpenAI).
|
|
108
|
+
const providerCache = getProviderCacheTokens(tags['ai.response.providerMetadata'])
|
|
109
|
+
|
|
110
|
+
const cacheReadTokens = tags['ai.usage.cachedInputTokens'] ?? providerCache.cacheReadTokens
|
|
111
|
+
if (cacheReadTokens) usage.cacheReadTokens = cacheReadTokens
|
|
112
|
+
|
|
113
|
+
if (providerCache.cacheWriteTokens) usage.cacheWriteTokens = providerCache.cacheWriteTokens
|
|
114
|
+
|
|
115
|
+
// Normalize `inputTokens` to the sum convention used by `bedrockruntime.js`.
|
|
116
|
+
// Some SDK combinations (e.g. `ai@5` + `@ai-sdk/amazon-bedrock@3`) pass the
|
|
117
|
+
// raw fresh count through, which makes `nonCached = input - cacheRead -
|
|
118
|
+
// cacheWrite` go negative downstream.
|
|
119
|
+
//
|
|
120
|
+
// Detection: if `inputTokens < cacheSum`, the value cannot already be a sum
|
|
121
|
+
// that includes them (non-negative arithmetic). This is provider/version
|
|
122
|
+
// agnostic and won't double-count on stacks where the SDK already
|
|
123
|
+
// normalized (`ai@6` + `bedrock@4` / `anthropic@3`, OpenAI, Google).
|
|
124
|
+
if (usage.inputTokens != null) {
|
|
125
|
+
const cacheSum = (usage.cacheReadTokens || 0) + (usage.cacheWriteTokens || 0)
|
|
126
|
+
if (usage.inputTokens < cacheSum) {
|
|
127
|
+
usage.inputTokens += cacheSum
|
|
128
|
+
if (usage.totalTokens != null) {
|
|
129
|
+
usage.totalTokens = usage.inputTokens + (usage.outputTokens || 0)
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
90
134
|
return usage
|
|
91
135
|
}
|
|
92
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Extract prompt-cache token counts from the stringified
|
|
139
|
+
* `ai.response.providerMetadata` attribute.
|
|
140
|
+
*
|
|
141
|
+
* The AI SDK does not standardize cache WRITE tokens on the usage object, and
|
|
142
|
+
* earlier versions / providers may also omit `ai.usage.cachedInputTokens`, so
|
|
143
|
+
* we read the provider-specific shape directly. Only Bedrock and Anthropic
|
|
144
|
+
* are handled here as they are the providers that report cache writes today.
|
|
145
|
+
*
|
|
146
|
+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/amazon-bedrock#cache-points
|
|
147
|
+
* @see https://ai-sdk.dev/providers/ai-sdk-providers/anthropic#cache-control
|
|
148
|
+
*
|
|
149
|
+
* @param {string | undefined} providerMetadataJson
|
|
150
|
+
* @returns {{ cacheReadTokens?: number, cacheWriteTokens?: number }}
|
|
151
|
+
*/
|
|
152
|
+
function getProviderCacheTokens (providerMetadataJson) {
|
|
153
|
+
if (!providerMetadataJson) return {}
|
|
154
|
+
|
|
155
|
+
const metadata = getJsonStringValue(providerMetadataJson, null)
|
|
156
|
+
if (!metadata || typeof metadata !== 'object') return {}
|
|
157
|
+
|
|
158
|
+
const result = {}
|
|
159
|
+
|
|
160
|
+
const bedrockUsage = metadata.bedrock?.usage
|
|
161
|
+
if (bedrockUsage) {
|
|
162
|
+
if (bedrockUsage.cacheReadInputTokens != null) result.cacheReadTokens = bedrockUsage.cacheReadInputTokens
|
|
163
|
+
if (bedrockUsage.cacheWriteInputTokens != null) result.cacheWriteTokens = bedrockUsage.cacheWriteInputTokens
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const anthropic = metadata.anthropic
|
|
167
|
+
if (anthropic) {
|
|
168
|
+
if (result.cacheReadTokens == null && anthropic.cacheReadInputTokens != null) {
|
|
169
|
+
result.cacheReadTokens = anthropic.cacheReadInputTokens
|
|
170
|
+
}
|
|
171
|
+
if (result.cacheWriteTokens == null && anthropic.cacheCreationInputTokens != null) {
|
|
172
|
+
result.cacheWriteTokens = anthropic.cacheCreationInputTokens
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return result
|
|
177
|
+
}
|
|
178
|
+
|
|
93
179
|
/**
|
|
94
180
|
* Safely JSON parses a string value with a default fallback
|
|
95
181
|
* @template T typeof defaultValue
|
|
@@ -7,12 +7,22 @@ const {
|
|
|
7
7
|
extractTextAndResponseReason,
|
|
8
8
|
parseModelId,
|
|
9
9
|
extractTextAndResponseReasonFromStream,
|
|
10
|
+
extractConverseToolDefinitions,
|
|
11
|
+
extractRequestParamsConverse,
|
|
12
|
+
extractTextAndResponseReasonConverse,
|
|
13
|
+
extractTextAndResponseReasonConverseFromStream,
|
|
10
14
|
} = require('../../../../datadog-plugin-aws-sdk/src/services/bedrockruntime/utils')
|
|
11
15
|
const BaseLLMObsPlugin = require('./base')
|
|
12
16
|
|
|
13
17
|
const llmobsStore = storage('llmobs')
|
|
14
18
|
|
|
15
|
-
const ENABLED_OPERATIONS = new Set([
|
|
19
|
+
const ENABLED_OPERATIONS = new Set([
|
|
20
|
+
'invokeModel',
|
|
21
|
+
'invokeModelWithResponseStream',
|
|
22
|
+
'converse',
|
|
23
|
+
'converseStream',
|
|
24
|
+
])
|
|
25
|
+
const CONVERSE_OPERATIONS = new Set(['converse', 'converseStream'])
|
|
16
26
|
|
|
17
27
|
/**
|
|
18
28
|
* @typedef {{
|
|
@@ -79,10 +89,18 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
79
89
|
setLLMObsTags ({ ctx, request, span, response, modelProvider, modelName, tokensFromHeaders }) {
|
|
80
90
|
const isStream = request?.operation?.toLowerCase().includes('stream')
|
|
81
91
|
telemetry.incrementLLMObsSpanStartCount({ autoinstrumented: true, integration: 'bedrock' })
|
|
92
|
+
this.#registerSpan(span, request)
|
|
82
93
|
|
|
94
|
+
if (CONVERSE_OPERATIONS.has(request?.operation)) {
|
|
95
|
+
this.#tagConverseSpan({ ctx, request, span, response, tokensFromHeaders, isStream })
|
|
96
|
+
} else {
|
|
97
|
+
this.#tagInvokeModelSpan({ ctx, request, span, response, modelProvider, modelName, tokensFromHeaders, isStream })
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
#registerSpan (span, request) {
|
|
83
102
|
const parent = llmobsStore.getStore()?.span
|
|
84
103
|
// Use full modelId and unified provider for LLMObs (required for backend cost estimation).
|
|
85
|
-
// Split modelProvider/modelName from parseModelId() are still used below for response parsing.
|
|
86
104
|
this._tagger.registerLLMObsSpan(span, {
|
|
87
105
|
parent,
|
|
88
106
|
modelName: request.params.modelId.toLowerCase(),
|
|
@@ -91,38 +109,42 @@ class BedrockRuntimeLLMObsPlugin extends BaseLLMObsPlugin {
|
|
|
91
109
|
name: 'bedrock-runtime.command',
|
|
92
110
|
integration: 'bedrock',
|
|
93
111
|
})
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
#tagConverseSpan ({ ctx, request, span, response, tokensFromHeaders, isStream }) {
|
|
115
|
+
const requestParams = extractRequestParamsConverse(request.params)
|
|
116
|
+
const textAndResponseReason = isStream
|
|
117
|
+
? extractTextAndResponseReasonConverseFromStream(ctx.chunks)
|
|
118
|
+
: extractTextAndResponseReasonConverse(response)
|
|
119
|
+
|
|
120
|
+
const toolDefinitions = extractConverseToolDefinitions(request.params)
|
|
121
|
+
if (toolDefinitions.length > 0) this._tagger.tagToolDefinitions(span, toolDefinitions)
|
|
122
|
+
if (textAndResponseReason.finishReason) {
|
|
123
|
+
this._tagger.tagMetadata(span, { stop_reason: textAndResponseReason.finishReason })
|
|
124
|
+
}
|
|
125
|
+
this.#tagCommon({ span, requestParams, textAndResponseReason, tokensFromHeaders })
|
|
126
|
+
}
|
|
94
127
|
|
|
128
|
+
#tagInvokeModelSpan ({ ctx, request, span, response, modelProvider, modelName, tokensFromHeaders, isStream }) {
|
|
95
129
|
const requestParams = extractRequestParams(request.params, modelProvider)
|
|
96
130
|
// for streamed responses, we'll use the coerced response object we formed in the stream handler
|
|
97
131
|
const textAndResponseReason = isStream
|
|
98
132
|
? extractTextAndResponseReasonFromStream(ctx.chunks, modelProvider, modelName)
|
|
99
133
|
: extractTextAndResponseReason(response, modelProvider, modelName)
|
|
100
134
|
|
|
101
|
-
|
|
135
|
+
this.#tagCommon({ span, requestParams, textAndResponseReason, tokensFromHeaders })
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
#tagCommon ({ span, requestParams, textAndResponseReason, tokensFromHeaders }) {
|
|
102
139
|
this._tagger.tagMetadata(span, {
|
|
103
140
|
temperature: Number.parseFloat(requestParams.temperature) || 0,
|
|
104
141
|
max_tokens: Number.parseInt(requestParams.maxTokens) || 0,
|
|
105
142
|
})
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
this._tagger.tagLLMIO(
|
|
109
|
-
span,
|
|
110
|
-
requestParams.prompt,
|
|
111
|
-
[{ content: textAndResponseReason.message, role: textAndResponseReason.role }]
|
|
112
|
-
)
|
|
113
|
-
|
|
114
|
-
// add token metrics
|
|
115
|
-
const { inputTokens, outputTokens, totalTokens, cacheReadTokens, cacheWriteTokens } = extractTokens({
|
|
143
|
+
this._tagger.tagLLMIO(span, requestParams.prompt, textAndResponseReason.messages)
|
|
144
|
+
this._tagger.tagMetrics(span, extractTokens({
|
|
116
145
|
tokensFromHeaders,
|
|
117
146
|
usage: textAndResponseReason.usage,
|
|
118
|
-
})
|
|
119
|
-
this._tagger.tagMetrics(span, {
|
|
120
|
-
inputTokens,
|
|
121
|
-
outputTokens,
|
|
122
|
-
totalTokens,
|
|
123
|
-
cacheReadTokens,
|
|
124
|
-
cacheWriteTokens,
|
|
125
|
-
})
|
|
147
|
+
}))
|
|
126
148
|
}
|
|
127
149
|
}
|
|
128
150
|
|