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
@@ -5,6 +5,7 @@ const {
5
5
  getOperation,
6
6
  extractMetrics,
7
7
  extractMetadata,
8
+ extractToolDefinitions,
8
9
  aggregateStreamingChunks,
9
10
  formatInputMessages,
10
11
  formatEmbeddingInput,
@@ -79,6 +80,9 @@ class GenAiLLMObsPlugin extends LLMObsPlugin {
79
80
  const metadata = extractMetadata(config)
80
81
  this._tagger.tagMetadata(span, metadata)
81
82
 
83
+ const toolDefinitions = extractToolDefinitions(config)
84
+ if (toolDefinitions.length > 0) this._tagger.tagToolDefinitions(span, toolDefinitions)
85
+
82
86
  if (error) {
83
87
  this._tagger.tagLLMIO(span, inputMessages, [{ content: '' }])
84
88
  return
@@ -155,6 +155,50 @@ function extractMetadata (config) {
155
155
  return metadata
156
156
  }
157
157
 
158
+ /**
159
+ * Extract tool definitions from config
160
+ * @param {object} config
161
+ * @returns {Array}
162
+ */
163
+ function extractToolDefinitions (config) {
164
+ const toolDefinitions = []
165
+
166
+ if (!Array.isArray(config?.tools)) {
167
+ return toolDefinitions
168
+ }
169
+
170
+ for (const tool of config.tools) {
171
+ // Only extract tools with valid function declarations
172
+ if (!Array.isArray(tool?.functionDeclarations)) {
173
+ continue
174
+ }
175
+
176
+ for (const currDeclaration of tool.functionDeclarations) {
177
+ // A valid declaration must have a name
178
+ if (!currDeclaration?.name) {
179
+ continue
180
+ }
181
+
182
+ const toolDef = { name: currDeclaration.name }
183
+
184
+ if (currDeclaration.description !== undefined) {
185
+ toolDef.description = currDeclaration.description
186
+ }
187
+
188
+ // Parameters can be in two different fields depending on user input
189
+ if (currDeclaration.parameters !== undefined) {
190
+ toolDef.schema = currDeclaration.parameters
191
+ } else if (currDeclaration.parametersJsonSchema !== undefined) {
192
+ toolDef.schema = currDeclaration.parametersJsonSchema
193
+ }
194
+
195
+ toolDefinitions.push(toolDef)
196
+ }
197
+ }
198
+
199
+ return toolDefinitions
200
+ }
201
+
158
202
  /**
159
203
  * Format function call message
160
204
  * @param {Array} parts
@@ -498,6 +542,7 @@ module.exports = {
498
542
  getOperation,
499
543
  extractMetrics,
500
544
  extractMetadata,
545
+ extractToolDefinitions,
501
546
  aggregateStreamingChunks,
502
547
  formatInputMessages,
503
548
  formatEmbeddingInput,
@@ -259,7 +259,7 @@ class LLMObs extends NoopLLMObs {
259
259
  throw new Error('LLMObs span must have a span kind specified')
260
260
  }
261
261
 
262
- const { inputData, outputData, metadata, metrics, tags, prompt, costTags } = options
262
+ const { inputData, outputData, metadata, metrics, tags, prompt, costTags, toolDefinitions } = options
263
263
 
264
264
  if (inputData || outputData) {
265
265
  if (spanKind === 'llm') {
@@ -289,6 +289,9 @@ class LLMObs extends NoopLLMObs {
289
289
  if (prompt) {
290
290
  this._tagger.tagPrompt(span, prompt)
291
291
  }
292
+ if (toolDefinitions != null) {
293
+ this._tagger.tagToolDefinitions(span, toolDefinitions)
294
+ }
292
295
  } catch (e) {
293
296
  if (e.ddErrorTag) {
294
297
  err = e.ddErrorTag
@@ -332,8 +332,24 @@ class LLMObsSpanProcessor {
332
332
  return tags
333
333
  }
334
334
 
335
+ /**
336
+ * @param {Record<string, unknown>} tags
337
+ */
335
338
  #objectTagsToStringArrayTags (tags) {
336
- return Object.entries(tags).map(([key, value]) => `${key}:${value ?? ''}`)
339
+ const out = []
340
+ for (const [key, value] of Object.entries(tags)) {
341
+ // Comma is the intake-side tag delimiter, so a single `"key:v1,v2"`
342
+ // entry fans into two orphan tags. One-per-element keeps each value
343
+ // addressable; empty arrays fall through to the scalar branch and
344
+ // still emit `key:` so `_dd.cost_tags` references keep finding a
345
+ // wire entry.
346
+ if (Array.isArray(value) && value.length > 0) {
347
+ for (const item of value) out.push(`${key}:${item ?? ''}`)
348
+ } else {
349
+ out.push(`${key}:${value ?? ''}`)
350
+ }
351
+ }
352
+ return out
337
353
  }
338
354
 
339
355
  /**
@@ -43,7 +43,7 @@ const {
43
43
  INSTRUMENTATION_METHOD_ANNOTATED,
44
44
  } = require('./constants/tags')
45
45
  const { storage } = require('./storage')
46
- const { findGenAIAncestorSpanId, validateCostTags, writeBridgeTags } = require('./util')
46
+ const { findGenAIAncestorSpanId, validateCostTags, writeBridgeTags, validateToolDefinitions } = require('./util')
47
47
 
48
48
  // global registry of LLMObs spans
49
49
  // maps LLMObs spans to their annotations
@@ -176,8 +176,10 @@ class LLMObsTagger {
176
176
  }
177
177
 
178
178
  tagToolDefinitions (span, toolDefinitions) {
179
- if (Array.isArray(toolDefinitions) && toolDefinitions.length > 0) {
180
- this._setTag(span, TOOL_DEFINITIONS, toolDefinitions)
179
+ const validatedToolDefinitions = validateToolDefinitions(toolDefinitions)
180
+
181
+ if (validatedToolDefinitions.length > 0) {
182
+ this._setTag(span, TOOL_DEFINITIONS, validatedToolDefinitions)
181
183
  } else {
182
184
  this.#handleFailure('Tool definitions must be a non-empty array.', 'invalid_tool_definitions')
183
185
  }
@@ -89,6 +89,59 @@ function validateCostTags (span, costTags, source, spanTags) {
89
89
  return [...validatedCostTags]
90
90
  }
91
91
 
92
+ // Validates tool definition entires
93
+ function validateToolDefinitions (toolDefinitions) {
94
+ if (!Array.isArray(toolDefinitions)) {
95
+ log.warn('toolDefinitions must be an array.')
96
+ return []
97
+ }
98
+ const validated = []
99
+
100
+ for (let i = 0; i < toolDefinitions.length; i++) {
101
+ const currToolDef = toolDefinitions[i]
102
+ if (!currToolDef || typeof currToolDef !== 'object') {
103
+ log.warn('Tool definition at index %d must be an object. Skipping.', i)
104
+ continue
105
+ }
106
+
107
+ // Name is not optional
108
+ if (!currToolDef.name || typeof currToolDef.name !== 'string' || currToolDef.name.length <= 0) {
109
+ log.warn('Tool definition at index %d must have a non empty string "name". Skipping.', i)
110
+ continue
111
+ }
112
+ const validatedToolDef = { name: currToolDef.name }
113
+
114
+ // Description, Schema, and Version are optional types
115
+ if (currToolDef.description !== undefined) {
116
+ if (typeof currToolDef.description === 'string') {
117
+ validatedToolDef.description = currToolDef.description
118
+ } else {
119
+ log.warn('Tool definition "description" at index %d must be a string. Skipping field.', i)
120
+ }
121
+ }
122
+
123
+ if (currToolDef.schema !== undefined) {
124
+ if (currToolDef.schema !== null && typeof currToolDef.schema === 'object' && !Array.isArray(currToolDef.schema)) {
125
+ validatedToolDef.schema = currToolDef.schema
126
+ } else {
127
+ log.warn('Tool definition "schema" at index %d must be a plain object. Skipping field.', i)
128
+ }
129
+ }
130
+
131
+ if (currToolDef.version !== undefined) {
132
+ if (typeof currToolDef.version === 'string') {
133
+ validatedToolDef.version = currToolDef.version
134
+ } else {
135
+ log.warn('Tool definition "version" at index %d must be a string. Skipping field.', i)
136
+ }
137
+ }
138
+
139
+ validated.push(validatedToolDef)
140
+ }
141
+
142
+ return validated
143
+ }
144
+
92
145
  // extracts the argument names from a function string
93
146
  function parseArgumentNames (str) {
94
147
  const result = []
@@ -318,4 +371,5 @@ module.exports = {
318
371
  safeJsonParse,
319
372
  spanHasError,
320
373
  writeBridgeTags,
374
+ validateToolDefinitions,
321
375
  }
@@ -14,7 +14,6 @@ const {
14
14
  EVP_SUBDOMAIN_HEADER_NAME,
15
15
  EVP_PROXY_AGENT_BASE_PATH,
16
16
  } = require('../constants/writers')
17
- const { getAgentUrl } = require('../../agent/url')
18
17
  const { parseResponseAndLog } = require('./util')
19
18
 
20
19
  class LLMObsBuffer {
@@ -210,7 +209,7 @@ class BaseLLMObsWriter {
210
209
 
211
210
  const overrideOriginEnv = getEnvironmentVariable('_DD_LLMOBS_OVERRIDE_ORIGIN')
212
211
  const overrideOriginUrl = overrideOriginEnv && new URL(overrideOriginEnv)
213
- const base = overrideOriginUrl ?? getAgentUrl(this._config)
212
+ const base = overrideOriginUrl ?? this._config.url
214
213
 
215
214
  return {
216
215
  url: base,
@@ -4,7 +4,6 @@ const logger = require('../../log')
4
4
  const { EVP_PROXY_AGENT_BASE_PATH } = require('../constants/writers')
5
5
  const telemetry = require('../telemetry')
6
6
  const { fetchAgentInfo } = require('../../agent/info')
7
- const { getAgentUrl } = require('../../agent/url')
8
7
 
9
8
  /**
10
9
  * @param {import('../../config/config-base')} config
@@ -17,7 +16,7 @@ function setAgentStrategy (config, setWritersAgentlessValue) {
17
16
  return
18
17
  }
19
18
 
20
- fetchAgentInfo(getAgentUrl(config), (err, agentInfo) => {
19
+ fetchAgentInfo(config.url, (err, agentInfo) => {
21
20
  if (err) {
22
21
  setWritersAgentlessValue(true)
23
22
  return
@@ -2,7 +2,6 @@
2
2
 
3
3
  const request = require('../../exporters/common/request')
4
4
  const { safeJSONStringify } = require('../../exporters/common/util')
5
- const { getAgentUrl } = require('../../agent/url')
6
5
 
7
6
  const log = require('../../log')
8
7
 
@@ -37,7 +36,7 @@ class BaseFFEWriter {
37
36
 
38
37
  this._config = config
39
38
  this._endpoint = endpoint
40
- this._baseUrl = agentUrl ?? this._getAgentUrl()
39
+ this._baseUrl = agentUrl ?? config.url
41
40
  this._payloadSizeLimit = payloadSizeLimit
42
41
  this._eventSizeLimit = eventSizeLimit
43
42
  this._headers = headers || {}
@@ -154,14 +153,6 @@ class BaseFFEWriter {
154
153
  }
155
154
  }
156
155
 
157
- /**
158
- * @private
159
- * @returns {URL} Constructs agent URL from config
160
- */
161
- _getAgentUrl () {
162
- return getAgentUrl(this._config)
163
- }
164
-
165
156
  /**
166
157
  * @private
167
158
  * @param {Array<object>} payload - Payload to encode
@@ -3,7 +3,6 @@
3
3
  const logger = require('../../log')
4
4
  const { EVP_PROXY_AGENT_BASE_PATH } = require('../constants/constants')
5
5
  const { fetchAgentInfo } = require('../../agent/info')
6
- const { getAgentUrl } = require('../../agent/url')
7
6
 
8
7
  /**
9
8
  * Determines if the agent supports EVP proxy and sets the writer enabled state accordingly
@@ -11,7 +10,7 @@ const { getAgentUrl } = require('../../agent/url')
11
10
  * @param {Function} setWriterEnabledValue - Callback to set the writer enabled state
12
11
  */
13
12
  function setAgentStrategy (config, setWriterEnabledValue) {
14
- fetchAgentInfo(getAgentUrl(config), (err, agentInfo) => {
13
+ fetchAgentInfo(config.url, (err, agentInfo) => {
15
14
  if (err) {
16
15
  logger.debug('FFE Writer disabled - error getting agent info:', err.message)
17
16
  setWriterEnabledValue(false)
@@ -50,7 +50,7 @@ class Instrument {
50
50
  * Creates a measurement object for recording metric values.
51
51
  * @param {string} type - Metric type from METRIC_TYPES
52
52
  * @param {number} value - Numeric value to record
53
- * @param {Attributes} attributes - Key-value pairs for metric dimensions
53
+ * @param {Attributes} [attributes] - Key-value pairs for metric dimensions
54
54
  * @returns {Measurement} Measurement object with metadata and timestamp
55
55
  */
56
56
  createMeasurement (type, value, attributes) {
@@ -73,9 +73,9 @@ class Instrument {
73
73
  * @class Counter
74
74
  */
75
75
  class Counter extends Instrument {
76
- add (value, attributes = {}) {
76
+ add (value, attributes) {
77
77
  if (value < 0) return
78
- this.reader?.record(this.createMeasurement(METRIC_TYPES.COUNTER, value, attributes))
78
+ this.reader.record(this.createMeasurement(METRIC_TYPES.COUNTER, value, attributes))
79
79
  }
80
80
  }
81
81
 
@@ -85,8 +85,8 @@ class Counter extends Instrument {
85
85
  * @class UpDownCounter
86
86
  */
87
87
  class UpDownCounter extends Instrument {
88
- add (value, attributes = {}) {
89
- this.reader?.record(this.createMeasurement(METRIC_TYPES.UPDOWNCOUNTER, value, attributes))
88
+ add (value, attributes) {
89
+ this.reader.record(this.createMeasurement(METRIC_TYPES.UPDOWNCOUNTER, value, attributes))
90
90
  }
91
91
  }
92
92
 
@@ -96,9 +96,9 @@ class UpDownCounter extends Instrument {
96
96
  * @class Histogram
97
97
  */
98
98
  class Histogram extends Instrument {
99
- record (value, attributes = {}) {
99
+ record (value, attributes) {
100
100
  if (value < 0) return
101
- this.reader?.record(this.createMeasurement(METRIC_TYPES.HISTOGRAM, value, attributes))
101
+ this.reader.record(this.createMeasurement(METRIC_TYPES.HISTOGRAM, value, attributes))
102
102
  }
103
103
  }
104
104
 
@@ -108,8 +108,8 @@ class Histogram extends Instrument {
108
108
  * @class Gauge
109
109
  */
110
110
  class Gauge extends Instrument {
111
- record (value, attributes = {}) {
112
- this.reader?.record(this.createMeasurement(METRIC_TYPES.GAUGE, value, attributes))
111
+ record (value, attributes) {
112
+ this.reader.record(this.createMeasurement(METRIC_TYPES.GAUGE, value, attributes))
113
113
  }
114
114
  }
115
115
 
@@ -136,7 +136,7 @@ class ObservableInstrument extends Instrument {
136
136
  addCallback (callback) {
137
137
  if (typeof callback !== 'function') return
138
138
  this.#callbacks.push(callback)
139
- this.reader?.observableInstruments.add(this)
139
+ this.reader.observableInstruments.add(this)
140
140
  }
141
141
 
142
142
  /**
@@ -150,7 +150,7 @@ class ObservableInstrument extends Instrument {
150
150
  this.#callbacks.splice(index, 1)
151
151
  if (this.#callbacks.length === 0) {
152
152
  // Remove instrument from collection when no callbacks remain
153
- this.reader?.observableInstruments.delete(this)
153
+ this.reader.observableInstruments.delete(this)
154
154
  }
155
155
  }
156
156
  }
@@ -163,8 +163,8 @@ class ObservableInstrument extends Instrument {
163
163
  collect () {
164
164
  const observations = []
165
165
  const observableResult = {
166
- observe: (value, attributes = {}) => {
167
- observations.push(this.createMeasurement(this.#type, value, attributes))
166
+ observe: (value, attributes) => {
167
+ observations.push(this.createObservation(value, attributes))
168
168
  },
169
169
  }
170
170
 
@@ -179,6 +179,18 @@ class ObservableInstrument extends Instrument {
179
179
 
180
180
  return observations
181
181
  }
182
+
183
+ /**
184
+ * Builds a measurement for this instrument's metric type. Keeps the type
185
+ * encapsulated so callers (e.g. batch observable callbacks) don't read it.
186
+ *
187
+ * @param {number} value
188
+ * @param {Attributes} attributes
189
+ * @returns {Measurement}
190
+ */
191
+ createObservation (value, attributes) {
192
+ return this.createMeasurement(this.#type, value, attributes)
193
+ }
182
194
  }
183
195
 
184
196
  /**
@@ -219,6 +231,7 @@ module.exports = {
219
231
  UpDownCounter,
220
232
  Histogram,
221
233
  Gauge,
234
+ ObservableInstrument,
222
235
  ObservableGauge,
223
236
  ObservableCounter,
224
237
  ObservableUpDownCounter,
@@ -1,7 +1,6 @@
1
1
  'use strict'
2
2
 
3
3
  const { VERSION: packageVersion } = require('../../../../../version')
4
- const log = require('../../log')
5
4
  const {
6
5
  Counter, UpDownCounter, Histogram, Gauge, ObservableGauge, ObservableCounter, ObservableUpDownCounter,
7
6
  } = require('./instruments')
@@ -148,23 +147,21 @@ class Meter {
148
147
  }
149
148
 
150
149
  /**
151
- * Adds a batch observable callback (not implemented).
150
+ * Registers a batch observable callback for the given observables.
152
151
  *
153
- * @param {Function} callback - Batch observable callback
154
- * @param {Array} observables - Array of observable instruments
152
+ * @param {Function} callback
153
+ * @param {Array} observables
155
154
  */
156
155
  addBatchObservableCallback (callback, observables) {
157
- log.warn('addBatchObservableCallback is not implemented')
156
+ this.meterProvider.reader.addBatchObservableCallback(callback, observables)
158
157
  }
159
158
 
160
159
  /**
161
- * Removes a batch observable callback (not implemented).
162
- *
163
- * @param {Function} callback - Batch observable callback
164
- * @param {Array} observables - Array of observable instruments
160
+ * @param {Function} callback
161
+ * @param {Array} observables
165
162
  */
166
163
  removeBatchObservableCallback (callback, observables) {
167
- log.warn('removeBatchObservableCallback is not implemented')
164
+ this.meterProvider.reader.removeBatchObservableCallback(callback, observables)
168
165
  }
169
166
  }
170
167
 
@@ -5,6 +5,7 @@ const { stableStringify } = require('../otlp/otlp_transformer_base')
5
5
  const {
6
6
  METRIC_TYPES, TEMPORALITY, DEFAULT_HISTOGRAM_BUCKETS, DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE,
7
7
  } = require('./constants')
8
+ const { ObservableInstrument } = require('./instruments')
8
9
 
9
10
  /**
10
11
  * @typedef {import('@opentelemetry/api').Attributes} Attributes
@@ -102,6 +103,7 @@ class PeriodicMetricReader {
102
103
  #isShutdown = false
103
104
  #exportInterval
104
105
  #aggregator
106
+ #batchCallbacks = []
105
107
 
106
108
  /**
107
109
  * Creates a new PeriodicMetricReader instance.
@@ -132,6 +134,66 @@ class PeriodicMetricReader {
132
134
  this.#measurements.push(measurement)
133
135
  }
134
136
 
137
+ /**
138
+ * Registers a batch observable callback. Mirrors
139
+ * `@opentelemetry/sdk-metrics` `ObservableRegistry.addBatchCallback`.
140
+ *
141
+ * @param {Function} callback
142
+ * @param {Array} observables
143
+ */
144
+ addBatchObservableCallback (callback, observables) {
145
+ if (typeof callback !== 'function') return
146
+ const instruments = new Set(observables?.filter(isObservableInstrument))
147
+ if (instruments.size === 0) return
148
+ if (this.#findBatchCallback(callback, instruments) !== -1) return
149
+ this.#batchCallbacks.push({ callback, instruments })
150
+ }
151
+
152
+ /**
153
+ * @param {Function} callback
154
+ * @param {Array} observables
155
+ */
156
+ removeBatchObservableCallback (callback, observables) {
157
+ const instruments = new Set(observables?.filter(isObservableInstrument))
158
+ const idx = this.#findBatchCallback(callback, instruments)
159
+ if (idx !== -1) this.#batchCallbacks.splice(idx, 1)
160
+ }
161
+
162
+ /**
163
+ * @param {Function} callback
164
+ * @param {Set} instruments
165
+ * @returns {number} index in #batchCallbacks, or -1
166
+ */
167
+ #findBatchCallback (callback, instruments) {
168
+ return this.#batchCallbacks.findIndex(record =>
169
+ record.callback === callback && setEquals(record.instruments, instruments))
170
+ }
171
+
172
+ /**
173
+ * Invokes batch observable callbacks and returns the produced measurements.
174
+ *
175
+ * @returns {Measurement[]}
176
+ */
177
+ #collectBatchObservables () {
178
+ if (this.#batchCallbacks.length === 0) return []
179
+ const out = []
180
+ for (const { callback, instruments } of this.#batchCallbacks) {
181
+ const result = {
182
+ observe: (instrument, value, attributes) => {
183
+ if (instruments.has(instrument)) {
184
+ out.push(instrument.createObservation(value, attributes))
185
+ }
186
+ },
187
+ }
188
+ try {
189
+ callback(result)
190
+ } catch (e) {
191
+ log.error('Error running batch observable callback', e)
192
+ }
193
+ }
194
+ return out
195
+ }
196
+
135
197
  /**
136
198
  * Forces an immediate collection and export of all metrics.
137
199
  * @returns {void}
@@ -210,6 +272,17 @@ class PeriodicMetricReader {
210
272
  }
211
273
  }
212
274
 
275
+ const batchMeasurements = this.#collectBatchObservables()
276
+ if (batchMeasurements.length > 0) {
277
+ const remainingCapacity = DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE - allMeasurements.length
278
+ if (batchMeasurements.length <= remainingCapacity) {
279
+ allMeasurements.push(...batchMeasurements)
280
+ } else {
281
+ allMeasurements.push(...batchMeasurements.slice(0, remainingCapacity))
282
+ this.#droppedCount += batchMeasurements.length - remainingCapacity
283
+ }
284
+ }
285
+
213
286
  if (this.#droppedCount > 0) {
214
287
  log.warn('Metric queue exceeded limit (max: %d). Dropping %d measurements.',
215
288
  DEFAULT_MAX_MEASUREMENT_QUEUE_SIZE, this.#droppedCount)
@@ -555,4 +628,23 @@ class MetricAggregator {
555
628
  }
556
629
  }
557
630
 
631
+ /**
632
+ * @param {object} x
633
+ * @returns {boolean}
634
+ */
635
+ function isObservableInstrument (x) {
636
+ return x instanceof ObservableInstrument
637
+ }
638
+
639
+ /**
640
+ * @param {Set} a
641
+ * @param {Set} b
642
+ * @returns {boolean}
643
+ */
644
+ function setEquals (a, b) {
645
+ if (a.size !== b.size) return false
646
+ for (const x of a) if (!b.has(x)) return false
647
+ return true
648
+ }
649
+
558
650
  module.exports = PeriodicMetricReader
@@ -4,6 +4,7 @@ const OtlpTransformerBase = require('../otlp/otlp_transformer_base')
4
4
  const { getProtobufTypes } = require('../otlp/protobuf_loader')
5
5
  const { VERSION } = require('../../../../../version')
6
6
  const id = require('../../id')
7
+ const { eventTimeNano } = require('../../encode/tags-processors')
7
8
 
8
9
  const { protoSpanKind } = getProtobufTypes()
9
10
  const SPAN_KIND_UNSPECIFIED = protoSpanKind.values.SPAN_KIND_UNSPECIFIED
@@ -16,6 +17,11 @@ const SPAN_KIND_CONSUMER = protoSpanKind.values.SPAN_KIND_CONSUMER
16
17
  // Cached zero Identifier used to detect zero IDs without re-allocating per span.
17
18
  const ZERO_ID = id('0')
18
19
 
20
+ // DD propagation tag carrying the upper 64 bits of a 128-bit trace ID as 16 hex chars.
21
+ // span_format.js#extractChunkTags only copies this onto the first-in-chunk span, so the
22
+ // transformer scans the batch to find it and applies it to every span's traceId.
23
+ const TRACE_ID_128 = '_dd.p.tid'
24
+
19
25
  /**
20
26
  * @typedef {import('../../id').Identifier} Identifier
21
27
  *
@@ -28,7 +34,7 @@ const ZERO_ID = id('0')
28
34
  *
29
35
  * @typedef {object} DDSpanEvent
30
36
  * @property {string} name - Event name
31
- * @property {number} time_unix_nano - Event time in nanoseconds since epoch
37
+ * @property {number} startTime - Event start time in milliseconds (sub-ms precision)
32
38
  * @property {Record<string, string | number | boolean>} [attributes] - Event attributes
33
39
  *
34
40
  * @typedef {object} DDFormattedSpan
@@ -65,6 +71,7 @@ const STATUS_CODE_ERROR = 2
65
71
  const EXCLUDED_META_KEYS = new Set([
66
72
  '_dd.span_links',
67
73
  'span.kind',
74
+ TRACE_ID_128,
68
75
  ])
69
76
 
70
77
  /**
@@ -113,6 +120,18 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
113
120
  * @returns {object[]} Array of scope span objects
114
121
  */
115
122
  #transformScopeSpans (spans) {
123
+ let traceKey
124
+ let traceIdHigh
125
+ const otlpSpans = spans.map((span) => {
126
+ // `_dd.p.tid` lives only on the first-in-chunk span of each trace.
127
+ // Reset at each trace boundary for batching of multiple traces.
128
+ const key = span.trace_id.toString(16)
129
+ if (key !== traceKey) {
130
+ traceKey = key
131
+ traceIdHigh = span.meta?.[TRACE_ID_128]?.toLowerCase()
132
+ }
133
+ return this.#transformSpan(span, traceIdHigh)
134
+ })
116
135
  return [{
117
136
  scope: {
118
137
  name: 'dd-trace-js',
@@ -121,7 +140,7 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
121
140
  droppedAttributesCount: 0,
122
141
  },
123
142
  schemaUrl: '',
124
- spans: spans.map(span => this.#transformSpan(span)),
143
+ spans: otlpSpans,
125
144
  }]
126
145
  }
127
146
 
@@ -129,14 +148,15 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
129
148
  * Transforms a single DD-formatted span to an OTLP Span object.
130
149
  *
131
150
  * @param {DDFormattedSpan} span - DD-formatted span to transform
151
+ * @param {string | undefined} traceIdHigh - 16-char hex of the upper 64 bits of the trace ID
132
152
  * @returns {object} OTLP Span object
133
153
  */
134
- #transformSpan (span) {
154
+ #transformSpan (span, traceIdHigh) {
135
155
  const parentId = span.parent_id
136
156
  const links = this.#extractLinks(span.meta?.['_dd.span_links'])
137
157
 
138
158
  return {
139
- traceId: this.#idToBytes(span.trace_id, 16),
159
+ traceId: span.trace_id.toTraceIdHex(traceIdHigh).padStart(32, '0'),
140
160
  spanId: this.#idToBytes(span.span_id, 8),
141
161
  parentSpanId: (parentId && !parentId.equals(ZERO_ID)) ? this.#idToBytes(parentId, 8) : undefined,
142
162
  name: span.resource,
@@ -255,7 +275,7 @@ class OtlpTraceTransformer extends OtlpTransformerBase {
255
275
  */
256
276
  #transformEvent (event) {
257
277
  return {
258
- timeUnixNano: event.time_unix_nano,
278
+ timeUnixNano: eventTimeNano(event),
259
279
  name: event.name || '',
260
280
  attributes: this.transformAttributes(event.attributes ?? {}),
261
281
  droppedAttributesCount: 0,