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.
Files changed (108) hide show
  1. package/index.d.ts +20 -1
  2. package/package.json +5 -7
  3. package/packages/datadog-core/src/storage.js +47 -48
  4. package/packages/datadog-esbuild/index.js +6 -1
  5. package/packages/datadog-instrumentations/src/ai.js +12 -3
  6. package/packages/datadog-instrumentations/src/aws-sdk.js +3 -2
  7. package/packages/datadog-instrumentations/src/body-parser.js +5 -2
  8. package/packages/datadog-instrumentations/src/connect.js +3 -2
  9. package/packages/datadog-instrumentations/src/cookie-parser.js +3 -2
  10. package/packages/datadog-instrumentations/src/cucumber-worker-threads.js +19 -0
  11. package/packages/datadog-instrumentations/src/cucumber.js +319 -152
  12. package/packages/datadog-instrumentations/src/express-mongo-sanitize.js +7 -5
  13. package/packages/datadog-instrumentations/src/express-session.js +12 -11
  14. package/packages/datadog-instrumentations/src/express.js +24 -20
  15. package/packages/datadog-instrumentations/src/fastify.js +18 -6
  16. package/packages/datadog-instrumentations/src/helpers/openai-ai-guard.js +27 -12
  17. package/packages/datadog-instrumentations/src/http/client.js +9 -12
  18. package/packages/datadog-instrumentations/src/http/server.js +30 -16
  19. package/packages/datadog-instrumentations/src/http2/client.js +15 -12
  20. package/packages/datadog-instrumentations/src/http2/server.js +15 -8
  21. package/packages/datadog-instrumentations/src/jest/bail-reporter.js +42 -0
  22. package/packages/datadog-instrumentations/src/jest.js +143 -73
  23. package/packages/datadog-instrumentations/src/mocha/main.js +43 -8
  24. package/packages/datadog-instrumentations/src/mocha/utils.js +128 -17
  25. package/packages/datadog-instrumentations/src/multer.js +3 -2
  26. package/packages/datadog-instrumentations/src/mysql2.js +34 -0
  27. package/packages/datadog-instrumentations/src/net.js +8 -6
  28. package/packages/datadog-instrumentations/src/openai.js +19 -7
  29. package/packages/datadog-instrumentations/src/pg.js +19 -0
  30. package/packages/datadog-instrumentations/src/router.js +12 -10
  31. package/packages/datadog-instrumentations/src/vitest.js +29 -4
  32. package/packages/datadog-plugin-aws-sdk/src/base.js +0 -3
  33. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/tracing.js +1 -1
  34. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +218 -4
  35. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +62 -11
  36. package/packages/datadog-plugin-cucumber/src/index.js +2 -0
  37. package/packages/datadog-plugin-cypress/src/support.js +31 -1
  38. package/packages/datadog-plugin-http/src/client.js +0 -3
  39. package/packages/datadog-plugin-http/src/server.js +11 -1
  40. package/packages/datadog-plugin-mocha/src/index.js +2 -0
  41. package/packages/datadog-plugin-pg/src/index.js +10 -0
  42. package/packages/dd-trace/src/aiguard/index.js +34 -15
  43. package/packages/dd-trace/src/aiguard/sdk.js +34 -3
  44. package/packages/dd-trace/src/aiguard/tags.js +6 -0
  45. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +1 -1
  46. package/packages/dd-trace/src/config/defaults.js +14 -0
  47. package/packages/dd-trace/src/config/generated-config-types.d.ts +1 -1
  48. package/packages/dd-trace/src/config/helper.js +1 -0
  49. package/packages/dd-trace/src/config/index.js +5 -9
  50. package/packages/dd-trace/src/config/parsers.js +8 -0
  51. package/packages/dd-trace/src/config/supported-configurations.json +13 -6
  52. package/packages/dd-trace/src/crashtracking/crashtracker.js +2 -2
  53. package/packages/dd-trace/src/datastreams/writer.js +1 -2
  54. package/packages/dd-trace/src/debugger/config.js +1 -1
  55. package/packages/dd-trace/src/debugger/devtools_client/config.js +3 -2
  56. package/packages/dd-trace/src/debugger/index.js +1 -2
  57. package/packages/dd-trace/src/dogstatsd.js +2 -3
  58. package/packages/dd-trace/src/encode/0.4.js +49 -41
  59. package/packages/dd-trace/src/encode/agentless-json.js +5 -1
  60. package/packages/dd-trace/src/encode/tags-processors.js +14 -0
  61. package/packages/dd-trace/src/exporters/agent/index.js +1 -2
  62. package/packages/dd-trace/src/exporters/agentless/index.js +6 -10
  63. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +1 -2
  64. package/packages/dd-trace/src/exporters/common/request.js +26 -0
  65. package/packages/dd-trace/src/exporters/span-stats/index.js +1 -2
  66. package/packages/dd-trace/src/id.js +15 -0
  67. package/packages/dd-trace/src/llmobs/plugins/ai/util.js +91 -5
  68. package/packages/dd-trace/src/llmobs/plugins/bedrockruntime.js +43 -21
  69. package/packages/dd-trace/src/llmobs/plugins/genai/index.js +4 -0
  70. package/packages/dd-trace/src/llmobs/plugins/genai/util.js +45 -0
  71. package/packages/dd-trace/src/llmobs/sdk.js +4 -1
  72. package/packages/dd-trace/src/llmobs/span_processor.js +17 -1
  73. package/packages/dd-trace/src/llmobs/tagger.js +5 -3
  74. package/packages/dd-trace/src/llmobs/util.js +54 -0
  75. package/packages/dd-trace/src/llmobs/writers/base.js +1 -2
  76. package/packages/dd-trace/src/llmobs/writers/util.js +1 -2
  77. package/packages/dd-trace/src/openfeature/writers/base.js +1 -10
  78. package/packages/dd-trace/src/openfeature/writers/util.js +1 -2
  79. package/packages/dd-trace/src/opentelemetry/metrics/instruments.js +26 -13
  80. package/packages/dd-trace/src/opentelemetry/metrics/meter.js +7 -10
  81. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +92 -0
  82. package/packages/dd-trace/src/opentelemetry/trace/otlp_transformer.js +25 -5
  83. package/packages/dd-trace/src/opentracing/propagation/text_map.js +2 -10
  84. package/packages/dd-trace/src/opentracing/span.js +23 -18
  85. package/packages/dd-trace/src/opentracing/span_context.js +1 -3
  86. package/packages/dd-trace/src/opentracing/tracer.js +16 -12
  87. package/packages/dd-trace/src/plugins/ci_plugin.js +131 -46
  88. package/packages/dd-trace/src/priority_sampler.js +6 -5
  89. package/packages/dd-trace/src/profiling/config.js +11 -25
  90. package/packages/dd-trace/src/profiling/exporters/agent.js +11 -10
  91. package/packages/dd-trace/src/profiling/profiler.js +19 -9
  92. package/packages/dd-trace/src/profiling/profilers/wall.js +2 -3
  93. package/packages/dd-trace/src/proxy.js +13 -10
  94. package/packages/dd-trace/src/remote_config/index.js +1 -2
  95. package/packages/dd-trace/src/runtime_metrics/client.js +30 -0
  96. package/packages/dd-trace/src/runtime_metrics/index.js +12 -2
  97. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +284 -0
  98. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -11
  99. package/packages/dd-trace/src/service-naming/source-resolver.js +5 -1
  100. package/packages/dd-trace/src/span_format.js +33 -25
  101. package/packages/dd-trace/src/span_stats.js +1 -1
  102. package/packages/dd-trace/src/startup-log.js +1 -2
  103. package/packages/dd-trace/src/telemetry/send-data.js +1 -1
  104. package/packages/dd-trace/src/tracer.js +1 -1
  105. package/vendor/dist/@apm-js-collab/code-transformer/index.js +2 -2
  106. package/vendor/dist/shell-quote/index.js +1 -1
  107. package/packages/dd-trace/src/agent/url.js +0 -28
  108. 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 = getAgentUrl(updates)
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(getAgentUrl(config), (err, agentInfo) => {
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 || config.port) {
195
- clientConfig.metricsProxyUrl = getAgentUrl(config)
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
- // TODO: this is currently a main cost driver. By unifying it with the formatter
118
- // it should be possible to improve performance significantly overall.
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`. The shape is fixed by
129
- * `extractSpanEvents` (`{ name, time_unix_nano, attributes? }`) and attribute
130
- * values are pre-sanitized to primitives or arrays of primitives, so we can
131
- * skip everything `JSON.stringify` does for the generic case (toJSON probing,
132
- * key iteration over the prototype chain, replacer hooks). Output matches
133
- * `JSON.stringify(spanEvents)` byte-for-byte for the post-sanitization shape.
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: string, time_unix_nano: number, attributes?: object }>} spanEvents
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
- // instead of throwing in `escapeJsonString`.
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.time_unix_nano)
152
- if (event.attributes) {
153
- result += ',"attributes":' + stringifyAttributes(event.attributes)
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 doesn't need to reshape `span_events`
245
- // because `#encodeSpanEvents` works directly on the raw attributes.
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
- // Almost every span carries `error: 0` or `error: 1` AND a nanosecond
324
- // `start` timestamp ≥ 2³² (so `start` always encodes as a u64). When
325
- // both hold, the block fuses error key+value, the start key + 0xCF
326
- // type byte + 8-byte timestamp, and the duration key into the per-span
327
- // reserve. The fallback path covers synthetic/test inputs with small
328
- // starts and rare non-binary error flags by keeping per-field emits so
329
- // each integer picks the shortest msgpack encoding.
330
- const errorIsFixint = span.error === 0 || span.error === 1
331
- const startFitsU64 = span.start >= 0x1_00_00_00_00
332
- const fuseTail = errorIsFixint && startFitsU64
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 += KEY_ERROR_0.length + KEY_START_PREFIX.length + 8 + KEY_DURATION.length
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(span.error === 0 ? KEY_ERROR_0 : KEY_ERROR_1, cursor)
381
- cursor += KEY_ERROR_0.length
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
- if (span.error === 0) {
394
- bytes.set(KEY_ERROR_0)
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: string, time_unix_nano: number, attributes?: object }>} spanEvents
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.time_unix_nano)
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
- span.meta.events = JSON.stringify(span.span_events)
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 = getAgentUrl(config)
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 { site = 'datadoghq.com', url } = config
28
+ const site = config.site ?? 'datadoghq.com'
30
29
 
31
30
  try {
32
- this._url = url ? new URL(url) : new URL(`https://public-trace-http-intake.logs.${site}`)
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 = getAgentUrl(tracerConfig)
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 = getAgentUrl(config)
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
- * Supports both AI SDK v4 (promptTokens/completionTokens) and v5 (inputTokens/outputTokens)
71
- * @template T extends {inputTokens: number, outputTokens: number, totalTokens: number}
72
- * @param {T} tags
73
- * @returns {Pick<T, 'inputTokens' | 'outputTokens' | 'totalTokens'>}
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(['invokeModel', 'invokeModelWithResponseStream'])
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
- // add metadata tags
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
- // add I/O tags
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