@mastra/datadog 1.4.15 → 1.4.16-alpha.1
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/dist/bridge.d.ts +24 -0
- package/dist/bridge.d.ts.map +1 -1
- package/dist/index.cjs +94 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +94 -4
- package/dist/index.js.map +1 -1
- package/dist/tracing.d.ts +22 -0
- package/dist/tracing.d.ts.map +1 -1
- package/dist/utils.d.ts +11 -0
- package/dist/utils.d.ts.map +1 -1
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -59,6 +59,20 @@ function formatUsageMetrics(usage) {
|
|
|
59
59
|
* Utility functions for Datadog LLM Observability Exporter
|
|
60
60
|
*/
|
|
61
61
|
/**
|
|
62
|
+
* Datadog span links for Mastra span links, such as the caller of a served MCP
|
|
63
|
+
* request. Each link is resolved from a W3C `traceparent`, so only full-length
|
|
64
|
+
* W3C IDs are kept, and links dd-trace can't resolve are skipped.
|
|
65
|
+
*/
|
|
66
|
+
function toDatadogLinks(links) {
|
|
67
|
+
const ddLinks = [];
|
|
68
|
+
for (const link of links ?? []) {
|
|
69
|
+
if (!/^[0-9a-f]{32}$/.test(link.traceId) || !/^[0-9a-f]{16}$/.test(link.spanId)) continue;
|
|
70
|
+
const context = tracer.extract("text_map", { traceparent: `00-${link.traceId}-${link.spanId}-01` });
|
|
71
|
+
if (context) ddLinks.push({ context });
|
|
72
|
+
}
|
|
73
|
+
return ddLinks;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
62
76
|
* Maps Mastra SpanTypes to Datadog LLMObs span kinds for the legacy hierarchy
|
|
63
77
|
* (no `model-inference-span` feature). MODEL_STEP is the actual API call here
|
|
64
78
|
* because MODEL_INFERENCE doesn't exist.
|
|
@@ -309,10 +323,18 @@ var DatadogBridge = class extends BaseExporter {
|
|
|
309
323
|
name = "datadog-bridge";
|
|
310
324
|
config;
|
|
311
325
|
ddSpanMap = /* @__PURE__ */ new Map();
|
|
326
|
+
/** Keys of the links each dd span was started with, so links set later are added once at finish. */
|
|
327
|
+
startLinkKeys = /* @__PURE__ */ new WeakMap();
|
|
312
328
|
traceContext = /* @__PURE__ */ new Map();
|
|
313
329
|
openSpanCounts = /* @__PURE__ */ new Map();
|
|
330
|
+
flushGraceMs;
|
|
331
|
+
/** True when spans or evaluations were handed to dd-trace since the last flush() */
|
|
332
|
+
pendingSend = false;
|
|
333
|
+
/** Grace wait in progress, shared by flush() calls that overlap it */
|
|
334
|
+
graceWait = null;
|
|
314
335
|
constructor(config = {}) {
|
|
315
336
|
super(config);
|
|
337
|
+
this.flushGraceMs = config.flushGraceMs ?? 2e3;
|
|
316
338
|
const mlApp = config.mlApp ?? process.env.DD_LLMOBS_ML_APP;
|
|
317
339
|
const apiKey = config.apiKey ?? process.env.DD_API_KEY;
|
|
318
340
|
const site = config.site ?? process.env.DD_SITE ?? "datadoghq.com";
|
|
@@ -372,10 +394,13 @@ var DatadogBridge = class extends BaseExporter {
|
|
|
372
394
|
if (apmParentDdSpan) parentSource = "active-scope";
|
|
373
395
|
}
|
|
374
396
|
if (!llmobsParentDdSpan && externalParentId) llmobsParentDdSpan = apmParentDdSpan;
|
|
397
|
+
const links = toDatadogLinks(options.links);
|
|
375
398
|
const ddSpan = tracer.startSpan(options.name, {
|
|
376
399
|
...apmParentDdSpan ? { childOf: apmParentDdSpan } : {},
|
|
377
|
-
...options.startTime ? { startTime: toDate(options.startTime).getTime() } : {}
|
|
400
|
+
...options.startTime ? { startTime: toDate(options.startTime).getTime() } : {},
|
|
401
|
+
...links.length > 0 ? { links } : {}
|
|
378
402
|
});
|
|
403
|
+
if (options.links?.length) this.startLinkKeys.set(ddSpan, new Set(options.links.map((link) => `${link.traceId}:${link.spanId}`)));
|
|
379
404
|
const ddContext = ddSpan.context?.();
|
|
380
405
|
const spanId = ddContext?.toSpanId?.(true) ?? generateSpanId();
|
|
381
406
|
const traceId = ddContext?.toTraceId?.(true) ?? (externalParentId ? options.parent?.traceId ?? generateTraceId() : generateTraceId());
|
|
@@ -416,6 +441,7 @@ var DatadogBridge = class extends BaseExporter {
|
|
|
416
441
|
return;
|
|
417
442
|
}
|
|
418
443
|
try {
|
|
444
|
+
this.pendingSend = true;
|
|
419
445
|
tracer.llmobs.submitEvaluation({
|
|
420
446
|
traceId: score.traceId,
|
|
421
447
|
spanId: toDatadogSpanId(score.spanId)
|
|
@@ -538,7 +564,20 @@ var DatadogBridge = class extends BaseExporter {
|
|
|
538
564
|
});
|
|
539
565
|
}
|
|
540
566
|
try {
|
|
541
|
-
|
|
567
|
+
const startKeys = this.startLinkKeys.get(ddSpan);
|
|
568
|
+
const lateLinks = toDatadogLinks(span.links?.filter((link) => !startKeys?.has(`${link.traceId}:${link.spanId}`)));
|
|
569
|
+
if (lateLinks.length > 0 && typeof ddSpan.addLinks === "function") ddSpan.addLinks(lateLinks);
|
|
570
|
+
} catch (error) {
|
|
571
|
+
this.logger.error("[DatadogBridge] Failed to add span links", {
|
|
572
|
+
error,
|
|
573
|
+
spanId: span.id
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
if (typeof ddSpan.finish === "function") {
|
|
578
|
+
this.pendingSend = true;
|
|
579
|
+
ddSpan.finish(endTime.getTime());
|
|
580
|
+
}
|
|
542
581
|
} catch (error) {
|
|
543
582
|
this.logger.error("[DatadogBridge] Failed to finish dd span", {
|
|
544
583
|
error,
|
|
@@ -643,6 +682,7 @@ var DatadogBridge = class extends BaseExporter {
|
|
|
643
682
|
} catch (e) {
|
|
644
683
|
this.logger.error("Error flushing llmobs", { error: e });
|
|
645
684
|
}
|
|
685
|
+
await this.waitForBackgroundSend();
|
|
646
686
|
try {
|
|
647
687
|
await flushApmExporter();
|
|
648
688
|
this.logger.debug("Datadog APM exporter flushed");
|
|
@@ -650,11 +690,32 @@ var DatadogBridge = class extends BaseExporter {
|
|
|
650
690
|
this.logger.error("Error flushing Datadog APM exporter", { error: e });
|
|
651
691
|
}
|
|
652
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* `llmobs.flush()` returns before the background writer's HTTP request
|
|
695
|
+
* completes and dd-trace gives nothing to await, so wait a bounded time
|
|
696
|
+
* instead. Starts one wait per batch of new data, so the repeated flushes
|
|
697
|
+
* during shutdown don't stack. A flush that overlaps a running wait joins
|
|
698
|
+
* it, so no caller resolves before the data it produced had its wait.
|
|
699
|
+
*/
|
|
700
|
+
async waitForBackgroundSend() {
|
|
701
|
+
if (!(this.flushGraceMs > 0) || !Number.isFinite(this.flushGraceMs)) return;
|
|
702
|
+
if (this.pendingSend) {
|
|
703
|
+
this.pendingSend = false;
|
|
704
|
+
const wait = new Promise((resolve) => setTimeout(resolve, this.flushGraceMs)).finally(() => {
|
|
705
|
+
if (this.graceWait === wait) this.graceWait = null;
|
|
706
|
+
});
|
|
707
|
+
this.graceWait = wait;
|
|
708
|
+
}
|
|
709
|
+
await this.graceWait;
|
|
710
|
+
}
|
|
653
711
|
async shutdown() {
|
|
654
712
|
for (const [spanId, ddSpan] of this.ddSpanMap) {
|
|
655
713
|
this.logger.warn(`[DatadogBridge] Force-finishing APM span that was not properly closed [id=${spanId}]`);
|
|
656
714
|
try {
|
|
657
|
-
if (typeof ddSpan.finish === "function")
|
|
715
|
+
if (typeof ddSpan.finish === "function") {
|
|
716
|
+
this.pendingSend = true;
|
|
717
|
+
ddSpan.finish();
|
|
718
|
+
}
|
|
658
719
|
} catch {}
|
|
659
720
|
}
|
|
660
721
|
this.ddSpanMap.clear();
|
|
@@ -719,8 +780,14 @@ var DatadogExporter = class extends BaseExporter {
|
|
|
719
780
|
config;
|
|
720
781
|
traceContext = /* @__PURE__ */ new Map();
|
|
721
782
|
traceState = /* @__PURE__ */ new Map();
|
|
783
|
+
flushGraceMs;
|
|
784
|
+
/** True when spans or evaluations were handed to dd-trace since the last flush() */
|
|
785
|
+
pendingSend = false;
|
|
786
|
+
/** Grace wait in progress, shared by flush() calls that overlap it */
|
|
787
|
+
graceWait = null;
|
|
722
788
|
constructor(config = {}) {
|
|
723
789
|
super(config);
|
|
790
|
+
this.flushGraceMs = config.flushGraceMs ?? 2e3;
|
|
724
791
|
const mlApp = config.mlApp ?? process.env.DD_LLMOBS_ML_APP;
|
|
725
792
|
const apiKey = config.apiKey ?? process.env.DD_API_KEY;
|
|
726
793
|
const site = config.site ?? process.env.DD_SITE ?? "datadoghq.com";
|
|
@@ -898,6 +965,7 @@ var DatadogExporter = class extends BaseExporter {
|
|
|
898
965
|
return;
|
|
899
966
|
}
|
|
900
967
|
try {
|
|
968
|
+
this.pendingSend = true;
|
|
901
969
|
tracer.llmobs.submitEvaluation({
|
|
902
970
|
traceId: exported.traceId,
|
|
903
971
|
spanId: exported.spanId
|
|
@@ -932,6 +1000,25 @@ var DatadogExporter = class extends BaseExporter {
|
|
|
932
1000
|
} catch (e) {
|
|
933
1001
|
this.logger.error("Error flushing llmobs", { error: e });
|
|
934
1002
|
}
|
|
1003
|
+
await this.waitForBackgroundSend();
|
|
1004
|
+
}
|
|
1005
|
+
/**
|
|
1006
|
+
* `llmobs.flush()` returns before the background writer's HTTP request
|
|
1007
|
+
* completes and dd-trace gives nothing to await, so wait a bounded time
|
|
1008
|
+
* instead. Starts one wait per batch of new data, so the repeated flushes
|
|
1009
|
+
* during shutdown don't stack. A flush that overlaps a running wait joins
|
|
1010
|
+
* it, so no caller resolves before the data it produced had its wait.
|
|
1011
|
+
*/
|
|
1012
|
+
async waitForBackgroundSend() {
|
|
1013
|
+
if (!(this.flushGraceMs > 0) || !Number.isFinite(this.flushGraceMs)) return;
|
|
1014
|
+
if (this.pendingSend) {
|
|
1015
|
+
this.pendingSend = false;
|
|
1016
|
+
const wait = new Promise((resolve) => setTimeout(resolve, this.flushGraceMs)).finally(() => {
|
|
1017
|
+
if (this.graceWait === wait) this.graceWait = null;
|
|
1018
|
+
});
|
|
1019
|
+
this.graceWait = wait;
|
|
1020
|
+
}
|
|
1021
|
+
await this.graceWait;
|
|
935
1022
|
}
|
|
936
1023
|
/**
|
|
937
1024
|
* Gracefully shuts down the exporter.
|
|
@@ -1108,6 +1195,7 @@ var DatadogExporter = class extends BaseExporter {
|
|
|
1108
1195
|
provider: ownAttrs?.provider ?? inheritedModelAttrs?.provider
|
|
1109
1196
|
};
|
|
1110
1197
|
const startTime = toDate(span.startTime);
|
|
1198
|
+
const links = toDatadogLinks(span.links);
|
|
1111
1199
|
const endTime = span.endTime ? toDate(span.endTime) : span.isEvent ? startTime : /* @__PURE__ */ new Date();
|
|
1112
1200
|
return {
|
|
1113
1201
|
traceOptions: {
|
|
@@ -1117,7 +1205,8 @@ var DatadogExporter = class extends BaseExporter {
|
|
|
1117
1205
|
userId: traceCtx.userId,
|
|
1118
1206
|
startTime,
|
|
1119
1207
|
...kind === "llm" && attrs?.model ? { modelName: attrs.model } : {},
|
|
1120
|
-
...kind === "llm" && attrs?.provider ? { modelProvider: attrs.provider } : {}
|
|
1208
|
+
...kind === "llm" && attrs?.provider ? { modelProvider: attrs.provider } : {},
|
|
1209
|
+
...links.length > 0 ? { links } : {}
|
|
1121
1210
|
},
|
|
1122
1211
|
endTimeMs: endTime.getTime()
|
|
1123
1212
|
};
|
|
@@ -1134,6 +1223,7 @@ var DatadogExporter = class extends BaseExporter {
|
|
|
1134
1223
|
model: span.attributes?.model,
|
|
1135
1224
|
provider: span.attributes?.provider
|
|
1136
1225
|
} : inheritedModelAttrs;
|
|
1226
|
+
this.pendingSend = true;
|
|
1137
1227
|
tracer.llmobs.trace(traceOptions, (ddSpan) => {
|
|
1138
1228
|
const annotations = this.buildAnnotations(span);
|
|
1139
1229
|
if (Object.keys(annotations).length > 0) tracer.llmobs.annotate(ddSpan, annotations);
|