dd-trace 4.47.1 → 4.49.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 (156) hide show
  1. package/LICENSE-3rdparty.csv +1 -1
  2. package/ext/types.d.ts +1 -0
  3. package/ext/types.js +1 -0
  4. package/index.d.ts +361 -0
  5. package/package.json +18 -13
  6. package/packages/datadog-code-origin/index.js +38 -0
  7. package/packages/datadog-core/index.js +2 -2
  8. package/packages/datadog-core/src/utils/src/parse-tags.js +33 -0
  9. package/packages/datadog-esbuild/index.js +4 -2
  10. package/packages/datadog-instrumentations/src/amqplib.js +65 -5
  11. package/packages/datadog-instrumentations/src/avsc.js +37 -0
  12. package/packages/datadog-instrumentations/src/azure-functions.js +48 -0
  13. package/packages/datadog-instrumentations/src/child_process.js +144 -27
  14. package/packages/datadog-instrumentations/src/express.js +37 -4
  15. package/packages/datadog-instrumentations/src/fastify.js +12 -1
  16. package/packages/datadog-instrumentations/src/fs.js +27 -7
  17. package/packages/datadog-instrumentations/src/helpers/hooks.js +6 -0
  18. package/packages/datadog-instrumentations/src/helpers/register.js +9 -0
  19. package/packages/datadog-instrumentations/src/jest.js +2 -1
  20. package/packages/datadog-instrumentations/src/kafkajs.js +123 -63
  21. package/packages/datadog-instrumentations/src/mocha/common.js +1 -1
  22. package/packages/datadog-instrumentations/src/mocha/utils.js +2 -2
  23. package/packages/datadog-instrumentations/src/multer.js +37 -0
  24. package/packages/datadog-instrumentations/src/mysql2.js +220 -1
  25. package/packages/datadog-instrumentations/src/openai.js +2 -2
  26. package/packages/datadog-instrumentations/src/protobufjs.js +127 -0
  27. package/packages/datadog-instrumentations/src/url.js +84 -0
  28. package/packages/datadog-instrumentations/src/utils/src/extract-package-and-module-path.js +7 -4
  29. package/packages/datadog-instrumentations/src/winston.js +22 -0
  30. package/packages/datadog-plugin-amqplib/src/consumer.js +4 -4
  31. package/packages/datadog-plugin-avsc/src/index.js +9 -0
  32. package/packages/datadog-plugin-avsc/src/schema_iterator.js +169 -0
  33. package/packages/datadog-plugin-aws-sdk/src/services/eventbridge.js +1 -0
  34. package/packages/datadog-plugin-aws-sdk/src/services/kinesis.js +1 -0
  35. package/packages/datadog-plugin-aws-sdk/src/services/s3.js +1 -0
  36. package/packages/datadog-plugin-aws-sdk/src/services/sqs.js +1 -0
  37. package/packages/datadog-plugin-azure-functions/src/index.js +77 -0
  38. package/packages/datadog-plugin-fastify/src/code_origin.js +31 -0
  39. package/packages/datadog-plugin-fastify/src/index.js +10 -12
  40. package/packages/datadog-plugin-fastify/src/tracing.js +19 -0
  41. package/packages/datadog-plugin-google-cloud-pubsub/src/consumer.js +8 -1
  42. package/packages/datadog-plugin-google-cloud-pubsub/src/producer.js +8 -0
  43. package/packages/datadog-plugin-grpc/src/client.js +3 -0
  44. package/packages/datadog-plugin-grpc/src/server.js +3 -0
  45. package/packages/datadog-plugin-kafkajs/src/batch-consumer.js +6 -3
  46. package/packages/datadog-plugin-kafkajs/src/consumer.js +8 -4
  47. package/packages/datadog-plugin-kafkajs/src/producer.js +10 -4
  48. package/packages/datadog-plugin-mocha/src/index.js +4 -1
  49. package/packages/datadog-plugin-openai/src/index.js +9 -1015
  50. package/packages/datadog-plugin-openai/src/tracing.js +1023 -0
  51. package/packages/datadog-plugin-protobufjs/src/index.js +14 -0
  52. package/packages/datadog-plugin-protobufjs/src/schema_iterator.js +180 -0
  53. package/packages/dd-trace/src/appsec/addresses.js +8 -1
  54. package/packages/dd-trace/src/appsec/channels.js +7 -1
  55. package/packages/dd-trace/src/appsec/iast/analyzers/cookie-analyzer.js +13 -1
  56. package/packages/dd-trace/src/appsec/iast/analyzers/path-traversal-analyzer.js +8 -1
  57. package/packages/dd-trace/src/appsec/iast/iast-plugin.js +1 -1
  58. package/packages/dd-trace/src/appsec/iast/index.js +3 -0
  59. package/packages/dd-trace/src/appsec/iast/taint-tracking/csi-methods.js +1 -0
  60. package/packages/dd-trace/src/appsec/iast/taint-tracking/plugin.js +55 -7
  61. package/packages/dd-trace/src/appsec/iast/taint-tracking/taint-tracking-impl.js +15 -0
  62. package/packages/dd-trace/src/appsec/iast/vulnerability-reporter.js +4 -2
  63. package/packages/dd-trace/src/appsec/index.js +61 -43
  64. package/packages/dd-trace/src/appsec/rasp/command_injection.js +49 -0
  65. package/packages/dd-trace/src/appsec/rasp/fs-plugin.js +99 -0
  66. package/packages/dd-trace/src/appsec/rasp/index.js +27 -10
  67. package/packages/dd-trace/src/appsec/rasp/lfi.js +112 -0
  68. package/packages/dd-trace/src/appsec/rasp/sql_injection.js +24 -4
  69. package/packages/dd-trace/src/appsec/rasp/ssrf.js +4 -3
  70. package/packages/dd-trace/src/appsec/rasp/utils.js +4 -2
  71. package/packages/dd-trace/src/appsec/recommended.json +3 -7
  72. package/packages/dd-trace/src/appsec/remote_config/capabilities.js +6 -1
  73. package/packages/dd-trace/src/appsec/remote_config/index.js +10 -0
  74. package/packages/dd-trace/src/appsec/reporter.js +17 -9
  75. package/packages/dd-trace/src/appsec/sdk/track_event.js +10 -3
  76. package/packages/dd-trace/src/appsec/waf/waf_context_wrapper.js +1 -1
  77. package/packages/dd-trace/src/appsec/waf/waf_manager.js +4 -0
  78. package/packages/dd-trace/src/azure_metadata.js +120 -0
  79. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/index.js +97 -0
  80. package/packages/dd-trace/src/ci-visibility/dynamic-instrumentation/worker/index.js +90 -0
  81. package/packages/dd-trace/src/ci-visibility/early-flake-detection/get-known-tests.js +2 -14
  82. package/packages/dd-trace/src/ci-visibility/exporters/agent-proxy/index.js +19 -1
  83. package/packages/dd-trace/src/ci-visibility/exporters/agentless/di-logs-writer.js +53 -0
  84. package/packages/dd-trace/src/ci-visibility/exporters/agentless/index.js +8 -1
  85. package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +43 -0
  86. package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +53 -0
  87. package/packages/dd-trace/src/config.js +86 -6
  88. package/packages/dd-trace/src/constants.js +3 -1
  89. package/packages/dd-trace/src/datastreams/pathway.js +1 -0
  90. package/packages/dd-trace/src/datastreams/schemas/schema_builder.js +25 -17
  91. package/packages/dd-trace/src/debugger/devtools_client/config.js +2 -0
  92. package/packages/dd-trace/src/debugger/devtools_client/index.js +52 -5
  93. package/packages/dd-trace/src/debugger/devtools_client/remote_config.js +4 -4
  94. package/packages/dd-trace/src/debugger/devtools_client/send.js +29 -2
  95. package/packages/dd-trace/src/debugger/devtools_client/snapshot/collector.js +187 -0
  96. package/packages/dd-trace/src/debugger/devtools_client/snapshot/index.js +40 -0
  97. package/packages/dd-trace/src/debugger/devtools_client/snapshot/processor.js +252 -0
  98. package/packages/dd-trace/src/debugger/devtools_client/snapshot/symbols.js +6 -0
  99. package/packages/dd-trace/src/debugger/devtools_client/state.js +19 -4
  100. package/packages/dd-trace/src/debugger/index.js +10 -3
  101. package/packages/dd-trace/src/exporters/common/request.js +8 -34
  102. package/packages/dd-trace/src/exporters/common/url-to-http-options-polyfill.js +31 -0
  103. package/packages/dd-trace/src/llmobs/constants/tags.js +34 -0
  104. package/packages/dd-trace/src/llmobs/constants/text.js +6 -0
  105. package/packages/dd-trace/src/llmobs/constants/writers.js +13 -0
  106. package/packages/dd-trace/src/llmobs/index.js +103 -0
  107. package/packages/dd-trace/src/llmobs/noop.js +82 -0
  108. package/packages/dd-trace/src/llmobs/plugins/base.js +65 -0
  109. package/packages/dd-trace/src/llmobs/plugins/openai.js +205 -0
  110. package/packages/dd-trace/src/llmobs/sdk.js +377 -0
  111. package/packages/dd-trace/src/llmobs/span_processor.js +195 -0
  112. package/packages/dd-trace/src/llmobs/storage.js +7 -0
  113. package/packages/dd-trace/src/llmobs/tagger.js +322 -0
  114. package/packages/dd-trace/src/llmobs/util.js +176 -0
  115. package/packages/dd-trace/src/llmobs/writers/base.js +111 -0
  116. package/packages/dd-trace/src/llmobs/writers/evaluations.js +29 -0
  117. package/packages/dd-trace/src/llmobs/writers/spans/agentProxy.js +23 -0
  118. package/packages/dd-trace/src/llmobs/writers/spans/agentless.js +17 -0
  119. package/packages/dd-trace/src/llmobs/writers/spans/base.js +49 -0
  120. package/packages/dd-trace/src/noop/proxy.js +3 -0
  121. package/packages/dd-trace/src/noop/span.js +3 -0
  122. package/packages/dd-trace/src/opentelemetry/span.js +1 -1
  123. package/packages/dd-trace/src/opentelemetry/tracer.js +1 -0
  124. package/packages/dd-trace/src/opentracing/propagation/text_map.js +73 -12
  125. package/packages/dd-trace/src/opentracing/span.js +12 -0
  126. package/packages/dd-trace/src/opentracing/tracer.js +8 -1
  127. package/packages/dd-trace/src/payload-tagging/config/aws.json +71 -3
  128. package/packages/dd-trace/src/payload-tagging/index.js +1 -1
  129. package/packages/dd-trace/src/payload-tagging/jsonpath-plus.js +2094 -0
  130. package/packages/dd-trace/src/plugin_manager.js +4 -2
  131. package/packages/dd-trace/src/plugins/ci_plugin.js +2 -0
  132. package/packages/dd-trace/src/plugins/index.js +3 -0
  133. package/packages/dd-trace/src/plugins/log_plugin.js +1 -1
  134. package/packages/dd-trace/src/plugins/outbound.js +9 -0
  135. package/packages/dd-trace/src/plugins/schema.js +35 -0
  136. package/packages/dd-trace/src/plugins/util/ci.js +23 -1
  137. package/packages/dd-trace/src/plugins/util/serverless.js +7 -0
  138. package/packages/dd-trace/src/plugins/util/stacktrace.js +94 -0
  139. package/packages/dd-trace/src/plugins/util/tags.js +7 -0
  140. package/packages/dd-trace/src/plugins/util/test.js +20 -22
  141. package/packages/dd-trace/src/plugins/util/web.js +6 -4
  142. package/packages/dd-trace/src/priority_sampler.js +16 -0
  143. package/packages/dd-trace/src/profiling/config.js +3 -1
  144. package/packages/dd-trace/src/profiling/exporters/agent.js +7 -5
  145. package/packages/dd-trace/src/profiling/profiler.js +24 -14
  146. package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
  147. package/packages/dd-trace/src/profiling/profilers/wall.js +95 -66
  148. package/packages/dd-trace/src/proxy.js +20 -1
  149. package/packages/dd-trace/src/service-naming/schemas/v0/index.js +2 -1
  150. package/packages/dd-trace/src/service-naming/schemas/v0/serverless.js +12 -0
  151. package/packages/dd-trace/src/service-naming/schemas/v1/index.js +2 -1
  152. package/packages/dd-trace/src/service-naming/schemas/v1/serverless.js +12 -0
  153. package/packages/dd-trace/src/span_processor.js +5 -0
  154. package/packages/dd-trace/src/telemetry/index.js +11 -1
  155. package/packages/datadog-core/src/storage/async_resource.js +0 -108
  156. package/packages/datadog-core/src/storage/index.js +0 -5
