dd-trace 5.82.0 → 5.83.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 (134) hide show
  1. package/LICENSE-3rdparty.csv +78 -79
  2. package/ci/init.js +6 -6
  3. package/index.d.ts +152 -3
  4. package/loader-hook.mjs +1 -1
  5. package/package.json +58 -55
  6. package/packages/datadog-core/src/storage.js +7 -7
  7. package/packages/datadog-esbuild/index.js +6 -0
  8. package/packages/datadog-instrumentations/src/ai.js +7 -3
  9. package/packages/datadog-instrumentations/src/child_process.js +1 -1
  10. package/packages/datadog-instrumentations/src/cucumber.js +1 -1
  11. package/packages/datadog-instrumentations/src/graphql.js +1 -1
  12. package/packages/datadog-instrumentations/src/helpers/instrumentations.js +4 -3
  13. package/packages/datadog-instrumentations/src/helpers/register.js +3 -7
  14. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +1 -1
  15. package/packages/datadog-instrumentations/src/http/client.js +2 -2
  16. package/packages/datadog-instrumentations/src/jest.js +35 -14
  17. package/packages/datadog-instrumentations/src/koa.js +2 -1
  18. package/packages/datadog-instrumentations/src/light-my-request.js +2 -2
  19. package/packages/datadog-instrumentations/src/mocha/main.js +2 -2
  20. package/packages/datadog-instrumentations/src/mocha/worker.js +1 -1
  21. package/packages/datadog-instrumentations/src/mocha.js +1 -1
  22. package/packages/datadog-instrumentations/src/mysql.js +1 -1
  23. package/packages/datadog-instrumentations/src/mysql2.js +2 -2
  24. package/packages/datadog-instrumentations/src/net.js +13 -5
  25. package/packages/datadog-instrumentations/src/nyc.js +1 -1
  26. package/packages/datadog-instrumentations/src/otel-sdk-trace.js +4 -4
  27. package/packages/datadog-instrumentations/src/pg.js +4 -2
  28. package/packages/datadog-instrumentations/src/playwright.js +3 -3
  29. package/packages/datadog-instrumentations/src/selenium.js +2 -2
  30. package/packages/datadog-instrumentations/src/undici.js +12 -1
  31. package/packages/datadog-plugin-aws-sdk/src/base.js +4 -4
  32. package/packages/datadog-plugin-azure-event-hubs/src/producer.js +2 -2
  33. package/packages/datadog-plugin-azure-service-bus/src/producer.js +2 -2
  34. package/packages/datadog-plugin-cucumber/src/index.js +2 -2
  35. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +2 -2
  36. package/packages/datadog-plugin-dd-trace-api/src/index.js +2 -2
  37. package/packages/datadog-plugin-express/src/code_origin.js +21 -15
  38. package/packages/datadog-plugin-fastify/src/code_origin.js +17 -4
  39. package/packages/datadog-plugin-jest/src/index.js +2 -2
  40. package/packages/datadog-plugin-mocha/src/index.js +2 -2
  41. package/packages/datadog-plugin-mongodb-core/src/index.js +2 -2
  42. package/packages/datadog-plugin-playwright/src/index.js +3 -3
  43. package/packages/datadog-plugin-undici/src/index.js +305 -2
  44. package/packages/datadog-plugin-vitest/src/index.js +5 -5
  45. package/packages/dd-trace/index.js +19 -0
  46. package/packages/dd-trace/src/appsec/iast/taint-tracking/rewriter.js +1 -1
  47. package/packages/dd-trace/src/appsec/rasp/index.js +2 -4
  48. package/packages/dd-trace/src/azure_metadata.js +8 -3
  49. package/packages/dd-trace/src/baggage.js +36 -11
  50. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +5 -1
  51. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +2 -2
  52. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +2 -2
  53. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +2 -2
  54. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +2 -2
  55. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +3 -2
  56. package/packages/dd-trace/src/ci-visibility/exporters/git/git_metadata.js +3 -3
  57. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/index.js +4 -4
  58. package/packages/dd-trace/src/ci-visibility/exporters/test-worker/writer.js +1 -1
  59. package/packages/dd-trace/src/ci-visibility/intelligent-test-runner/get-skippable-suites.js +2 -2
  60. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +4 -4
  61. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +4 -4
  62. package/packages/dd-trace/src/ci-visibility/test-management/get-test-management-tests.js +2 -2
  63. package/packages/dd-trace/src/{config_defaults.js → config/defaults.js} +3 -3
  64. package/packages/dd-trace/src/{config-helper.js → config/helper.js} +88 -15
  65. package/packages/dd-trace/src/{config.js → config/index.js} +92 -45
  66. package/packages/dd-trace/src/config/remote_config.js +187 -19
  67. package/packages/dd-trace/src/{config_stable.js → config/stable.js} +20 -32
  68. package/packages/dd-trace/src/{supported-configurations.json → config/supported-configurations.json} +2 -0
  69. package/packages/dd-trace/src/crashtracking/crashtracker.js +1 -1
  70. package/packages/dd-trace/src/datastreams/processor.js +1 -1
  71. package/packages/dd-trace/src/datastreams/writer.js +1 -1
  72. package/packages/dd-trace/src/debugger/devtools_client/condition.js +1 -1
  73. package/packages/dd-trace/src/debugger/devtools_client/config.js +1 -1
  74. package/packages/dd-trace/src/debugger/devtools_client/send.js +3 -3
  75. package/packages/dd-trace/src/debugger/devtools_client/snapshot/constants.js +1 -1
  76. package/packages/dd-trace/src/debugger/index.js +83 -15
  77. package/packages/dd-trace/src/dogstatsd.js +2 -2
  78. package/packages/dd-trace/src/encode/0.4.js +2 -2
  79. package/packages/dd-trace/src/exporter.js +1 -1
  80. package/packages/dd-trace/src/exporters/agent/index.js +2 -4
  81. package/packages/dd-trace/src/exporters/agent/writer.js +9 -14
  82. package/packages/dd-trace/src/exporters/common/agent-info-exporter.js +1 -1
  83. package/packages/dd-trace/src/exporters/common/docker.js +2 -2
  84. package/packages/dd-trace/src/exporters/common/request.js +1 -1
  85. package/packages/dd-trace/src/exporters/common/util.js +2 -2
  86. package/packages/dd-trace/src/exporters/span-stats/index.js +1 -1
  87. package/packages/dd-trace/src/flare/index.js +1 -1
  88. package/packages/dd-trace/src/guardrails/telemetry.js +1 -1
  89. package/packages/dd-trace/src/index.js +4 -4
  90. package/packages/dd-trace/src/lambda/handler.js +2 -2
  91. package/packages/dd-trace/src/lambda/index.js +2 -2
  92. package/packages/dd-trace/src/lambda/runtime/patch.js +2 -2
  93. package/packages/dd-trace/src/lambda/runtime/ritm.js +2 -2
  94. package/packages/dd-trace/src/llmobs/constants/tags.js +8 -1
  95. package/packages/dd-trace/src/llmobs/index.js +2 -2
  96. package/packages/dd-trace/src/llmobs/noop.js +2 -0
  97. package/packages/dd-trace/src/llmobs/plugins/openai/index.js +3 -4
  98. package/packages/dd-trace/src/llmobs/sdk.js +33 -6
  99. package/packages/dd-trace/src/llmobs/span_processor.js +17 -7
  100. package/packages/dd-trace/src/llmobs/tagger.js +175 -1
  101. package/packages/dd-trace/src/llmobs/writers/base.js +116 -37
  102. package/packages/dd-trace/src/llmobs/writers/spans.js +4 -3
  103. package/packages/dd-trace/src/log/index.js +5 -5
  104. package/packages/dd-trace/src/noop/proxy.js +3 -3
  105. package/packages/dd-trace/src/openfeature/writers/base.js +7 -8
  106. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +2 -2
  107. package/packages/dd-trace/src/opentelemetry/tracer.js +48 -6
  108. package/packages/dd-trace/src/opentracing/propagation/text_map.js +45 -21
  109. package/packages/dd-trace/src/opentracing/span.js +4 -4
  110. package/packages/dd-trace/src/plugin_manager.js +8 -6
  111. package/packages/dd-trace/src/plugins/util/ci.js +5 -8
  112. package/packages/dd-trace/src/plugins/util/git-cache.js +3 -3
  113. package/packages/dd-trace/src/plugins/util/test.js +1 -1
  114. package/packages/dd-trace/src/plugins/util/user-provided-git.js +41 -43
  115. package/packages/dd-trace/src/profiler.js +4 -39
  116. package/packages/dd-trace/src/profiling/config.js +74 -31
  117. package/packages/dd-trace/src/profiling/exporter_cli.js +5 -5
  118. package/packages/dd-trace/src/profiling/exporters/agent.js +1 -1
  119. package/packages/dd-trace/src/profiling/exporters/event_serializer.js +9 -2
  120. package/packages/dd-trace/src/profiling/index.js +1 -1
  121. package/packages/dd-trace/src/profiling/libuv-size.js +1 -1
  122. package/packages/dd-trace/src/profiling/profiler.js +57 -2
  123. package/packages/dd-trace/src/proxy.js +34 -5
  124. package/packages/dd-trace/src/remote_config/capabilities.js +3 -0
  125. package/packages/dd-trace/src/remote_config/index.js +1 -1
  126. package/packages/dd-trace/src/ritm.js +8 -4
  127. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +2 -2
  128. package/packages/dd-trace/src/serverless.js +2 -2
  129. package/packages/dd-trace/src/span_processor.js +2 -2
  130. package/packages/dd-trace/src/startup-log.js +6 -15
  131. package/packages/dd-trace/src/telemetry/endpoints.js +67 -5
  132. package/packages/dd-trace/src/telemetry/send-data.js +103 -4
  133. package/packages/dd-trace/src/telemetry/telemetry.js +229 -110
  134. /package/packages/dd-trace/src/{git_properties.js → config/git_properties.js} +0 -0
