dd-trace 5.123.0 → 5.124.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.
- package/ci/test-optimization-validation/framework-adapters/cypress.js +9 -11
- package/ci/test-optimization-validation/scenarios/ci-wiring.js +5 -4
- package/index.d.ts +128 -26
- package/package.json +10 -10
- package/packages/datadog-instrumentations/src/bunyan.js +4 -2
- package/packages/datadog-instrumentations/src/helpers/bunyan.js +12 -3
- package/packages/datadog-instrumentations/src/helpers/pool-acquire.js +1 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/instrumentations/webdriverio.js +28 -0
- package/packages/datadog-instrumentations/src/helpers/rewriter/targets.json +1 -0
- package/packages/datadog-instrumentations/src/jest.js +140 -18
- package/packages/datadog-instrumentations/src/mariadb-bundle.js +1597 -0
- package/packages/datadog-instrumentations/src/mariadb.js +7 -0
- package/packages/datadog-instrumentations/src/pino.js +15 -4
- package/packages/datadog-instrumentations/src/playwright.js +34 -20
- package/packages/datadog-instrumentations/src/vitest-main-no-worker-init.js +7 -4
- package/packages/datadog-instrumentations/src/vitest-worker.js +11 -0
- package/packages/datadog-instrumentations/src/webdriverio.js +26 -0
- package/packages/datadog-instrumentations/src/winston.js +6 -3
- package/packages/datadog-plugin-cypress/src/support.js +1 -0
- package/packages/datadog-plugin-http/src/client.js +2 -15
- package/packages/datadog-plugin-http2/src/client.js +2 -15
- package/packages/datadog-plugin-openai-agents/src/integration.js +15 -11
- package/packages/datadog-plugin-playwright/src/index.js +9 -1
- package/packages/datadog-plugin-undici/src/index.js +2 -15
- package/packages/dd-trace/src/aiguard/evaluation.js +94 -20
- package/packages/dd-trace/src/aiguard/integrations/vercel-ai.js +2 -3
- package/packages/dd-trace/src/aiguard/messages/anthropic.js +21 -9
- package/packages/dd-trace/src/aiguard/noop.js +9 -1
- package/packages/dd-trace/src/aiguard/redaction.js +183 -0
- package/packages/dd-trace/src/aiguard/sdk.js +7 -2
- package/packages/dd-trace/src/aiguard/tags.js +2 -0
- package/packages/dd-trace/src/appsec/iast/vulnerabilities-formatter/utils.js +10 -9
- package/packages/dd-trace/src/appsec/rule_manager.js +2 -0
- package/packages/dd-trace/src/ci-visibility/exporters/agentless/request-tracker.js +51 -26
- package/packages/dd-trace/src/ci-visibility/exporters/ci-visibility-exporter.js +5 -11
- package/packages/dd-trace/src/ci-visibility/final-flush.js +20 -0
- package/packages/dd-trace/src/ci-visibility/log-submission/log-submission-plugin.js +196 -29
- package/packages/dd-trace/src/config/generated-config-types.d.ts +6 -0
- package/packages/dd-trace/src/config/index.js +2 -1
- package/packages/dd-trace/src/config/supported-configurations.json +27 -0
- package/packages/dd-trace/src/debugger/devtools_client/send.js +4 -3
- package/packages/dd-trace/src/dogstatsd.js +45 -18
- package/packages/dd-trace/src/encode/agentless-json.js +5 -4
- package/packages/dd-trace/src/exporters/agent/index.js +22 -2
- package/packages/dd-trace/src/exporters/agent/writer.js +4 -4
- package/packages/dd-trace/src/exporters/common/writer.js +24 -2
- package/packages/dd-trace/src/exporters/span-stats/index.js +23 -4
- package/packages/dd-trace/src/external-logger/src/index.js +4 -3
- package/packages/dd-trace/src/flush.js +102 -0
- package/packages/dd-trace/src/llmobs/experiments/client.js +4 -0
- package/packages/dd-trace/src/llmobs/experiments/dataset.js +51 -2
- package/packages/dd-trace/src/llmobs/experiments/experiment.js +420 -105
- package/packages/dd-trace/src/llmobs/experiments/index.js +54 -42
- package/packages/dd-trace/src/llmobs/experiments/noop.js +39 -8
- package/packages/dd-trace/src/llmobs/experiments/result.js +1 -0
- package/packages/dd-trace/src/llmobs/experiments/util.js +21 -0
- package/packages/dd-trace/src/llmobs/index.js +80 -17
- package/packages/dd-trace/src/llmobs/noop.js +2 -1
- package/packages/dd-trace/src/llmobs/plugins/anthropic/util.js +8 -4
- package/packages/dd-trace/src/llmobs/plugins/claude-agent-sdk/index.js +6 -3
- package/packages/dd-trace/src/llmobs/plugins/openai/utils.js +13 -6
- package/packages/dd-trace/src/llmobs/plugins/openai-agents/utils.js +5 -7
- package/packages/dd-trace/src/llmobs/writers/base.js +60 -24
- package/packages/dd-trace/src/noop/span.js +5 -0
- package/packages/dd-trace/src/openfeature/writers/exposures.js +8 -1
- package/packages/dd-trace/src/opentelemetry/logs/batch_log_processor.js +45 -3
- package/packages/dd-trace/src/opentelemetry/logs/index.js +4 -1
- package/packages/dd-trace/src/opentelemetry/logs/logger_provider.js +7 -4
- package/packages/dd-trace/src/opentelemetry/metrics/index.js +3 -1
- package/packages/dd-trace/src/opentelemetry/metrics/meter_provider.js +8 -0
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_http_metric_exporter.js +4 -2
- package/packages/dd-trace/src/opentelemetry/metrics/otlp_span_stats_exporter.js +4 -2
- package/packages/dd-trace/src/opentelemetry/metrics/periodic_metric_reader.js +25 -5
- package/packages/dd-trace/src/opentelemetry/otlp/otlp_http_exporter_base.js +61 -25
- package/packages/dd-trace/src/opentelemetry/tracer.js +26 -2
- package/packages/dd-trace/src/opentracing/propagation/text_map.js +410 -342
- package/packages/dd-trace/src/opentracing/span.js +28 -0
- package/packages/dd-trace/src/otel-thread-ctx.js +23 -5
- package/packages/dd-trace/src/plugin_manager.js +2 -0
- package/packages/dd-trace/src/plugins/util/status-validator.js +98 -0
- package/packages/dd-trace/src/plugins/util/test.js +74 -21
- package/packages/dd-trace/src/plugins/util/web.js +2 -65
- package/packages/dd-trace/src/profiling/oom.js +4 -3
- package/packages/dd-trace/src/profiling/profiler.js +7 -0
- package/packages/dd-trace/src/profiling/profilers/events.js +3 -3
- package/packages/dd-trace/src/profiling/profilers/wall.js +14 -14
- package/packages/dd-trace/src/proxy.js +36 -8
- package/packages/dd-trace/src/runtime_metrics/index.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/otlp_runtime_metrics.js +5 -0
- package/packages/dd-trace/src/runtime_metrics/runtime_metrics.js +17 -6
- package/packages/dd-trace/src/serverless/telemetry-delivery-tracker.js +55 -0
- package/packages/dd-trace/src/serverless/vercel.js +122 -0
- package/packages/dd-trace/src/serverless.js +37 -20
- package/packages/dd-trace/src/span_stats.js +40 -3
- package/packages/dd-trace/src/tracer.js +22 -0
- package/packages/dd-trace/src/web-tags-cache.js +67 -13
- package/vendor/dist/@datadog/openfeature-node-server/index.js +1 -1
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
const log = require('../../log')
|
|
4
4
|
const { ExperimentsClient } = require('./client')
|
|
5
|
-
const { Dataset
|
|
5
|
+
const { Dataset } = require('./dataset')
|
|
6
6
|
const { Experiment, ExternalExperiment } = require('./experiment')
|
|
7
7
|
const { validateTagsList } = require('./util')
|
|
8
8
|
const NoopExperiments = require('./noop')
|
|
9
9
|
|
|
10
|
+
const DEFAULT_PROJECT_NAME = 'default-project'
|
|
11
|
+
|
|
10
12
|
// Poll `attempt` with exponential backoff until it returns true or the time
|
|
11
13
|
// budget is spent. Used for eventually-consistent reads (pullDataset).
|
|
12
14
|
async function retryWithBackoff (attempt, { maxTotalMs = 30_000, baseDelayMs = 250, maxDelayMs = 8000 } = {}) {
|
|
@@ -33,8 +35,8 @@ class Experiments {
|
|
|
33
35
|
|
|
34
36
|
constructor (config, llmobs) {
|
|
35
37
|
this.#config = config
|
|
36
|
-
this.#llmobs = llmobs
|
|
37
|
-
this.#projectName = config.llmobs?.
|
|
38
|
+
this.#llmobs = config.llmobs?.mlApp || config.service ? llmobs : undefined
|
|
39
|
+
this.#projectName = config.llmobs?.projectName || DEFAULT_PROJECT_NAME
|
|
38
40
|
this.#client = this.#clientForProject(this.#projectName)
|
|
39
41
|
}
|
|
40
42
|
|
|
@@ -51,27 +53,26 @@ class Experiments {
|
|
|
51
53
|
})
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
/**
|
|
57
|
+
* @param {string | undefined} projectName
|
|
58
|
+
* @returns {ExperimentsClient}
|
|
59
|
+
*/
|
|
60
|
+
#clientForOperation (projectName) {
|
|
61
|
+
if (projectName !== undefined && projectName !== this.#projectName) {
|
|
62
|
+
return this.#clientForProject(projectName)
|
|
63
|
+
}
|
|
64
|
+
if (this.#client === undefined) this.#client = this.#clientForProject(projectName)
|
|
65
|
+
return this.#client
|
|
66
|
+
}
|
|
67
|
+
|
|
54
68
|
// Create a local dataset buffer. Pushed remotely on first experiment run.
|
|
55
69
|
createDataset (name, descriptionOrOptions = '') {
|
|
56
70
|
const options = typeof descriptionOrOptions === 'string'
|
|
57
71
|
? { description: descriptionOrOptions }
|
|
58
72
|
: (descriptionOrOptions ?? {})
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
if ((options.records) != null)
|
|
62
|
-
for (const record of options.records) {
|
|
63
|
-
if (record.id !== undefined && (typeof record.id !== 'string' || record.id.length === 0)) {
|
|
64
|
-
throw new Error('record id must be a non-empty string')
|
|
65
|
-
}
|
|
66
|
-
if (record.id !== undefined) {
|
|
67
|
-
if (recordIds.has(record.id)) throw new Error(`Duplicate record id '${record.id}'`)
|
|
68
|
-
recordIds.add(record.id)
|
|
69
|
-
}
|
|
70
|
-
dataset.addRecord(
|
|
71
|
-
new DatasetRecord(record.inputData, record.expectedOutput, record.metadata, record.id, record.tags)
|
|
72
|
-
)
|
|
73
|
-
}
|
|
74
|
-
}
|
|
73
|
+
const client = this.#clientForOperation(options.projectName)
|
|
74
|
+
const dataset = new Dataset(client, name, options.description ?? '')
|
|
75
|
+
if ((options.records) != null) dataset.addRecords(options.records)
|
|
75
76
|
return dataset
|
|
76
77
|
}
|
|
77
78
|
|
|
@@ -80,9 +81,11 @@ class Experiments {
|
|
|
80
81
|
// wait until that many records are readable. Pass `tags` to filter records by
|
|
81
82
|
// dataset record tags.
|
|
82
83
|
async pullDataset (name, options = {}) {
|
|
83
|
-
const { expectedRecordCount, maxWaitMs = 30_000, tags, version } = options
|
|
84
|
+
const { expectedRecordCount, maxWaitMs = 30_000, projectName, tags, version } = options
|
|
84
85
|
const filterTags = validateTagsList(tags)
|
|
85
|
-
const
|
|
86
|
+
const client = this.#clientForOperation(projectName)
|
|
87
|
+
const resolvedProjectName = projectName ?? this.#projectName
|
|
88
|
+
const projectId = await client.ensureProjectId()
|
|
86
89
|
|
|
87
90
|
let pulledDataset = null
|
|
88
91
|
let records = []
|
|
@@ -93,7 +96,7 @@ class Experiments {
|
|
|
93
96
|
const succeeded = await retryWithBackoff(async () => {
|
|
94
97
|
try {
|
|
95
98
|
if (pulledDataset === null) {
|
|
96
|
-
const datasets = await
|
|
99
|
+
const datasets = await client.listDatasets(projectId, { name })
|
|
97
100
|
for (const dataset of datasets) {
|
|
98
101
|
if (dataset.name() === name) {
|
|
99
102
|
pulledDataset = dataset
|
|
@@ -109,7 +112,7 @@ class Experiments {
|
|
|
109
112
|
// Follow the meta.after / page[cursor] pagination until the last page.
|
|
110
113
|
for (;;) {
|
|
111
114
|
// eslint-disable-next-line no-await-in-loop
|
|
112
|
-
const page = await
|
|
115
|
+
const page = await client.listDatasetRecords(projectId, pulledDataset.id(), {
|
|
113
116
|
cursor,
|
|
114
117
|
tags: filterTags,
|
|
115
118
|
version: datasetVersion,
|
|
@@ -129,13 +132,13 @@ class Experiments {
|
|
|
129
132
|
}, { maxTotalMs: maxWaitMs })
|
|
130
133
|
|
|
131
134
|
if (pulledDataset === null && lastError) {
|
|
132
|
-
throw new Error(`Failed to list datasets in project '${
|
|
135
|
+
throw new Error(`Failed to list datasets in project '${resolvedProjectName}': ${lastError}`)
|
|
133
136
|
}
|
|
134
137
|
if (pulledDataset === null) {
|
|
135
|
-
throw new Error(`Dataset '${name}' not found in project '${
|
|
138
|
+
throw new Error(`Dataset '${name}' not found in project '${resolvedProjectName}' (after ${maxWaitMs}ms)`)
|
|
136
139
|
}
|
|
137
140
|
if (!succeeded && lastError) {
|
|
138
|
-
throw new Error(`Failed to fetch records for dataset '${name}' in project '${
|
|
141
|
+
throw new Error(`Failed to fetch records for dataset '${name}' in project '${resolvedProjectName}': ${lastError}`)
|
|
139
142
|
}
|
|
140
143
|
if (!succeeded && expectedRecordCount != null) {
|
|
141
144
|
throw new Error(
|
|
@@ -151,7 +154,7 @@ class Experiments {
|
|
|
151
154
|
}
|
|
152
155
|
|
|
153
156
|
return Dataset.fromExisting(
|
|
154
|
-
|
|
157
|
+
client,
|
|
155
158
|
name,
|
|
156
159
|
pulledDataset.description(),
|
|
157
160
|
pulledDataset.id(),
|
|
@@ -163,9 +166,26 @@ class Experiments {
|
|
|
163
166
|
)
|
|
164
167
|
}
|
|
165
168
|
|
|
166
|
-
// Build an experiment
|
|
169
|
+
// Build an experiment with a dataset, task, evaluators, and optional project/config/tags.
|
|
167
170
|
experiment (options) {
|
|
168
|
-
|
|
171
|
+
const datasetProjectName = options?.dataset?.projectName?.()
|
|
172
|
+
if (options?.projectName !== undefined &&
|
|
173
|
+
datasetProjectName !== undefined &&
|
|
174
|
+
options.projectName !== datasetProjectName) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`Experiment project '${options.projectName}' does not match dataset project '${datasetProjectName}'`
|
|
177
|
+
)
|
|
178
|
+
}
|
|
179
|
+
const projectName = options?.projectName ?? datasetProjectName
|
|
180
|
+
const client = this.#clientForOperation(projectName)
|
|
181
|
+
const usesDatasetOverride = datasetProjectName !== undefined && datasetProjectName !== this.#projectName
|
|
182
|
+
const resolvedProjectName = projectName ?? this.#config.llmobs?.projectName
|
|
183
|
+
const experimentOptions = options?.projectName === undefined &&
|
|
184
|
+
(usesDatasetOverride || this.#config.llmobs?.projectName !== undefined) &&
|
|
185
|
+
resolvedProjectName !== undefined
|
|
186
|
+
? { ...options, projectName: resolvedProjectName }
|
|
187
|
+
: options
|
|
188
|
+
return new Experiment(client, experimentOptions, this.#llmobs)
|
|
169
189
|
}
|
|
170
190
|
|
|
171
191
|
/**
|
|
@@ -177,10 +197,11 @@ class Experiments {
|
|
|
177
197
|
* @returns {Promise<ExternalExperiment>}
|
|
178
198
|
*/
|
|
179
199
|
startExperiment (options) {
|
|
180
|
-
const client = options?.projectName
|
|
181
|
-
|
|
182
|
-
: this.#
|
|
183
|
-
|
|
200
|
+
const client = this.#clientForOperation(options?.projectName)
|
|
201
|
+
const experimentOptions = options?.projectName === undefined && this.#config.llmobs?.projectName !== undefined
|
|
202
|
+
? { ...options, projectName: this.#config.llmobs.projectName }
|
|
203
|
+
: options
|
|
204
|
+
return new Experiment(client, { ...experimentOptions, external: true }).start()
|
|
184
205
|
.then(experiment => new ExternalExperiment(experiment))
|
|
185
206
|
}
|
|
186
207
|
}
|
|
@@ -195,15 +216,6 @@ function createExperiments (config, llmobs) {
|
|
|
195
216
|
log.warn('LLMObs experiments: missing api and/or app keys, set DD_API_KEY and DD_APP_KEY')
|
|
196
217
|
return new NoopExperiments('DD_API_KEY and DD_APP_KEY are required for experiments')
|
|
197
218
|
}
|
|
198
|
-
if (!config.llmobs?.mlApp && !config.service) {
|
|
199
|
-
const reason = 'no project name configured; set the DD_LLMOBS_ML_APP environment variable (or llmobs.mlApp in ' +
|
|
200
|
-
'tracer.init()) to name the LLM Obs project, or DD_SERVICE (or service in tracer.init()) as a fallback, ' +
|
|
201
|
-
'then retry'
|
|
202
|
-
const experiments = new Experiments(config, llmobs)
|
|
203
|
-
return new NoopExperiments(reason, {
|
|
204
|
-
startExperiment: (options) => experiments.startExperiment(options),
|
|
205
|
-
})
|
|
206
|
-
}
|
|
207
219
|
return new Experiments(config, llmobs)
|
|
208
220
|
}
|
|
209
221
|
|
|
@@ -7,6 +7,15 @@ const NOOP_EXPERIMENT_ID = '00000000-0000-0000-0000-000000000000'
|
|
|
7
7
|
const NOOP_SPAN_ID = '0000000000000000'
|
|
8
8
|
const NOOP_TRACE_ID = '00000000000000000000000000000000'
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {object} DatasetRecordNew
|
|
12
|
+
* @property {string} [id]
|
|
13
|
+
* @property {unknown} inputData
|
|
14
|
+
* @property {unknown} [expectedOutput]
|
|
15
|
+
* @property {Record<string, unknown>} [metadata]
|
|
16
|
+
* @property {string[]} [tags]
|
|
17
|
+
*/
|
|
18
|
+
|
|
10
19
|
class NoopDataset {
|
|
11
20
|
#name
|
|
12
21
|
#description
|
|
@@ -37,6 +46,24 @@ class NoopDataset {
|
|
|
37
46
|
return this
|
|
38
47
|
}
|
|
39
48
|
|
|
49
|
+
/**
|
|
50
|
+
* Add multiple records to a dataset.
|
|
51
|
+
* @param {DatasetRecordNew[]} records
|
|
52
|
+
* @returns {NoopDataset} This dataset for chaining.
|
|
53
|
+
*/
|
|
54
|
+
addRecords (records) {
|
|
55
|
+
for (const record of records) {
|
|
56
|
+
this.#records.push({
|
|
57
|
+
id: record.id ?? null,
|
|
58
|
+
input: record.inputData,
|
|
59
|
+
expectedOutput: record.expectedOutput ?? null,
|
|
60
|
+
metadata: record.metadata ?? {},
|
|
61
|
+
tags: [...(record.tags ?? [])],
|
|
62
|
+
})
|
|
63
|
+
}
|
|
64
|
+
return this
|
|
65
|
+
}
|
|
66
|
+
|
|
40
67
|
update (index, fields) {
|
|
41
68
|
const record = this.#records[index]
|
|
42
69
|
if (record == null) return this
|
|
@@ -114,6 +141,10 @@ class NoopDataset {
|
|
|
114
141
|
return null
|
|
115
142
|
}
|
|
116
143
|
|
|
144
|
+
projectName () {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
|
|
117
148
|
version () {
|
|
118
149
|
return null
|
|
119
150
|
}
|
|
@@ -165,7 +196,13 @@ class NoopExperiment {
|
|
|
165
196
|
}
|
|
166
197
|
|
|
167
198
|
run () {
|
|
168
|
-
return Promise.resolve({
|
|
199
|
+
return Promise.resolve({
|
|
200
|
+
experimentId: null,
|
|
201
|
+
rows: [],
|
|
202
|
+
summaryEvaluations: {},
|
|
203
|
+
runs: [],
|
|
204
|
+
url: null,
|
|
205
|
+
})
|
|
169
206
|
}
|
|
170
207
|
|
|
171
208
|
/**
|
|
@@ -200,11 +237,9 @@ class NoopExperiment {
|
|
|
200
237
|
// throwing, so intentionally disabled experiments remain graceful.
|
|
201
238
|
class NoopExperiments {
|
|
202
239
|
#reason
|
|
203
|
-
#startExperiment
|
|
204
240
|
|
|
205
|
-
constructor (reason
|
|
241
|
+
constructor (reason) {
|
|
206
242
|
this.#reason = reason || 'LLMObs experiments are not available'
|
|
207
|
-
this.#startExperiment = options.startExperiment
|
|
208
243
|
}
|
|
209
244
|
|
|
210
245
|
#warn () {
|
|
@@ -231,10 +266,6 @@ class NoopExperiments {
|
|
|
231
266
|
* @returns {Promise<ExternalExperiment>}
|
|
232
267
|
*/
|
|
233
268
|
startExperiment (options = {}) {
|
|
234
|
-
if (this.#startExperiment !== undefined && options.projectName) {
|
|
235
|
-
return this.#startExperiment(options)
|
|
236
|
-
}
|
|
237
|
-
|
|
238
269
|
this.#warn()
|
|
239
270
|
return Promise.resolve(new ExternalExperiment(new NoopExperiment(options.name, true)))
|
|
240
271
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
+
const { randomUUID } = require('node:crypto')
|
|
4
|
+
|
|
3
5
|
const log = require('../../log')
|
|
4
6
|
|
|
5
7
|
// Matches the backend and dd-trace-py evaluator metric label contract.
|
|
@@ -42,6 +44,23 @@ function tagOperationsAreEmpty (operations) {
|
|
|
42
44
|
)
|
|
43
45
|
}
|
|
44
46
|
|
|
47
|
+
/**
|
|
48
|
+
* @param {unknown} value
|
|
49
|
+
* @param {string} name
|
|
50
|
+
* @returns {number}
|
|
51
|
+
*/
|
|
52
|
+
function normalizePositiveInteger (value, name) {
|
|
53
|
+
if (!Number.isInteger(value) || value < 1) throw new Error(`${name} must be a positive integer`)
|
|
54
|
+
return value
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* @returns {string}
|
|
59
|
+
*/
|
|
60
|
+
function generateRunId () {
|
|
61
|
+
return randomUUID()
|
|
62
|
+
}
|
|
63
|
+
|
|
45
64
|
/**
|
|
46
65
|
* @param {string} name
|
|
47
66
|
*/
|
|
@@ -297,11 +316,13 @@ module.exports = {
|
|
|
297
316
|
buildSpanMetadata,
|
|
298
317
|
buildTags,
|
|
299
318
|
durationNs,
|
|
319
|
+
generateRunId,
|
|
300
320
|
hasEntries,
|
|
301
321
|
inferMetricType,
|
|
302
322
|
mergeTags,
|
|
303
323
|
normalizeEvaluators,
|
|
304
324
|
normalizeJsonMetricValue,
|
|
325
|
+
normalizePositiveInteger,
|
|
305
326
|
recordTagsToObject,
|
|
306
327
|
sleep,
|
|
307
328
|
stringify,
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
const { channel } = require('dc-polyfill')
|
|
4
4
|
|
|
5
5
|
const { readDatadogTags, writeDatadogTags } = require('../carrier')
|
|
6
|
+
const { registerTelemetryFlusher } = require('../flush')
|
|
6
7
|
const log = require('../log')
|
|
8
|
+
const { createServerlessDeliveryTracker } = require('../serverless')
|
|
7
9
|
const { DD_MAJOR } = require('../../../../version')
|
|
8
10
|
const startupLogs = require('../startup-log')
|
|
9
11
|
const {
|
|
@@ -57,6 +59,8 @@ let spanWriter
|
|
|
57
59
|
/** @type {LLMObsEvalMetricsWriter | null} */
|
|
58
60
|
let evalWriter
|
|
59
61
|
|
|
62
|
+
let unregisterTelemetryFlusher
|
|
63
|
+
|
|
60
64
|
/** @type {import('../config/config-base')} */
|
|
61
65
|
let globalTracerConfig
|
|
62
66
|
|
|
@@ -66,23 +70,36 @@ let globalTracerConfig
|
|
|
66
70
|
function enable (config) {
|
|
67
71
|
globalTracerConfig = config
|
|
68
72
|
|
|
73
|
+
const retiredSpanWriter = spanWriter
|
|
74
|
+
const retiredEvalWriter = evalWriter
|
|
75
|
+
const isReinitializing = Boolean(retiredSpanWriter || retiredEvalWriter)
|
|
76
|
+
unregisterTelemetryFlusher?.()
|
|
77
|
+
retireWriters(retiredSpanWriter, retiredEvalWriter)
|
|
78
|
+
|
|
69
79
|
const startTime = performance.now()
|
|
70
80
|
// create writers and eval writer append and flush channels
|
|
71
81
|
// span writer append is handled by the span processor
|
|
72
82
|
evalWriter = new LLMObsEvalMetricsWriter(config)
|
|
73
83
|
spanWriter = new LLMObsSpanWriter(config)
|
|
84
|
+
const currentEvalWriter = evalWriter
|
|
85
|
+
const currentSpanWriter = spanWriter
|
|
86
|
+
unregisterTelemetryFlusher = registerTelemetryFlusher(done => {
|
|
87
|
+
flushWriters(done, currentSpanWriter, currentEvalWriter)
|
|
88
|
+
})
|
|
74
89
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
90
|
+
if (!isReinitializing) {
|
|
91
|
+
evalMetricAppendCh.subscribe(handleEvalMetricAppend)
|
|
92
|
+
flushCh.subscribe(handleFlush)
|
|
93
|
+
registerUserSpanProcessorCh.subscribe(handleRegisterProcessor)
|
|
94
|
+
}
|
|
78
95
|
|
|
79
96
|
// span processing
|
|
80
97
|
spanProcessor = new LLMObsSpanProcessor(config)
|
|
81
98
|
spanProcessor.setWriter(spanWriter)
|
|
82
|
-
spanFinishCh.subscribe(handleSpanProcess)
|
|
99
|
+
if (!isReinitializing) spanFinishCh.subscribe(handleSpanProcess)
|
|
83
100
|
|
|
84
101
|
// distributed tracing for llmobs
|
|
85
|
-
injectCh.subscribe(handleLLMObsInjection)
|
|
102
|
+
if (!isReinitializing) injectCh.subscribe(handleLLMObsInjection)
|
|
86
103
|
|
|
87
104
|
setAgentStrategy(config, useAgentless => {
|
|
88
105
|
if (useAgentless && !(config.DD_API_KEY && config.site)) {
|
|
@@ -94,8 +111,10 @@ function enable (config) {
|
|
|
94
111
|
}
|
|
95
112
|
}
|
|
96
113
|
|
|
97
|
-
|
|
98
|
-
|
|
114
|
+
// A disable can happen while transport selection is still pending. Keep
|
|
115
|
+
// configuring these writers so their queued lifecycle flushes can drain.
|
|
116
|
+
currentEvalWriter.setAgentless(useAgentless)
|
|
117
|
+
currentSpanWriter.setAgentless(useAgentless)
|
|
99
118
|
|
|
100
119
|
telemetry.recordLLMObsEnabled(startTime, config)
|
|
101
120
|
log.debug('[LLMObs] Enabled LLM Observability with configuration: %o', config.llmobs)
|
|
@@ -109,16 +128,39 @@ function disable () {
|
|
|
109
128
|
if (injectCh.hasSubscribers) injectCh.unsubscribe(handleLLMObsInjection)
|
|
110
129
|
if (registerUserSpanProcessorCh.hasSubscribers) registerUserSpanProcessorCh.unsubscribe(handleRegisterProcessor)
|
|
111
130
|
|
|
112
|
-
spanWriter
|
|
113
|
-
evalWriter
|
|
131
|
+
const retiredSpanWriter = spanWriter
|
|
132
|
+
const retiredEvalWriter = evalWriter
|
|
114
133
|
spanProcessor?.setWriter(null)
|
|
134
|
+
unregisterTelemetryFlusher?.()
|
|
135
|
+
unregisterTelemetryFlusher = undefined
|
|
115
136
|
|
|
116
137
|
spanWriter = null
|
|
117
138
|
evalWriter = null
|
|
118
139
|
|
|
140
|
+
retireWriters(retiredSpanWriter, retiredEvalWriter)
|
|
141
|
+
|
|
119
142
|
log.debug('[LLMObs] Disabled LLM Observability')
|
|
120
143
|
}
|
|
121
144
|
|
|
145
|
+
/**
|
|
146
|
+
* Keeps retired writers reachable until their destroy-triggered deliveries complete.
|
|
147
|
+
* @param {LLMObsSpanWriter | null} retiredSpanWriter
|
|
148
|
+
* @param {LLMObsEvalMetricsWriter | null} retiredEvalWriter
|
|
149
|
+
* @returns {void}
|
|
150
|
+
*/
|
|
151
|
+
function retireWriters (retiredSpanWriter, retiredEvalWriter) {
|
|
152
|
+
const retiredWriters = [retiredSpanWriter, retiredEvalWriter].filter(Boolean)
|
|
153
|
+
if (retiredWriters.length === 0) return
|
|
154
|
+
let remainingWriters = retiredWriters.length
|
|
155
|
+
const unregisterRetiredFlusher = registerTelemetryFlusher(done => {
|
|
156
|
+
flushWriters(done, retiredSpanWriter, retiredEvalWriter)
|
|
157
|
+
})
|
|
158
|
+
function onWriterDestroyed () {
|
|
159
|
+
if (--remainingWriters === 0) unregisterRetiredFlusher?.()
|
|
160
|
+
}
|
|
161
|
+
for (const writer of retiredWriters) writer.destroy(onWriterDestroyed)
|
|
162
|
+
}
|
|
163
|
+
|
|
122
164
|
// since LLMObs traces can extend between services and be the same trace,
|
|
123
165
|
// we need to propagate the parent id, mlApp, session id, and sampling rate/decision.
|
|
124
166
|
function handleLLMObsInjection ({ carrier }) {
|
|
@@ -192,15 +234,36 @@ function handleLLMObsInjection ({ carrier }) {
|
|
|
192
234
|
if (tags !== existing) writeDatadogTags(carrier, tags)
|
|
193
235
|
}
|
|
194
236
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
237
|
+
/**
|
|
238
|
+
* Flushes the specified LLMObs writers and joins deliveries active at the boundary.
|
|
239
|
+
* @param {Function} [done]
|
|
240
|
+
* @param {LLMObsSpanWriter | null} [currentSpanWriter]
|
|
241
|
+
* @param {LLMObsEvalMetricsWriter | null} [currentEvalWriter]
|
|
242
|
+
* @returns {boolean} `true` when a writer throws synchronously.
|
|
243
|
+
*/
|
|
244
|
+
function flushWriters (done, currentSpanWriter = spanWriter, currentEvalWriter = evalWriter) {
|
|
245
|
+
let failed = false
|
|
246
|
+
const deliveryTracker = createServerlessDeliveryTracker()
|
|
247
|
+
const flush = writer => {
|
|
248
|
+
try {
|
|
249
|
+
if (deliveryTracker && writer) deliveryTracker.track(complete => writer.flush(complete))
|
|
250
|
+
// Non-serverless flushes retain the existing writer behavior.
|
|
251
|
+
else writer?.flush()
|
|
252
|
+
} catch (error) {
|
|
253
|
+
failed = true
|
|
254
|
+
log.warn('Failed to flush LLMObs writer:', error.message)
|
|
255
|
+
}
|
|
203
256
|
}
|
|
257
|
+
|
|
258
|
+
flush(currentSpanWriter)
|
|
259
|
+
flush(currentEvalWriter)
|
|
260
|
+
deliveryTracker?.waitForIdle(done)
|
|
261
|
+
if (!deliveryTracker) done?.()
|
|
262
|
+
return failed
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
function handleFlush () {
|
|
266
|
+
const err = flushWriters() ? 'writer_flush_error' : ''
|
|
204
267
|
telemetry.recordUserFlush(err)
|
|
205
268
|
}
|
|
206
269
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
let NoopExperiments
|
|
4
4
|
|
|
5
5
|
class NoopLLMObs {
|
|
6
6
|
constructor (noopTracer) {
|
|
@@ -12,6 +12,7 @@ class NoopLLMObs {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
get experiments () {
|
|
15
|
+
NoopExperiments ??= require('./experiments/noop')
|
|
15
16
|
return new NoopExperiments('LLM Observability is not enabled')
|
|
16
17
|
}
|
|
17
18
|
|
|
@@ -38,16 +38,20 @@ function formatAnthropicToolResultContent (content) {
|
|
|
38
38
|
if (typeof content === 'string') {
|
|
39
39
|
return content
|
|
40
40
|
} else if (Array.isArray(content)) {
|
|
41
|
-
|
|
41
|
+
let formattedContent = ''
|
|
42
42
|
for (const toolResultBlock of content) {
|
|
43
|
+
let part
|
|
43
44
|
if (toolResultBlock.text) {
|
|
44
|
-
|
|
45
|
+
part = toolResultBlock.text
|
|
45
46
|
} else if (toolResultBlock.type === 'image') {
|
|
46
|
-
|
|
47
|
+
part = '([IMAGE DETECTED])'
|
|
47
48
|
}
|
|
49
|
+
if (part === undefined) continue
|
|
50
|
+
if (formattedContent) formattedContent += ','
|
|
51
|
+
formattedContent += part
|
|
48
52
|
}
|
|
49
53
|
|
|
50
|
-
return formattedContent
|
|
54
|
+
return formattedContent
|
|
51
55
|
}
|
|
52
56
|
return JSON.stringify(content)
|
|
53
57
|
}
|
|
@@ -18,12 +18,15 @@ function getToolOutputText (raw) {
|
|
|
18
18
|
if (raw == null) return
|
|
19
19
|
|
|
20
20
|
if (Array.isArray(raw)) {
|
|
21
|
-
|
|
21
|
+
let output = ''
|
|
22
|
+
let separator = ''
|
|
22
23
|
for (const block of raw) {
|
|
23
24
|
const text = getToolOutputText(block)
|
|
24
|
-
if (text)
|
|
25
|
+
if (!text) continue
|
|
26
|
+
output += separator + text
|
|
27
|
+
separator = '\n'
|
|
25
28
|
}
|
|
26
|
-
return output
|
|
29
|
+
return output
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
if (raw.type === 'tool_result') return getToolOutputText(raw.content)
|
|
@@ -134,15 +134,17 @@ function hasMultimodalInputs (variables) {
|
|
|
134
134
|
* @returns {{ content: string, audioParts: Array<{ mimeType: string, content: string }> }}
|
|
135
135
|
*/
|
|
136
136
|
function extractContentParts (parts) {
|
|
137
|
-
|
|
137
|
+
let content = ''
|
|
138
|
+
let hasContent = false
|
|
138
139
|
const audioParts = []
|
|
139
140
|
|
|
140
141
|
for (const part of parts) {
|
|
141
142
|
const partType = part?.type ?? ''
|
|
143
|
+
let extracted
|
|
142
144
|
if (partType === 'text') {
|
|
143
|
-
extracted
|
|
145
|
+
extracted = part.text ?? ''
|
|
144
146
|
} else if (partType === 'image_url') {
|
|
145
|
-
extracted
|
|
147
|
+
extracted = IMAGE_FALLBACK
|
|
146
148
|
} else if (partType === 'input_audio') {
|
|
147
149
|
const inputAudio = part.input_audio ?? {}
|
|
148
150
|
const data = inputAudio.data
|
|
@@ -151,14 +153,19 @@ function extractContentParts (parts) {
|
|
|
151
153
|
// is needed. Only fall back to "[audio]" when there's no audio to capture.
|
|
152
154
|
audioParts.push(formatAudioPart(data, audioMimeTypeFromFormat(inputAudio.format, AUDIO_MIME_TYPES)))
|
|
153
155
|
} else {
|
|
154
|
-
extracted
|
|
156
|
+
extracted = AUDIO_FALLBACK
|
|
155
157
|
}
|
|
156
158
|
} else {
|
|
157
|
-
extracted
|
|
159
|
+
extracted = `[${partType}]`
|
|
158
160
|
}
|
|
161
|
+
|
|
162
|
+
if (extracted === undefined) continue
|
|
163
|
+
if (hasContent) content += '\n'
|
|
164
|
+
content += extracted
|
|
165
|
+
hasContent = true
|
|
159
166
|
}
|
|
160
167
|
|
|
161
|
-
return { content
|
|
168
|
+
return { content, audioParts }
|
|
162
169
|
}
|
|
163
170
|
|
|
164
171
|
/**
|
|
@@ -11,23 +11,23 @@ const { safeJsonParse } = require('../../util')
|
|
|
11
11
|
* @returns {{ content: string, audioParts: Array<{ mimeType: string, content: string }> }}
|
|
12
12
|
*/
|
|
13
13
|
function extractMessageContent (parts) {
|
|
14
|
-
|
|
14
|
+
let content = ''
|
|
15
15
|
const audioParts = []
|
|
16
16
|
|
|
17
17
|
for (const part of parts) {
|
|
18
18
|
if (!part) continue
|
|
19
19
|
const text = extractTextFromContentItem(part)
|
|
20
20
|
if (text) {
|
|
21
|
-
|
|
21
|
+
content += text
|
|
22
22
|
continue
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
const extracted = extractContentParts([part])
|
|
26
|
-
if (extracted.content)
|
|
26
|
+
if (extracted.content) content += extracted.content
|
|
27
27
|
if (extracted.audioParts.length > 0) audioParts.push(...extracted.audioParts)
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
return { content
|
|
30
|
+
return { content, audioParts }
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
@@ -167,13 +167,11 @@ function extractOutputMessages (result) {
|
|
|
167
167
|
if (item.type === 'message') {
|
|
168
168
|
let content = ''
|
|
169
169
|
if (Array.isArray(item.content)) {
|
|
170
|
-
const textParts = []
|
|
171
170
|
for (const contentItem of item.content) {
|
|
172
171
|
if (contentItem?.type === 'output_text' && contentItem.text) {
|
|
173
|
-
|
|
172
|
+
content += contentItem.text
|
|
174
173
|
}
|
|
175
174
|
}
|
|
176
|
-
content = textParts.join('')
|
|
177
175
|
} else if (typeof item.content === 'string') {
|
|
178
176
|
content = item.content
|
|
179
177
|
}
|