Skip to content

Commit d8a536f

Browse files
authored
chore(llmobs): add telemetry metrics for public llmobs SDK methods (#5470)
* Add raw span size metric * Add processed span event size metrics * Refactor * Add llmobs.annotate telemetry * Add telemetry for flush, exportSpan, submitEval * fmt * minor fixes * Revert tagging err propagation, just add in handleFailure * lint * Fix try/catch/finally, typo in user_flush * Revert "Fix try/catch/finally, typo in user_flush" This reverts commit ddd647f. * Revert "Revert "Fix try/catch/finally, typo in user_flush"" This reverts commit eb3cc92.
1 parent 837bef3 commit d8a536f

File tree

4 files changed

+213
-123
lines changed

4 files changed

+213
-123
lines changed

packages/dd-trace/src/llmobs/index.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ const log = require('../log')
44
const { PROPAGATED_PARENT_ID_KEY } = require('./constants/tags')
55
const { storage } = require('./storage')
66

7+
const telemetry = require('./telemetry')
78
const LLMObsSpanProcessor = require('./span_processor')
89

9-
const telemetry = require('./telemetry')
1010
const { channel } = require('dc-polyfill')
1111
const spanProcessCh = channel('dd-trace:span:process')
1212
const evalMetricAppendCh = channel('llmobs:eval-metric:append')
@@ -94,12 +94,15 @@ function handleLLMObsParentIdInjection ({ carrier }) {
9494
}
9595

9696
function handleFlush () {
97+
let err = ''
9798
try {
9899
spanWriter.flush()
99100
evalWriter.flush()
100101
} catch (e) {
102+
err = 'writer_flush_error'
101103
log.warn(`Failed to flush LLMObs spans and evaluation metrics: ${e.message}`)
102104
}
105+
telemetry.recordUserFlush(err)
103106
}
104107

105108
function handleSpanProcess (data) {

packages/dd-trace/src/llmobs/sdk.js

Lines changed: 146 additions & 112 deletions
Original file line numberDiff line numberDiff line change
@@ -201,7 +201,7 @@ class LLMObs extends NoopLLMObs {
201201
return this._tracer.wrap(name, spanOptions, wrapped)
202202
}
203203

204-
annotate (span, options) {
204+
annotate (span, options, autoinstrumented = false) {
205205
if (!this.enabled) return
206206

207207
if (!span) {
@@ -213,150 +213,184 @@ class LLMObs extends NoopLLMObs {
213213
span = this._active()
214214
}
215215

216-
if (!span) {
217-
throw new Error('No span provided and no active LLMObs-generated span found')
218-
}
219-
if (!options) {
220-
throw new Error('No options provided for annotation.')
221-
}
216+
let err = ''
222217

223-
if (!LLMObsTagger.tagMap.has(span)) {
224-
throw new Error('Span must be an LLMObs-generated span')
225-
}
226-
if (span._duration !== undefined) {
227-
throw new Error('Cannot annotate a finished span')
228-
}
218+
try {
219+
if (!span) {
220+
err = 'invalid_span_no_active_spans'
221+
throw new Error('No span provided and no active LLMObs-generated span found')
222+
}
223+
if (!options) {
224+
err = 'invalid_options'
225+
throw new Error('No options provided for annotation.')
226+
}
229227

230-
const spanKind = LLMObsTagger.tagMap.get(span)[SPAN_KIND]
231-
if (!spanKind) {
232-
throw new Error('LLMObs span must have a span kind specified')
233-
}
228+
if (!LLMObsTagger.tagMap.has(span)) {
229+
err = 'invalid_span_type'
230+
throw new Error('Span must be an LLMObs-generated span')
231+
}
232+
if (span._duration !== undefined) {
233+
err = 'invalid_finished_span'
234+
throw new Error('Cannot annotate a finished span')
235+
}
234236

235-
const { inputData, outputData, metadata, metrics, tags } = options
236-
237-
if (inputData || outputData) {
238-
if (spanKind === 'llm') {
239-
this._tagger.tagLLMIO(span, inputData, outputData)
240-
} else if (spanKind === 'embedding') {
241-
this._tagger.tagEmbeddingIO(span, inputData, outputData)
242-
} else if (spanKind === 'retrieval') {
243-
this._tagger.tagRetrievalIO(span, inputData, outputData)
244-
} else {
245-
this._tagger.tagTextIO(span, inputData, outputData)
237+
const spanKind = LLMObsTagger.tagMap.get(span)[SPAN_KIND]
238+
if (!spanKind) {
239+
err = 'invalid_no_span_kind'
240+
throw new Error('LLMObs span must have a span kind specified')
246241
}
247-
}
248242

249-
if (metadata) {
250-
this._tagger.tagMetadata(span, metadata)
251-
}
243+
const { inputData, outputData, metadata, metrics, tags } = options
252244

253-
if (metrics) {
254-
this._tagger.tagMetrics(span, metrics)
255-
}
245+
if (inputData || outputData) {
246+
if (spanKind === 'llm') {
247+
this._tagger.tagLLMIO(span, inputData, outputData)
248+
} else if (spanKind === 'embedding') {
249+
this._tagger.tagEmbeddingIO(span, inputData, outputData)
250+
} else if (spanKind === 'retrieval') {
251+
this._tagger.tagRetrievalIO(span, inputData, outputData)
252+
} else {
253+
this._tagger.tagTextIO(span, inputData, outputData)
254+
}
255+
}
256256

257-
if (tags) {
258-
this._tagger.tagSpanTags(span, tags)
257+
if (metadata) {
258+
this._tagger.tagMetadata(span, metadata)
259+
}
260+
if (metrics) {
261+
this._tagger.tagMetrics(span, metrics)
262+
}
263+
if (tags) {
264+
this._tagger.tagSpanTags(span, tags)
265+
}
266+
} catch (e) {
267+
if (e.ddErrorTag) {
268+
err = e.ddErrorTag
269+
}
270+
throw e
271+
} finally {
272+
if (autoinstrumented === false) {
273+
telemetry.recordLLMObsAnnotate(span, err)
274+
}
259275
}
260276
}
261277

262278
exportSpan (span) {
263279
span = span || this._active()
264-
265-
if (!span) {
266-
throw new Error('No span provided and no active LLMObs-generated span found')
267-
}
268-
269-
if (!(span instanceof Span)) {
270-
throw new Error('Span must be a valid Span object.')
271-
}
272-
273-
if (!LLMObsTagger.tagMap.has(span)) {
274-
throw new Error('Span must be an LLMObs-generated span')
280+
let err = ''
281+
try {
282+
if (!span) {
283+
err = 'no_active_span'
284+
throw new Error('No span provided and no active LLMObs-generated span found')
285+
}
286+
if (!(span instanceof Span)) {
287+
err = 'invalid_span'
288+
throw new Error('Span must be a valid Span object.')
289+
}
290+
if (!LLMObsTagger.tagMap.has(span)) {
291+
err = 'invalid_span'
292+
throw new Error('Span must be an LLMObs-generated span')
293+
}
294+
} catch (e) {
295+
telemetry.recordExportSpan(span, err)
296+
throw e
275297
}
276-
277298
try {
278299
return {
279300
traceId: span.context().toTraceId(true),
280301
spanId: span.context().toSpanId()
281302
}
282303
} catch {
283-
logger.warn('Faild to export span. Span must be a valid Span object.')
304+
err = 'invalid_span'
305+
logger.warn('Failed to export span. Span must be a valid Span object.')
306+
} finally {
307+
telemetry.recordExportSpan(span, err)
284308
}
285309
}
286310

287311
submitEvaluation (llmobsSpanContext, options = {}) {
288312
if (!this.enabled) return
289313

314+
let err = ''
290315
const { traceId, spanId } = llmobsSpanContext
291-
if (!traceId || !spanId) {
292-
throw new Error(
293-
'spanId and traceId must both be specified for the given evaluation metric to be submitted.'
294-
)
295-
}
296-
297-
const mlApp = options.mlApp || this._config.llmobs.mlApp
298-
if (!mlApp) {
299-
throw new Error(
300-
'ML App name is required for sending evaluation metrics. Evaluation metric data will not be sent.'
301-
)
302-
}
303-
304-
const timestampMs = options.timestampMs || Date.now()
305-
if (typeof timestampMs !== 'number' || timestampMs < 0) {
306-
throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
307-
}
316+
try {
317+
if (!traceId || !spanId) {
318+
err = 'invalid_span'
319+
throw new Error(
320+
'spanId and traceId must both be specified for the given evaluation metric to be submitted.'
321+
)
322+
}
323+
const mlApp = options.mlApp || this._config.llmobs.mlApp
324+
if (!mlApp) {
325+
err = 'missing_ml_app'
326+
throw new Error(
327+
'ML App name is required for sending evaluation metrics. Evaluation metric data will not be sent.'
328+
)
329+
}
308330

309-
const { label, value, tags } = options
310-
const metricType = options.metricType?.toLowerCase()
311-
if (!label) {
312-
throw new Error('label must be the specified name of the evaluation metric')
313-
}
314-
if (!metricType || !['categorical', 'score'].includes(metricType)) {
315-
throw new Error('metricType must be one of "categorical" or "score"')
316-
}
331+
const timestampMs = options.timestampMs || Date.now()
332+
if (typeof timestampMs !== 'number' || timestampMs < 0) {
333+
err = 'invalid_timestamp'
334+
throw new Error('timestampMs must be a non-negative integer. Evaluation metric data will not be sent')
335+
}
317336

318-
if (metricType === 'categorical' && typeof value !== 'string') {
319-
throw new Error('value must be a string for a categorical metric.')
320-
}
321-
if (metricType === 'score' && typeof value !== 'number') {
322-
throw new Error('value must be a number for a score metric.')
323-
}
337+
const { label, value, tags } = options
338+
const metricType = options.metricType?.toLowerCase()
339+
if (!label) {
340+
err = 'invalid_metric_label'
341+
throw new Error('label must be the specified name of the evaluation metric')
342+
}
343+
if (!metricType || !['categorical', 'score'].includes(metricType)) {
344+
err = 'invalid_metric_type'
345+
throw new Error('metricType must be one of "categorical" or "score"')
346+
}
347+
if (metricType === 'categorical' && typeof value !== 'string') {
348+
err = 'invalid_metric_value'
349+
throw new Error('value must be a string for a categorical metric.')
350+
}
351+
if (metricType === 'score' && typeof value !== 'number') {
352+
err = 'invalid_metric_value'
353+
throw new Error('value must be a number for a score metric.')
354+
}
324355

325-
const evaluationTags = {
326-
'ddtrace.version': tracerVersion,
327-
ml_app: mlApp
328-
}
356+
const evaluationTags = {
357+
'ddtrace.version': tracerVersion,
358+
ml_app: mlApp
359+
}
329360

330-
if (tags) {
331-
for (const key in tags) {
332-
const tag = tags[key]
333-
if (typeof tag === 'string') {
334-
evaluationTags[key] = tag
335-
} else if (typeof tag.toString === 'function') {
336-
evaluationTags[key] = tag.toString()
337-
} else if (tag == null) {
338-
evaluationTags[key] = Object.prototype.toString.call(tag)
339-
} else {
340-
// should be a rare case
341-
// every object in JS has a toString, otherwise every primitive has its own toString
342-
// null and undefined are handled above
343-
throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
361+
if (tags) {
362+
for (const key in tags) {
363+
const tag = tags[key]
364+
if (typeof tag === 'string') {
365+
evaluationTags[key] = tag
366+
} else if (typeof tag.toString === 'function') {
367+
evaluationTags[key] = tag.toString()
368+
} else if (tag == null) {
369+
evaluationTags[key] = Object.prototype.toString.call(tag)
370+
} else {
371+
// should be a rare case
372+
// every object in JS has a toString, otherwise every primitive has its own toString
373+
// null and undefined are handled above
374+
err = 'invalid_tags'
375+
throw new Error('Failed to parse tags. Tags for evaluation metrics must be strings')
376+
}
344377
}
345378
}
346-
}
347379

348-
const payload = {
349-
span_id: spanId,
350-
trace_id: traceId,
351-
label,
352-
metric_type: metricType,
353-
ml_app: mlApp,
354-
[`${metricType}_value`]: value,
355-
timestamp_ms: timestampMs,
356-
tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`)
380+
const payload = {
381+
span_id: spanId,
382+
trace_id: traceId,
383+
label,
384+
metric_type: metricType,
385+
ml_app: mlApp,
386+
[`${metricType}_value`]: value,
387+
timestamp_ms: timestampMs,
388+
tags: Object.entries(evaluationTags).map(([key, value]) => `${key}:${value}`)
389+
}
390+
evalMetricAppendCh.publish(payload)
391+
} finally {
392+
telemetry.recordSubmitEvaluation(options, err)
357393
}
358-
359-
evalMetricAppendCh.publish(payload)
360394
}
361395

362396
flush () {
@@ -375,7 +409,7 @@ class LLMObs extends NoopLLMObs {
375409
annotations.outputData = output
376410
}
377411

378-
this.annotate(span, annotations)
412+
this.annotate(span, annotations, true)
379413
}
380414

381415
_active () {

0 commit comments

Comments
 (0)