@@ -1,5 +1,7 @@
1
1
  'use strict'
2
2
 
3
+ const { inspect } = require('util')
4
+
3
5
  const request = require('../common/request')
4
6
  const { startupLog } = require('../../startup-log')
5
7
  const runtimeMetrics = require('../../runtime_metrics')
@@ -26,7 +28,10 @@ class AgentWriter extends BaseWriter {
26
28
  runtimeMetrics.increment(`${METRIC_PREFIX}.requests`, true)
27
29
 
28
30
  const { _headers, _lookup, _protocolVersion, _url } = this
29
- makeRequest(_protocolVersion, data, count, _url, _headers, _lookup, true, (err, res, status) => {
31
+ makeRequest(_protocolVersion, data, count, _url, _headers, _lookup, (err, res, status) => {
32
+ // Note that logging will only happen once, regardless of how many times this is called.
33
+ startupLog(status !== 404 && status !== 200 ? { status, message: err?.message ?? inspect(err) } : undefined)
34
+
30
35
  if (status) {
31
36
  runtimeMetrics.increment(`${METRIC_PREFIX}.responses`, true)
32
37
  runtimeMetrics.increment(`${METRIC_PREFIX}.responses.by.status`, `status:${status}`, true)
@@ -39,8 +44,6 @@ class AgentWriter extends BaseWriter {
39
44
  }
40
45
  }
41
46
 
42
- startupLog({ agentError: err })
43
-
44
47
  if (err) {
45
48
  log.errorWithoutTelemetry('Error sending payload to the agent (status code: %s)', err.status, err)
46
49
  done()
@@ -68,7 +71,7 @@ function getEncoder (protocolVersion) {
68
71
  : require('../../encode/0.4').AgentEncoder
69
72
  }
70
73
 
71
- function makeRequest (version, data, count, url, headers, lookup, needsStartupLog, cb) {
74
+ function makeRequest (version, data, count, url, headers, lookup, cb) {
72
75
  const options = {
73
76
  path: `/v${version}/traces`,
74
77
  method: 'PUT',
@@ -79,7 +82,7 @@ function makeRequest (version, data, count, url, headers, lookup, needsStartupLo
79
82
  'X-Datadog-Trace-Count': String(count),
80
83
  'Datadog-Meta-Lang': 'nodejs',
81
84
  'Datadog-Meta-Lang-Version': process.version,
82
- 'Datadog-Meta-Lang-Interpreter': process.jsEngine || 'v8'
85
+ 'Datadog-Meta-Lang-Interpreter': process.versions.bun ? 'JavaScriptCore' : 'v8'
83
86
  },
84
87
  lookup,
85
88
  url
@@ -87,15 +90,7 @@ function makeRequest (version, data, count, url, headers, lookup, needsStartupLo
87
90
 
88
91
  log.debug('Request to the agent: %j', options)
89
92
 
90
- request(data, options, (err, res, status) => {
91
- if (needsStartupLog) {
92
- // Note that logging will only happen once, regardless of how many times this is called.
93
- startupLog({
94
- agentError: status !== 404 && status !== 200 ? err : undefined
95
- })
96
- }
97
- cb(err, res, status)
98
- })
93
+ request(data, options, cb)
99
94
  }
100
95
 
101
96
  module.exports = AgentWriter
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { URL, format } = require('url')
4
4
 
5
- const defaults = require('../../config_defaults')
5
+ const defaults = require('../../config/defaults')
6
6
  const { incrementCountMetric, TELEMETRY_EVENTS_ENQUEUED_FOR_SERIALIZATION } = require('../../ci-visibility/telemetry')
7
7
  const request = require('./request')
8
8
 
@@ -1,9 +1,9 @@
1
1
  'use strict'
2
2
 
3
3
  const fs = require('fs')
4
- const { getEnvironmentVariable } = require('../../config-helper')
4
+ const { getValueFromEnvSources } = require('../../config/helper')
5
5
 
6
- const DD_EXTERNAL_ENV = getEnvironmentVariable('DD_EXTERNAL_ENV')
6
+ const DD_EXTERNAL_ENV = getValueFromEnvSources('DD_EXTERNAL_ENV')
7
7
 
8
8
  // The second part is the PCF / Garden regexp. We currently assume no suffix($) to avoid matching pod UIDs
9
9
  // See https://github.com/DataDog/datadog-agent/blob/7.40.x/pkg/util/cgroups/reader.go#L50
@@ -77,7 +77,7 @@ function request (data, options, callback) {
77
77
  res.on('data', chunk => {
78
78
  chunks.push(chunk)
79
79
  })
80
- res.on('end', () => {
80
+ res.once('end', () => {
81
81
  activeRequests--
82
82
  const buffer = Buffer.concat(chunks)
83
83
 
@@ -1,12 +1,12 @@
1
1
  'use strict'
2
2
 
3
- const { getEnvironmentVariable } = require('../../config-helper')
3
+ const { getValueFromEnvSources } = require('../../config/helper')
4
4
 
5
5
  function safeJSONStringify (value) {
6
6
  return JSON.stringify(
7
7
  value,
8
8
  (key, value) => key === 'dd-api-key' ? undefined : value,
9
- getEnvironmentVariable('DD_TRACE_BEAUTIFUL_LOGS') ? 2 : undefined
9
+ getValueFromEnvSources('DD_TRACE_BEAUTIFUL_LOGS') ? 2 : undefined
10
10
  )
11
11
  }
12
12
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  const { URL, format } = require('url')
4
4
 
5
- const defaults = require('../../config_defaults')
5
+ const defaults = require('../../config/defaults')
6
6
  const { Writer } = require('./writer')
7
7
 
8
8
  class SpanStatsExporter {
@@ -64,7 +64,7 @@ const flare = {
64
64
  },
65
65
 
66
66
  _sendFile (task, file, filename) {
67
- if (!file) return
67
+ if (!file || file.length === 0) return
68
68
 
69
69
  const form = new FormData()
70
70
 
@@ -80,7 +80,7 @@ function sendTelemetry (name, tags, resultMetadata) {
80
80
  proc.on('error', function () {
81
81
  log.error('Failed to spawn telemetry forwarder')
82
82
  })
83
- proc.on('exit', function (code) {
83
+ proc.once('exit', function (code) {
84
84
  if (code !== 0) {
85
85
  log.error('Telemetry forwarder exited with code', code)
86
86
  }
@@ -1,14 +1,14 @@
1
1
  'use strict'
2
2
 
3
- const { getEnvironmentVariable } = require('../../dd-trace/src/config-helper')
3
+ const { getValueFromEnvSources } = require('./config/helper')
4
4
  const { isFalse } = require('./util')
5
5
 
6
6
  // Global `jest` is only present in Jest workers.
7
7
  const inJestWorker = typeof jest !== 'undefined'
8
8
 
9
- const ddTraceDisabled = getEnvironmentVariable('DD_TRACE_ENABLED')
10
- ? isFalse(getEnvironmentVariable('DD_TRACE_ENABLED'))
11
- : String(getEnvironmentVariable('OTEL_TRACES_EXPORTER')).toLowerCase() === 'none'
9
+ const ddTraceDisabled = getValueFromEnvSources('DD_TRACE_ENABLED')
10
+ ? isFalse(getValueFromEnvSources('DD_TRACE_ENABLED'))
11
+ : String(getValueFromEnvSources('OTEL_TRACES_EXPORTER')).toLowerCase() === 'none'
12
12
 
13
13
  module.exports = ddTraceDisabled || inJestWorker
14
14
  ? require('./noop/proxy')
@@ -3,7 +3,7 @@
3
3
  const log = require('../log')
4
4
  const { channel } = require('../../../datadog-instrumentations/src/helpers/instrument')
5
5
  const { ERROR_MESSAGE, ERROR_TYPE } = require('../constants')
6
- const { getEnvironmentVariable } = require('../config-helper')
6
+ const { getValueFromEnvSources } = require('../config/helper')
7
7
  const { ImpendingTimeout } = require('./runtime/errors')
8
8
 
9
9
  const globalTracer = global._ddtrace
@@ -26,7 +26,7 @@ let __lambdaTimeout
26
26
  function checkTimeout (context) {
27
27
  const remainingTimeInMillis = context.getRemainingTimeInMillis()
28
28
 
29
- let apmFlushDeadline = Number.parseInt(getEnvironmentVariable('DD_APM_FLUSH_DEADLINE_MILLISECONDS')) || 100
29
+ let apmFlushDeadline = Number.parseInt(getValueFromEnvSources('DD_APM_FLUSH_DEADLINE_MILLISECONDS')) || 100
30
30
  apmFlushDeadline = apmFlushDeadline < 0 ? 100 : apmFlushDeadline
31
31
 
32
32
  __lambdaTimeout = setTimeout(() => {
@@ -1,13 +1,13 @@
1
1
  'use strict'
2
2
 
3
- const { getEnvironmentVariable } = require('../config-helper')
3
+ const { getValueFromEnvSources } = require('../config/helper')
4
4
  const { registerLambdaHook } = require('./runtime/ritm')
5
5
 
6
6
  /**
7
7
  * It is safe to do it this way, since customers will never be expected to disable
8
8
  * this specific instrumentation through the init config object.
9
9
  */
10
- const _DD_TRACE_DISABLED_INSTRUMENTATIONS = getEnvironmentVariable('DD_TRACE_DISABLED_INSTRUMENTATIONS') || ''
10
+ const _DD_TRACE_DISABLED_INSTRUMENTATIONS = getValueFromEnvSources('DD_TRACE_DISABLED_INSTRUMENTATIONS') || ''
11
11
  const _disabledInstrumentations = new Set(
12
12
  _DD_TRACE_DISABLED_INSTRUMENTATIONS ? _DD_TRACE_DISABLED_INSTRUMENTATIONS.split(',') : []
13
13
  )
@@ -5,7 +5,7 @@ const path = require('path')
5
5
  const { datadog } = require('../handler')
6
6
  const { addHook } = require('../../../../datadog-instrumentations/src/helpers/instrument')
7
7
  const shimmer = require('../../../../datadog-shimmer')
8
- const { getEnvironmentVariable } = require('../../config-helper')
8
+ const { getEnvironmentVariable, getValueFromEnvSources } = require('../../config/helper')
9
9
  const { _extractModuleNameAndHandlerPath, _extractModuleRootAndHandler, _getLambdaFilePaths } = require('./ritm')
10
10
 
11
11
  /**
@@ -59,7 +59,7 @@ function patchLambdaHandler (lambdaHandler) {
59
59
  }
60
60
 
61
61
  const lambdaTaskRoot = getEnvironmentVariable('LAMBDA_TASK_ROOT')
62
- const originalLambdaHandler = getEnvironmentVariable('DD_LAMBDA_HANDLER')
62
+ const originalLambdaHandler = getValueFromEnvSources('DD_LAMBDA_HANDLER')
63
63
 
64
64
  if (originalLambdaHandler === undefined) {
65
65
  // Instrumentation is done manually.
@@ -10,7 +10,7 @@
10
10
  const path = require('path')
11
11
 
12
12
  const log = require('../../log')
13
- const { getEnvironmentVariable } = require('../../config-helper')
13
+ const { getEnvironmentVariable, getValueFromEnvSources } = require('../../config/helper')
14
14
  const Hook = require('../../../../datadog-instrumentations/src/helpers/hook')
15
15
  const instrumentations = require('../../../../datadog-instrumentations/src/helpers/instrumentations')
16
16
  const {
@@ -79,7 +79,7 @@ function _getLambdaFilePaths (lambdaStylePath) {
79
79
  */
80
80
  const registerLambdaHook = () => {
81
81
  const lambdaTaskRoot = getEnvironmentVariable('LAMBDA_TASK_ROOT')
82
- const originalLambdaHandler = getEnvironmentVariable('DD_LAMBDA_HANDLER')
82
+ const originalLambdaHandler = getValueFromEnvSources('DD_LAMBDA_HANDLER')
83
83
 
84
84
  if (originalLambdaHandler !== undefined && lambdaTaskRoot !== undefined) {
85
85
  const [moduleRoot, moduleAndHandler] = _extractModuleRootAndHandler(originalLambdaHandler)
@@ -17,6 +17,9 @@ module.exports = {
17
17
  TRACE_ID: '_ml_obs.trace_id',
18
18
  PROPAGATED_TRACE_ID_KEY: '_dd.p.llmobs_trace_id',
19
19
  ROOT_PARENT_ID: 'undefined',
20
+ DEFAULT_PROMPT_NAME: 'unnamed-prompt',
21
+ INTERNAL_CONTEXT_VARIABLE_KEYS: '_dd_context_variable_keys',
22
+ INTERNAL_QUERY_VARIABLE_KEYS: '_dd_query_variable_keys',
20
23
 
21
24
  MODEL_NAME: '_ml_obs.meta.model_name',
22
25
  MODEL_PROVIDER: '_ml_obs.meta.model_provider',
@@ -24,6 +27,7 @@ module.exports = {
24
27
  INPUT_DOCUMENTS: '_ml_obs.meta.input.documents',
25
28
  INPUT_MESSAGES: '_ml_obs.meta.input.messages',
26
29
  INPUT_VALUE: '_ml_obs.meta.input.value',
30
+ INPUT_PROMPT: '_ml_obs.meta.input.prompt',
27
31
 
28
32
  OUTPUT_DOCUMENTS: '_ml_obs.meta.output.documents',
29
33
  OUTPUT_MESSAGES: '_ml_obs.meta.output.messages',
@@ -42,5 +46,8 @@ module.exports = {
42
46
  PROMPT_MULTIMODAL: 'prompt_multimodal',
43
47
  INSTRUMENTATION_METHOD_AUTO: 'auto',
44
48
  INSTRUMENTATION_METHOD_ANNOTATED: 'annotated',
45
- INSTRUMENTATION_METHOD_UNKNOWN: 'unknown'
49
+ INSTRUMENTATION_METHOD_UNKNOWN: 'unknown',
50
+
51
+ ROUTING_API_KEY: '_dd.llmobs.routing.api_key',
52
+ ROUTING_SITE: '_dd.llmobs.routing.site'
46
53
  }
@@ -136,9 +136,9 @@ function handleSpanProcess (span) {
136
136
  spanProcessor.process(span)
137
137
  }
138
138
 
139
- function handleEvalMetricAppend (payload) {
139
+ function handleEvalMetricAppend ({ payload, routing }) {
140
140
  try {
141
- evalWriter.append(payload)
141
+ evalWriter.append(payload, routing)
142
142
  } catch (e) {
143
143
  log.warn(
144
144
  // eslint-disable-next-line @stylistic/max-len
@@ -81,6 +81,8 @@ class NoopLLMObs {
81
81
  deregisterProcessor () {}
82
82
 
83
83
  annotationContext (options, fn) { return fn() }
84
+
85
+ routingContext (options, fn) { return fn() }
84
86
  }
85
87
 
86
88
  module.exports = NoopLLMObs
@@ -422,16 +422,15 @@ class OpenAiLLMObsPlugin extends LLMObsPlugin {
422
422
  // Handle prompt tracking for reusable prompts
423
423
  if (inputs.prompt && response?.prompt) {
424
424
  const { id, version } = response.prompt // ResponsePrompt
425
- // TODO: Add proper tagger API for prompt metadata
426
425
  if (id && version) {
427
426
  const normalizedVariables = normalizePromptVariables(inputs.prompt.variables)
428
427
  const chatTemplate = extractChatTemplateFromInstructions(response.instructions, normalizedVariables)
429
- this._tagger._setTag(span, '_ml_obs.meta.input.prompt', {
428
+ this._tagger.tagPrompt(span, {
430
429
  id,
431
430
  version,
432
431
  variables: normalizedVariables,
433
- chat_template: chatTemplate
434
- })
432
+ template: chatTemplate
433
+ }, true)
435
434
  const tags = { [PROMPT_TRACKING_INSTRUMENTATION_METHOD]: INSTRUMENTATION_METHOD_AUTO }
436
435
  if (hasMultimodalInputs(inputs.prompt.variables)) {
437
436
  tags[PROMPT_MULTIMODAL] = 'true'
@@ -5,7 +5,7 @@ const { channel } = require('dc-polyfill')
5
5
  const { isTrue, isError } = require('../util')
6
6
  const tracerVersion = require('../../../../package.json').version
7
7
  const logger = require('../log')
8
- const { getEnvironmentVariable } = require('../config-helper')
8
+ const { getValueFromEnvSources } = require('../config/helper')
9
9
  const Span = require('../opentracing/span')
10
10
  const { SPAN_KIND, OUTPUT_VALUE, INPUT_VALUE } = require('./constants/tags')
11
11
  const {
@@ -49,7 +49,7 @@ class LLMObs extends NoopLLMObs {
49
49
 
50
50
  logger.debug('Enabling LLMObs')
51
51
 
52
- const DD_LLMOBS_ENABLED = getEnvironmentVariable('DD_LLMOBS_ENABLED')
52
+ const DD_LLMOBS_ENABLED = getValueFromEnvSources('DD_LLMOBS_ENABLED')
53
53
 
54
54
  if (DD_LLMOBS_ENABLED != null && !isTrue(DD_LLMOBS_ENABLED)) {
55
55
  logger.debug('LLMObs.enable() called when DD_LLMOBS_ENABLED is false. No action taken.')
@@ -62,7 +62,7 @@ class LLMObs extends NoopLLMObs {
62
62
  }
63
63
  // TODO: This will update config telemetry with the origin 'code', which is not ideal when `enable()` is called
64
64
  // based on `APM_TRACING` RC product updates.
65
- this._config.configure({ llmobs })
65
+ this._config.updateOptions({ llmobs })
66
66
 
67
67
  // configure writers and channel subscribers
68
68
  this._llmobsModule.enable(this._config)
@@ -241,7 +241,7 @@ class LLMObs extends NoopLLMObs {
241
241
  throw new Error('LLMObs span must have a span kind specified')
242
242
  }
243
243
 
244
- const { inputData, outputData, metadata, metrics, tags } = options
244
+ const { inputData, outputData, metadata, metrics, tags, prompt } = options
245
245
 
246
246
  if (inputData || outputData) {
247
247
  if (spanKind === 'llm') {
@@ -264,6 +264,9 @@ class LLMObs extends NoopLLMObs {
264
264
  if (tags) {
265
265
  this._tagger.tagSpanTags(span, tags)
266
266
  }
267
+ if (prompt) {
268
+ this._tagger.tagPrompt(span, prompt)
269
+ }
267
270
  } catch (e) {
268
271
  if (e.ddErrorTag) {
269
272
  err = e.ddErrorTag
@@ -404,7 +407,7 @@ class LLMObs extends NoopLLMObs {
404
407
  }
405
408
 
406
409
  // When OTel tracing is enabled, add source:otel tag to allow backend to wait for OTel span conversion
407
- if (isTrue(getEnvironmentVariable('DD_TRACE_OTEL_ENABLED'))) {
410
+ if (isTrue(getValueFromEnvSources('DD_TRACE_OTEL_ENABLED'))) {
408
411
  evaluationTags.source = 'otel'
409
412
  }
410
413
 
@@ -418,7 +421,9 @@ class LLMObs extends NoopLLMObs {
418
421
  timestamp_ms: timestampMs,
419
422
  tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`)
420
423
  }
421
- evalMetricAppendCh.publish(payload)
424
+ const currentStore = storage.getStore()
425
+ const routing = currentStore?.routingContext
426
+ evalMetricAppendCh.publish({ payload, routing })
422
427
  } finally {
423
428
  telemetry.recordSubmitEvaluation(options, err)
424
429
  }
@@ -440,6 +445,28 @@ class LLMObs extends NoopLLMObs {
440
445
  return storage.run(store, fn)
441
446
  }
442
447
 
448
+ routingContext (options, fn) {
449
+ if (!this.enabled) return fn()
450
+ if (!options?.ddApiKey) {
451
+ throw new Error('ddApiKey is required for routing context')
452
+ }
453
+ const currentStore = storage.getStore()
454
+ if (currentStore?.routingContext) {
455
+ logger.warn(
456
+ '[LLM Observability] Nested routing context detected. Inner context will override outer context. ' +
457
+ 'Spans created in the inner context will only be sent to the inner context.'
458
+ )
459
+ }
460
+ const store = {
461
+ ...currentStore,
462
+ routingContext: {
463
+ apiKey: options.ddApiKey,
464
+ site: options.ddSite
465
+ }
466
+ }
467
+ return storage.run(store, fn)
468
+ }
469
+
443
470
  flush () {
444
471
  if (!this.enabled) return
445
472
 
@@ -26,7 +26,10 @@ const {
26
26
  TAGS,
27
27
  PARENT_ID_KEY,
28
28
  SESSION_ID,
29
- NAME
29
+ NAME,
30
+ INPUT_PROMPT,
31
+ ROUTING_API_KEY,
32
+ ROUTING_SITE
30
33
  } = require('./constants/tags')
31
34
  const { UNSERIALIZABLE_VALUE_TEXT } = require('./constants/text')
32
35
  const telemetry = require('./telemetry')
@@ -78,7 +81,13 @@ class LLMObsSpanProcessor {
78
81
  telemetry.incrementLLMObsSpanFinishedCount(span)
79
82
  if (formattedEvent == null) return
80
83
 
81
- this.#writer.append(formattedEvent)
84
+ const mlObsTags = LLMObsTagger.tagMap.get(span)
85
+ const routing = {
86
+ apiKey: mlObsTags[ROUTING_API_KEY],
87
+ site: mlObsTags[ROUTING_SITE]
88
+ }
89
+
90
+ this.#writer.append(formattedEvent, routing)
82
91
  } catch (e) {
83
92
  // this should be a rare case
84
93
  // we protect against unserializable properties in the format function, and in
@@ -122,11 +131,6 @@ class LLMObsSpanProcessor {
122
131
  inputType = 'value'
123
132
  }
124
133
 
125
- // Handle prompt metadata for reusable prompts
126
- if (mlObsTags['_ml_obs.meta.input.prompt']) {
127
- input.prompt = mlObsTags['_ml_obs.meta.input.prompt']
128
- }
129
-
130
134
  if (spanKind === 'llm' && mlObsTags[OUTPUT_MESSAGES]) {
131
135
  llmObsSpan.output = mlObsTags[OUTPUT_MESSAGES]
132
136
  outputType = 'messages'
@@ -177,6 +181,12 @@ class LLMObsSpanProcessor {
177
181
  if (input) meta.input = input
178
182
  if (output) meta.output = output
179
183
 
184
+ const prompt = mlObsTags[INPUT_PROMPT]
185
+ if (prompt && spanKind === 'llm') {
186
+ // by this point, we should have logged a warning if the span kind was not llm
187
+ meta.input.prompt = prompt
188
+ }
189
+
180
190
  const llmObsSpanEvent = {
181
191
  trace_id: span.context().toTraceId(true),
182
192
  span_id: span.context().toSpanId(),
@@ -28,7 +28,15 @@ const {
28
28
  REASONING_OUTPUT_TOKENS_METRIC_KEY,
29
29
  INTEGRATION,
30
30
  DECORATOR,
31
- PROPAGATED_ML_APP_KEY
31
+ PROPAGATED_ML_APP_KEY,
32
+ DEFAULT_PROMPT_NAME,
33
+ INTERNAL_CONTEXT_VARIABLE_KEYS,
34
+ INTERNAL_QUERY_VARIABLE_KEYS,
35
+ INPUT_PROMPT,
36
+ ROUTING_API_KEY,
37
+ ROUTING_SITE,
38
+ PROMPT_TRACKING_INSTRUMENTATION_METHOD,
39
+ INSTRUMENTATION_METHOD_ANNOTATED
32
40
  } = require('./constants/tags')
33
41
  const { storage } = require('./storage')
34
42
 
@@ -110,6 +118,18 @@ class LLMObsTagger {
110
118
  // apply annotation context name
111
119
  const annotationContextName = annotationContext?.name
112
120
  if (annotationContextName) this._setTag(span, NAME, annotationContextName)
121
+
122
+ // apply annotation context prompt
123
+ const annotationContextPrompt = annotationContext?.prompt
124
+ if (annotationContextPrompt) this.tagPrompt(span, annotationContextPrompt)
125
+
126
+ const routing = storage.getStore()?.routingContext
127
+ if (routing) {
128
+ this._setTag(span, ROUTING_API_KEY, routing.apiKey)
129
+ if (routing.site) {
130
+ this._setTag(span, ROUTING_SITE, routing.site)
131
+ }
132
+ }
113
133
  }
114
134
 
115
135
  // TODO: similarly for the following `tag` methods,
@@ -194,6 +214,160 @@ class LLMObsTagger {
194
214
  }
195
215
  }
196
216
 
217
+ /**
218
+ * Tags a prompt on an LLMObs span.
219
+ * @param {import('../opentracing/span')} span
220
+ * @param {string | Record<string, unknown>} prompt
221
+ * @param {boolean?} strictValidation
222
+ * whether to validate the prompt against the strict schema, used for auto-instrumentation
223
+ */
224
+ tagPrompt (span, prompt, strictValidation = false) {
225
+ const spanKind = registry.get(span)?.[SPAN_KIND]
226
+ if (spanKind !== 'llm') {
227
+ log.warn('Dropping prompt on non-LLM span kind, annotating prompts is only supported for LLM span kinds.')
228
+ return
229
+ }
230
+
231
+ if (!prompt || typeof prompt !== 'object') {
232
+ this.#handleFailure('Prompt must be an object.', 'invalid_prompt')
233
+ return
234
+ }
235
+
236
+ const mlApp = registry.get(span)?.[ML_APP] // this should be defined at this point
237
+ const {
238
+ id,
239
+ version,
240
+ tags,
241
+ variables,
242
+ template,
243
+ contextVariables,
244
+ queryVariables,
245
+ } = prompt
246
+
247
+ if (strictValidation) {
248
+ if (id == null) {
249
+ this.#handleFailure('Prompt ID is required.', 'invalid_prompt')
250
+ return
251
+ }
252
+
253
+ if (template == null) {
254
+ this.#handleFailure('Prompt template is required.', 'invalid_prompt')
255
+ return
256
+ }
257
+ }
258
+
259
+ const finalPromptId = id ?? `${mlApp}_${DEFAULT_PROMPT_NAME}`
260
+ const finalCtxVariablesKeys = contextVariables ?? ['context']
261
+ const finalQueryVariablesKeys = queryVariables ?? ['question']
262
+
263
+ // validate prompt id
264
+ if (typeof finalPromptId !== 'string') {
265
+ this.#handleFailure('Prompt ID must be a string.', 'invalid_prompt')
266
+ return
267
+ }
268
+
269
+ // validate prompt context variables keys
270
+ if (Array.isArray(finalCtxVariablesKeys)) {
271
+ for (const key of finalCtxVariablesKeys) {
272
+ if (typeof key !== 'string') {
273
+ this.#handleFailure('Prompt context variables keys must be an array of strings.', 'invalid_prompt')
274
+ return
275
+ }
276
+ }
277
+ } else if (finalCtxVariablesKeys) {
278
+ this.#handleFailure('Prompt context variables keys must be an array.', 'invalid_prompt')
279
+ return
280
+ }
281
+
282
+ // validate prompt query variables keys
283
+ if (Array.isArray(finalQueryVariablesKeys)) {
284
+ for (const key of finalQueryVariablesKeys) {
285
+ if (typeof key !== 'string') {
286
+ this.#handleFailure('Prompt query variables keys must be an array of strings.', 'invalid_prompt')
287
+ return
288
+ }
289
+ }
290
+ } else if (finalQueryVariablesKeys) {
291
+ this.#handleFailure('Prompt query variables keys must be an array.', 'invalid_prompt')
292
+ return
293
+ }
294
+
295
+ // validate prompt version
296
+ if (version && typeof version !== 'string') {
297
+ this.#handleFailure('Prompt version must be a string.', 'invalid_prompt')
298
+ return
299
+ }
300
+
301
+ // validate prompt tags
302
+ if (tags && (typeof tags !== 'object' || tags instanceof Map)) {
303
+ this.#handleFailure('Prompt tags must be an non-Map object.', 'invalid_prompt')
304
+ return
305
+ } else if (tags) {
306
+ for (const [key, value] of Object.entries(tags)) {
307
+ if (typeof key !== 'string' || typeof value !== 'string') {
308
+ this.#handleFailure('Prompt tags must be an object of string key-value pairs.', 'invalid_prompt')
309
+ return
310
+ }
311
+ }
312
+ }
313
+
314
+ // validate prompt template is either string or list of messages
315
+ if (template && !(typeof template === 'string' || Array.isArray(template))) {
316
+ this.#handleFailure('Prompt template must be a string or an array of messages.', 'invalid_prompt')
317
+ return
318
+ }
319
+
320
+ if (Array.isArray(template)) {
321
+ for (const message of template) {
322
+ if (typeof message !== 'object' || !message.role || !message.content) {
323
+ this.#handleFailure(
324
+ 'Prompt chat template must be an array of objects with role and content properties.', 'invalid_prompt'
325
+ )
326
+ return
327
+ }
328
+ }
329
+ }
330
+
331
+ // validate variables are a string-string mapping
332
+ if (variables && (typeof variables !== 'object' || variables instanceof Map)) {
333
+ this.#handleFailure('Prompt variables must be an non-Map object.', 'invalid_prompt')
334
+ return
335
+ } else if (variables) {
336
+ for (const [key, value] of Object.entries(variables)) {
337
+ if (typeof key !== 'string' || typeof value !== 'string') {
338
+ this.#handleFailure('Prompt variables must be an object of string key-value pairs.', 'invalid_prompt')
339
+ return
340
+ }
341
+ }
342
+ }
343
+
344
+ let finalTemplate, finalChatTemplate
345
+ if (typeof template === 'string') {
346
+ finalTemplate = template
347
+ } else if (Array.isArray(template)) {
348
+ finalChatTemplate = template.map(message => ({ role: message.role, content: message.content }))
349
+ }
350
+
351
+ const validatedPrompt = {}
352
+ if (finalPromptId) validatedPrompt.id = finalPromptId
353
+ if (version) validatedPrompt.version = version
354
+ if (variables) validatedPrompt.variables = variables
355
+ if (finalTemplate) validatedPrompt.template = finalTemplate
356
+ if (finalChatTemplate?.length) validatedPrompt.chat_template = finalChatTemplate
357
+ if (tags) validatedPrompt.tags = tags
358
+ if (finalCtxVariablesKeys) validatedPrompt[INTERNAL_CONTEXT_VARIABLE_KEYS] = finalCtxVariablesKeys
359
+ if (finalQueryVariablesKeys) validatedPrompt[INTERNAL_QUERY_VARIABLE_KEYS] = finalQueryVariablesKeys
360
+
361
+ const currentPrompt = registry.get(span)?.[INPUT_PROMPT]
362
+ if (currentPrompt) {
363
+ Object.assign(currentPrompt, validatedPrompt)
364
+ } else {
365
+ this._setTag(span, INPUT_PROMPT, validatedPrompt)
366
+ }
367
+
368
+ this.tagSpanTags(span, { [PROMPT_TRACKING_INSTRUMENTATION_METHOD]: INSTRUMENTATION_METHOD_ANNOTATED })
369
+ }
370
+
197
371
  changeKind (span, newKind) {
198
372
  this._setTag(span, SPAN_KIND, newKind)
199
373
  }