@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/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
- if (typeof ddSpan.finish === "function") ddSpan.finish(endTime.getTime());
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") ddSpan.finish();
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);