dd-trace 6.12.0 → 6.14.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 (251) hide show
  1. package/LICENSE-3rdparty.csv +3 -1
  2. package/ci/init.js +8 -3
  3. package/ci/test-optimization-validation/ci-package-scripts.js +2 -2
  4. package/ci/test-optimization-validation/ci-remediation.js +6 -6
  5. package/ci/test-optimization-validation/command-blocker.js +7 -5
  6. package/ci/test-optimization-validation/framework-adapters/cucumber.js +1 -1
  7. package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
  8. package/ci/test-optimization-validation/framework-adapters/vitest.js +3 -3
  9. package/ci/test-optimization-validation/package-check.js +1 -1
  10. package/ci/test-optimization-validation/report-writer.js +7 -5
  11. package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
  12. package/ci/vitest-no-worker-init-runner-setup.mjs +8 -0
  13. package/ci/vitest-no-worker-init-setup.mjs +13 -13
  14. package/ci/vitest-no-worker-init-v5-setup.mjs +4 -0
  15. package/index.d.ts +127 -26
  16. package/package.json +22 -19
  17. package/packages/datadog-esbuild/index.js +3 -1
  18. package/packages/datadog-instrumentations/src/bunyan.js +4 -2
  19. package/packages/datadog-instrumentations/src/cypress-config.js +19 -17
  20. package/packages/datadog-instrumentations/src/fs.js +24 -20
  21. package/packages/datadog-instrumentations/src/google-cloud-vertexai.js +2 -2
  22. package/packages/datadog-instrumentations/src/graphql.js +16 -16
  23. package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
  24. package/packages/datadog-instrumentations/src/helpers/graphql-jit-runtime.js +504 -0
  25. package/packages/datadog-instrumentations/src/helpers/hooks.js +2 -0
  26. package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +266 -0
  27. package/packages/datadog-instrumentations/src/helpers/rewriter/compiler.js +1 -2
  28. package/packages/datadog-instrumentations/src/helpers/rewriter/index.js +95 -30
  29. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/ai.js +0 -25
  30. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql-jit.js +83 -0
  31. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/graphql.js +85 -1
  32. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/index.js +1 -0
  33. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/mercurius.js +30 -12
  34. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/playwright.js +13 -0
  35. package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +279 -0
  36. package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +12 -1
  37. package/packages/datadog-instrumentations/src/helpers/rewriter/transforms.js +562 -27
  38. package/packages/datadog-instrumentations/src/jest.js +165 -38
  39. package/packages/datadog-instrumentations/src/knex.js +76 -0
  40. package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
  41. package/packages/datadog-instrumentations/src/mariadb.js +13 -0
  42. package/packages/datadog-instrumentations/src/mocha/common.js +59 -2
  43. package/packages/datadog-instrumentations/src/mocha/main.js +25 -15
  44. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -1
  45. package/packages/datadog-instrumentations/src/mocha/webdriverio-protocol.js +98 -0
  46. package/packages/datadog-instrumentations/src/mocha/worker.js +12 -8
  47. package/packages/datadog-instrumentations/src/net.js +14 -17
  48. package/packages/datadog-instrumentations/src/next.js +212 -62
  49. package/packages/datadog-instrumentations/src/openai.js +42 -40
  50. package/packages/datadog-instrumentations/src/pg.js +15 -0
  51. package/packages/datadog-instrumentations/src/pino.js +15 -4
  52. package/packages/datadog-instrumentations/src/playwright.js +137 -27
  53. package/packages/datadog-instrumentations/src/rum-browser-scripts.js +40 -0
  54. package/packages/datadog-instrumentations/src/sequelize.js +94 -2
  55. package/packages/datadog-instrumentations/src/undici.js +53 -4
  56. package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +44 -7
  57. package/packages/datadog-instrumentations/src/vitest-main.js +92 -9
  58. package/packages/datadog-instrumentations/src/vitest-worker.js +82 -14
  59. package/packages/datadog-instrumentations/src/webdriverio.js +986 -1
  60. package/packages/datadog-instrumentations/src/winston.js +6 -3
  61. package/packages/datadog-plugin-amqp10/src/consumer.js +3 -1
  62. package/packages/datadog-plugin-amqp10/src/producer.js +3 -1
  63. package/packages/datadog-plugin-amqp10/src/util.js +2 -1
  64. package/packages/datadog-plugin-avsc/src/schema_iterator.js +6 -1
  65. package/packages/datadog-plugin-aws-sdk/src/services/bedrockruntime/utils.js +16 -5
  66. package/packages/datadog-plugin-cassandra-driver/src/index.js +8 -3
  67. package/packages/datadog-plugin-cucumber/src/index.js +0 -2
  68. package/packages/datadog-plugin-cypress/src/cypress-plugin.js +262 -48
  69. package/packages/datadog-plugin-cypress/src/support.js +1 -0
  70. package/packages/datadog-plugin-google-cloud-pubsub/src/client.js +4 -1
  71. package/packages/datadog-plugin-graphql/src/execute.js +998 -196
  72. package/packages/datadog-plugin-graphql/src/index.js +27 -7
  73. package/packages/datadog-plugin-graphql/src/jit.js +136 -0
  74. package/packages/datadog-plugin-graphql/src/request.js +18 -27
  75. package/packages/datadog-plugin-graphql/src/resolve-error.js +108 -0
  76. package/packages/datadog-plugin-graphql/src/tools/signature.js +1 -2
  77. package/packages/datadog-plugin-graphql/src/tools/transforms.js +45 -3
  78. package/packages/datadog-plugin-graphql/src/utils.js +208 -120
  79. package/packages/datadog-plugin-graphql/src/validate.js +10 -23
  80. package/packages/datadog-plugin-http/src/client.js +2 -15
  81. package/packages/datadog-plugin-http2/src/client.js +2 -15
  82. package/packages/datadog-plugin-jest/src/index.js +0 -6
  83. package/packages/datadog-plugin-langchain/src/tracing.js +7 -0
  84. package/packages/datadog-plugin-mocha/src/index.js +403 -75
  85. package/packages/datadog-plugin-mongodb-core/src/query.js +6 -0
  86. package/packages/datadog-plugin-mysql/src/index.js +41 -15
  87. package/packages/datadog-plugin-net/src/tcp.js +3 -1
  88. package/packages/datadog-plugin-next/src/index.js +63 -29
  89. package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
  90. package/packages/datadog-plugin-playwright/src/index.js +176 -28
  91. package/packages/datadog-plugin-undici/src/index.js +29 -56
  92. package/packages/datadog-plugin-vitest/src/index.js +6 -12
  93. package/packages/dd-trace/src/aiguard/evaluation.js +96 -21
  94. package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
  95. package/packages/dd-trace/src/aiguard/messages/anthropic.js +29 -10
  96. package/packages/dd-trace/src/aiguard/messages/openai.js +8 -1
  97. package/packages/dd-trace/src/aiguard/messages/vercel-ai.js +20 -6
  98. package/packages/dd-trace/src/aiguard/noop.js +9 -1
  99. package/packages/dd-trace/src/aiguard/redaction.js +183 -0
  100. package/packages/dd-trace/src/aiguard/sdk.js +7 -2
  101. package/packages/dd-trace/src/aiguard/tags.js +2 -0
  102. package/packages/dd-trace/src/appsec/api_security/index.js +23 -6
  103. package/packages/dd-trace/src/appsec/api_security/sampler.js +99 -40
  104. package/packages/dd-trace/src/appsec/graphql.js +6 -2
  105. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +3 -3
  106. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +2 -1
  107. package/packages/dd-trace/src/appsec/iast/telemetry/span-tags.js +8 -2
  108. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/sql-sensitive-analyzer.js +11 -36
  109. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/evidence-redaction/sensitive-analyzers/url-sensitive-analyzer.js +1 -1
  110. package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
  111. package/packages/dd-trace/src/appsec/lambda.js +44 -26
  112. package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
  113. package/packages/dd-trace/src/ci-visibility/encode/json-encoder.js +32 -2
  114. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +8 -8
  115. package/packages/dd-trace/src/ci-visibility/exporters/agentless/coverage-writer.js +6 -4
  116. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +20 -4
  117. package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +7 -150
  118. package/packages/dd-trace/src/ci-visibility/exporters/agentless/writer.js +7 -6
  119. package/packages/dd-trace/src/ci-visibility/exporters/agents.js +54 -0
  120. package/packages/dd-trace/src/ci-visibility/exporters/ci-validation/writer.js +1 -2
  121. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +397 -234
  122. package/packages/dd-trace/src/ci-visibility/exporters/limits.js +9 -0
  123. package/packages/dd-trace/src/ci-visibility/exporters/request.js +323 -31
  124. package/packages/dd-trace/src/ci-visibility/exporters/settings-cache-key.js +42 -0
  125. package/packages/dd-trace/src/ci-visibility/final-flush.js +21 -0
  126. package/packages/dd-trace/src/ci-visibility/requests/fs-cache.js +57 -18
  127. package/packages/dd-trace/src/ci-visibility/requests/get-library-configuration.js +0 -3
  128. package/packages/dd-trace/src/ci-visibility/requests/upload-test-screenshot.js +164 -71
  129. package/packages/dd-trace/src/ci-visibility/requests/video-request.js +176 -0
  130. package/packages/dd-trace/src/ci-visibility/telemetry.js +10 -7
  131. package/packages/dd-trace/src/ci-visibility/test-video.js +61 -0
  132. package/packages/dd-trace/src/config/generated-config-types.d.ts +10 -0
  133. package/packages/dd-trace/src/config/index.js +75 -14
  134. package/packages/dd-trace/src/config/parsers.js +11 -1
  135. package/packages/dd-trace/src/config/stable.js +3 -3
  136. package/packages/dd-trace/src/config/supported-configurations.json +47 -2
  137. package/packages/dd-trace/src/crashtracking/crashtracker.js +82 -18
  138. package/packages/dd-trace/src/debugger/config.js +12 -1
  139. package/packages/dd-trace/src/debugger/constants.js +1 -0
  140. package/packages/dd-trace/src/debugger/devtools_client/breakpoints.js +2 -1
  141. package/packages/dd-trace/src/debugger/devtools_client/request-options.js +33 -0
  142. package/packages/dd-trace/src/debugger/devtools_client/send.js +7 -39
  143. package/packages/dd-trace/src/debugger/devtools_client/status.js +11 -6
  144. package/packages/dd-trace/src/debugger/devtools_client/tags.js +41 -0
  145. package/packages/dd-trace/src/debugger/index.js +23 -3
  146. package/packages/dd-trace/src/dogstatsd.js +532 -71
  147. package/packages/dd-trace/src/encode/0.4-cross-payload.js +170 -0
  148. package/packages/dd-trace/src/encode/0.4.js +5 -5
  149. package/packages/dd-trace/src/encode/agentless-ci-visibility.js +9 -8
  150. package/packages/dd-trace/src/evp_proxy/direct.js +27 -23
  151. package/packages/dd-trace/src/exporters/agent/index.js +24 -3
  152. package/packages/dd-trace/src/exporters/agent/writer.js +28 -11
  153. package/packages/dd-trace/src/exporters/agentless/index.js +7 -9
  154. package/packages/dd-trace/src/exporters/agentless/writer.js +100 -118
  155. package/packages/dd-trace/src/exporters/common/agents.js +28 -6
  156. package/packages/dd-trace/src/exporters/common/buffering-exporter.js +2 -2
  157. package/packages/dd-trace/src/exporters/common/final-flush-request-tracker.js +191 -0
  158. package/packages/dd-trace/src/exporters/common/request.js +31 -11
  159. package/packages/dd-trace/src/exporters/common/url.js +40 -1
  160. package/packages/dd-trace/src/exporters/common/writer.js +31 -7
  161. package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
  162. package/packages/dd-trace/src/external-logger/src/index.js +4 -3
  163. package/packages/dd-trace/src/flush.js +105 -0
  164. package/packages/dd-trace/src/id.js +14 -0
  165. package/packages/dd-trace/src/index.js +5 -2
  166. package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
  167. package/packages/dd-trace/src/llmobs/experiments/dataset.js +52 -2
  168. package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
  169. package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
  170. package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
  171. package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
  172. package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
  173. package/packages/dd-trace/src/llmobs/index.js +111 -48
  174. package/packages/dd-trace/src/llmobs/noop.js +2 -1
  175. package/packages/dd-trace/src/llmobs/plugins/ai/ddTelemetry.js +22 -71
  176. package/packages/dd-trace/src/llmobs/plugins/ai/vercelTelemetry.js +23 -13
  177. package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
  178. package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
  179. package/packages/dd-trace/src/llmobs/plugins/langchain/index.js +7 -0
  180. package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
  181. package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
  182. package/packages/dd-trace/src/llmobs/util.js +0 -33
  183. package/packages/dd-trace/src/llmobs/writers/base.js +106 -53
  184. package/packages/dd-trace/src/log/index.js +7 -1
  185. package/packages/dd-trace/src/log-submission/log-submission-plugin.js +249 -0
  186. package/packages/dd-trace/src/noop/span.js +5 -0
  187. package/packages/dd-trace/src/openfeature/writers/base.js +14 -12
  188. package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
  189. package/packages/dd-trace/src/openfeature/writers/util.js +1 -1
  190. package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
  191. package/packages/dd-trace/src/opentelemetry/logs/index.js +9 -25
  192. package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
  193. package/packages/dd-trace/src/opentelemetry/logs/otlp_http_log_exporter.js +14 -4
  194. package/packages/dd-trace/src/opentelemetry/metrics/index.js +18 -23
  195. package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
  196. package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +18 -6
  197. package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +14 -2
  198. package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
  199. package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
  200. package/packages/dd-trace/src/opentelemetry/otlp/otlp_transformer_base.js +19 -4
  201. package/packages/dd-trace/src/opentelemetry/resource-attributes.js +55 -0
  202. package/packages/dd-trace/src/opentelemetry/trace/index.js +6 -1
  203. package/packages/dd-trace/src/opentelemetry/trace/otlp_http_trace_exporter.js +10 -0
  204. package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
  205. package/packages/dd-trace/src/opentracing/propagation/text_map.js +494 -351
  206. package/packages/dd-trace/src/opentracing/span.js +28 -0
  207. package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
  208. package/packages/dd-trace/src/payload-tagging/index.js +2 -1
  209. package/packages/dd-trace/src/plugin_manager.js +2 -0
  210. package/packages/dd-trace/src/plugins/ci_plugin.js +1 -6
  211. package/packages/dd-trace/src/plugins/tracing.js +12 -10
  212. package/packages/dd-trace/src/plugins/util/ci.js +3 -1
  213. package/packages/dd-trace/src/plugins/util/inferred_proxy.js +16 -15
  214. package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
  215. package/packages/dd-trace/src/plugins/util/test.js +157 -41
  216. package/packages/dd-trace/src/plugins/util/url.js +2 -2
  217. package/packages/dd-trace/src/plugins/util/web.js +6 -74
  218. package/packages/dd-trace/src/profiling/config.js +1 -0
  219. package/packages/dd-trace/src/profiling/oom.js +11 -5
  220. package/packages/dd-trace/src/profiling/profiler.js +30 -13
  221. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  222. package/packages/dd-trace/src/profiling/profilers/space.js +38 -11
  223. package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
  224. package/packages/dd-trace/src/profiling/webspan-utils.js +7 -4
  225. package/packages/dd-trace/src/proxy.js +108 -10
  226. package/packages/dd-trace/src/remote_config/index.js +319 -101
  227. package/packages/dd-trace/src/remote_config/scheduler.js +37 -12
  228. package/packages/dd-trace/src/runtime_metrics/client.js +37 -8
  229. package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
  230. package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +10 -1
  231. package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +22 -7
  232. package/packages/dd-trace/src/sampling_rule.js +36 -9
  233. package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +51 -0
  234. package/packages/dd-trace/src/serverless/vercel.js +155 -0
  235. package/packages/dd-trace/src/serverless.js +45 -21
  236. package/packages/dd-trace/src/service-naming/schemas/util.js +1 -1
  237. package/packages/dd-trace/src/span_stats.js +64 -31
  238. package/packages/dd-trace/src/startup-log.js +3 -2
  239. package/packages/dd-trace/src/telemetry/agentless-url.js +28 -0
  240. package/packages/dd-trace/src/telemetry/send-data.js +18 -16
  241. package/packages/dd-trace/src/telemetry/session-propagation.js +3 -2
  242. package/packages/dd-trace/src/tracer.js +26 -5
  243. package/packages/dd-trace/src/tracer_metadata.js +11 -3
  244. package/packages/dd-trace/src/web-tags-cache.js +67 -13
  245. package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
  246. package/vendor/dist/@datadog/sketches-js/index.js +1 -1
  247. package/vendor/dist/protobufjs/index.js +1 -1
  248. package/vendor/dist/protobufjs/minimal/index.js +1 -1
  249. package/packages/datadog-instrumentations/src/playwright-browser-scripts.js +0 -27
  250. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +0 -53
  251. package/packages/dd-trace/src/encode/agentless-json.js +0 -209
