dd-trace 5.66.0 → 5.67.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/index.d.ts
CHANGED
|
@@ -2566,6 +2566,25 @@ declare namespace tracer {
|
|
|
2566
2566
|
annotate (options: llmobs.AnnotationOptions): void
|
|
2567
2567
|
annotate (span: tracer.Span | undefined, options: llmobs.AnnotationOptions): void
|
|
2568
2568
|
|
|
2569
|
+
/**
|
|
2570
|
+
* Register a processor to be called on each LLMObs span.
|
|
2571
|
+
*
|
|
2572
|
+
* This can be used to modify the span before it is sent to LLMObs. For example, you can modify the input/output.
|
|
2573
|
+
* You can also return `null` to omit the span entirely from being sent to LLM Observability.
|
|
2574
|
+
*
|
|
2575
|
+
* Otherwise, if the return value from the processor is not an instance of `LLMObservabilitySpan`, the span will be dropped.
|
|
2576
|
+
*
|
|
2577
|
+
* To deregister the processor, call `llmobs.deregisterProcessor()`
|
|
2578
|
+
* @param processor A function that will be called for each span.
|
|
2579
|
+
* @throws {Error} If a processor is already registered.
|
|
2580
|
+
*/
|
|
2581
|
+
registerProcessor (processor: ((span: LLMObservabilitySpan) => LLMObservabilitySpan | null)): void
|
|
2582
|
+
|
|
2583
|
+
/**
|
|
2584
|
+
* Deregister a processor.
|
|
2585
|
+
*/
|
|
2586
|
+
deregisterProcessor (): void
|
|
2587
|
+
|
|
2569
2588
|
/**
|
|
2570
2589
|
* Submits a custom evaluation metric for a given span ID and trace ID.
|
|
2571
2590
|
* @param spanContext The span context of the span to submit the evaluation metric for.
|
|
@@ -2579,6 +2598,25 @@ declare namespace tracer {
|
|
|
2579
2598
|
flush (): void
|
|
2580
2599
|
}
|
|
2581
2600
|
|
|
2601
|
+
interface LLMObservabilitySpan {
|
|
2602
|
+
/**
|
|
2603
|
+
* The input content associated with the span.
|
|
2604
|
+
*/
|
|
2605
|
+
input: { content: string, role?: string }[]
|
|
2606
|
+
|
|
2607
|
+
/**
|
|
2608
|
+
* The output content associated with the span.
|
|
2609
|
+
*/
|
|
2610
|
+
output: { content: string, role?: string }[]
|
|
2611
|
+
|
|
2612
|
+
/**
|
|
2613
|
+
* Get a tag from the span.
|
|
2614
|
+
* @param key The key of the tag to get.
|
|
2615
|
+
* @returns The value of the tag, or `undefined` if the tag does not exist.
|
|
2616
|
+
*/
|
|
2617
|
+
getTag (key: string): string | undefined
|
|
2618
|
+
}
|
|
2619
|
+
|
|
2582
2620
|
interface EvaluationOptions {
|
|
2583
2621
|
/**
|
|
2584
2622
|
* The name of the evaluation metric
|
package/package.json
CHANGED
|
@@ -16,6 +16,7 @@ const spanProcessCh = channel('dd-trace:span:process')
|
|
|
16
16
|
const evalMetricAppendCh = channel('llmobs:eval-metric:append')
|
|
17
17
|
const flushCh = channel('llmobs:writers:flush')
|
|
18
18
|
const injectCh = channel('dd-trace:span:inject')
|
|
19
|
+
const registerUserSpanProcessorCh = channel('llmobs:register-processor')
|
|
19
20
|
|
|
20
21
|
const LLMObsEvalMetricsWriter = require('./writers/evaluations')
|
|
21
22
|
const LLMObsTagger = require('./tagger')
|
|
@@ -56,6 +57,7 @@ function enable (config) {
|
|
|
56
57
|
|
|
57
58
|
evalMetricAppendCh.subscribe(handleEvalMetricAppend)
|
|
58
59
|
flushCh.subscribe(handleFlush)
|
|
60
|
+
registerUserSpanProcessorCh.subscribe(handleRegisterProcessor)
|
|
59
61
|
|
|
60
62
|
// span processing
|
|
61
63
|
spanProcessor = new LLMObsSpanProcessor(config)
|
|
@@ -86,6 +88,7 @@ function disable () {
|
|
|
86
88
|
if (flushCh.hasSubscribers) flushCh.unsubscribe(handleFlush)
|
|
87
89
|
if (spanProcessCh.hasSubscribers) spanProcessCh.unsubscribe(handleSpanProcess)
|
|
88
90
|
if (injectCh.hasSubscribers) injectCh.unsubscribe(handleLLMObsParentIdInjection)
|
|
91
|
+
if (registerUserSpanProcessorCh.hasSubscribers) registerUserSpanProcessorCh.unsubscribe(handleRegisterProcessor)
|
|
89
92
|
|
|
90
93
|
spanWriter?.destroy()
|
|
91
94
|
evalWriter?.destroy()
|
|
@@ -126,6 +129,10 @@ function handleFlush () {
|
|
|
126
129
|
telemetry.recordUserFlush(err)
|
|
127
130
|
}
|
|
128
131
|
|
|
132
|
+
function handleRegisterProcessor (userSpanProcessor) {
|
|
133
|
+
spanProcessor.setUserSpanProcessor(userSpanProcessor)
|
|
134
|
+
}
|
|
135
|
+
|
|
129
136
|
function handleSpanProcess (data) {
|
|
130
137
|
spanProcessor.process(data)
|
|
131
138
|
}
|
|
@@ -23,9 +23,16 @@ const LLMObsTagger = require('./tagger')
|
|
|
23
23
|
const { channel } = require('dc-polyfill')
|
|
24
24
|
const evalMetricAppendCh = channel('llmobs:eval-metric:append')
|
|
25
25
|
const flushCh = channel('llmobs:writers:flush')
|
|
26
|
+
const registerUserSpanProcessorCh = channel('llmobs:register-processor')
|
|
26
27
|
const NoopLLMObs = require('./noop')
|
|
27
28
|
|
|
28
29
|
class LLMObs extends NoopLLMObs {
|
|
30
|
+
/**
|
|
31
|
+
* flag representing if a user span processor has been registered
|
|
32
|
+
* @type {boolean}
|
|
33
|
+
*/
|
|
34
|
+
#hasUserSpanProcessor = false
|
|
35
|
+
|
|
29
36
|
constructor (tracer, llmobsModule, config) {
|
|
30
37
|
super(tracer)
|
|
31
38
|
|
|
@@ -309,6 +316,27 @@ class LLMObs extends NoopLLMObs {
|
|
|
309
316
|
}
|
|
310
317
|
}
|
|
311
318
|
|
|
319
|
+
registerProcessor (processor) {
|
|
320
|
+
if (!this.enabled) return
|
|
321
|
+
|
|
322
|
+
if (this.#hasUserSpanProcessor) {
|
|
323
|
+
throw new Error(
|
|
324
|
+
'[LLMObs] Only one user span processor can be registered. ' +
|
|
325
|
+
'To register a new processor, deregister the existing processor first using `llmobs.deregisterProcessor()`.'
|
|
326
|
+
)
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
this.#hasUserSpanProcessor = true
|
|
330
|
+
registerUserSpanProcessorCh.publish(processor)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
deregisterProcessor () {
|
|
334
|
+
if (!this.enabled) return
|
|
335
|
+
|
|
336
|
+
this.#hasUserSpanProcessor = false
|
|
337
|
+
registerUserSpanProcessorCh.publish(null)
|
|
338
|
+
}
|
|
339
|
+
|
|
312
340
|
submitEvaluation (llmobsSpanContext, options = {}) {
|
|
313
341
|
if (!this.enabled) return
|
|
314
342
|
|
|
@@ -34,25 +34,55 @@ const LLMObsTagger = require('./tagger')
|
|
|
34
34
|
const tracerVersion = require('../../../../package.json').version
|
|
35
35
|
const logger = require('../log')
|
|
36
36
|
|
|
37
|
+
const util = require('node:util')
|
|
38
|
+
|
|
39
|
+
class LLMObservabilitySpan {
|
|
40
|
+
constructor () {
|
|
41
|
+
this.input = []
|
|
42
|
+
this.output = []
|
|
43
|
+
|
|
44
|
+
this._tags = {}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
getTag (key) {
|
|
48
|
+
return this._tags[key]
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
class LLMObsSpanProcessor {
|
|
53
|
+
/** @type {import('../config')} */
|
|
54
|
+
#config
|
|
55
|
+
|
|
56
|
+
/** @type {((span: LLMObservabilitySpan) => LLMObservabilitySpan | null) | null} */
|
|
57
|
+
#userSpanProcessor
|
|
58
|
+
|
|
59
|
+
/** @type {import('./writers/spans')} */
|
|
60
|
+
#writer
|
|
61
|
+
|
|
38
62
|
constructor (config) {
|
|
39
|
-
this
|
|
63
|
+
this.#config = config
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
setUserSpanProcessor (userSpanProcessor) {
|
|
67
|
+
this.#userSpanProcessor = userSpanProcessor
|
|
40
68
|
}
|
|
41
69
|
|
|
42
70
|
setWriter (writer) {
|
|
43
|
-
this
|
|
71
|
+
this.#writer = writer
|
|
44
72
|
}
|
|
45
73
|
|
|
46
74
|
// TODO: instead of relying on the tagger's weakmap registry, can we use some namespaced storage correlation?
|
|
47
75
|
process ({ span }) {
|
|
48
|
-
if (!this.
|
|
76
|
+
if (!this.#config.llmobs.enabled) return
|
|
49
77
|
// if the span is not in our private tagger map, it is not an llmobs span
|
|
50
78
|
if (!LLMObsTagger.tagMap.has(span)) return
|
|
51
79
|
|
|
52
80
|
try {
|
|
53
81
|
const formattedEvent = this.format(span)
|
|
54
82
|
telemetry.incrementLLMObsSpanFinishedCount(span)
|
|
55
|
-
|
|
83
|
+
if (formattedEvent == null) return
|
|
84
|
+
|
|
85
|
+
this.#writer.append(formattedEvent)
|
|
56
86
|
} catch (e) {
|
|
57
87
|
// this should be a rare case
|
|
58
88
|
// we protect against unserializable properties in the format function, and in
|
|
@@ -65,6 +95,9 @@ class LLMObsSpanProcessor {
|
|
|
65
95
|
}
|
|
66
96
|
|
|
67
97
|
format (span) {
|
|
98
|
+
const llmObsSpan = new LLMObservabilitySpan()
|
|
99
|
+
let inputType, outputType
|
|
100
|
+
|
|
68
101
|
const spanTags = span.context()._tags
|
|
69
102
|
const mlObsTags = LLMObsTagger.tagMap.get(span)
|
|
70
103
|
|
|
@@ -78,26 +111,29 @@ class LLMObsSpanProcessor {
|
|
|
78
111
|
meta.model_name = mlObsTags[MODEL_NAME] || 'custom'
|
|
79
112
|
meta.model_provider = (mlObsTags[MODEL_PROVIDER] || 'custom').toLowerCase()
|
|
80
113
|
}
|
|
114
|
+
|
|
81
115
|
if (mlObsTags[METADATA]) {
|
|
82
|
-
this
|
|
116
|
+
this.#addObject(mlObsTags[METADATA], meta.metadata = {})
|
|
83
117
|
}
|
|
118
|
+
|
|
84
119
|
if (spanKind === 'llm' && mlObsTags[INPUT_MESSAGES]) {
|
|
85
|
-
input
|
|
86
|
-
|
|
87
|
-
if (mlObsTags[
|
|
88
|
-
input.value = mlObsTags[INPUT_VALUE]
|
|
89
|
-
}
|
|
90
|
-
if (spanKind === 'llm' && mlObsTags[OUTPUT_MESSAGES]) {
|
|
91
|
-
output.messages = mlObsTags[OUTPUT_MESSAGES]
|
|
92
|
-
}
|
|
93
|
-
if (spanKind === 'embedding' && mlObsTags[INPUT_DOCUMENTS]) {
|
|
120
|
+
llmObsSpan.input = mlObsTags[INPUT_MESSAGES]
|
|
121
|
+
inputType = 'messages'
|
|
122
|
+
} else if (spanKind === 'embedding' && mlObsTags[INPUT_DOCUMENTS]) {
|
|
94
123
|
input.documents = mlObsTags[INPUT_DOCUMENTS]
|
|
124
|
+
} else if (mlObsTags[INPUT_VALUE]) {
|
|
125
|
+
llmObsSpan.input = [{ role: '', content: mlObsTags[INPUT_VALUE] }]
|
|
126
|
+
inputType = 'value'
|
|
95
127
|
}
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
128
|
+
|
|
129
|
+
if (spanKind === 'llm' && mlObsTags[OUTPUT_MESSAGES]) {
|
|
130
|
+
llmObsSpan.output = mlObsTags[OUTPUT_MESSAGES]
|
|
131
|
+
outputType = 'messages'
|
|
132
|
+
} else if (spanKind === 'retrieval' && mlObsTags[OUTPUT_DOCUMENTS]) {
|
|
100
133
|
output.documents = mlObsTags[OUTPUT_DOCUMENTS]
|
|
134
|
+
} else if (mlObsTags[OUTPUT_VALUE]) {
|
|
135
|
+
llmObsSpan.output = [{ role: '', content: mlObsTags[OUTPUT_VALUE] }]
|
|
136
|
+
outputType = 'value'
|
|
101
137
|
}
|
|
102
138
|
|
|
103
139
|
const error = spanTags.error || spanTags[ERROR_TYPE]
|
|
@@ -107,9 +143,6 @@ class LLMObsSpanProcessor {
|
|
|
107
143
|
meta[ERROR_STACK] = spanTags[ERROR_STACK] || error.stack
|
|
108
144
|
}
|
|
109
145
|
|
|
110
|
-
if (input) meta.input = input
|
|
111
|
-
if (output) meta.output = output
|
|
112
|
-
|
|
113
146
|
const metrics = mlObsTags[METRICS] || {}
|
|
114
147
|
|
|
115
148
|
const mlApp = mlObsTags[ML_APP]
|
|
@@ -118,12 +151,37 @@ class LLMObsSpanProcessor {
|
|
|
118
151
|
|
|
119
152
|
const name = mlObsTags[NAME] || span._name
|
|
120
153
|
|
|
154
|
+
const tags = this.#getTags(span, mlApp, sessionId, error)
|
|
155
|
+
llmObsSpan._tags = tags
|
|
156
|
+
|
|
157
|
+
const processedSpan = this.#runProcessor(llmObsSpan)
|
|
158
|
+
if (processedSpan === null) return null
|
|
159
|
+
|
|
160
|
+
if (processedSpan.input) {
|
|
161
|
+
if (inputType === 'messages') {
|
|
162
|
+
input.messages = processedSpan.input
|
|
163
|
+
} else if (inputType === 'value') {
|
|
164
|
+
input.value = processedSpan.input[0].content
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (processedSpan.output) {
|
|
169
|
+
if (outputType === 'messages') {
|
|
170
|
+
output.messages = processedSpan.output
|
|
171
|
+
} else if (outputType === 'value') {
|
|
172
|
+
output.value = processedSpan.output[0].content
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
if (input) meta.input = input
|
|
177
|
+
if (output) meta.output = output
|
|
178
|
+
|
|
121
179
|
const llmObsSpanEvent = {
|
|
122
180
|
trace_id: span.context().toTraceId(true),
|
|
123
181
|
span_id: span.context().toSpanId(),
|
|
124
182
|
parent_id: parentId,
|
|
125
183
|
name,
|
|
126
|
-
tags: this
|
|
184
|
+
tags: this.#objectTagsToStringArrayTags(tags),
|
|
127
185
|
start_ns: Math.round(span._startTime * 1e6),
|
|
128
186
|
duration: Math.round(span._duration * 1e6),
|
|
129
187
|
status: error ? 'error' : 'ok',
|
|
@@ -144,7 +202,7 @@ class LLMObsSpanProcessor {
|
|
|
144
202
|
// However, we want to protect against circular references or BigInts (unserializable)
|
|
145
203
|
// This function can be reused for other fields if needed
|
|
146
204
|
// Messages, Documents, and Metrics are safeguarded in `llmobs/tagger.js`
|
|
147
|
-
|
|
205
|
+
#addObject (obj, carrier) {
|
|
148
206
|
const seenObjects = new WeakSet()
|
|
149
207
|
seenObjects.add(obj) // capture root object
|
|
150
208
|
|
|
@@ -176,12 +234,12 @@ class LLMObsSpanProcessor {
|
|
|
176
234
|
add(obj, carrier)
|
|
177
235
|
}
|
|
178
236
|
|
|
179
|
-
|
|
237
|
+
#getTags (span, mlApp, sessionId, error) {
|
|
180
238
|
let tags = {
|
|
181
|
-
...this.
|
|
182
|
-
version: this.
|
|
183
|
-
env: this.
|
|
184
|
-
service: this.
|
|
239
|
+
...this.#config.parsedDdTags,
|
|
240
|
+
version: this.#config.version,
|
|
241
|
+
env: this.#config.env,
|
|
242
|
+
service: this.#config.service,
|
|
185
243
|
source: 'integration',
|
|
186
244
|
ml_app: mlApp,
|
|
187
245
|
'ddtrace.version': tracerVersion,
|
|
@@ -191,13 +249,51 @@ class LLMObsSpanProcessor {
|
|
|
191
249
|
|
|
192
250
|
const errType = span.context()._tags[ERROR_TYPE] || error?.name
|
|
193
251
|
if (errType) tags.error_type = errType
|
|
252
|
+
|
|
194
253
|
if (sessionId) tags.session_id = sessionId
|
|
254
|
+
|
|
195
255
|
const integration = LLMObsTagger.tagMap.get(span)?.[INTEGRATION]
|
|
196
256
|
if (integration) tags.integration = integration
|
|
257
|
+
|
|
197
258
|
const existingTags = LLMObsTagger.tagMap.get(span)?.[TAGS] || {}
|
|
198
259
|
if (existingTags) tags = { ...tags, ...existingTags }
|
|
260
|
+
|
|
261
|
+
return tags
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
#objectTagsToStringArrayTags (tags) {
|
|
199
265
|
return Object.entries(tags).map(([key, value]) => `${key}:${value ?? ''}`)
|
|
200
266
|
}
|
|
267
|
+
|
|
268
|
+
/**
|
|
269
|
+
* Runs the user span processor, emitting telemetry and adding some guardrails against invalid return types
|
|
270
|
+
* @param {LLMObservabilitySpan} span
|
|
271
|
+
* @returns {LLMObservabilitySpan | null}
|
|
272
|
+
*/
|
|
273
|
+
#runProcessor (span) {
|
|
274
|
+
const processor = this.#userSpanProcessor
|
|
275
|
+
if (!processor) return span
|
|
276
|
+
|
|
277
|
+
let error = false
|
|
278
|
+
|
|
279
|
+
try {
|
|
280
|
+
const processedLLMObsSpan = processor(span)
|
|
281
|
+
if (processedLLMObsSpan === null) return null
|
|
282
|
+
|
|
283
|
+
if (!(processedLLMObsSpan instanceof LLMObservabilitySpan)) {
|
|
284
|
+
error = true
|
|
285
|
+
logger.warn('User span processor must return an instance of an LLMObservabilitySpan or null, dropping span.')
|
|
286
|
+
return null
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return processedLLMObsSpan
|
|
290
|
+
} catch (e) {
|
|
291
|
+
logger.error(`[LLMObs] Error in LLMObs span processor (${util.inspect(processor)}): ${util.inspect(e)}`)
|
|
292
|
+
error = true
|
|
293
|
+
} finally {
|
|
294
|
+
telemetry.recordLLMObsUserProcessorCalled(error)
|
|
295
|
+
}
|
|
296
|
+
}
|
|
201
297
|
}
|
|
202
298
|
|
|
203
299
|
module.exports = LLMObsSpanProcessor
|
|
@@ -154,6 +154,11 @@ function recordSubmitEvaluation (options, err, value = 1) {
|
|
|
154
154
|
llmobsMetrics.count('evals_submitted', tags).inc(value)
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
function recordLLMObsUserProcessorCalled (error, value = 1) {
|
|
158
|
+
const tags = { error: error ? 1 : 0 }
|
|
159
|
+
llmobsMetrics.count('user_processor_called', tags).inc(value)
|
|
160
|
+
}
|
|
161
|
+
|
|
157
162
|
module.exports = {
|
|
158
163
|
recordLLMObsEnabled,
|
|
159
164
|
incrementLLMObsSpanStartCount,
|
|
@@ -164,5 +169,6 @@ module.exports = {
|
|
|
164
169
|
recordLLMObsAnnotate,
|
|
165
170
|
recordUserFlush,
|
|
166
171
|
recordExportSpan,
|
|
167
|
-
recordSubmitEvaluation
|
|
172
|
+
recordSubmitEvaluation,
|
|
173
|
+
recordLLMObsUserProcessorCalled
|
|
168
174
|
}
|