@@ -0,0 +1,322 @@
1
+ 'use strict'
2
+
3
+ const log = require('../log')
4
+ const {
5
+ MODEL_NAME,
6
+ MODEL_PROVIDER,
7
+ SESSION_ID,
8
+ ML_APP,
9
+ SPAN_KIND,
10
+ INPUT_VALUE,
11
+ OUTPUT_DOCUMENTS,
12
+ INPUT_DOCUMENTS,
13
+ OUTPUT_VALUE,
14
+ METADATA,
15
+ METRICS,
16
+ PARENT_ID_KEY,
17
+ INPUT_MESSAGES,
18
+ OUTPUT_MESSAGES,
19
+ TAGS,
20
+ NAME,
21
+ PROPAGATED_PARENT_ID_KEY,
22
+ ROOT_PARENT_ID,
23
+ INPUT_TOKENS_METRIC_KEY,
24
+ OUTPUT_TOKENS_METRIC_KEY,
25
+ TOTAL_TOKENS_METRIC_KEY
26
+ } = require('./constants/tags')
27
+
28
+ // global registry of LLMObs spans
29
+ // maps LLMObs spans to their annotations
30
+ const registry = new WeakMap()
31
+
32
+ class LLMObsTagger {
33
+ constructor (config, softFail = false) {
34
+ this._config = config
35
+
36
+ this.softFail = softFail
37
+ }
38
+
39
+ static get tagMap () {
40
+ return registry
41
+ }
42
+
43
+ registerLLMObsSpan (span, {
44
+ modelName,
45
+ modelProvider,
46
+ sessionId,
47
+ mlApp,
48
+ parent,
49
+ kind,
50
+ name
51
+ } = {}) {
52
+ if (!this._config.llmobs.enabled) return
53
+ if (!kind) return // do not register it in the map if it doesn't have an llmobs span kind
54
+
55
+ this._register(span)
56
+
57
+ if (name) this._setTag(span, NAME, name)
58
+
59
+ this._setTag(span, SPAN_KIND, kind)
60
+ if (modelName) this._setTag(span, MODEL_NAME, modelName)
61
+ if (modelProvider) this._setTag(span, MODEL_PROVIDER, modelProvider)
62
+
63
+ sessionId = sessionId || parent?.context()._tags[SESSION_ID]
64
+ if (sessionId) this._setTag(span, SESSION_ID, sessionId)
65
+
66
+ if (!mlApp) mlApp = parent?.context()._tags[ML_APP] || this._config.llmobs.mlApp
67
+ this._setTag(span, ML_APP, mlApp)
68
+
69
+ const parentId =
70
+ parent?.context().toSpanId() ||
71
+ span.context()._trace.tags[PROPAGATED_PARENT_ID_KEY] ||
72
+ ROOT_PARENT_ID
73
+ this._setTag(span, PARENT_ID_KEY, parentId)
74
+ }
75
+
76
+ // TODO: similarly for the following `tag` methods,
77
+ // how can we transition from a span weakmap to core API functionality
78
+ tagLLMIO (span, inputData, outputData) {
79
+ this._tagMessages(span, inputData, INPUT_MESSAGES)
80
+ this._tagMessages(span, outputData, OUTPUT_MESSAGES)
81
+ }
82
+
83
+ tagEmbeddingIO (span, inputData, outputData) {
84
+ this._tagDocuments(span, inputData, INPUT_DOCUMENTS)
85
+ this._tagText(span, outputData, OUTPUT_VALUE)
86
+ }
87
+
88
+ tagRetrievalIO (span, inputData, outputData) {
89
+ this._tagText(span, inputData, INPUT_VALUE)
90
+ this._tagDocuments(span, outputData, OUTPUT_DOCUMENTS)
91
+ }
92
+
93
+ tagTextIO (span, inputData, outputData) {
94
+ this._tagText(span, inputData, INPUT_VALUE)
95
+ this._tagText(span, outputData, OUTPUT_VALUE)
96
+ }
97
+
98
+ tagMetadata (span, metadata) {
99
+ this._setTag(span, METADATA, metadata)
100
+ }
101
+
102
+ tagMetrics (span, metrics) {
103
+ const filterdMetrics = {}
104
+ for (const [key, value] of Object.entries(metrics)) {
105
+ let processedKey = key
106
+
107
+ // processing these specifically for our metrics ingestion
108
+ switch (key) {
109
+ case 'inputTokens':
110
+ processedKey = INPUT_TOKENS_METRIC_KEY
111
+ break
112
+ case 'outputTokens':
113
+ processedKey = OUTPUT_TOKENS_METRIC_KEY
114
+ break
115
+ case 'totalTokens':
116
+ processedKey = TOTAL_TOKENS_METRIC_KEY
117
+ break
118
+ }
119
+
120
+ if (typeof value === 'number') {
121
+ filterdMetrics[processedKey] = value
122
+ } else {
123
+ this._handleFailure(`Value for metric '${key}' must be a number, instead got ${value}`)
124
+ }
125
+ }
126
+
127
+ this._setTag(span, METRICS, filterdMetrics)
128
+ }
129
+
130
+ tagSpanTags (span, tags) {
131
+ // new tags will be merged with existing tags
132
+ const currentTags = registry.get(span)?.[TAGS]
133
+ if (currentTags) {
134
+ Object.assign(tags, currentTags)
135
+ }
136
+ this._setTag(span, TAGS, tags)
137
+ }
138
+
139
+ _tagText (span, data, key) {
140
+ if (data) {
141
+ if (typeof data === 'string') {
142
+ this._setTag(span, key, data)
143
+ } else {
144
+ try {
145
+ this._setTag(span, key, JSON.stringify(data))
146
+ } catch {
147
+ const type = key === INPUT_VALUE ? 'input' : 'output'
148
+ this._handleFailure(`Failed to parse ${type} value, must be JSON serializable.`)
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ _tagDocuments (span, data, key) {
155
+ if (data) {
156
+ if (!Array.isArray(data)) {
157
+ data = [data]
158
+ }
159
+
160
+ const documents = data.map(document => {
161
+ if (typeof document === 'string') {
162
+ return { text: document }
163
+ }
164
+
165
+ if (document == null || typeof document !== 'object') {
166
+ this._handleFailure('Documents must be a string, object, or list of objects.')
167
+ return undefined
168
+ }
169
+
170
+ const { text, name, id, score } = document
171
+ let validDocument = true
172
+
173
+ if (typeof text !== 'string') {
174
+ this._handleFailure('Document text must be a string.')
175
+ validDocument = false
176
+ }
177
+
178
+ const documentObj = { text }
179
+
180
+ validDocument = this._tagConditionalString(name, 'Document name', documentObj, 'name') && validDocument
181
+ validDocument = this._tagConditionalString(id, 'Document ID', documentObj, 'id') && validDocument
182
+ validDocument = this._tagConditionalNumber(score, 'Document score', documentObj, 'score') && validDocument
183
+
184
+ return validDocument ? documentObj : undefined
185
+ }).filter(doc => !!doc)
186
+
187
+ if (documents.length) {
188
+ this._setTag(span, key, documents)
189
+ }
190
+ }
191
+ }
192
+
193
+ _tagMessages (span, data, key) {
194
+ if (data) {
195
+ if (!Array.isArray(data)) {
196
+ data = [data]
197
+ }
198
+
199
+ const messages = data.map(message => {
200
+ if (typeof message === 'string') {
201
+ return { content: message }
202
+ }
203
+
204
+ if (message == null || typeof message !== 'object') {
205
+ this._handleFailure('Messages must be a string, object, or list of objects')
206
+ return undefined
207
+ }
208
+
209
+ let validMessage = true
210
+
211
+ const { content = '', role } = message
212
+ let toolCalls = message.toolCalls
213
+ const messageObj = { content }
214
+
215
+ if (typeof content !== 'string') {
216
+ this._handleFailure('Message content must be a string.')
217
+ validMessage = false
218
+ }
219
+
220
+ validMessage = this._tagConditionalString(role, 'Message role', messageObj, 'role') && validMessage
221
+
222
+ if (toolCalls) {
223
+ if (!Array.isArray(toolCalls)) {
224
+ toolCalls = [toolCalls]
225
+ }
226
+
227
+ const filteredToolCalls = toolCalls.map(toolCall => {
228
+ if (typeof toolCall !== 'object') {
229
+ this._handleFailure('Tool call must be an object.')
230
+ return undefined
231
+ }
232
+
233
+ let validTool = true
234
+
235
+ const { name, arguments: args, toolId, type } = toolCall
236
+ const toolCallObj = {}
237
+
238
+ validTool = this._tagConditionalString(name, 'Tool name', toolCallObj, 'name') && validTool
239
+ validTool = this._tagConditionalObject(args, 'Tool arguments', toolCallObj, 'arguments') && validTool
240
+ validTool = this._tagConditionalString(toolId, 'Tool ID', toolCallObj, 'tool_id') && validTool
241
+ validTool = this._tagConditionalString(type, 'Tool type', toolCallObj, 'type') && validTool
242
+
243
+ return validTool ? toolCallObj : undefined
244
+ }).filter(toolCall => !!toolCall)
245
+
246
+ if (filteredToolCalls.length) {
247
+ messageObj.tool_calls = filteredToolCalls
248
+ }
249
+ }
250
+
251
+ return validMessage ? messageObj : undefined
252
+ }).filter(msg => !!msg)
253
+
254
+ if (messages.length) {
255
+ this._setTag(span, key, messages)
256
+ }
257
+ }
258
+ }
259
+
260
+ _tagConditionalString (data, type, carrier, key) {
261
+ if (!data) return true
262
+ if (typeof data !== 'string') {
263
+ this._handleFailure(`"${type}" must be a string.`)
264
+ return false
265
+ }
266
+ carrier[key] = data
267
+ return true
268
+ }
269
+
270
+ _tagConditionalNumber (data, type, carrier, key) {
271
+ if (!data) return true
272
+ if (typeof data !== 'number') {
273
+ this._handleFailure(`"${type}" must be a number.`)
274
+ return false
275
+ }
276
+ carrier[key] = data
277
+ return true
278
+ }
279
+
280
+ _tagConditionalObject (data, type, carrier, key) {
281
+ if (!data) return true
282
+ if (typeof data !== 'object') {
283
+ this._handleFailure(`"${type}" must be an object.`)
284
+ return false
285
+ }
286
+ carrier[key] = data
287
+ return true
288
+ }
289
+
290
+ // any public-facing LLMObs APIs using this tagger should not soft fail
291
+ // auto-instrumentation should soft fail
292
+ _handleFailure (msg) {
293
+ if (this.softFail) {
294
+ log.warn(msg)
295
+ } else {
296
+ throw new Error(msg)
297
+ }
298
+ }
299
+
300
+ _register (span) {
301
+ if (!this._config.llmobs.enabled) return
302
+ if (registry.has(span)) {
303
+ this._handleFailure(`LLMObs Span "${span._name}" already registered.`)
304
+ return
305
+ }
306
+
307
+ registry.set(span, {})
308
+ }
309
+
310
+ _setTag (span, key, value) {
311
+ if (!this._config.llmobs.enabled) return
312
+ if (!registry.has(span)) {
313
+ this._handleFailure('Span must be an LLMObs generated span.')
314
+ return
315
+ }
316
+
317
+ const tagsCarrier = registry.get(span)
318
+ Object.assign(tagsCarrier, { [key]: value })
319
+ }
320
+ }
321
+
322
+ module.exports = LLMObsTagger
@@ -0,0 +1,176 @@
1
+ 'use strict'
2
+
3
+ const { SPAN_KINDS } = require('./constants/tags')
4
+
5
+ function encodeUnicode (str) {
6
+ if (!str) return str
7
+ return str.split('').map(char => {
8
+ const code = char.charCodeAt(0)
9
+ if (code > 127) {
10
+ return `\\u${code.toString(16).padStart(4, '0')}`
11
+ }
12
+ return char
13
+ }).join('')
14
+ }
15
+
16
+ function validateKind (kind) {
17
+ if (!SPAN_KINDS.includes(kind)) {
18
+ throw new Error(`
19
+ Invalid span kind specified: "${kind}"
20
+ Must be one of: ${SPAN_KINDS.join(', ')}
21
+ `)
22
+ }
23
+
24
+ return kind
25
+ }
26
+
27
+ // extracts the argument names from a function string
28
+ function parseArgumentNames (str) {
29
+ const result = []
30
+ let current = ''
31
+ let closerCount = 0
32
+ let recording = true
33
+ let inSingleLineComment = false
34
+ let inMultiLineComment = false
35
+
36
+ for (let i = 0; i < str.length; i++) {
37
+ const char = str[i]
38
+ const nextChar = str[i + 1]
39
+
40
+ // Handle single-line comments
41
+ if (!inMultiLineComment && char === '/' && nextChar === '/') {
42
+ inSingleLineComment = true
43
+ i++ // Skip the next character
44
+ continue
45
+ }
46
+
47
+ // Handle multi-line comments
48
+ if (!inSingleLineComment && char === '/' && nextChar === '*') {
49
+ inMultiLineComment = true
50
+ i++ // Skip the next character
51
+ continue
52
+ }
53
+
54
+ // End of single-line comment
55
+ if (inSingleLineComment && char === '\n') {
56
+ inSingleLineComment = false
57
+ continue
58
+ }
59
+
60
+ // End of multi-line comment
61
+ if (inMultiLineComment && char === '*' && nextChar === '/') {
62
+ inMultiLineComment = false
63
+ i++ // Skip the next character
64
+ continue
65
+ }
66
+
67
+ // Skip characters inside comments
68
+ if (inSingleLineComment || inMultiLineComment) {
69
+ continue
70
+ }
71
+
72
+ if (['{', '[', '('].includes(char)) {
73
+ closerCount++
74
+ } else if (['}', ']', ')'].includes(char)) {
75
+ closerCount--
76
+ } else if (char === '=' && nextChar !== '>' && closerCount === 0) {
77
+ recording = false
78
+ // record the variable name early, and stop counting characters until we reach the next comma
79
+ result.push(current.trim())
80
+ current = ''
81
+ continue
82
+ } else if (char === ',' && closerCount === 0) {
83
+ if (recording) {
84
+ result.push(current.trim())
85
+ current = ''
86
+ }
87
+
88
+ recording = true
89
+ continue
90
+ }
91
+
92
+ if (recording) {
93
+ current += char
94
+ }
95
+ }
96
+
97
+ if (current && recording) {
98
+ result.push(current.trim())
99
+ }
100
+
101
+ return result
102
+ }
103
+
104
+ // finds the bounds of the arguments in a function string
105
+ function findArgumentsBounds (str) {
106
+ let start = -1
107
+ let end = -1
108
+ let closerCount = 0
109
+
110
+ for (let i = 0; i < str.length; i++) {
111
+ const char = str[i]
112
+
113
+ if (char === '(') {
114
+ if (closerCount === 0) {
115
+ start = i
116
+ }
117
+
118
+ closerCount++
119
+ } else if (char === ')') {
120
+ closerCount--
121
+
122
+ if (closerCount === 0) {
123
+ end = i
124
+ break
125
+ }
126
+ }
127
+ }
128
+
129
+ return { start, end }
130
+ }
131
+
132
+ const memo = new WeakMap()
133
+ function getFunctionArguments (fn, args = []) {
134
+ if (!fn) return
135
+ if (!args.length) return
136
+ if (args.length === 1) return args[0]
137
+
138
+ try {
139
+ let names
140
+ if (memo.has(fn)) {
141
+ names = memo.get(fn)
142
+ } else {
143
+ const fnString = fn.toString()
144
+ const { start, end } = findArgumentsBounds(fnString)
145
+ names = parseArgumentNames(fnString.slice(start + 1, end))
146
+ memo.set(fn, names)
147
+ }
148
+
149
+ const argsObject = {}
150
+
151
+ for (const argIdx in args) {
152
+ const name = names[argIdx]
153
+ const arg = args[argIdx]
154
+
155
+ const spread = name?.startsWith('...')
156
+
157
+ // this can only be the last argument
158
+ if (spread) {
159
+ argsObject[name.slice(3)] = args.slice(argIdx)
160
+ break
161
+ }
162
+
163
+ argsObject[name] = arg
164
+ }
165
+
166
+ return argsObject
167
+ } catch {
168
+ return args
169
+ }
170
+ }
171
+
172
+ module.exports = {
173
+ encodeUnicode,
174
+ validateKind,
175
+ getFunctionArguments
176
+ }
@@ -0,0 +1,111 @@
1
+ 'use strict'
2
+
3
+ const request = require('../../exporters/common/request')
4
+ const { URL, format } = require('url')
5
+
6
+ const logger = require('../../log')
7
+
8
+ const { encodeUnicode } = require('../util')
9
+ const log = require('../../log')
10
+
11
+ class BaseLLMObsWriter {
12
+ constructor ({ interval, timeout, endpoint, intake, eventType, protocol, port }) {
13
+ this._interval = interval || 1000 // 1s
14
+ this._timeout = timeout || 5000 // 5s
15
+ this._eventType = eventType
16
+
17
+ this._buffer = []
18
+ this._bufferLimit = 1000
19
+ this._bufferSize = 0
20
+
21
+ this._url = new URL(format({
22
+ protocol: protocol || 'https:',
23
+ hostname: intake,
24
+ port: port || 443,
25
+ pathname: endpoint
26
+ }))
27
+
28
+ this._headers = {
29
+ 'Content-Type': 'application/json'
30
+ }
31
+
32
+ this._periodic = setInterval(() => {
33
+ this.flush()
34
+ }, this._interval).unref()
35
+
36
+ process.once('beforeExit', () => {
37
+ this.destroy()
38
+ })
39
+
40
+ this._destroyed = false
41
+
42
+ logger.debug(`Started ${this.constructor.name} to ${this._url}`)
43
+ }
44
+
45
+ append (event, byteLength) {
46
+ if (this._buffer.length >= this._bufferLimit) {
47
+ logger.warn(`${this.constructor.name} event buffer full (limit is ${this._bufferLimit}), dropping event`)
48
+ return
49
+ }
50
+
51
+ this._bufferSize += byteLength || Buffer.from(JSON.stringify(event)).byteLength
52
+ this._buffer.push(event)
53
+ }
54
+
55
+ flush () {
56
+ if (this._buffer.length === 0) {
57
+ return
58
+ }
59
+
60
+ const events = this._buffer
61
+ this._buffer = []
62
+ this._bufferSize = 0
63
+ const payload = this._encode(this.makePayload(events))
64
+
65
+ const options = {
66
+ headers: this._headers,
67
+ method: 'POST',
68
+ url: this._url,
69
+ timeout: this._timeout
70
+ }
71
+
72
+ log.debug(`Encoded LLMObs payload: ${payload}`)
73
+
74
+ request(payload, options, (err, resp, code) => {
75
+ if (err) {
76
+ logger.error(
77
+ `Error sending ${events.length} LLMObs ${this._eventType} events to ${this._url}: ${err.message}`
78
+ )
79
+ } else if (code >= 300) {
80
+ logger.error(
81
+ `Error sending ${events.length} LLMObs ${this._eventType} events to ${this._url}: ${code}`
82
+ )
83
+ } else {
84
+ logger.debug(`Sent ${events.length} LLMObs ${this._eventType} events to ${this._url}`)
85
+ }
86
+ })
87
+ }
88
+
89
+ makePayload (events) {}
90
+
91
+ destroy () {
92
+ if (!this._destroyed) {
93
+ logger.debug(`Stopping ${this.constructor.name}`)
94
+ clearInterval(this._periodic)
95
+ process.removeListener('beforeExit', this.destroy)
96
+ this.flush()
97
+ this._destroyed = true
98
+ }
99
+ }
100
+
101
+ _encode (payload) {
102
+ return JSON.stringify(payload, (key, value) => {
103
+ if (typeof value === 'string') {
104
+ return encodeUnicode(value) // serialize unicode characters
105
+ }
106
+ return value
107
+ }).replace(/\\\\u/g, '\\u') // remove double escaping
108
+ }
109
+ }
110
+
111
+ module.exports = BaseLLMObsWriter
@@ -0,0 +1,29 @@
1
+ 'use strict'
2
+
3
+ const { AGENTLESS_EVALULATIONS_ENDPOINT } = require('../constants/writers')
4
+ const BaseWriter = require('./base')
5
+
6
+ class LLMObsEvalMetricsWriter extends BaseWriter {
7
+ constructor (config) {
8
+ super({
9
+ endpoint: AGENTLESS_EVALULATIONS_ENDPOINT,
10
+ intake: `api.${config.site}`,
11
+ eventType: 'evaluation_metric'
12
+ })
13
+
14
+ this._headers['DD-API-KEY'] = config.apiKey
15
+ }
16
+
17
+ makePayload (events) {
18
+ return {
19
+ data: {
20
+ type: this._eventType,
21
+ attributes: {
22
+ metrics: events
23
+ }
24
+ }
25
+ }
26
+ }
27
+ }
28
+
29
+ module.exports = LLMObsEvalMetricsWriter
@@ -0,0 +1,23 @@
1
+ 'use strict'
2
+
3
+ const {
4
+ EVP_SUBDOMAIN_HEADER_NAME,
5
+ EVP_SUBDOMAIN_HEADER_VALUE,
6
+ EVP_PROXY_AGENT_ENDPOINT
7
+ } = require('../../constants/writers')
8
+ const LLMObsBaseSpanWriter = require('./base')
9
+
10
+ class LLMObsAgentProxySpanWriter extends LLMObsBaseSpanWriter {
11
+ constructor (config) {
12
+ super({
13
+ intake: config.hostname || 'localhost',
14
+ protocol: 'http:',
15
+ endpoint: EVP_PROXY_AGENT_ENDPOINT,
16
+ port: config.port
17
+ })
18
+
19
+ this._headers[EVP_SUBDOMAIN_HEADER_NAME] = EVP_SUBDOMAIN_HEADER_VALUE
20
+ }
21
+ }
22
+
23
+ module.exports = LLMObsAgentProxySpanWriter
@@ -0,0 +1,17 @@
1
+ 'use strict'
2
+
3
+ const { AGENTLESS_SPANS_ENDPOINT } = require('../../constants/writers')
4
+ const LLMObsBaseSpanWriter = require('./base')
5
+
6
+ class LLMObsAgentlessSpanWriter extends LLMObsBaseSpanWriter {
7
+ constructor (config) {
8
+ super({
9
+ intake: `llmobs-intake.${config.site}`,
10
+ endpoint: AGENTLESS_SPANS_ENDPOINT
11
+ })
12
+
13
+ this._headers['DD-API-KEY'] = config.apiKey
14
+ }
15
+ }
16
+
17
+ module.exports = LLMObsAgentlessSpanWriter