@@ -4,6 +4,7 @@ const { URL, format } = require('node:url')
4
4
  const path = require('node:path')
5
5
  const request = require('../../exporters/common/request')
6
6
  const { getEnvironmentVariable } = require('../../config/helper')
7
+ const { createServerlessDeliveryTracker } = require('../../serverless')
7
8
 
8
9
  const logger = require('../../log')
9
10
 
@@ -16,13 +17,20 @@ const {
16
17
  } = require('../constants/writers')
17
18
  const { parseResponseAndLog } = require('./util')
18
19
 
20
+ const MAX_BUFFER_EVENTS = 1000
21
+
19
22
  class LLMObsBuffer {
20
- constructor ({ events, size, routing = {}, isDefault = false, limit = 1000 }) {
23
+ /**
24
+ * @param {{
25
+ * events: Record<string, unknown>[],
26
+ * size: number,
27
+ * routing?: { apiKey?: string, site?: string }
28
+ * }} options
29
+ */
30
+ constructor ({ events, size, routing = {} }) {
21
31
  this.events = events
22
32
  this.size = size
23
33
  this.routing = routing
24
- this.isDefault = isDefault
25
- this.limit = limit
26
34
  }
27
35
 
28
36
  clear () {
@@ -33,6 +41,9 @@ class LLMObsBuffer {
33
41
 
34
42
  class BaseLLMObsWriter {
35
43
  #destroyer
44
+ /** @type {Function[]} */
45
+ #pendingFlushes = []
46
+ #serverlessDeliveryTracker = createServerlessDeliveryTracker()
36
47
  /** @type {Map<string, LLMObsBuffer>} */
37
48
  #multiTenantBuffers = new Map()
38
49
 
@@ -42,7 +53,7 @@ class BaseLLMObsWriter {
42
53
  this._eventType = eventType
43
54
 
44
55
  /** @type {LLMObsBuffer} */
45
- this._buffer = new LLMObsBuffer({ events: [], size: 0, isDefault: true })
56
+ this._buffer = new LLMObsBuffer({ events: [], size: 0 })
46
57
 
47
58
  /** @type {import('../../config/config-base')} */
48
59
  this._config = config
@@ -98,8 +109,8 @@ class BaseLLMObsWriter {
98
109
  append (event, routing, byteLength) {
99
110
  const buffer = this._getBuffer(routing)
100
111
 
101
- if (buffer.events.length >= buffer.limit) {
102
- logger.warn(`${this.constructor.name} event buffer full (limit is ${buffer.limit}), dropping event`)
112
+ if (buffer.events.length >= MAX_BUFFER_EVENTS) {
113
+ logger.warn(`${this.constructor.name} event buffer full (limit is ${MAX_BUFFER_EVENTS}), dropping event`)
103
114
  telemetry.recordDroppedPayload(1, this._eventType, 'buffer_full')
104
115
  return false
105
116
  }
@@ -111,80 +122,118 @@ class BaseLLMObsWriter {
111
122
  return true
112
123
  }
113
124
 
114
- flush () {
125
+ /**
126
+ * Drains buffered events and joins requests active at this flush boundary.
127
+ * @param {Function} [done]
128
+ */
129
+ flush (done) {
115
130
  if (this._agentless == null) {
131
+ if (done) this.#pendingFlushes.push(done)
116
132
  return
117
133
  }
118
134
 
119
- // Flush default buffer
120
- if (this._buffer.events.length > 0) {
121
- const events = this._buffer.events
122
- this._buffer.clear()
123
-
124
- const payload = this._encode(this.makePayload(events))
135
+ const requests = this.#drainBuffers()
136
+ if (!this.#serverlessDeliveryTracker) {
137
+ // Only invocation-retaining platforms need completion-aware delivery.
138
+ for (const request of requests) this.#sendSafely(request)
139
+ done?.()
140
+ return
141
+ }
125
142
 
126
- log.debug('Encoded LLMObs payload: %s', payload)
143
+ for (const request of requests) this.#sendSafely(request)
144
+ this.#serverlessDeliveryTracker.waitForIdle(done)
145
+ }
127
146
 
128
- const options = this._getOptions()
147
+ #sendSafely (requestToSend) {
148
+ try {
149
+ if (this.#serverlessDeliveryTracker) {
150
+ this.#serverlessDeliveryTracker.track(done => this.#send(requestToSend, done))
151
+ } else {
152
+ this.#send(requestToSend)
153
+ }
154
+ } catch (error) {
155
+ logger.error('Failed to send LLMObs %s events: %s', this._eventType, error.message)
156
+ }
157
+ }
129
158
 
130
- request(payload, options, (err, resp, code) => {
131
- parseResponseAndLog(err, code, events.length, this.url, this._eventType)
132
- })
159
+ #drainBuffers () {
160
+ const requests = []
161
+ if (this._buffer.events.length > 0) {
162
+ const events = this._buffer.events
163
+ this._buffer.clear()
164
+ requests.push({ events, options: this._getOptions(), url: this.url })
133
165
  }
134
166
 
135
- // Flush multi-tenant buffers
136
167
  for (const [apiKey, buffer] of this.#multiTenantBuffers) {
137
- if (buffer.events.length === 0) continue
138
-
139
- const events = buffer.events
140
- buffer.clear()
141
-
142
- const payload = this._encode(this.makePayload(events))
168
+ if (buffer.events.length === 0) {
169
+ this.#multiTenantBuffers.delete(apiKey)
170
+ continue
171
+ }
143
172
  const site = buffer.routing.site || this._config.site
144
- const options = {
145
- headers: {
146
- 'Content-Type': 'application/json',
147
- 'DD-API-KEY': apiKey,
148
- },
149
- method: 'POST',
150
- timeout: this._timeout,
151
- url: new URL(format({
152
- protocol: 'https:',
153
- hostname: `${this._intake}.${site}`,
154
- })),
155
- path: this._baseEndpoint,
173
+ const maskedApiKey = `****${apiKey.slice(-4)}`
174
+ let options
175
+ let url
176
+ try {
177
+ options = {
178
+ headers: {
179
+ 'Content-Type': 'application/json',
180
+ 'DD-API-KEY': apiKey,
181
+ },
182
+ method: 'POST',
183
+ timeout: this._timeout,
184
+ url: new URL(format({
185
+ protocol: 'https:',
186
+ hostname: `${this._intake}.${site}`,
187
+ })),
188
+ path: this._baseEndpoint,
189
+ }
190
+ url = this.#buildUrl(options.url.href, options.path)
191
+ } catch (error) {
192
+ buffer.clear()
193
+ this.#multiTenantBuffers.delete(apiKey)
194
+ logger.error(
195
+ 'Failed to route LLMObs %s events for API key %s: %s',
196
+ this._eventType,
197
+ maskedApiKey,
198
+ error.message
199
+ )
200
+ continue
156
201
  }
157
- const url = this.#buildUrl(options.url.href, options.path)
158
- const maskedApiKey = apiKey ? `****${apiKey.slice(-4)}` : ''
159
202
 
203
+ const events = buffer.events
204
+ buffer.clear()
205
+ this.#multiTenantBuffers.delete(apiKey)
160
206
  log.debug('Encoding and flushing multi-tenant buffer for %s', maskedApiKey)
161
- log.debug('Encoded LLMObs payload: %s', payload)
162
-
163
- request(payload, options, (err, resp, code) => {
164
- parseResponseAndLog(err, code, events.length, url, this._eventType)
165
- })
207
+ requests.push({ events, options, url })
166
208
  }
167
209
 
168
- this.#cleanupEmptyBuffers()
210
+ return requests
169
211
  }
170
212
 
171
- #cleanupEmptyBuffers () {
172
- for (const [key, buffer] of this.#multiTenantBuffers) {
173
- if (buffer.events.length === 0) {
174
- this.#multiTenantBuffers.delete(key)
175
- }
176
- }
213
+ #send ({ events, options, url }, done) {
214
+ const payload = this._encode(this.makePayload(events))
215
+ log.debug('Encoded LLMObs payload: %s', payload)
216
+ request(payload, options, (err, resp, code) => {
217
+ parseResponseAndLog(err, code, events.length, url, this._eventType)
218
+ done?.()
219
+ })
177
220
  }
178
221
 
179
222
  makePayload (events) {}
180
223
 
181
- destroy () {
224
+ /**
225
+ * Stops periodic flushing and drains buffered events.
226
+ * @param {Function} [done] Called after queued and active deliveries complete.
227
+ */
228
+ destroy (done) {
182
229
  if (this.#destroyer) {
183
230
  logger.debug(`Stopping ${this.constructor.name}`)
184
231
  clearInterval(this._periodic)
185
232
  globalThis[Symbol.for('dd-trace')].beforeExitHandlers.delete(this.#destroyer)
186
- this.flush()
233
+ this.flush(done)
187
234
  this.#destroyer = undefined
235
+ } else {
236
+ done?.()
188
237
  }
189
238
  }
190
239
 
@@ -196,6 +245,10 @@ class BaseLLMObsWriter {
196
245
  this._endpoint = endpoint
197
246
 
198
247
  logger.debug(`Configuring ${this.constructor.name} to ${this.url}`)
248
+
249
+ const pendingFlushes = this.#pendingFlushes
250
+ this.#pendingFlushes = []
251
+ for (const done of pendingFlushes) this.flush(done)
199
252
  }
200
253
 
201
254
  _getUrlAndPath () {
@@ -29,7 +29,13 @@ const log = {
29
29
  const stack = logRecord.stack.split('\n')
30
30
  const fn = stack[1].replace(/^\s+at ([^\s]+) .+/, '$1')
31
31
  const options = { depth: 2, breakLength: Infinity, compact: true, maxArrayLength: Infinity }
32
- const params = args.map(a => inspect(a, options)).join(', ')
32
+ let params = ''
33
+ let isFirstParameter = true
34
+ for (const argument of args) {
35
+ if (!isFirstParameter) params += ', '
36
+ params += inspect(argument, options)
37
+ isFirstParameter = false
38
+ }
33
39
 
34
40
  stack[0] = `Trace: ${fn}(${params})`
35
41
 
@@ -0,0 +1,249 @@
1
+ 'use strict'
2
+
3
+ const { Writable } = require('node:stream')
4
+
5
+ const FinalFlushRequestTracker = require('../exporters/common/final-flush-request-tracker')
6
+ const request = require('../exporters/common/request')
7
+ const log = require('../log')
8
+ const Plugin = require('../plugins/plugin')
9
+
10
+ const MAX_BATCH_BYTES = 5 * 1024 * 1024
11
+ const MAX_BATCH_LOGS = 1000
12
+ const BATCH_FLUSH_INTERVAL = 1000
13
+ const FINAL_FLUSH_TIMEOUT = 60_000
14
+
15
+ /**
16
+ * @returns {Error & { code: string }}
17
+ */
18
+ function createFinalFlushTimeoutError () {
19
+ return Object.assign(
20
+ new Error('Timed out waiting for automatic log submission to flush'),
21
+ { code: 'ERR_DD_LOG_SUBMISSION_FLUSH_TIMEOUT' }
22
+ )
23
+ }
24
+
25
+ /**
26
+ * @param {import('../config/config-base')} config
27
+ * @returns {URL | void}
28
+ */
29
+ function getLogSubmissionUrl (config) {
30
+ if (config.DD_AGENTLESS_LOG_SUBMISSION_URL) {
31
+ try {
32
+ const url = new URL(config.DD_AGENTLESS_LOG_SUBMISSION_URL)
33
+ if (url.protocol === 'http:' || url.protocol === 'https:') return url
34
+
35
+ log.error('Unsupported automatic log submission URL protocol: %s', url.protocol)
36
+ } catch {
37
+ log.error('Could not parse DD_AGENTLESS_LOG_SUBMISSION_URL')
38
+ }
39
+ return
40
+ }
41
+
42
+ const hostname = `http-intake.logs.${config.site}`.toLowerCase()
43
+ try {
44
+ const url = new URL(`https://${hostname}`)
45
+ if (url.hostname === hostname) return url
46
+ } catch {}
47
+
48
+ log.error('Could not parse automatic log submission site: %s', config.site)
49
+ }
50
+
51
+ /**
52
+ * @param {unknown} config
53
+ * @returns {import('../config/config-base')}
54
+ */
55
+ function asTracerConfig (config) {
56
+ return /** @type {import('../config/config-base')} */ (config)
57
+ }
58
+
59
+ /**
60
+ * @param {import('../config/config-base')} config
61
+ * @param {string} source
62
+ * @returns {string}
63
+ */
64
+ function getLogSubmissionPath (config, source) {
65
+ return `/api/v2/logs?${new URLSearchParams({ ddsource: source, service: config.service })}`
66
+ }
67
+
68
+ class LogSubmissionPlugin extends Plugin {
69
+ static id = 'log-submission'
70
+
71
+ /** @type {string[]} */
72
+ #batch = []
73
+ #batchBytes = 2
74
+ #batchSource
75
+ /** @type {import('../config/config-base') | undefined} */
76
+ #config
77
+ #logSubmissionUrl
78
+ #requestTracker
79
+ #timer
80
+ #beforeExitHandler = () => this.#flushLogs()
81
+ #createWinstonJsonFormat
82
+ #winstonStreamClass
83
+ // Winston formats records inside its transports, not at logger.write time, so (unlike Bunyan/Pino)
84
+ // the instrumentation can't publish a post-format line. A Stream transport pipes format.json()
85
+ // output through this Writable into the shared sender, reusing Winston's own cycle-safe serializer.
86
+ #winstonOutput = new Writable({
87
+ decodeStrings: false,
88
+ write: (message, encoding, callback) => {
89
+ this.#enqueueLog({ source: 'winston', message })
90
+ callback()
91
+ },
92
+ })
93
+
94
+ /**
95
+ * @param {object} tracer
96
+ * @param {unknown} tracerConfig
97
+ */
98
+ constructor (tracer, tracerConfig) {
99
+ super(tracer, asTracerConfig(tracerConfig))
100
+ this.#requestTracker = new FinalFlushRequestTracker(
101
+ (done) => {
102
+ this.#flushLogs()
103
+ done?.()
104
+ },
105
+ createFinalFlushTimeoutError
106
+ )
107
+
108
+ // The main-module hook (configure) and the logger.js hook (add-transport) can fire in either
109
+ // order depending on how Winston is required, so buffer loggers that arrive before configure.
110
+ const pendingWinstonLoggers = new Set()
111
+ const addWinstonTransport = (logger) => {
112
+ if (!this.#winstonStreamClass || !this.#createWinstonJsonFormat) {
113
+ pendingWinstonLoggers.add(logger)
114
+ return
115
+ }
116
+
117
+ logger.add(new this.#winstonStreamClass({
118
+ format: this.#createWinstonJsonFormat(),
119
+ stream: this.#winstonOutput,
120
+ }))
121
+ }
122
+
123
+ this.addSub('ci:log-submission:winston:configure', ({ StreamTransport, createJsonFormat }) => {
124
+ this.#winstonStreamClass = StreamTransport
125
+ this.#createWinstonJsonFormat = createJsonFormat
126
+
127
+ for (const logger of pendingWinstonLoggers) {
128
+ addWinstonTransport(logger)
129
+ }
130
+ pendingWinstonLoggers.clear()
131
+ })
132
+
133
+ this.addSub('ci:log-submission:winston:add-transport', addWinstonTransport)
134
+
135
+ this.addSub('ci:log-submission:log', (payload) => {
136
+ this.#enqueueLog(payload)
137
+ })
138
+ this.addSub('ci:log-submission:flush', ({ onDone } = {}) => {
139
+ if (!onDone) {
140
+ this.#flushLogs()
141
+ return
142
+ }
143
+
144
+ this.#requestTracker.flush(onDone, {
145
+ deadline: Date.now() + FINAL_FLUSH_TIMEOUT,
146
+ })
147
+ })
148
+ }
149
+
150
+ /**
151
+ * @param {boolean | (Record<string, unknown> & { enabled: boolean })} config
152
+ * @returns {void}
153
+ */
154
+ configure (config) {
155
+ if (this._enabled) this.#flushLogs()
156
+
157
+ const isEnabled = typeof config === 'boolean' ? config : config.enabled
158
+ this.#config = isEnabled && typeof config !== 'boolean' ? asTracerConfig(config) : undefined
159
+ this.#logSubmissionUrl = this.#config
160
+ ? getLogSubmissionUrl(this.#config)
161
+ : undefined
162
+ super.configure(config)
163
+
164
+ const beforeExitHandlers = globalThis[Symbol.for('dd-trace')].beforeExitHandlers
165
+ if (this._enabled) {
166
+ beforeExitHandlers.add(this.#beforeExitHandler)
167
+ } else {
168
+ beforeExitHandlers.delete(this.#beforeExitHandler)
169
+ }
170
+ }
171
+
172
+ /**
173
+ * @param {{ source: string, message: string | Record<string, unknown> }} payload
174
+ * @returns {void}
175
+ */
176
+ #enqueueLog ({ source, message }) {
177
+ if (!this.#logSubmissionUrl) return
178
+
179
+ let serializedMessage
180
+ try {
181
+ serializedMessage = typeof message === 'string' ? message : JSON.stringify(message)
182
+ } catch (error) {
183
+ log.error('Could not serialize %s log for automatic submission', source, error)
184
+ return
185
+ }
186
+ if (serializedMessage === undefined) return
187
+
188
+ const messageBytes = Buffer.byteLength(serializedMessage)
189
+ if (messageBytes + 2 > MAX_BATCH_BYTES) {
190
+ log.error('Could not submit %s log because it exceeds the %d byte payload limit', source, MAX_BATCH_BYTES)
191
+ return
192
+ }
193
+
194
+ if (this.#batch.length > 0 &&
195
+ (this.#batchSource !== source || this.#batchBytes + messageBytes + 1 > MAX_BATCH_BYTES)) {
196
+ this.#flushLogs()
197
+ if (!this.#logSubmissionUrl) return
198
+ }
199
+
200
+ this.#batchSource = source
201
+ if (this.#batch.length > 0) this.#batchBytes++
202
+ this.#batch.push(serializedMessage)
203
+ this.#batchBytes += messageBytes
204
+
205
+ if (this.#batch.length === MAX_BATCH_LOGS || this.#batchBytes === MAX_BATCH_BYTES) {
206
+ this.#flushLogs()
207
+ } else if (this.#timer === undefined) {
208
+ this.#timer = setTimeout(() => this.#flushLogs(), BATCH_FLUSH_INTERVAL)
209
+ this.#timer.unref?.()
210
+ }
211
+ }
212
+
213
+ /**
214
+ * @returns {void}
215
+ */
216
+ #flushLogs () {
217
+ clearTimeout(this.#timer)
218
+ this.#timer = undefined
219
+
220
+ const config = this.#config
221
+ if (this.#batch.length === 0 || !this.#logSubmissionUrl || !config) return
222
+
223
+ const source = this.#batchSource
224
+ const data = `[${this.#batch.join(',')}]`
225
+ this.#batch = []
226
+ this.#batchBytes = 2
227
+ this.#batchSource = undefined
228
+ const options = {
229
+ path: getLogSubmissionPath(config, source),
230
+ method: 'POST',
231
+ headers: {
232
+ 'DD-API-KEY': config.DD_API_KEY,
233
+ 'Content-Type': 'application/json',
234
+ },
235
+ url: this.#logSubmissionUrl,
236
+ }
237
+
238
+ try {
239
+ this.#requestTracker.send(request, data, options, error => {
240
+ if (error) log.error('Error submitting %s logs', source, error)
241
+ })
242
+ } catch (error) {
243
+ this.#logSubmissionUrl = undefined
244
+ log.error('Error submitting %s logs', source, error)
245
+ }
246
+ }
247
+ }
248
+
249
+ module.exports = LogSubmissionPlugin
@@ -26,6 +26,11 @@ class NoopSpan {
26
26
  addLink (link) { return this }
27
27
  addLinks (links) { return this }
28
28
  addSpanPointer (ptrKind, ptrDir, ptrHash) { return this }
29
+ /**
30
+ * @param {import('../../../../index').Exception} exception
31
+ * @param {import('../../../../index').SpanEventAttributes} [attributes]
32
+ */
33
+ recordException (exception, attributes) {}
29
34
  log () { return this }
30
35
  logEvent () {}
31
36
  finish (finishTime) {}
@@ -214,21 +214,23 @@ class BaseFFEWriter {
214
214
  * @returns {ActiveWriterRoute} Active route state
215
215
  */
216
216
  #createRoute (route) {
217
+ const requestOptions = {
218
+ headers: {
219
+ ...route.headers,
220
+ 'Content-Type': 'application/json',
221
+ },
222
+ method: 'POST',
223
+ retry: true,
224
+ timeout: this._timeout,
225
+ url: route.url,
226
+ path: route.endpoint,
227
+ }
228
+ if (route.agent) requestOptions.agent = route.agent
229
+
217
230
  return {
218
231
  url: route.url,
219
232
  endpoint: route.endpoint,
220
- requestOptions: {
221
- ...(route.agent && { agent: route.agent }),
222
- headers: {
223
- ...route.headers,
224
- 'Content-Type': 'application/json',
225
- },
226
- method: 'POST',
227
- retry: true,
228
- timeout: this._timeout,
229
- url: route.url,
230
- path: route.endpoint,
231
- },
233
+ requestOptions,
232
234
  }
233
235
  }
234
236
 
@@ -37,6 +37,7 @@ const PENDING_MAX_EVENTS = 1000
37
37
  * @property {string} flag.key - Flag key
38
38
  * @property {object} variant - Variant information
39
39
  * @property {string} variant.key - Variant key
40
+ * @property {number} [serial_id] - Serial id of the split the subject landed in
40
41
  * @property {object} subject - Subject (user/entity) information
41
42
  * @property {string} subject.id - Subject identifier
42
43
  * @property {string} [subject.type] - Subject type
@@ -204,7 +205,7 @@ class ExposuresWriter extends BaseFFEWriter {
204
205
  makePayload (events) {
205
206
  const formattedEvents = events.map(event => {
206
207
  /** @type {ExposureEvent} */
207
- return {
208
+ const formatted = {
208
209
  timestamp: event.timestamp || Date.now(),
209
210
  allocation: {
210
211
  key: event.allocation?.key || event['allocation.key'],
@@ -221,6 +222,12 @@ class ExposuresWriter extends BaseFFEWriter {
221
222
  attributes: event.subject?.attributes,
222
223
  },
223
224
  }
225
+
226
+ if (typeof event.serial_id === 'number') {
227
+ formatted.serial_id = event.serial_id
228
+ }
229
+
230
+ return formatted
224
231
  })
225
232
 
226
233
  return {
@@ -75,8 +75,8 @@ function setAgentlessStrategy (config, setWriterEnabledValue) {
75
75
  headers: {
76
76
  [EVP_SUBDOMAIN_HEADER_NAME]: EVP_EVENT_PLATFORM_SUBDOMAIN,
77
77
  },
78
- ...(directRoute && { fallback: directRoute }),
79
78
  }
79
+ if (directRoute) route.fallback = directRoute
80
80
  logger.debug('FFE Writer enabled with local EVP route %s', route.basePath)
81
81
  setWriterEnabledValue(true, route)
82
82
  return
@@ -1,5 +1,8 @@
1
1
  'use strict'
2
2
 
3
+ const log = require('../../log')
4
+ const { createServerlessDeliveryTracker } = require('../../serverless')
5
+
3
6
  /**
4
7
  * @typedef {import('@opentelemetry/api-logs').LogRecord} LogRecord
5
8
  * @typedef {import('@opentelemetry/core').InstrumentationScope} InstrumentationScope
@@ -54,10 +57,48 @@ class BatchLogRecordProcessor {
54
57
 
55
58
  /**
56
59
  * Forces an immediate flush of all pending log records.
57
- * @returns {undefined} Promise that resolves when flush is complete
60
+ * @param {Function} [done] Called after all pending log exports complete
58
61
  */
59
- forceFlush () {
60
- this.#export()
62
+ forceFlush (done) {
63
+ this.#clearTimer()
64
+
65
+ const deliveryTracker = createServerlessDeliveryTracker()
66
+ if (!deliveryTracker) {
67
+ // Normal processes preserve the existing fire-and-forget batch flush.
68
+ this.#export()
69
+ done?.()
70
+ return
71
+ }
72
+
73
+ // Flush only records present at this boundary. New records belong to the
74
+ // later request that produced them and must not extend this lifecycle flush.
75
+ const logRecords = this.#logRecords
76
+ this.#logRecords = []
77
+
78
+ // Join exports already active at this boundary before draining this snapshot.
79
+ if (typeof this.exporter.flush === 'function') {
80
+ deliveryTracker.track(complete => this.exporter.flush(complete))
81
+ }
82
+
83
+ deliveryTracker.track(complete => {
84
+ const flushNext = () => {
85
+ if (logRecords.length === 0) {
86
+ complete()
87
+ return
88
+ }
89
+
90
+ // Drain the boundary snapshot one batch at a time.
91
+ const batch = logRecords.splice(0, this.#maxExportBatchSize)
92
+ try {
93
+ this.exporter.export(batch, flushNext)
94
+ } catch (error) {
95
+ log.error('Error exporting OTLP logs:', error)
96
+ complete()
97
+ }
98
+ }
99
+ flushNext()
100
+ })
101
+ deliveryTracker.waitForIdle(done)
61
102
  }
62
103
 
63
104
  /**
@@ -79,6 +120,7 @@ class BatchLogRecordProcessor {
79
120
  * @private
80
121
  */
81
122
  #export () {
123
+ if (this.#logRecords.length === 0) return
82
124
  const logRecords = this.#logRecords.slice(0, this.#maxExportBatchSize)
83
125
  this.#logRecords = this.#logRecords.slice(this.#maxExportBatchSize)
84
126