@mastra/datadog 1.3.5 → 1.3.6

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.cjs CHANGED
@@ -1,1233 +1,1168 @@
1
- 'use strict';
2
-
3
- var observability$1 = require('@mastra/core/observability');
4
- var utils = require('@mastra/core/utils');
5
- var observability = require('@mastra/observability');
6
- var tracer3 = require('dd-trace');
7
- var features = require('@mastra/core/features');
8
-
9
- function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
10
-
11
- var tracer3__default = /*#__PURE__*/_interopDefault(tracer3);
12
-
13
- // src/bridge.ts
14
- var FEATURE = "model-inference-span";
15
- var observabilityFeatures;
16
- var featureLoadPromise;
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ //#region \0rolldown/runtime.js
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+ //#endregion
24
+ let _mastra_core_observability = require("@mastra/core/observability");
25
+ let _mastra_core_utils = require("@mastra/core/utils");
26
+ let _mastra_observability = require("@mastra/observability");
27
+ let dd_trace = require("dd-trace");
28
+ dd_trace = __toESM(dd_trace, 1);
29
+ let _mastra_core_features = require("@mastra/core/features");
30
+ //#region src/features.ts
31
+ /**
32
+ * Feature detection for paired @mastra/core and @mastra/observability versions.
33
+ *
34
+ * `coreFeatures` is a hard peer dependency, so it can be imported statically.
35
+ * `observabilityFeatures` is loaded via dynamic import so this exporter degrades
36
+ * gracefully against older `@mastra/observability` versions that don't export it
37
+ * (per the pattern documented in `observability/mastra/src/features.ts`).
38
+ *
39
+ * Detection runs once at module load. Callers consume the result through the
40
+ * sync `isModelInferenceEnabled()` accessor, which conservatively returns
41
+ * `false` until detection settles — long enough only to cover the microtask
42
+ * window before any spans are emitted in practice.
43
+ */
44
+ const FEATURE = "model-inference-span";
45
+ let observabilityFeatures;
46
+ let featureLoadPromise;
17
47
  function loadObservabilityFeatures() {
18
- if (!featureLoadPromise) {
19
- featureLoadPromise = import('@mastra/observability').then((mod) => {
20
- observabilityFeatures = mod.observabilityFeatures;
21
- }).catch(() => {
22
- });
23
- }
24
- return featureLoadPromise;
48
+ if (!featureLoadPromise) featureLoadPromise = import("@mastra/observability").then((mod) => {
49
+ observabilityFeatures = mod.observabilityFeatures;
50
+ }).catch(() => {});
51
+ return featureLoadPromise;
25
52
  }
26
- void loadObservabilityFeatures();
53
+ loadObservabilityFeatures();
54
+ /**
55
+ * Returns true when both packages report the `model-inference-span` feature,
56
+ * meaning MODEL_INFERENCE spans are emitted by the tracker. Drives the
57
+ * Datadog kind mapping and usage-source switch (legacy: MODEL_STEP carries
58
+ * 'llm' kind + usage; new: MODEL_INFERENCE does).
59
+ */
27
60
  function isModelInferenceEnabled() {
28
- return observabilityFeatures?.has(FEATURE) === true && features.coreFeatures.has(FEATURE);
61
+ return observabilityFeatures?.has(FEATURE) === true && _mastra_core_features.coreFeatures.has(FEATURE);
29
62
  }
30
-
31
- // src/metrics.ts
63
+ //#endregion
64
+ //#region src/metrics.ts
32
65
  function formatUsageMetrics(usage) {
33
- if (!usage) return void 0;
34
- const result = {};
35
- const inputTokens = usage.inputTokens;
36
- if (inputTokens !== void 0) result.inputTokens = inputTokens;
37
- const outputTokens = usage.outputTokens;
38
- if (outputTokens !== void 0) result.outputTokens = outputTokens;
39
- if (inputTokens !== void 0 && outputTokens !== void 0) {
40
- result.totalTokens = inputTokens + outputTokens;
41
- }
42
- if (usage?.outputDetails?.reasoning !== void 0) {
43
- result.reasoningOutputTokens = usage.outputDetails.reasoning;
44
- }
45
- const cacheReadTokens = usage?.inputDetails?.cacheRead;
46
- if (cacheReadTokens !== void 0) {
47
- result.cacheReadTokens = cacheReadTokens;
48
- }
49
- const cacheWriteTokens = usage?.inputDetails?.cacheWrite;
50
- if (cacheWriteTokens !== void 0) {
51
- result.cacheWriteTokens = cacheWriteTokens;
52
- }
53
- return Object.keys(result).length > 0 ? result : void 0;
66
+ if (!usage) return void 0;
67
+ const result = {};
68
+ const inputTokens = usage.inputTokens;
69
+ if (inputTokens !== void 0) result.inputTokens = inputTokens;
70
+ const outputTokens = usage.outputTokens;
71
+ if (outputTokens !== void 0) result.outputTokens = outputTokens;
72
+ if (inputTokens !== void 0 && outputTokens !== void 0) result.totalTokens = inputTokens + outputTokens;
73
+ if (usage?.outputDetails?.reasoning !== void 0) result.reasoningOutputTokens = usage.outputDetails.reasoning;
74
+ const cacheReadTokens = usage?.inputDetails?.cacheRead;
75
+ if (cacheReadTokens !== void 0) result.cacheReadTokens = cacheReadTokens;
76
+ const cacheWriteTokens = usage?.inputDetails?.cacheWrite;
77
+ if (cacheWriteTokens !== void 0) result.cacheWriteTokens = cacheWriteTokens;
78
+ return Object.keys(result).length > 0 ? result : void 0;
54
79
  }
55
- var SPAN_TYPE_TO_KIND_LEGACY = {
56
- [observability$1.SpanType.AGENT_RUN]: "agent",
57
- [observability$1.SpanType.MODEL_GENERATION]: "workflow",
58
- [observability$1.SpanType.MODEL_STEP]: "llm",
59
- [observability$1.SpanType.TOOL_CALL]: "tool",
60
- [observability$1.SpanType.MCP_TOOL_CALL]: "tool",
61
- [observability$1.SpanType.PROVIDER_TOOL_CALL]: "tool",
62
- [observability$1.SpanType.WORKFLOW_RUN]: "workflow"
80
+ //#endregion
81
+ //#region src/utils.ts
82
+ /**
83
+ * Utility functions for Datadog LLM Observability Exporter
84
+ */
85
+ /**
86
+ * Maps Mastra SpanTypes to Datadog LLMObs span kinds for the legacy hierarchy
87
+ * (no `model-inference-span` feature). MODEL_STEP is the actual API call here
88
+ * because MODEL_INFERENCE doesn't exist.
89
+ *
90
+ * Unmapped types fall back to 'task'.
91
+ */
92
+ const SPAN_TYPE_TO_KIND_LEGACY = {
93
+ [_mastra_core_observability.SpanType.AGENT_RUN]: "agent",
94
+ [_mastra_core_observability.SpanType.MODEL_GENERATION]: "workflow",
95
+ [_mastra_core_observability.SpanType.MODEL_STEP]: "llm",
96
+ [_mastra_core_observability.SpanType.TOOL_CALL]: "tool",
97
+ [_mastra_core_observability.SpanType.MCP_TOOL_CALL]: "tool",
98
+ [_mastra_core_observability.SpanType.PROVIDER_TOOL_CALL]: "tool",
99
+ [_mastra_core_observability.SpanType.WORKFLOW_RUN]: "workflow"
63
100
  };
64
- var SPAN_TYPE_TO_KIND_INFERENCE = {
65
- [observability$1.SpanType.AGENT_RUN]: "agent",
66
- [observability$1.SpanType.MODEL_GENERATION]: "workflow",
67
- [observability$1.SpanType.MODEL_STEP]: "workflow",
68
- [observability$1.SpanType.MODEL_INFERENCE]: "llm",
69
- [observability$1.SpanType.TOOL_CALL]: "tool",
70
- [observability$1.SpanType.MCP_TOOL_CALL]: "tool",
71
- [observability$1.SpanType.PROVIDER_TOOL_CALL]: "tool",
72
- [observability$1.SpanType.WORKFLOW_RUN]: "workflow"
101
+ /**
102
+ * Maps Mastra SpanTypes to Datadog LLMObs span kinds for the new hierarchy
103
+ * (`model-inference-span` feature). MODEL_INFERENCE is the LLM API call;
104
+ * MODEL_STEP wraps processors + inference + tool execution as a workflow.
105
+ *
106
+ * Unmapped types fall back to 'task'.
107
+ */
108
+ const SPAN_TYPE_TO_KIND_INFERENCE = {
109
+ [_mastra_core_observability.SpanType.AGENT_RUN]: "agent",
110
+ [_mastra_core_observability.SpanType.MODEL_GENERATION]: "workflow",
111
+ [_mastra_core_observability.SpanType.MODEL_STEP]: "workflow",
112
+ [_mastra_core_observability.SpanType.MODEL_INFERENCE]: "llm",
113
+ [_mastra_core_observability.SpanType.TOOL_CALL]: "tool",
114
+ [_mastra_core_observability.SpanType.MCP_TOOL_CALL]: "tool",
115
+ [_mastra_core_observability.SpanType.PROVIDER_TOOL_CALL]: "tool",
116
+ [_mastra_core_observability.SpanType.WORKFLOW_RUN]: "workflow"
73
117
  };
118
+ /**
119
+ * Resolves the active span-type → Datadog kind mapping based on whether the
120
+ * paired @mastra/core + @mastra/observability emit MODEL_INFERENCE spans.
121
+ * Re-evaluated on each call so tests can flip the feature flag at runtime.
122
+ */
74
123
  function getSpanTypeToKind() {
75
- return isModelInferenceEnabled() ? SPAN_TYPE_TO_KIND_INFERENCE : SPAN_TYPE_TO_KIND_LEGACY;
124
+ return isModelInferenceEnabled() ? SPAN_TYPE_TO_KIND_INFERENCE : SPAN_TYPE_TO_KIND_LEGACY;
76
125
  }
77
- var tracerInitFlag = { done: false };
126
+ /**
127
+ * Singleton flag to prevent multiple tracer initializations.
128
+ * dd-trace should only be initialized once per process.
129
+ */
130
+ const tracerInitFlag = { done: false };
131
+ /**
132
+ * Ensures dd-trace is initialized exactly once.
133
+ * Respects any existing tracer initialization by the application.
134
+ */
78
135
  function ensureTracer(config) {
79
- if (tracerInitFlag.done) return;
80
- if (config.site) {
81
- process.env.DD_SITE = config.site;
82
- }
83
- if (config.apiKey) {
84
- process.env.DD_API_KEY = config.apiKey;
85
- }
86
- const alreadyStarted = tracer3__default.default._tracer?.started;
87
- if (!alreadyStarted) {
88
- tracer3__default.default.init({
89
- service: config.service || config.mlApp,
90
- env: config.env || process.env.DD_ENV,
91
- // Disable automatic integrations by default to avoid surprise instrumentation
92
- plugins: config.integrationsEnabled ?? false
93
- });
94
- }
95
- tracer3__default.default.llmobs.enable({
96
- mlApp: config.mlApp,
97
- agentlessEnabled: config.agentless
98
- });
99
- tracerInitFlag.done = true;
136
+ if (tracerInitFlag.done) return;
137
+ if (config.site) process.env.DD_SITE = config.site;
138
+ if (config.apiKey) process.env.DD_API_KEY = config.apiKey;
139
+ if (!dd_trace.default._tracer?.started) dd_trace.default.init({
140
+ service: config.service || config.mlApp,
141
+ env: config.env || process.env.DD_ENV,
142
+ plugins: config.integrationsEnabled ?? false
143
+ });
144
+ dd_trace.default.llmobs.enable({
145
+ mlApp: config.mlApp,
146
+ agentlessEnabled: config.agentless
147
+ });
148
+ tracerInitFlag.done = true;
100
149
  }
150
+ /**
151
+ * Returns the Datadog kind for a Mastra span type, using the mapping that
152
+ * matches the active span hierarchy (legacy vs MODEL_INFERENCE).
153
+ */
101
154
  function kindFor(spanType) {
102
- return getSpanTypeToKind()[spanType] || "task";
155
+ return getSpanTypeToKind()[spanType] || "task";
103
156
  }
157
+ /**
158
+ * Converts a value to a Date object.
159
+ */
104
160
  function toDate(value) {
105
- return value instanceof Date ? value : new Date(value);
161
+ return value instanceof Date ? value : new Date(value);
106
162
  }
163
+ /**
164
+ * Safely stringifies data, handling circular references.
165
+ */
107
166
  function safeStringify(data) {
108
- try {
109
- return JSON.stringify(data) ?? "";
110
- } catch {
111
- if (typeof data === "object" && data !== null) {
112
- return `[Non-serializable ${data.constructor?.name || "Object"}]`;
113
- }
114
- return String(data);
115
- }
167
+ try {
168
+ return JSON.stringify(data) ?? "";
169
+ } catch {
170
+ if (typeof data === "object" && data !== null) return `[Non-serializable ${data.constructor?.name || "Object"}]`;
171
+ return String(data);
172
+ }
116
173
  }
174
+ /**
175
+ * Checks if data is already in message array format ({role, content}[]).
176
+ */
117
177
  function isMessageArray(data) {
118
- return Array.isArray(data) && data.every((m) => m?.role && (m?.content !== void 0 || Array.isArray(m.toolCalls)));
178
+ return Array.isArray(data) && data.every((m) => m?.role && (m?.content !== void 0 || Array.isArray(m.toolCalls)));
119
179
  }
120
180
  function isModelDataSpan(spanType) {
121
- return spanType === observability$1.SpanType.MODEL_GENERATION || spanType === observability$1.SpanType.MODEL_STEP || spanType === observability$1.SpanType.MODEL_INFERENCE;
181
+ return spanType === _mastra_core_observability.SpanType.MODEL_GENERATION || spanType === _mastra_core_observability.SpanType.MODEL_STEP || spanType === _mastra_core_observability.SpanType.MODEL_INFERENCE;
122
182
  }
183
+ /**
184
+ * Checks if data is in Gemini content array format ({role, parts}[]).
185
+ */
123
186
  function isGeminiContentArray(data) {
124
- return Array.isArray(data) && data.every((m) => m?.role && Array.isArray(m?.parts));
187
+ return Array.isArray(data) && data.every((m) => m?.role && Array.isArray(m?.parts));
125
188
  }
126
189
  function toMessageContent(content) {
127
- return typeof content === "string" ? content : safeStringify(content);
190
+ return typeof content === "string" ? content : safeStringify(content);
128
191
  }
192
+ /**
193
+ * Maps a {role, content}[] message array into the Datadog message shape,
194
+ * stringifying any non-string content (e.g. multimodal part arrays).
195
+ */
129
196
  function toDatadogMessages(messages) {
130
- return messages.map((m) => {
131
- const message = {
132
- role: m.role,
133
- content: m.content == null ? "" : toMessageContent(m.content)
134
- };
135
- if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
136
- message.toolCalls = toDatadogToolCalls(m.toolCalls);
137
- }
138
- return message;
139
- }).filter((m) => !(m.role === "user" && m.content.trim().length === 0));
197
+ return messages.map((m) => {
198
+ const message = {
199
+ role: m.role,
200
+ content: m.content == null ? "" : toMessageContent(m.content)
201
+ };
202
+ if (Array.isArray(m.toolCalls) && m.toolCalls.length > 0) message.toolCalls = toDatadogToolCalls(m.toolCalls);
203
+ return message;
204
+ }).filter((m) => !(m.role === "user" && m.content.trim().length === 0));
140
205
  }
141
206
  function parseToolArguments(args) {
142
- if (args == null) return void 0;
143
- if (typeof args === "string") {
144
- try {
145
- const parsed = JSON.parse(args);
146
- if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
147
- return parsed;
148
- }
149
- } catch {
150
- }
151
- return { value: args };
152
- }
153
- if (typeof args === "object" && !Array.isArray(args)) return args;
154
- return { value: args };
207
+ if (args == null) return void 0;
208
+ if (typeof args === "string") {
209
+ try {
210
+ const parsed = JSON.parse(args);
211
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) return parsed;
212
+ } catch {}
213
+ return { value: args };
214
+ }
215
+ if (typeof args === "object" && !Array.isArray(args)) return args;
216
+ return { value: args };
155
217
  }
156
218
  function toDatadogToolCalls(toolCalls) {
157
- return toolCalls.map((c) => ({
158
- name: c?.toolName ?? c?.name ?? c?.function?.name ?? "unknown",
159
- arguments: parseToolArguments(c?.args ?? c?.input ?? c?.arguments ?? c?.function?.arguments),
160
- toolId: c?.toolCallId ?? c?.toolId ?? c?.id,
161
- type: c?.type === "function" ? c.type : "function"
162
- }));
219
+ return toolCalls.map((c) => ({
220
+ name: c?.toolName ?? c?.name ?? c?.function?.name ?? "unknown",
221
+ arguments: parseToolArguments(c?.args ?? c?.input ?? c?.arguments ?? c?.function?.arguments),
222
+ toolId: c?.toolCallId ?? c?.toolId ?? c?.id,
223
+ type: c?.type === "function" ? c.type : "function"
224
+ }));
163
225
  }
226
+ /**
227
+ * Converts a Gemini content item to Datadog message format.
228
+ * Extracts text from parts, skips binary data to avoid bloating traces.
229
+ */
164
230
  function geminiContentToMessage(item) {
165
- const text = item.parts.map((p) => {
166
- if (typeof p === "string") return p;
167
- if (p?.text) return p.text;
168
- if (p?.inlineData) return `[${p.inlineData.mimeType ?? "binary"}]`;
169
- if (p?.functionCall) return `[tool: ${p.functionCall.name ?? "unknown"}]`;
170
- return safeStringify(p);
171
- }).join("");
172
- return { role: item.role, content: text };
231
+ const text = item.parts.map((p) => {
232
+ if (typeof p === "string") return p;
233
+ if (p?.text) return p.text;
234
+ if (p?.inlineData) return `[${p.inlineData.mimeType ?? "binary"}]`;
235
+ if (p?.functionCall) return `[tool: ${p.functionCall.name ?? "unknown"}]`;
236
+ return safeStringify(p);
237
+ }).join("");
238
+ return {
239
+ role: item.role,
240
+ content: text
241
+ };
173
242
  }
243
+ /**
244
+ * Formats input data for Datadog annotations.
245
+ * Model spans use message array format; others use raw or stringified data.
246
+ */
174
247
  function formatInput(input, spanType) {
175
- if (isModelDataSpan(spanType)) {
176
- if (isMessageArray(input)) {
177
- return toDatadogMessages(input);
178
- }
179
- if (isGeminiContentArray(input)) {
180
- return toDatadogMessages(input.map(geminiContentToMessage));
181
- }
182
- if (input && typeof input === "object" && !Array.isArray(input)) {
183
- if (isMessageArray(input.messages)) {
184
- return toDatadogMessages(input.messages);
185
- }
186
- if (isGeminiContentArray(input.messages)) {
187
- return toDatadogMessages(input.messages.map(geminiContentToMessage));
188
- }
189
- if (isGeminiContentArray(input.contents)) {
190
- return toDatadogMessages(input.contents.map(geminiContentToMessage));
191
- }
192
- }
193
- if (typeof input === "string") {
194
- return toDatadogMessages([{ role: "user", content: input }]);
195
- }
196
- return toDatadogMessages([{ role: "user", content: safeStringify(input) }]);
197
- }
198
- if (typeof input === "string" || Array.isArray(input)) return input;
199
- return safeStringify(input);
248
+ if (isModelDataSpan(spanType)) {
249
+ if (isMessageArray(input)) return toDatadogMessages(input);
250
+ if (isGeminiContentArray(input)) return toDatadogMessages(input.map(geminiContentToMessage));
251
+ if (input && typeof input === "object" && !Array.isArray(input)) {
252
+ if (isMessageArray(input.messages)) return toDatadogMessages(input.messages);
253
+ if (isGeminiContentArray(input.messages)) return toDatadogMessages(input.messages.map(geminiContentToMessage));
254
+ if (isGeminiContentArray(input.contents)) return toDatadogMessages(input.contents.map(geminiContentToMessage));
255
+ }
256
+ if (typeof input === "string") return toDatadogMessages([{
257
+ role: "user",
258
+ content: input
259
+ }]);
260
+ return toDatadogMessages([{
261
+ role: "user",
262
+ content: safeStringify(input)
263
+ }]);
264
+ }
265
+ if (typeof input === "string" || Array.isArray(input)) return input;
266
+ return safeStringify(input);
200
267
  }
268
+ /**
269
+ * Formats output data for Datadog annotations.
270
+ * Model spans use message array format; others use raw or stringified data.
271
+ */
201
272
  function formatOutput(output, spanType) {
202
- if (isModelDataSpan(spanType)) {
203
- if (isMessageArray(output)) {
204
- return toDatadogMessages(output);
205
- }
206
- if (typeof output === "string") {
207
- return [{ role: "assistant", content: output }];
208
- }
209
- if (output && typeof output === "object") {
210
- if (Array.isArray(output.toolCalls) && output.toolCalls.length > 0) {
211
- return [
212
- {
213
- role: "assistant",
214
- content: typeof output.text === "string" ? output.text : "",
215
- toolCalls: toDatadogToolCalls(output.toolCalls)
216
- }
217
- ];
218
- }
219
- if (typeof output.text === "string" && output.text.length > 0) {
220
- return [{ role: "assistant", content: output.text }];
221
- }
222
- if (output.object !== void 0) {
223
- return [{ role: "assistant", content: safeStringify(output.object) }];
224
- }
225
- }
226
- return [{ role: "assistant", content: safeStringify(output) }];
227
- }
228
- if (typeof output === "string") return output;
229
- return safeStringify(output);
273
+ if (isModelDataSpan(spanType)) {
274
+ if (isMessageArray(output)) return toDatadogMessages(output);
275
+ if (typeof output === "string") return [{
276
+ role: "assistant",
277
+ content: output
278
+ }];
279
+ if (output && typeof output === "object") {
280
+ if (Array.isArray(output.toolCalls) && output.toolCalls.length > 0) return [{
281
+ role: "assistant",
282
+ content: typeof output.text === "string" ? output.text : "",
283
+ toolCalls: toDatadogToolCalls(output.toolCalls)
284
+ }];
285
+ if (typeof output.text === "string" && output.text.length > 0) return [{
286
+ role: "assistant",
287
+ content: output.text
288
+ }];
289
+ if (output.object !== void 0) return [{
290
+ role: "assistant",
291
+ content: safeStringify(output.object)
292
+ }];
293
+ }
294
+ return [{
295
+ role: "assistant",
296
+ content: safeStringify(output)
297
+ }];
298
+ }
299
+ if (typeof output === "string") return output;
300
+ return safeStringify(output);
230
301
  }
231
-
232
- // src/bridge.ts
302
+ //#endregion
303
+ //#region src/bridge.ts
233
304
  function flushApmExporter() {
234
- const exporterFlush = tracer3__default.default?._tracer?._exporter?.flush;
235
- if (typeof exporterFlush !== "function") {
236
- return Promise.resolve();
237
- }
238
- return new Promise((resolve, reject) => {
239
- let settled = false;
240
- const done = (error) => {
241
- if (settled) return;
242
- settled = true;
243
- if (error) {
244
- reject(error);
245
- return;
246
- }
247
- resolve();
248
- };
249
- try {
250
- exporterFlush.call(tracer3__default.default._tracer._exporter, done);
251
- } catch (error) {
252
- reject(error);
253
- }
254
- });
305
+ const exporterFlush = dd_trace.default?._tracer?._exporter?.flush;
306
+ if (typeof exporterFlush !== "function") return Promise.resolve();
307
+ return new Promise((resolve, reject) => {
308
+ let settled = false;
309
+ const done = (error) => {
310
+ if (settled) return;
311
+ settled = true;
312
+ if (error) {
313
+ reject(error);
314
+ return;
315
+ }
316
+ resolve();
317
+ };
318
+ try {
319
+ exporterFlush.call(dd_trace.default._tracer._exporter, done);
320
+ } catch (error) {
321
+ reject(error);
322
+ }
323
+ });
255
324
  }
256
- var DatadogBridge = class extends observability.BaseExporter {
257
- name = "datadog-bridge";
258
- config;
259
- ddSpanMap = /* @__PURE__ */ new Map();
260
- traceContext = /* @__PURE__ */ new Map();
261
- openSpanCounts = /* @__PURE__ */ new Map();
262
- constructor(config = {}) {
263
- super(config);
264
- const mlApp = config.mlApp ?? process.env.DD_LLMOBS_ML_APP;
265
- const apiKey = config.apiKey ?? process.env.DD_API_KEY;
266
- const site = config.site ?? process.env.DD_SITE ?? "datadoghq.com";
267
- const env = config.env ?? process.env.DD_ENV;
268
- const envAgentless = process.env.DD_LLMOBS_AGENTLESS_ENABLED?.toLowerCase();
269
- const agentless = config.agentless ?? (envAgentless === "true" || envAgentless === "1" ? true : false);
270
- if (!mlApp) {
271
- this.setDisabled(`Missing required mlApp. Set DD_LLMOBS_ML_APP environment variable or pass mlApp in config.`);
272
- this.config = config;
273
- return;
274
- }
275
- if (agentless && !apiKey) {
276
- this.setDisabled(
277
- `Missing required apiKey for agentless mode. Set DD_API_KEY environment variable or pass apiKey in config.`
278
- );
279
- this.config = config;
280
- return;
281
- }
282
- this.config = {
283
- ...config,
284
- mlApp,
285
- site,
286
- apiKey,
287
- agentless,
288
- env
289
- };
290
- ensureTracer({
291
- mlApp,
292
- site,
293
- apiKey,
294
- agentless,
295
- service: config.service,
296
- env,
297
- integrationsEnabled: config.integrationsEnabled ?? true
298
- });
299
- this.logger.info("Datadog bridge initialized", { mlApp, site, agentless });
300
- }
301
- /**
302
- * Create a dd-trace span eagerly when a Mastra span is constructed.
303
- */
304
- createSpan(options) {
305
- if (this.isDisabled) return void 0;
306
- try {
307
- let apmParentDdSpan = void 0;
308
- let llmobsParentDdSpan = void 0;
309
- let parentSource = "none";
310
- const externalParentId = observability.getExternalParentId(options);
311
- if (externalParentId) {
312
- apmParentDdSpan = this.ddSpanMap.get(externalParentId);
313
- llmobsParentDdSpan = apmParentDdSpan;
314
- if (apmParentDdSpan) {
315
- parentSource = "external-parent";
316
- }
317
- }
318
- if (!apmParentDdSpan) {
319
- apmParentDdSpan = tracer3__default.default.scope().active() ?? void 0;
320
- if (apmParentDdSpan) {
321
- parentSource = "active-scope";
322
- }
323
- }
324
- if (!llmobsParentDdSpan && externalParentId) {
325
- llmobsParentDdSpan = apmParentDdSpan;
326
- }
327
- const ddSpan = tracer3__default.default.startSpan(options.name, {
328
- ...apmParentDdSpan ? { childOf: apmParentDdSpan } : {},
329
- ...options.startTime ? { startTime: toDate(options.startTime).getTime() } : {}
330
- });
331
- const ddContext = ddSpan.context?.();
332
- const spanId = ddContext?.toSpanId?.(true) ?? generateSpanId();
333
- const traceId = ddContext?.toTraceId?.(true) ?? (externalParentId ? options.parent?.traceId ?? generateTraceId() : generateTraceId());
334
- const parentContext = apmParentDdSpan?.context?.();
335
- const parentSpanId = parentContext?.toSpanId?.(true) ?? externalParentId;
336
- this.captureTraceContext(traceId, options);
337
- this.openSpanCounts.set(traceId, (this.openSpanCounts.get(traceId) ?? 0) + 1);
338
- this.ddSpanMap.set(spanId, ddSpan);
339
- this.registerLlmObsSpan(ddSpan, traceId, options, llmobsParentDdSpan);
340
- this.logger.debug(
341
- `[DatadogBridge.createSpan] Created APM span [spanId=${spanId}] [traceId=${traceId}] [parentSpanId=${parentSpanId}] [type=${options.type}] [mapSize=${this.ddSpanMap.size}] [parentSource=${parentSource}] [externalParentId=${externalParentId ?? "none"}]`
342
- );
343
- return { spanId, traceId, parentSpanId };
344
- } catch (error) {
345
- this.logger.error("[DatadogBridge] Failed to create span:", error);
346
- return void 0;
347
- }
348
- }
349
- /**
350
- * Execute an async function within the dd-trace context of a Mastra span.
351
- */
352
- executeInContext(spanId, fn) {
353
- return this.executeWithSpanContext(spanId, fn);
354
- }
355
- /**
356
- * Execute a synchronous function within the dd-trace context of a Mastra span.
357
- */
358
- executeInContextSync(spanId, fn) {
359
- return this.executeWithSpanContext(spanId, fn);
360
- }
361
- async onScoreEvent(event) {
362
- if (this.isDisabled || !tracer3__default.default.llmobs?.submitEvaluation) return;
363
- const { score } = event;
364
- if (!score.traceId || !score.spanId) {
365
- this.logger.warn("Datadog bridge: dropping score with no traceId/spanId", {
366
- scorerId: score.scorerId
367
- });
368
- return;
369
- }
370
- try {
371
- tracer3__default.default.llmobs.submitEvaluation(
372
- { traceId: score.traceId, spanId: toDatadogSpanId(score.spanId) },
373
- {
374
- label: score.scorerName ?? score.scorerId,
375
- value: score.score,
376
- metricType: "score",
377
- mlApp: this.config.mlApp,
378
- timestampMs: score.timestamp instanceof Date ? score.timestamp.getTime() : Date.now(),
379
- ...score.reason ? { reasoning: score.reason } : {},
380
- ...score.metadata ? { metadata: score.metadata } : {}
381
- }
382
- );
383
- } catch (error) {
384
- this.logger.error("Datadog bridge: Failed to submit evaluation", {
385
- error,
386
- traceId: score.traceId,
387
- spanId: score.spanId,
388
- scorerId: score.scorerId
389
- });
390
- }
391
- }
392
- executeWithSpanContext(spanId, fn) {
393
- const ddSpan = this.ddSpanMap.get(spanId);
394
- if (ddSpan) {
395
- return tracer3__default.default.scope().activate(ddSpan, () => {
396
- const llmobs = tracer3__default.default.llmobs;
397
- if (typeof llmobs?._activate === "function") {
398
- return llmobs._activate(ddSpan, void 0, fn);
399
- }
400
- return fn();
401
- });
402
- }
403
- return fn();
404
- }
405
- /**
406
- * Handle tracing events from the observability bus.
407
- */
408
- async _exportTracingEvent(event) {
409
- if (this.isDisabled) return;
410
- try {
411
- const span = event.exportedSpan;
412
- if (span.isEvent) {
413
- if (event.type === "span_started" || event.type === "span_ended") {
414
- this.annotateAndFinishSpan(span);
415
- }
416
- return;
417
- }
418
- switch (event.type) {
419
- case "span_started":
420
- case "span_updated":
421
- return;
422
- case "span_ended":
423
- this.annotateAndFinishSpan(span);
424
- return;
425
- }
426
- } catch (error) {
427
- this.logger.error("Datadog bridge error", {
428
- error,
429
- eventType: event.type,
430
- spanId: event.exportedSpan?.id,
431
- spanName: event.exportedSpan?.name
432
- });
433
- }
434
- }
435
- registerLlmObsSpan(ddSpan, traceId, options, parentDdSpan) {
436
- const tagger = this.getLlmObsTagger();
437
- if (!tagger) return;
438
- try {
439
- const kind = kindFor(options.type);
440
- const ownAttrs = options.attributes;
441
- const inheritedModelAttrs = options.parent?.type === observability$1.SpanType.MODEL_GENERATION ? options.parent.attributes : void 0;
442
- const traceCtx = this.resolveTraceContext(traceId, options);
443
- tagger.registerLLMObsSpan(ddSpan, {
444
- parent: parentDdSpan,
445
- kind,
446
- name: options.name,
447
- userId: traceCtx.userId,
448
- sessionId: traceCtx.sessionId,
449
- ...kind === "llm" && (ownAttrs?.model ?? inheritedModelAttrs?.model) ? { modelName: ownAttrs?.model ?? inheritedModelAttrs?.model } : {},
450
- ...kind === "llm" && (ownAttrs?.provider ?? inheritedModelAttrs?.provider) ? { modelProvider: ownAttrs?.provider ?? inheritedModelAttrs?.provider } : {}
451
- });
452
- } catch (error) {
453
- this.logger.warn("[DatadogBridge] Failed to register LLMObs span", {
454
- error,
455
- spanName: options.name,
456
- spanType: options.type
457
- });
458
- }
459
- }
460
- getLlmObsTagger() {
461
- const tagger = tracer3__default.default.llmobs?._tagger;
462
- if (tagger && typeof tagger.registerLLMObsSpan === "function") {
463
- return tagger;
464
- }
465
- return void 0;
466
- }
467
- /**
468
- * Annotate the eagerly-created dd span and finish it using the final Mastra
469
- * span state.
470
- */
471
- annotateAndFinishSpan(span) {
472
- const ddSpan = this.ddSpanMap.get(span.id);
473
- if (!ddSpan) {
474
- this.logger.warn("[DatadogBridge] No dd span found when finalizing Mastra span", {
475
- spanId: span.id,
476
- spanName: span.name,
477
- spanType: span.type,
478
- mapSize: this.ddSpanMap.size
479
- });
480
- return;
481
- }
482
- const endTime = span.endTime ? toDate(span.endTime) : span.isEvent ? toDate(span.startTime) : /* @__PURE__ */ new Date();
483
- const annotations = this.buildAnnotations(span);
484
- try {
485
- if (Object.keys(annotations).length > 0 && tracer3__default.default.llmobs?.annotate) {
486
- tracer3__default.default.llmobs.annotate(ddSpan, annotations);
487
- }
488
- } catch (error) {
489
- this.logger.error("[DatadogBridge] Failed to annotate span before finish", {
490
- error,
491
- spanId: span.id,
492
- spanName: span.name
493
- });
494
- }
495
- try {
496
- if (span.errorInfo) {
497
- this.setErrorTags(ddSpan, span.errorInfo);
498
- }
499
- } catch (error) {
500
- this.logger.error("[DatadogBridge] Failed to set error tags on dd span", {
501
- error,
502
- spanId: span.id,
503
- spanName: span.name
504
- });
505
- }
506
- try {
507
- if (typeof ddSpan.finish === "function") {
508
- ddSpan.finish(endTime.getTime());
509
- }
510
- } catch (error) {
511
- this.logger.error("[DatadogBridge] Failed to finish dd span", {
512
- error,
513
- spanId: span.id,
514
- spanName: span.name
515
- });
516
- } finally {
517
- this.ddSpanMap.delete(span.id);
518
- this.releaseTraceContext(span.traceId);
519
- }
520
- }
521
- captureTraceContext(traceId, options) {
522
- const existing = this.traceContext.get(traceId);
523
- const next = {
524
- userId: firstString(options.metadata?.userId, options.parent?.metadata?.userId, existing?.userId),
525
- sessionId: firstString(options.metadata?.sessionId, options.parent?.metadata?.sessionId, existing?.sessionId)
526
- };
527
- if (next.userId || next.sessionId) {
528
- this.traceContext.set(traceId, next);
529
- }
530
- }
531
- resolveTraceContext(traceId, options) {
532
- const stored = this.traceContext.get(traceId);
533
- return {
534
- userId: firstString(options.metadata?.userId, options.parent?.metadata?.userId, stored?.userId),
535
- sessionId: firstString(options.metadata?.sessionId, options.parent?.metadata?.sessionId, stored?.sessionId)
536
- };
537
- }
538
- releaseTraceContext(traceId) {
539
- const nextCount = (this.openSpanCounts.get(traceId) ?? 1) - 1;
540
- if (nextCount > 0) {
541
- this.openSpanCounts.set(traceId, nextCount);
542
- return;
543
- }
544
- this.openSpanCounts.delete(traceId);
545
- this.traceContext.delete(traceId);
546
- }
547
- buildAnnotations(span) {
548
- const annotations = {};
549
- if (span.input !== void 0) {
550
- annotations.inputData = formatInput(span.input, span.type);
551
- }
552
- if (span.output !== void 0) {
553
- annotations.outputData = formatOutput(span.output, span.type);
554
- }
555
- const usageSpanType = isModelInferenceEnabled() ? observability$1.SpanType.MODEL_INFERENCE : observability$1.SpanType.MODEL_STEP;
556
- if (span.type === usageSpanType) {
557
- const usage = span.attributes?.usage;
558
- const metrics = formatUsageMetrics(usage);
559
- if (metrics) {
560
- annotations.metrics = metrics;
561
- }
562
- }
563
- const knownFields = ["usage", "model", "provider"];
564
- const otherAttributes = utils.omitKeys(span.attributes ?? {}, knownFields);
565
- const contextKeySet = new Set(this.config.requestContextKeys ?? []);
566
- const flatContextTags = {};
567
- const remainingMetadata = {};
568
- for (const [key, value] of Object.entries(span.metadata ?? {})) {
569
- if (contextKeySet.has(key)) {
570
- flatContextTags[key] = value;
571
- } else {
572
- remainingMetadata[key] = value;
573
- }
574
- }
575
- const remainingAttributes = {};
576
- for (const [key, value] of Object.entries(otherAttributes)) {
577
- if (contextKeySet.has(key)) {
578
- if (!(key in flatContextTags)) {
579
- flatContextTags[key] = value;
580
- }
581
- } else {
582
- remainingAttributes[key] = value;
583
- }
584
- }
585
- const combinedMetadata = {
586
- ...remainingMetadata,
587
- ...remainingAttributes
588
- };
589
- if (span.errorInfo) {
590
- combinedMetadata["error.message"] = span.errorInfo.message;
591
- }
592
- if (Object.keys(combinedMetadata).length > 0) {
593
- annotations.metadata = combinedMetadata;
594
- }
595
- const tags = {
596
- ...flatContextTags
597
- };
598
- if (span.tags?.length) {
599
- for (const tag of span.tags) {
600
- const colonIndex = tag.indexOf(":");
601
- if (colonIndex > 0) {
602
- tags[tag.substring(0, colonIndex)] = tag.substring(colonIndex + 1);
603
- } else {
604
- tags[tag] = true;
605
- }
606
- }
607
- }
608
- if (span.errorInfo) {
609
- tags.error = true;
610
- if (span.errorInfo.id) {
611
- tags["error.id"] = span.errorInfo.id;
612
- }
613
- if (span.errorInfo.domain) {
614
- tags["error.domain"] = span.errorInfo.domain;
615
- }
616
- if (span.errorInfo.category) {
617
- tags["error.category"] = span.errorInfo.category;
618
- }
619
- }
620
- if (Object.keys(tags).length > 0) {
621
- annotations.tags = tags;
622
- }
623
- return annotations;
624
- }
625
- setErrorTags(ddSpan, errorInfo) {
626
- ddSpan.setTag("error", true);
627
- ddSpan.setTag("error.message", errorInfo.message);
628
- ddSpan.setTag("error.type", errorInfo.name ?? errorInfo.category ?? "Error");
629
- if (errorInfo.stack) {
630
- ddSpan.setTag("error.stack", errorInfo.stack);
631
- }
632
- }
633
- async flush() {
634
- if (this.isDisabled) return;
635
- if (tracer3__default.default.llmobs?.flush) {
636
- try {
637
- await tracer3__default.default.llmobs.flush();
638
- this.logger.debug("Datadog llmobs flushed");
639
- } catch (e) {
640
- this.logger.error("Error flushing llmobs", { error: e });
641
- }
642
- }
643
- try {
644
- await flushApmExporter();
645
- this.logger.debug("Datadog APM exporter flushed");
646
- } catch (e) {
647
- this.logger.error("Error flushing Datadog APM exporter", { error: e });
648
- }
649
- }
650
- async shutdown() {
651
- for (const [spanId, ddSpan] of this.ddSpanMap) {
652
- this.logger.warn(`[DatadogBridge] Force-finishing APM span that was not properly closed [id=${spanId}]`);
653
- try {
654
- if (typeof ddSpan.finish === "function") {
655
- ddSpan.finish();
656
- }
657
- } catch {
658
- }
659
- }
660
- this.ddSpanMap.clear();
661
- this.openSpanCounts.clear();
662
- this.traceContext.clear();
663
- await this.flush();
664
- if (tracer3__default.default.llmobs?.disable) {
665
- try {
666
- tracer3__default.default.llmobs.disable();
667
- } catch (e) {
668
- this.logger.error("Error disabling llmobs", { error: e });
669
- }
670
- }
671
- await super.shutdown();
672
- }
325
+ /**
326
+ * Datadog Bridge for Mastra Observability.
327
+ *
328
+ * Creates native dd-trace spans in real time during execution for proper APM
329
+ * context propagation, and uses the same live dd span for Datadog LLMObs
330
+ * tagging and annotations.
331
+ */
332
+ var DatadogBridge = class extends _mastra_observability.BaseExporter {
333
+ name = "datadog-bridge";
334
+ config;
335
+ ddSpanMap = /* @__PURE__ */ new Map();
336
+ traceContext = /* @__PURE__ */ new Map();
337
+ openSpanCounts = /* @__PURE__ */ new Map();
338
+ constructor(config = {}) {
339
+ super(config);
340
+ const mlApp = config.mlApp ?? process.env.DD_LLMOBS_ML_APP;
341
+ const apiKey = config.apiKey ?? process.env.DD_API_KEY;
342
+ const site = config.site ?? process.env.DD_SITE ?? "datadoghq.com";
343
+ const env = config.env ?? process.env.DD_ENV;
344
+ const envAgentless = process.env.DD_LLMOBS_AGENTLESS_ENABLED?.toLowerCase();
345
+ const agentless = config.agentless ?? (envAgentless === "true" || envAgentless === "1" ? true : false);
346
+ if (!mlApp) {
347
+ this.setDisabled(`Missing required mlApp. Set DD_LLMOBS_ML_APP environment variable or pass mlApp in config.`);
348
+ this.config = config;
349
+ return;
350
+ }
351
+ if (agentless && !apiKey) {
352
+ this.setDisabled(`Missing required apiKey for agentless mode. Set DD_API_KEY environment variable or pass apiKey in config.`);
353
+ this.config = config;
354
+ return;
355
+ }
356
+ this.config = {
357
+ ...config,
358
+ mlApp,
359
+ site,
360
+ apiKey,
361
+ agentless,
362
+ env
363
+ };
364
+ ensureTracer({
365
+ mlApp,
366
+ site,
367
+ apiKey,
368
+ agentless,
369
+ service: config.service,
370
+ env,
371
+ integrationsEnabled: config.integrationsEnabled ?? true
372
+ });
373
+ this.logger.info("Datadog bridge initialized", {
374
+ mlApp,
375
+ site,
376
+ agentless
377
+ });
378
+ }
379
+ /**
380
+ * Create a dd-trace span eagerly when a Mastra span is constructed.
381
+ */
382
+ createSpan(options) {
383
+ if (this.isDisabled) return void 0;
384
+ try {
385
+ let apmParentDdSpan = void 0;
386
+ let llmobsParentDdSpan = void 0;
387
+ let parentSource = "none";
388
+ const externalParentId = (0, _mastra_observability.getExternalParentId)(options);
389
+ if (externalParentId) {
390
+ apmParentDdSpan = this.ddSpanMap.get(externalParentId);
391
+ llmobsParentDdSpan = apmParentDdSpan;
392
+ if (apmParentDdSpan) parentSource = "external-parent";
393
+ }
394
+ if (!apmParentDdSpan) {
395
+ apmParentDdSpan = dd_trace.default.scope().active() ?? void 0;
396
+ if (apmParentDdSpan) parentSource = "active-scope";
397
+ }
398
+ if (!llmobsParentDdSpan && externalParentId) llmobsParentDdSpan = apmParentDdSpan;
399
+ const ddSpan = dd_trace.default.startSpan(options.name, {
400
+ ...apmParentDdSpan ? { childOf: apmParentDdSpan } : {},
401
+ ...options.startTime ? { startTime: toDate(options.startTime).getTime() } : {}
402
+ });
403
+ const ddContext = ddSpan.context?.();
404
+ const spanId = ddContext?.toSpanId?.(true) ?? generateSpanId();
405
+ const traceId = ddContext?.toTraceId?.(true) ?? (externalParentId ? options.parent?.traceId ?? generateTraceId() : generateTraceId());
406
+ const parentSpanId = (apmParentDdSpan?.context?.())?.toSpanId?.(true) ?? externalParentId;
407
+ this.captureTraceContext(traceId, options);
408
+ this.openSpanCounts.set(traceId, (this.openSpanCounts.get(traceId) ?? 0) + 1);
409
+ this.ddSpanMap.set(spanId, ddSpan);
410
+ this.registerLlmObsSpan(ddSpan, traceId, options, llmobsParentDdSpan);
411
+ this.logger.debug(`[DatadogBridge.createSpan] Created APM span [spanId=${spanId}] [traceId=${traceId}] [parentSpanId=${parentSpanId}] [type=${options.type}] [mapSize=${this.ddSpanMap.size}] [parentSource=${parentSource}] [externalParentId=${externalParentId ?? "none"}]`);
412
+ return {
413
+ spanId,
414
+ traceId,
415
+ parentSpanId
416
+ };
417
+ } catch (error) {
418
+ this.logger.error("[DatadogBridge] Failed to create span:", error);
419
+ return;
420
+ }
421
+ }
422
+ /**
423
+ * Execute an async function within the dd-trace context of a Mastra span.
424
+ */
425
+ executeInContext(spanId, fn) {
426
+ return this.executeWithSpanContext(spanId, fn);
427
+ }
428
+ /**
429
+ * Execute a synchronous function within the dd-trace context of a Mastra span.
430
+ */
431
+ executeInContextSync(spanId, fn) {
432
+ return this.executeWithSpanContext(spanId, fn);
433
+ }
434
+ async onScoreEvent(event) {
435
+ if (this.isDisabled || !dd_trace.default.llmobs?.submitEvaluation) return;
436
+ const { score } = event;
437
+ if (!score.traceId || !score.spanId) {
438
+ this.logger.warn("Datadog bridge: dropping score with no traceId/spanId", { scorerId: score.scorerId });
439
+ return;
440
+ }
441
+ try {
442
+ dd_trace.default.llmobs.submitEvaluation({
443
+ traceId: score.traceId,
444
+ spanId: toDatadogSpanId(score.spanId)
445
+ }, {
446
+ label: score.scorerName ?? score.scorerId,
447
+ value: score.score,
448
+ metricType: "score",
449
+ mlApp: this.config.mlApp,
450
+ timestampMs: score.timestamp instanceof Date ? score.timestamp.getTime() : Date.now(),
451
+ ...score.reason ? { reasoning: score.reason } : {},
452
+ ...score.metadata ? { metadata: score.metadata } : {}
453
+ });
454
+ } catch (error) {
455
+ this.logger.error("Datadog bridge: Failed to submit evaluation", {
456
+ error,
457
+ traceId: score.traceId,
458
+ spanId: score.spanId,
459
+ scorerId: score.scorerId
460
+ });
461
+ }
462
+ }
463
+ executeWithSpanContext(spanId, fn) {
464
+ const ddSpan = this.ddSpanMap.get(spanId);
465
+ if (ddSpan) return dd_trace.default.scope().activate(ddSpan, () => {
466
+ const llmobs = dd_trace.default.llmobs;
467
+ if (typeof llmobs?._activate === "function") return llmobs._activate(ddSpan, void 0, fn);
468
+ return fn();
469
+ });
470
+ return fn();
471
+ }
472
+ /**
473
+ * Handle tracing events from the observability bus.
474
+ */
475
+ async _exportTracingEvent(event) {
476
+ if (this.isDisabled) return;
477
+ try {
478
+ const span = event.exportedSpan;
479
+ if (span.isEvent) {
480
+ if (event.type === "span_started" || event.type === "span_ended") this.annotateAndFinishSpan(span);
481
+ return;
482
+ }
483
+ switch (event.type) {
484
+ case "span_started":
485
+ case "span_updated": return;
486
+ case "span_ended":
487
+ this.annotateAndFinishSpan(span);
488
+ return;
489
+ }
490
+ } catch (error) {
491
+ this.logger.error("Datadog bridge error", {
492
+ error,
493
+ eventType: event.type,
494
+ spanId: event.exportedSpan?.id,
495
+ spanName: event.exportedSpan?.name
496
+ });
497
+ }
498
+ }
499
+ registerLlmObsSpan(ddSpan, traceId, options, parentDdSpan) {
500
+ const tagger = this.getLlmObsTagger();
501
+ if (!tagger) return;
502
+ try {
503
+ const kind = kindFor(options.type);
504
+ const ownAttrs = options.attributes;
505
+ const inheritedModelAttrs = options.parent?.type === _mastra_core_observability.SpanType.MODEL_GENERATION ? options.parent.attributes : void 0;
506
+ const traceCtx = this.resolveTraceContext(traceId, options);
507
+ tagger.registerLLMObsSpan(ddSpan, {
508
+ parent: parentDdSpan,
509
+ kind,
510
+ name: options.name,
511
+ userId: traceCtx.userId,
512
+ sessionId: traceCtx.sessionId,
513
+ ...kind === "llm" && (ownAttrs?.model ?? inheritedModelAttrs?.model) ? { modelName: ownAttrs?.model ?? inheritedModelAttrs?.model } : {},
514
+ ...kind === "llm" && (ownAttrs?.provider ?? inheritedModelAttrs?.provider) ? { modelProvider: ownAttrs?.provider ?? inheritedModelAttrs?.provider } : {}
515
+ });
516
+ } catch (error) {
517
+ this.logger.warn("[DatadogBridge] Failed to register LLMObs span", {
518
+ error,
519
+ spanName: options.name,
520
+ spanType: options.type
521
+ });
522
+ }
523
+ }
524
+ getLlmObsTagger() {
525
+ const tagger = dd_trace.default.llmobs?._tagger;
526
+ if (tagger && typeof tagger.registerLLMObsSpan === "function") return tagger;
527
+ }
528
+ /**
529
+ * Annotate the eagerly-created dd span and finish it using the final Mastra
530
+ * span state.
531
+ */
532
+ annotateAndFinishSpan(span) {
533
+ const ddSpan = this.ddSpanMap.get(span.id);
534
+ if (!ddSpan) {
535
+ this.logger.warn("[DatadogBridge] No dd span found when finalizing Mastra span", {
536
+ spanId: span.id,
537
+ spanName: span.name,
538
+ spanType: span.type,
539
+ mapSize: this.ddSpanMap.size
540
+ });
541
+ return;
542
+ }
543
+ const endTime = span.endTime ? toDate(span.endTime) : span.isEvent ? toDate(span.startTime) : /* @__PURE__ */ new Date();
544
+ const annotations = this.buildAnnotations(span);
545
+ try {
546
+ if (Object.keys(annotations).length > 0 && dd_trace.default.llmobs?.annotate) dd_trace.default.llmobs.annotate(ddSpan, annotations);
547
+ } catch (error) {
548
+ this.logger.error("[DatadogBridge] Failed to annotate span before finish", {
549
+ error,
550
+ spanId: span.id,
551
+ spanName: span.name
552
+ });
553
+ }
554
+ try {
555
+ if (span.errorInfo) this.setErrorTags(ddSpan, span.errorInfo);
556
+ } catch (error) {
557
+ this.logger.error("[DatadogBridge] Failed to set error tags on dd span", {
558
+ error,
559
+ spanId: span.id,
560
+ spanName: span.name
561
+ });
562
+ }
563
+ try {
564
+ if (typeof ddSpan.finish === "function") ddSpan.finish(endTime.getTime());
565
+ } catch (error) {
566
+ this.logger.error("[DatadogBridge] Failed to finish dd span", {
567
+ error,
568
+ spanId: span.id,
569
+ spanName: span.name
570
+ });
571
+ } finally {
572
+ this.ddSpanMap.delete(span.id);
573
+ this.releaseTraceContext(span.traceId);
574
+ }
575
+ }
576
+ captureTraceContext(traceId, options) {
577
+ const existing = this.traceContext.get(traceId);
578
+ const next = {
579
+ userId: firstString(options.metadata?.userId, options.parent?.metadata?.userId, existing?.userId),
580
+ sessionId: firstString(options.metadata?.sessionId, options.parent?.metadata?.sessionId, existing?.sessionId)
581
+ };
582
+ if (next.userId || next.sessionId) this.traceContext.set(traceId, next);
583
+ }
584
+ resolveTraceContext(traceId, options) {
585
+ const stored = this.traceContext.get(traceId);
586
+ return {
587
+ userId: firstString(options.metadata?.userId, options.parent?.metadata?.userId, stored?.userId),
588
+ sessionId: firstString(options.metadata?.sessionId, options.parent?.metadata?.sessionId, stored?.sessionId)
589
+ };
590
+ }
591
+ releaseTraceContext(traceId) {
592
+ const nextCount = (this.openSpanCounts.get(traceId) ?? 1) - 1;
593
+ if (nextCount > 0) {
594
+ this.openSpanCounts.set(traceId, nextCount);
595
+ return;
596
+ }
597
+ this.openSpanCounts.delete(traceId);
598
+ this.traceContext.delete(traceId);
599
+ }
600
+ buildAnnotations(span) {
601
+ const annotations = {};
602
+ if (span.input !== void 0) annotations.inputData = formatInput(span.input, span.type);
603
+ if (span.output !== void 0) annotations.outputData = formatOutput(span.output, span.type);
604
+ const usageSpanType = isModelInferenceEnabled() ? _mastra_core_observability.SpanType.MODEL_INFERENCE : _mastra_core_observability.SpanType.MODEL_STEP;
605
+ if (span.type === usageSpanType) {
606
+ const usage = span.attributes?.usage;
607
+ const metrics = formatUsageMetrics(usage);
608
+ if (metrics) annotations.metrics = metrics;
609
+ }
610
+ const otherAttributes = (0, _mastra_core_utils.omitKeys)(span.attributes ?? {}, [
611
+ "usage",
612
+ "model",
613
+ "provider"
614
+ ]);
615
+ const contextKeySet = new Set(this.config.requestContextKeys ?? []);
616
+ const flatContextTags = {};
617
+ const remainingMetadata = {};
618
+ for (const [key, value] of Object.entries(span.metadata ?? {})) if (contextKeySet.has(key)) flatContextTags[key] = value;
619
+ else remainingMetadata[key] = value;
620
+ const remainingAttributes = {};
621
+ for (const [key, value] of Object.entries(otherAttributes)) if (contextKeySet.has(key)) {
622
+ if (!(key in flatContextTags)) flatContextTags[key] = value;
623
+ } else remainingAttributes[key] = value;
624
+ const combinedMetadata = {
625
+ ...remainingMetadata,
626
+ ...remainingAttributes
627
+ };
628
+ if (span.errorInfo) combinedMetadata["error.message"] = span.errorInfo.message;
629
+ if (Object.keys(combinedMetadata).length > 0) annotations.metadata = combinedMetadata;
630
+ const tags = { ...flatContextTags };
631
+ if (span.tags?.length) for (const tag of span.tags) {
632
+ const colonIndex = tag.indexOf(":");
633
+ if (colonIndex > 0) tags[tag.substring(0, colonIndex)] = tag.substring(colonIndex + 1);
634
+ else tags[tag] = true;
635
+ }
636
+ if (span.errorInfo) {
637
+ tags.error = true;
638
+ if (span.errorInfo.id) tags["error.id"] = span.errorInfo.id;
639
+ if (span.errorInfo.domain) tags["error.domain"] = span.errorInfo.domain;
640
+ if (span.errorInfo.category) tags["error.category"] = span.errorInfo.category;
641
+ }
642
+ if (Object.keys(tags).length > 0) annotations.tags = tags;
643
+ return annotations;
644
+ }
645
+ setErrorTags(ddSpan, errorInfo) {
646
+ ddSpan.setTag("error", true);
647
+ ddSpan.setTag("error.message", errorInfo.message);
648
+ ddSpan.setTag("error.type", errorInfo.name ?? errorInfo.category ?? "Error");
649
+ if (errorInfo.stack) ddSpan.setTag("error.stack", errorInfo.stack);
650
+ }
651
+ async flush() {
652
+ if (this.isDisabled) return;
653
+ if (dd_trace.default.llmobs?.flush) try {
654
+ await dd_trace.default.llmobs.flush();
655
+ this.logger.debug("Datadog llmobs flushed");
656
+ } catch (e) {
657
+ this.logger.error("Error flushing llmobs", { error: e });
658
+ }
659
+ try {
660
+ await flushApmExporter();
661
+ this.logger.debug("Datadog APM exporter flushed");
662
+ } catch (e) {
663
+ this.logger.error("Error flushing Datadog APM exporter", { error: e });
664
+ }
665
+ }
666
+ async shutdown() {
667
+ for (const [spanId, ddSpan] of this.ddSpanMap) {
668
+ this.logger.warn(`[DatadogBridge] Force-finishing APM span that was not properly closed [id=${spanId}]`);
669
+ try {
670
+ if (typeof ddSpan.finish === "function") ddSpan.finish();
671
+ } catch {}
672
+ }
673
+ this.ddSpanMap.clear();
674
+ this.openSpanCounts.clear();
675
+ this.traceContext.clear();
676
+ await this.flush();
677
+ if (dd_trace.default.llmobs?.disable) try {
678
+ dd_trace.default.llmobs.disable();
679
+ } catch (e) {
680
+ this.logger.error("Error disabling llmobs", { error: e });
681
+ }
682
+ await super.shutdown();
683
+ }
673
684
  };
674
685
  function firstString(...values) {
675
- for (const value of values) {
676
- if (typeof value === "string") {
677
- return value;
678
- }
679
- }
680
- return void 0;
686
+ for (const value of values) if (typeof value === "string") return value;
681
687
  }
682
688
  function fillRandomBytes(bytes) {
683
- try {
684
- const webCrypto = globalThis.crypto;
685
- if (webCrypto?.getRandomValues) {
686
- webCrypto.getRandomValues.call(webCrypto, bytes);
687
- return;
688
- }
689
- } catch {
690
- }
691
- for (let i = 0; i < bytes.length; i++) {
692
- bytes[i] = Math.floor(Math.random() * 256);
693
- }
689
+ try {
690
+ const webCrypto = globalThis.crypto;
691
+ if (webCrypto?.getRandomValues) {
692
+ webCrypto.getRandomValues.call(webCrypto, bytes);
693
+ return;
694
+ }
695
+ } catch {}
696
+ for (let i = 0; i < bytes.length; i++) bytes[i] = Math.floor(Math.random() * 256);
694
697
  }
695
698
  function toDatadogSpanId(spanId) {
696
- if (/^[0-9a-f]{16}$/i.test(spanId)) {
697
- return BigInt(`0x${spanId}`).toString(10);
698
- }
699
- return spanId;
699
+ if (/^[0-9a-f]{16}$/i.test(spanId)) return BigInt(`0x${spanId}`).toString(10);
700
+ return spanId;
700
701
  }
701
702
  function generateSpanId() {
702
- const bytes = new Uint8Array(8);
703
- fillRandomBytes(bytes);
704
- return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
703
+ const bytes = /* @__PURE__ */ new Uint8Array(8);
704
+ fillRandomBytes(bytes);
705
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
705
706
  }
706
707
  function generateTraceId() {
707
- const bytes = new Uint8Array(16);
708
- fillRandomBytes(bytes);
709
- return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
708
+ const bytes = /* @__PURE__ */ new Uint8Array(16);
709
+ fillRandomBytes(bytes);
710
+ return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
710
711
  }
711
- var MAX_TRACE_LIFETIME_MS = 30 * 60 * 1e3;
712
- var REGULAR_CLEANUP_INTERVAL_MS = 1 * 60 * 1e3;
713
- var DatadogExporter = class extends observability.BaseExporter {
714
- name = "datadog";
715
- config;
716
- traceContext = /* @__PURE__ */ new Map();
717
- traceState = /* @__PURE__ */ new Map();
718
- constructor(config = {}) {
719
- super(config);
720
- const mlApp = config.mlApp ?? process.env.DD_LLMOBS_ML_APP;
721
- const apiKey = config.apiKey ?? process.env.DD_API_KEY;
722
- const site = config.site ?? process.env.DD_SITE ?? "datadoghq.com";
723
- const env = config.env ?? process.env.DD_ENV;
724
- const envAgentless = process.env.DD_LLMOBS_AGENTLESS_ENABLED?.toLowerCase();
725
- const agentless = config.agentless ?? (envAgentless === "false" || envAgentless === "0" ? false : true);
726
- if (!mlApp) {
727
- this.setDisabled(`Missing required mlApp. Set DD_LLMOBS_ML_APP environment variable or pass mlApp in config.`);
728
- this.config = config;
729
- return;
730
- }
731
- if (agentless && !apiKey) {
732
- this.setDisabled(
733
- `Missing required apiKey for agentless mode. Set DD_API_KEY environment variable or pass apiKey in config.`
734
- );
735
- this.config = config;
736
- return;
737
- }
738
- this.config = { ...config, mlApp, site, apiKey, agentless, env };
739
- ensureTracer({
740
- mlApp,
741
- site,
742
- apiKey,
743
- agentless,
744
- service: config.service,
745
- env,
746
- integrationsEnabled: config.integrationsEnabled
747
- });
748
- this.logger.info("Datadog exporter initialized", { mlApp, site, agentless });
749
- }
750
- /**
751
- * Main entry point for tracing events from Mastra.
752
- */
753
- async _exportTracingEvent(event) {
754
- if (this.isDisabled || !tracer3__default.default.llmobs) return;
755
- try {
756
- const span = event.exportedSpan;
757
- if (span.isEvent) {
758
- if (event.type === "span_started") {
759
- this.captureTraceContext(span);
760
- this.enqueueSpan(span);
761
- }
762
- return;
763
- }
764
- switch (event.type) {
765
- case "span_started":
766
- this.captureTraceContext(span);
767
- return;
768
- case "span_updated":
769
- return;
770
- case "span_ended":
771
- this.enqueueSpan(span);
772
- return;
773
- }
774
- } catch (error) {
775
- this.logger.error("Datadog exporter error", {
776
- error,
777
- eventType: event.type,
778
- spanId: event.exportedSpan?.id,
779
- spanName: event.exportedSpan?.name
780
- });
781
- }
782
- }
783
- /**
784
- * Captures user/session context from root spans for tagging all spans in the trace.
785
- */
786
- captureTraceContext(span) {
787
- if (span.isRootSpan && !this.traceContext.has(span.traceId)) {
788
- this.traceContext.set(span.traceId, {
789
- userId: span.metadata?.userId,
790
- sessionId: span.metadata?.sessionId
791
- });
792
- }
793
- }
794
- /**
795
- * Queue span until its parent context is available, then emit spans parent-first.
796
- */
797
- enqueueSpan(span) {
798
- const state = this.getOrCreateTraceState(span.traceId);
799
- if (span.isRootSpan) {
800
- state.rootEnded = true;
801
- }
802
- state.buffer.set(span.id, span);
803
- this.tryEmitReadySpans(span.traceId);
804
- }
805
- /**
806
- * Sets native dd-trace error tags required by Datadog's Error Tracking UI.
807
- */
808
- setErrorTags(ddSpan, errorInfo) {
809
- ddSpan.setTag("error", true);
810
- ddSpan.setTag("error.message", errorInfo.message);
811
- ddSpan.setTag("error.type", errorInfo.name ?? errorInfo.category ?? "Error");
812
- if (errorInfo.stack) {
813
- ddSpan.setTag("error.stack", errorInfo.stack);
814
- }
815
- }
816
- /**
817
- * Builds annotations object for llmobs.annotate().
818
- * Uses dd-trace's expected property names: inputData, outputData, metadata, tags, metrics.
819
- */
820
- buildAnnotations(span) {
821
- const annotations = {};
822
- if (span.input !== void 0) {
823
- annotations.inputData = formatInput(span.input, span.type);
824
- }
825
- if (span.output !== void 0) {
826
- annotations.outputData = formatOutput(span.output, span.type);
827
- }
828
- const usageSpanType = isModelInferenceEnabled() ? observability$1.SpanType.MODEL_INFERENCE : observability$1.SpanType.MODEL_STEP;
829
- if (span.type === usageSpanType) {
830
- const usage = span.attributes?.usage;
831
- const metrics = formatUsageMetrics(usage);
832
- if (metrics) {
833
- annotations.metrics = metrics;
834
- }
835
- }
836
- const knownFields = ["usage", "model", "provider"];
837
- const otherAttributes = utils.omitKeys(span.attributes ?? {}, knownFields);
838
- const contextKeySet = new Set(this.config.requestContextKeys ?? []);
839
- const flatContextTags = {};
840
- const remainingMetadata = {};
841
- for (const [key, value] of Object.entries(span.metadata ?? {})) {
842
- if (contextKeySet.has(key)) {
843
- flatContextTags[key] = value;
844
- } else {
845
- remainingMetadata[key] = value;
846
- }
847
- }
848
- const remainingAttributes = {};
849
- for (const [key, value] of Object.entries(otherAttributes)) {
850
- if (contextKeySet.has(key)) {
851
- if (!(key in flatContextTags)) {
852
- flatContextTags[key] = value;
853
- }
854
- } else {
855
- remainingAttributes[key] = value;
856
- }
857
- }
858
- const combinedMetadata = {
859
- ...remainingMetadata,
860
- ...remainingAttributes
861
- };
862
- if (span.errorInfo) {
863
- combinedMetadata["error.message"] = span.errorInfo.message;
864
- }
865
- if (Object.keys(combinedMetadata).length > 0) {
866
- annotations.metadata = combinedMetadata;
867
- }
868
- const tags = {
869
- // Promote requestContextKeys values to flat, searchable LLM Observability tags
870
- ...flatContextTags
871
- };
872
- if (span.tags?.length) {
873
- for (const tag of span.tags) {
874
- const colonIndex = tag.indexOf(":");
875
- if (colonIndex > 0) {
876
- tags[tag.substring(0, colonIndex)] = tag.substring(colonIndex + 1);
877
- } else {
878
- tags[tag] = true;
879
- }
880
- }
881
- }
882
- if (span.errorInfo) {
883
- tags.error = true;
884
- if (span.errorInfo.id) {
885
- tags["error.id"] = span.errorInfo.id;
886
- }
887
- if (span.errorInfo.domain) {
888
- tags["error.domain"] = span.errorInfo.domain;
889
- }
890
- if (span.errorInfo.category) {
891
- tags["error.category"] = span.errorInfo.category;
892
- }
893
- }
894
- if (Object.keys(tags).length > 0) {
895
- annotations.tags = tags;
896
- }
897
- return annotations;
898
- }
899
- /**
900
- * Submit an eval score to Datadog LLM Observability for the matching ddSpan.
901
- *
902
- * Ordering constraint: the matching span must have already been emitted to dd-trace
903
- * (i.e. its `SPAN_ENDED` event must have been processed and the trace tree flushed).
904
- * On Mastra's normal scoring path this is always true — scorer hooks fire after the
905
- * scored entity completes, so the root span has ended by the time `onScoreEvent` runs.
906
- *
907
- * If a score arrives for an unexported span (either before `SPAN_ENDED` or after the
908
- * `traceState` entry has been cleaned up), the event is dropped and a warning is logged
909
- * so the misuse is observable. Scores must therefore only be submitted for spans whose
910
- * lifecycle has completed.
911
- */
912
- async onScoreEvent(event) {
913
- if (this.isDisabled || !tracer3__default.default.llmobs?.submitEvaluation) return;
914
- const { score } = event;
915
- if (!score.traceId || !score.spanId) {
916
- this.logger.warn("Datadog exporter: dropping score with no traceId/spanId", {
917
- scorerId: score.scorerId
918
- });
919
- return;
920
- }
921
- const ctx = this.traceState.get(score.traceId)?.contexts.get(score.spanId);
922
- const exported = ctx?.exported;
923
- if (!exported) {
924
- this.logger.warn(
925
- "Datadog exporter: dropping score for span that has not been emitted to dd-trace yet (span_ended must be processed before submitting a score for it)",
926
- {
927
- traceId: score.traceId,
928
- spanId: score.spanId,
929
- scorerId: score.scorerId
930
- }
931
- );
932
- return;
933
- }
934
- try {
935
- tracer3__default.default.llmobs.submitEvaluation(
936
- { traceId: exported.traceId, spanId: exported.spanId },
937
- {
938
- label: score.scorerName ?? score.scorerId,
939
- value: score.score,
940
- metricType: "score",
941
- mlApp: this.config.mlApp,
942
- timestampMs: score.timestamp instanceof Date ? score.timestamp.getTime() : Date.now(),
943
- ...score.reason ? { reasoning: score.reason } : {},
944
- ...score.metadata ? { metadata: score.metadata } : {}
945
- }
946
- );
947
- } catch (err) {
948
- this.logger.error("Datadog exporter: Failed to submit evaluation", {
949
- error: err,
950
- traceId: score.traceId,
951
- spanId: score.spanId,
952
- scorerId: score.scorerId
953
- });
954
- }
955
- }
956
- /**
957
- * Force flush any buffered spans without shutting down the exporter.
958
- * This is useful in serverless environments where you need to ensure spans
959
- * are exported before the runtime instance is terminated.
960
- */
961
- async flush() {
962
- if (this.isDisabled || !tracer3__default.default.llmobs) return;
963
- if (tracer3__default.default.llmobs?.flush) {
964
- try {
965
- await tracer3__default.default.llmobs.flush();
966
- this.logger.debug("Datadog llmobs flushed");
967
- } catch (e) {
968
- this.logger.error("Error flushing llmobs", { error: e });
969
- }
970
- }
971
- }
972
- /**
973
- * Gracefully shuts down the exporter.
974
- */
975
- async shutdown() {
976
- for (const [traceId, state] of this.traceState) {
977
- if (state.cleanupTimer) {
978
- clearTimeout(state.cleanupTimer);
979
- }
980
- if (state.maxLifetimeTimer) {
981
- clearTimeout(state.maxLifetimeTimer);
982
- }
983
- if (state.buffer.size > 0) {
984
- this.logger.warn("Shutdown with pending spans", {
985
- traceId,
986
- pendingCount: state.buffer.size,
987
- spanIds: Array.from(state.buffer.keys())
988
- });
989
- }
990
- }
991
- this.traceState.clear();
992
- await this.flush();
993
- if (tracer3__default.default.llmobs?.disable) {
994
- try {
995
- tracer3__default.default.llmobs.disable();
996
- } catch (e) {
997
- this.logger.error("Error disabling llmobs", { error: e });
998
- }
999
- }
1000
- this.traceContext.clear();
1001
- await super.shutdown();
1002
- }
1003
- /**
1004
- * Retrieve or initialize trace state for buffering and parent tracking.
1005
- */
1006
- getOrCreateTraceState(traceId) {
1007
- const existing = this.traceState.get(traceId);
1008
- if (existing) {
1009
- if (existing.cleanupTimer) {
1010
- clearTimeout(existing.cleanupTimer);
1011
- existing.cleanupTimer = void 0;
1012
- }
1013
- return existing;
1014
- }
1015
- const created = {
1016
- buffer: /* @__PURE__ */ new Map(),
1017
- contexts: /* @__PURE__ */ new Map(),
1018
- rootEnded: false,
1019
- treeEmitted: false,
1020
- createdAt: Date.now(),
1021
- cleanupTimer: void 0,
1022
- maxLifetimeTimer: void 0
1023
- };
1024
- const maxLifetimeTimer = setTimeout(() => {
1025
- const state = this.traceState.get(traceId);
1026
- if (state) {
1027
- if (state.buffer.size > 0 || state.contexts.size > 0) {
1028
- this.logger.warn("Discarding trace due to max lifetime exceeded", {
1029
- traceId,
1030
- bufferedSpans: state.buffer.size,
1031
- emittedSpans: state.contexts.size,
1032
- lifetimeMs: Date.now() - state.createdAt
1033
- });
1034
- }
1035
- if (state.cleanupTimer) {
1036
- clearTimeout(state.cleanupTimer);
1037
- }
1038
- this.traceState.delete(traceId);
1039
- this.traceContext.delete(traceId);
1040
- }
1041
- }, MAX_TRACE_LIFETIME_MS);
1042
- maxLifetimeTimer.unref?.();
1043
- created.maxLifetimeTimer = maxLifetimeTimer;
1044
- this.traceState.set(traceId, created);
1045
- return created;
1046
- }
1047
- /**
1048
- * Attempt to emit spans from the buffer.
1049
- *
1050
- * Two modes of operation:
1051
- * 1. Initial tree emission: When root span ends and tree hasn't been emitted yet,
1052
- * build a tree from all buffered spans and emit recursively using nested
1053
- * llmobs.trace() calls. This ensures proper parent-child relationships in Datadog.
1054
- * 2. Late-arriving spans: After the tree has been emitted, emit individual spans
1055
- * with their parent context for proper linking.
1056
- */
1057
- tryEmitReadySpans(traceId) {
1058
- const state = this.traceState.get(traceId);
1059
- if (!state) return;
1060
- if (!state.treeEmitted) {
1061
- if (!state.rootEnded) return;
1062
- const tree = this.buildSpanTree(state.buffer);
1063
- if (tree) {
1064
- this.emitSpanTree(tree, state);
1065
- }
1066
- state.buffer.clear();
1067
- state.treeEmitted = true;
1068
- } else {
1069
- let emitted = false;
1070
- do {
1071
- emitted = false;
1072
- for (const [spanId, span] of state.buffer) {
1073
- const parentCtx = span.parentSpanId ? state.contexts.get(span.parentSpanId) : void 0;
1074
- if (span.parentSpanId && !parentCtx) {
1075
- continue;
1076
- }
1077
- this.emitSingleSpan(span, state, parentCtx?.ddSpan);
1078
- state.buffer.delete(spanId);
1079
- emitted = true;
1080
- }
1081
- } while (emitted);
1082
- }
1083
- if (state.rootEnded && state.buffer.size === 0 && !state.cleanupTimer) {
1084
- const timer = setTimeout(() => {
1085
- const currentState = this.traceState.get(traceId);
1086
- if (currentState) {
1087
- if (currentState.buffer.size > 0) {
1088
- this.logger.warn("Discarding orphaned spans during cleanup", {
1089
- traceId,
1090
- orphanedCount: currentState.buffer.size,
1091
- spanIds: Array.from(currentState.buffer.keys())
1092
- });
1093
- }
1094
- if (currentState.maxLifetimeTimer) {
1095
- clearTimeout(currentState.maxLifetimeTimer);
1096
- }
1097
- }
1098
- this.traceState.delete(traceId);
1099
- this.traceContext.delete(traceId);
1100
- }, REGULAR_CLEANUP_INTERVAL_MS);
1101
- timer.unref?.();
1102
- state.cleanupTimer = timer;
1103
- }
1104
- }
1105
- /**
1106
- * Builds a tree structure from buffered spans based on parentSpanId relationships.
1107
- * Returns the root node of the tree, or undefined if no root span is found.
1108
- */
1109
- buildSpanTree(buffer) {
1110
- const nodes = /* @__PURE__ */ new Map();
1111
- let rootNode;
1112
- for (const span of buffer.values()) {
1113
- nodes.set(span.id, { span, children: [] });
1114
- }
1115
- for (const node of nodes.values()) {
1116
- if (node.span.isRootSpan) {
1117
- rootNode = node;
1118
- } else if (node.span.parentSpanId) {
1119
- const parentNode = nodes.get(node.span.parentSpanId);
1120
- if (parentNode) {
1121
- parentNode.children.push(node);
1122
- } else {
1123
- this.logger.warn("Orphaned span detected during tree build", {
1124
- spanId: node.span.id,
1125
- parentSpanId: node.span.parentSpanId,
1126
- traceId: node.span.traceId
1127
- });
1128
- }
1129
- }
1130
- }
1131
- for (const node of nodes.values()) {
1132
- node.children.sort((a, b) => {
1133
- const aTime = a.span.startTime instanceof Date ? a.span.startTime.getTime() : new Date(a.span.startTime).getTime();
1134
- const bTime = b.span.startTime instanceof Date ? b.span.startTime.getTime() : new Date(b.span.startTime).getTime();
1135
- return aTime - bTime;
1136
- });
1137
- }
1138
- return rootNode;
1139
- }
1140
- /**
1141
- * Builds LLMObs span options from a Mastra span.
1142
- * Handles trace context, timestamps, and conditional model information for LLM spans.
1143
- */
1144
- buildSpanOptions(span, inheritedModelAttrs) {
1145
- const traceCtx = this.traceContext.get(span.traceId) || {
1146
- userId: span.metadata?.userId,
1147
- sessionId: span.metadata?.sessionId
1148
- };
1149
- const kind = kindFor(span.type);
1150
- const ownAttrs = span.attributes;
1151
- const attrs = {
1152
- model: ownAttrs?.model ?? inheritedModelAttrs?.model,
1153
- provider: ownAttrs?.provider ?? inheritedModelAttrs?.provider
1154
- };
1155
- const startTime = toDate(span.startTime);
1156
- const endTime = span.endTime ? toDate(span.endTime) : span.isEvent ? startTime : /* @__PURE__ */ new Date();
1157
- return {
1158
- traceOptions: {
1159
- kind,
1160
- name: span.name,
1161
- sessionId: traceCtx.sessionId,
1162
- userId: traceCtx.userId,
1163
- startTime,
1164
- ...kind === "llm" && attrs?.model ? { modelName: attrs.model } : {},
1165
- ...kind === "llm" && attrs?.provider ? { modelProvider: attrs.provider } : {}
1166
- },
1167
- // endTime as milliseconds for ddSpan.finish() — dd-trace's llmobs.trace() does not
1168
- // honor endTime in options, so we must call finish(ms) explicitly on the span.
1169
- endTimeMs: endTime.getTime()
1170
- };
1171
- }
1172
- /**
1173
- * Recursively emits a span tree using nested llmobs.trace() calls.
1174
- * This ensures parent-child relationships are properly established in Datadog
1175
- * because child spans are created while their parent span is active in scope.
1176
- */
1177
- emitSpanTree(node, state, inheritedModelAttrs) {
1178
- const span = node.span;
1179
- const { traceOptions, endTimeMs } = this.buildSpanOptions(span, inheritedModelAttrs);
1180
- const childInheritedModelAttrs = span.type === observability$1.SpanType.MODEL_GENERATION ? {
1181
- model: span.attributes?.model,
1182
- provider: span.attributes?.provider
1183
- } : inheritedModelAttrs;
1184
- tracer3__default.default.llmobs.trace(traceOptions, (ddSpan) => {
1185
- const annotations = this.buildAnnotations(span);
1186
- if (Object.keys(annotations).length > 0) {
1187
- tracer3__default.default.llmobs.annotate(ddSpan, annotations);
1188
- }
1189
- if (span.errorInfo) {
1190
- this.setErrorTags(ddSpan, span.errorInfo);
1191
- }
1192
- const exported = tracer3__default.default.llmobs.exportSpan ? tracer3__default.default.llmobs.exportSpan(ddSpan) : void 0;
1193
- state.contexts.set(span.id, { ddSpan, exported });
1194
- for (const child of node.children) {
1195
- this.emitSpanTree(child, state, childInheritedModelAttrs);
1196
- }
1197
- if (typeof ddSpan.finish === "function") {
1198
- ddSpan.finish(endTimeMs);
1199
- }
1200
- });
1201
- }
1202
- /**
1203
- * Emit a single span with the proper Datadog parent context.
1204
- * Used for late-arriving spans after the main tree has been emitted.
1205
- */
1206
- emitSingleSpan(span, state, parent) {
1207
- const { traceOptions, endTimeMs } = this.buildSpanOptions(span);
1208
- const runTrace = () => tracer3__default.default.llmobs.trace(traceOptions, (ddSpan) => {
1209
- const annotations = this.buildAnnotations(span);
1210
- if (Object.keys(annotations).length > 0) {
1211
- tracer3__default.default.llmobs.annotate(ddSpan, annotations);
1212
- }
1213
- if (span.errorInfo) {
1214
- this.setErrorTags(ddSpan, span.errorInfo);
1215
- }
1216
- const exported = tracer3__default.default.llmobs.exportSpan ? tracer3__default.default.llmobs.exportSpan(ddSpan) : void 0;
1217
- state.contexts.set(span.id, { ddSpan, exported });
1218
- if (typeof ddSpan.finish === "function") {
1219
- ddSpan.finish(endTimeMs);
1220
- }
1221
- });
1222
- if (parent) {
1223
- tracer3__default.default.scope().activate(parent, runTrace);
1224
- } else {
1225
- runTrace();
1226
- }
1227
- }
712
+ //#endregion
713
+ //#region src/tracing.ts
714
+ /**
715
+ * Maximum lifetime for a trace state entry (30 minutes).
716
+ * This is a fallback cleanup mechanism for traces that never receive a root span
717
+ * or have all spans marked as non-root, preventing unbounded memory growth.
718
+ */
719
+ const MAX_TRACE_LIFETIME_MS = 1800 * 1e3;
720
+ /**
721
+ * Regular cleanup interval for trace state entries (1 minute).
722
+ */
723
+ const REGULAR_CLEANUP_INTERVAL_MS = 60 * 1e3;
724
+ /**
725
+ * Datadog LLM Observability Exporter for Mastra.
726
+ *
727
+ * Exports observability data to Datadog's LLM Observability product using
728
+ * a completion-only pattern where spans are emitted on span_ended events.
729
+ */
730
+ var DatadogExporter = class extends _mastra_observability.BaseExporter {
731
+ name = "datadog";
732
+ config;
733
+ traceContext = /* @__PURE__ */ new Map();
734
+ traceState = /* @__PURE__ */ new Map();
735
+ constructor(config = {}) {
736
+ super(config);
737
+ const mlApp = config.mlApp ?? process.env.DD_LLMOBS_ML_APP;
738
+ const apiKey = config.apiKey ?? process.env.DD_API_KEY;
739
+ const site = config.site ?? process.env.DD_SITE ?? "datadoghq.com";
740
+ const env = config.env ?? process.env.DD_ENV;
741
+ const envAgentless = process.env.DD_LLMOBS_AGENTLESS_ENABLED?.toLowerCase();
742
+ const agentless = config.agentless ?? (envAgentless === "false" || envAgentless === "0" ? false : true);
743
+ if (!mlApp) {
744
+ this.setDisabled(`Missing required mlApp. Set DD_LLMOBS_ML_APP environment variable or pass mlApp in config.`);
745
+ this.config = config;
746
+ return;
747
+ }
748
+ if (agentless && !apiKey) {
749
+ this.setDisabled(`Missing required apiKey for agentless mode. Set DD_API_KEY environment variable or pass apiKey in config.`);
750
+ this.config = config;
751
+ return;
752
+ }
753
+ this.config = {
754
+ ...config,
755
+ mlApp,
756
+ site,
757
+ apiKey,
758
+ agentless,
759
+ env
760
+ };
761
+ ensureTracer({
762
+ mlApp,
763
+ site,
764
+ apiKey,
765
+ agentless,
766
+ service: config.service,
767
+ env,
768
+ integrationsEnabled: config.integrationsEnabled
769
+ });
770
+ this.logger.info("Datadog exporter initialized", {
771
+ mlApp,
772
+ site,
773
+ agentless
774
+ });
775
+ }
776
+ /**
777
+ * Main entry point for tracing events from Mastra.
778
+ */
779
+ async _exportTracingEvent(event) {
780
+ if (this.isDisabled || !dd_trace.default.llmobs) return;
781
+ try {
782
+ const span = event.exportedSpan;
783
+ if (span.isEvent) {
784
+ if (event.type === "span_started") {
785
+ this.captureTraceContext(span);
786
+ this.enqueueSpan(span);
787
+ }
788
+ return;
789
+ }
790
+ switch (event.type) {
791
+ case "span_started":
792
+ this.captureTraceContext(span);
793
+ return;
794
+ case "span_updated": return;
795
+ case "span_ended":
796
+ this.enqueueSpan(span);
797
+ return;
798
+ }
799
+ } catch (error) {
800
+ this.logger.error("Datadog exporter error", {
801
+ error,
802
+ eventType: event.type,
803
+ spanId: event.exportedSpan?.id,
804
+ spanName: event.exportedSpan?.name
805
+ });
806
+ }
807
+ }
808
+ /**
809
+ * Captures user/session context from root spans for tagging all spans in the trace.
810
+ */
811
+ captureTraceContext(span) {
812
+ if (span.isRootSpan && !this.traceContext.has(span.traceId)) this.traceContext.set(span.traceId, {
813
+ userId: span.metadata?.userId,
814
+ sessionId: span.metadata?.sessionId
815
+ });
816
+ }
817
+ /**
818
+ * Queue span until its parent context is available, then emit spans parent-first.
819
+ */
820
+ enqueueSpan(span) {
821
+ const state = this.getOrCreateTraceState(span.traceId);
822
+ if (span.isRootSpan) state.rootEnded = true;
823
+ state.buffer.set(span.id, span);
824
+ this.tryEmitReadySpans(span.traceId);
825
+ }
826
+ /**
827
+ * Sets native dd-trace error tags required by Datadog's Error Tracking UI.
828
+ */
829
+ setErrorTags(ddSpan, errorInfo) {
830
+ ddSpan.setTag("error", true);
831
+ ddSpan.setTag("error.message", errorInfo.message);
832
+ ddSpan.setTag("error.type", errorInfo.name ?? errorInfo.category ?? "Error");
833
+ if (errorInfo.stack) ddSpan.setTag("error.stack", errorInfo.stack);
834
+ }
835
+ /**
836
+ * Builds annotations object for llmobs.annotate().
837
+ * Uses dd-trace's expected property names: inputData, outputData, metadata, tags, metrics.
838
+ */
839
+ buildAnnotations(span) {
840
+ const annotations = {};
841
+ if (span.input !== void 0) annotations.inputData = formatInput(span.input, span.type);
842
+ if (span.output !== void 0) annotations.outputData = formatOutput(span.output, span.type);
843
+ const usageSpanType = isModelInferenceEnabled() ? _mastra_core_observability.SpanType.MODEL_INFERENCE : _mastra_core_observability.SpanType.MODEL_STEP;
844
+ if (span.type === usageSpanType) {
845
+ const usage = span.attributes?.usage;
846
+ const metrics = formatUsageMetrics(usage);
847
+ if (metrics) annotations.metrics = metrics;
848
+ }
849
+ const otherAttributes = (0, _mastra_core_utils.omitKeys)(span.attributes ?? {}, [
850
+ "usage",
851
+ "model",
852
+ "provider"
853
+ ]);
854
+ const contextKeySet = new Set(this.config.requestContextKeys ?? []);
855
+ const flatContextTags = {};
856
+ const remainingMetadata = {};
857
+ for (const [key, value] of Object.entries(span.metadata ?? {})) if (contextKeySet.has(key)) flatContextTags[key] = value;
858
+ else remainingMetadata[key] = value;
859
+ const remainingAttributes = {};
860
+ for (const [key, value] of Object.entries(otherAttributes)) if (contextKeySet.has(key)) {
861
+ if (!(key in flatContextTags)) flatContextTags[key] = value;
862
+ } else remainingAttributes[key] = value;
863
+ const combinedMetadata = {
864
+ ...remainingMetadata,
865
+ ...remainingAttributes
866
+ };
867
+ if (span.errorInfo) combinedMetadata["error.message"] = span.errorInfo.message;
868
+ if (Object.keys(combinedMetadata).length > 0) annotations.metadata = combinedMetadata;
869
+ const tags = { ...flatContextTags };
870
+ if (span.tags?.length) for (const tag of span.tags) {
871
+ const colonIndex = tag.indexOf(":");
872
+ if (colonIndex > 0) tags[tag.substring(0, colonIndex)] = tag.substring(colonIndex + 1);
873
+ else tags[tag] = true;
874
+ }
875
+ if (span.errorInfo) {
876
+ tags.error = true;
877
+ if (span.errorInfo.id) tags["error.id"] = span.errorInfo.id;
878
+ if (span.errorInfo.domain) tags["error.domain"] = span.errorInfo.domain;
879
+ if (span.errorInfo.category) tags["error.category"] = span.errorInfo.category;
880
+ }
881
+ if (Object.keys(tags).length > 0) annotations.tags = tags;
882
+ return annotations;
883
+ }
884
+ /**
885
+ * Submit an eval score to Datadog LLM Observability for the matching ddSpan.
886
+ *
887
+ * Ordering constraint: the matching span must have already been emitted to dd-trace
888
+ * (i.e. its `SPAN_ENDED` event must have been processed and the trace tree flushed).
889
+ * On Mastra's normal scoring path this is always true — scorer hooks fire after the
890
+ * scored entity completes, so the root span has ended by the time `onScoreEvent` runs.
891
+ *
892
+ * If a score arrives for an unexported span (either before `SPAN_ENDED` or after the
893
+ * `traceState` entry has been cleaned up), the event is dropped and a warning is logged
894
+ * so the misuse is observable. Scores must therefore only be submitted for spans whose
895
+ * lifecycle has completed.
896
+ */
897
+ async onScoreEvent(event) {
898
+ if (this.isDisabled || !dd_trace.default.llmobs?.submitEvaluation) return;
899
+ const { score } = event;
900
+ if (!score.traceId || !score.spanId) {
901
+ this.logger.warn("Datadog exporter: dropping score with no traceId/spanId", { scorerId: score.scorerId });
902
+ return;
903
+ }
904
+ const exported = (this.traceState.get(score.traceId)?.contexts.get(score.spanId))?.exported;
905
+ if (!exported) {
906
+ this.logger.warn("Datadog exporter: dropping score for span that has not been emitted to dd-trace yet (span_ended must be processed before submitting a score for it)", {
907
+ traceId: score.traceId,
908
+ spanId: score.spanId,
909
+ scorerId: score.scorerId
910
+ });
911
+ return;
912
+ }
913
+ try {
914
+ dd_trace.default.llmobs.submitEvaluation({
915
+ traceId: exported.traceId,
916
+ spanId: exported.spanId
917
+ }, {
918
+ label: score.scorerName ?? score.scorerId,
919
+ value: score.score,
920
+ metricType: "score",
921
+ mlApp: this.config.mlApp,
922
+ timestampMs: score.timestamp instanceof Date ? score.timestamp.getTime() : Date.now(),
923
+ ...score.reason ? { reasoning: score.reason } : {},
924
+ ...score.metadata ? { metadata: score.metadata } : {}
925
+ });
926
+ } catch (err) {
927
+ this.logger.error("Datadog exporter: Failed to submit evaluation", {
928
+ error: err,
929
+ traceId: score.traceId,
930
+ spanId: score.spanId,
931
+ scorerId: score.scorerId
932
+ });
933
+ }
934
+ }
935
+ /**
936
+ * Force flush any buffered spans without shutting down the exporter.
937
+ * This is useful in serverless environments where you need to ensure spans
938
+ * are exported before the runtime instance is terminated.
939
+ */
940
+ async flush() {
941
+ if (this.isDisabled || !dd_trace.default.llmobs) return;
942
+ if (dd_trace.default.llmobs?.flush) try {
943
+ await dd_trace.default.llmobs.flush();
944
+ this.logger.debug("Datadog llmobs flushed");
945
+ } catch (e) {
946
+ this.logger.error("Error flushing llmobs", { error: e });
947
+ }
948
+ }
949
+ /**
950
+ * Gracefully shuts down the exporter.
951
+ */
952
+ async shutdown() {
953
+ for (const [traceId, state] of this.traceState) {
954
+ if (state.cleanupTimer) clearTimeout(state.cleanupTimer);
955
+ if (state.maxLifetimeTimer) clearTimeout(state.maxLifetimeTimer);
956
+ if (state.buffer.size > 0) this.logger.warn("Shutdown with pending spans", {
957
+ traceId,
958
+ pendingCount: state.buffer.size,
959
+ spanIds: Array.from(state.buffer.keys())
960
+ });
961
+ }
962
+ this.traceState.clear();
963
+ await this.flush();
964
+ if (dd_trace.default.llmobs?.disable) try {
965
+ dd_trace.default.llmobs.disable();
966
+ } catch (e) {
967
+ this.logger.error("Error disabling llmobs", { error: e });
968
+ }
969
+ this.traceContext.clear();
970
+ await super.shutdown();
971
+ }
972
+ /**
973
+ * Retrieve or initialize trace state for buffering and parent tracking.
974
+ */
975
+ getOrCreateTraceState(traceId) {
976
+ const existing = this.traceState.get(traceId);
977
+ if (existing) {
978
+ if (existing.cleanupTimer) {
979
+ clearTimeout(existing.cleanupTimer);
980
+ existing.cleanupTimer = void 0;
981
+ }
982
+ return existing;
983
+ }
984
+ const created = {
985
+ buffer: /* @__PURE__ */ new Map(),
986
+ contexts: /* @__PURE__ */ new Map(),
987
+ rootEnded: false,
988
+ treeEmitted: false,
989
+ createdAt: Date.now(),
990
+ cleanupTimer: void 0,
991
+ maxLifetimeTimer: void 0
992
+ };
993
+ const maxLifetimeTimer = setTimeout(() => {
994
+ const state = this.traceState.get(traceId);
995
+ if (state) {
996
+ if (state.buffer.size > 0 || state.contexts.size > 0) this.logger.warn("Discarding trace due to max lifetime exceeded", {
997
+ traceId,
998
+ bufferedSpans: state.buffer.size,
999
+ emittedSpans: state.contexts.size,
1000
+ lifetimeMs: Date.now() - state.createdAt
1001
+ });
1002
+ if (state.cleanupTimer) clearTimeout(state.cleanupTimer);
1003
+ this.traceState.delete(traceId);
1004
+ this.traceContext.delete(traceId);
1005
+ }
1006
+ }, MAX_TRACE_LIFETIME_MS);
1007
+ maxLifetimeTimer.unref?.();
1008
+ created.maxLifetimeTimer = maxLifetimeTimer;
1009
+ this.traceState.set(traceId, created);
1010
+ return created;
1011
+ }
1012
+ /**
1013
+ * Attempt to emit spans from the buffer.
1014
+ *
1015
+ * Two modes of operation:
1016
+ * 1. Initial tree emission: When root span ends and tree hasn't been emitted yet,
1017
+ * build a tree from all buffered spans and emit recursively using nested
1018
+ * llmobs.trace() calls. This ensures proper parent-child relationships in Datadog.
1019
+ * 2. Late-arriving spans: After the tree has been emitted, emit individual spans
1020
+ * with their parent context for proper linking.
1021
+ */
1022
+ tryEmitReadySpans(traceId) {
1023
+ const state = this.traceState.get(traceId);
1024
+ if (!state) return;
1025
+ if (!state.treeEmitted) {
1026
+ if (!state.rootEnded) return;
1027
+ const tree = this.buildSpanTree(state.buffer);
1028
+ if (tree) this.emitSpanTree(tree, state);
1029
+ state.buffer.clear();
1030
+ state.treeEmitted = true;
1031
+ } else {
1032
+ let emitted = false;
1033
+ do {
1034
+ emitted = false;
1035
+ for (const [spanId, span] of state.buffer) {
1036
+ const parentCtx = span.parentSpanId ? state.contexts.get(span.parentSpanId) : void 0;
1037
+ if (span.parentSpanId && !parentCtx) continue;
1038
+ this.emitSingleSpan(span, state, parentCtx?.ddSpan);
1039
+ state.buffer.delete(spanId);
1040
+ emitted = true;
1041
+ }
1042
+ } while (emitted);
1043
+ }
1044
+ if (state.rootEnded && state.buffer.size === 0 && !state.cleanupTimer) {
1045
+ const timer = setTimeout(() => {
1046
+ const currentState = this.traceState.get(traceId);
1047
+ if (currentState) {
1048
+ if (currentState.buffer.size > 0) this.logger.warn("Discarding orphaned spans during cleanup", {
1049
+ traceId,
1050
+ orphanedCount: currentState.buffer.size,
1051
+ spanIds: Array.from(currentState.buffer.keys())
1052
+ });
1053
+ if (currentState.maxLifetimeTimer) clearTimeout(currentState.maxLifetimeTimer);
1054
+ }
1055
+ this.traceState.delete(traceId);
1056
+ this.traceContext.delete(traceId);
1057
+ }, REGULAR_CLEANUP_INTERVAL_MS);
1058
+ timer.unref?.();
1059
+ state.cleanupTimer = timer;
1060
+ }
1061
+ }
1062
+ /**
1063
+ * Builds a tree structure from buffered spans based on parentSpanId relationships.
1064
+ * Returns the root node of the tree, or undefined if no root span is found.
1065
+ */
1066
+ buildSpanTree(buffer) {
1067
+ const nodes = /* @__PURE__ */ new Map();
1068
+ let rootNode;
1069
+ for (const span of buffer.values()) nodes.set(span.id, {
1070
+ span,
1071
+ children: []
1072
+ });
1073
+ for (const node of nodes.values()) if (node.span.isRootSpan) rootNode = node;
1074
+ else if (node.span.parentSpanId) {
1075
+ const parentNode = nodes.get(node.span.parentSpanId);
1076
+ if (parentNode) parentNode.children.push(node);
1077
+ else this.logger.warn("Orphaned span detected during tree build", {
1078
+ spanId: node.span.id,
1079
+ parentSpanId: node.span.parentSpanId,
1080
+ traceId: node.span.traceId
1081
+ });
1082
+ }
1083
+ for (const node of nodes.values()) node.children.sort((a, b) => {
1084
+ return (a.span.startTime instanceof Date ? a.span.startTime.getTime() : new Date(a.span.startTime).getTime()) - (b.span.startTime instanceof Date ? b.span.startTime.getTime() : new Date(b.span.startTime).getTime());
1085
+ });
1086
+ return rootNode;
1087
+ }
1088
+ /**
1089
+ * Builds LLMObs span options from a Mastra span.
1090
+ * Handles trace context, timestamps, and conditional model information for LLM spans.
1091
+ */
1092
+ buildSpanOptions(span, inheritedModelAttrs) {
1093
+ const traceCtx = this.traceContext.get(span.traceId) || {
1094
+ userId: span.metadata?.userId,
1095
+ sessionId: span.metadata?.sessionId
1096
+ };
1097
+ const kind = kindFor(span.type);
1098
+ const ownAttrs = span.attributes;
1099
+ const attrs = {
1100
+ model: ownAttrs?.model ?? inheritedModelAttrs?.model,
1101
+ provider: ownAttrs?.provider ?? inheritedModelAttrs?.provider
1102
+ };
1103
+ const startTime = toDate(span.startTime);
1104
+ const endTime = span.endTime ? toDate(span.endTime) : span.isEvent ? startTime : /* @__PURE__ */ new Date();
1105
+ return {
1106
+ traceOptions: {
1107
+ kind,
1108
+ name: span.name,
1109
+ sessionId: traceCtx.sessionId,
1110
+ userId: traceCtx.userId,
1111
+ startTime,
1112
+ ...kind === "llm" && attrs?.model ? { modelName: attrs.model } : {},
1113
+ ...kind === "llm" && attrs?.provider ? { modelProvider: attrs.provider } : {}
1114
+ },
1115
+ endTimeMs: endTime.getTime()
1116
+ };
1117
+ }
1118
+ /**
1119
+ * Recursively emits a span tree using nested llmobs.trace() calls.
1120
+ * This ensures parent-child relationships are properly established in Datadog
1121
+ * because child spans are created while their parent span is active in scope.
1122
+ */
1123
+ emitSpanTree(node, state, inheritedModelAttrs) {
1124
+ const span = node.span;
1125
+ const { traceOptions, endTimeMs } = this.buildSpanOptions(span, inheritedModelAttrs);
1126
+ const childInheritedModelAttrs = span.type === _mastra_core_observability.SpanType.MODEL_GENERATION ? {
1127
+ model: span.attributes?.model,
1128
+ provider: span.attributes?.provider
1129
+ } : inheritedModelAttrs;
1130
+ dd_trace.default.llmobs.trace(traceOptions, (ddSpan) => {
1131
+ const annotations = this.buildAnnotations(span);
1132
+ if (Object.keys(annotations).length > 0) dd_trace.default.llmobs.annotate(ddSpan, annotations);
1133
+ if (span.errorInfo) this.setErrorTags(ddSpan, span.errorInfo);
1134
+ const exported = dd_trace.default.llmobs.exportSpan ? dd_trace.default.llmobs.exportSpan(ddSpan) : void 0;
1135
+ state.contexts.set(span.id, {
1136
+ ddSpan,
1137
+ exported
1138
+ });
1139
+ for (const child of node.children) this.emitSpanTree(child, state, childInheritedModelAttrs);
1140
+ if (typeof ddSpan.finish === "function") ddSpan.finish(endTimeMs);
1141
+ });
1142
+ }
1143
+ /**
1144
+ * Emit a single span with the proper Datadog parent context.
1145
+ * Used for late-arriving spans after the main tree has been emitted.
1146
+ */
1147
+ emitSingleSpan(span, state, parent) {
1148
+ const { traceOptions, endTimeMs } = this.buildSpanOptions(span);
1149
+ const runTrace = () => dd_trace.default.llmobs.trace(traceOptions, (ddSpan) => {
1150
+ const annotations = this.buildAnnotations(span);
1151
+ if (Object.keys(annotations).length > 0) dd_trace.default.llmobs.annotate(ddSpan, annotations);
1152
+ if (span.errorInfo) this.setErrorTags(ddSpan, span.errorInfo);
1153
+ const exported = dd_trace.default.llmobs.exportSpan ? dd_trace.default.llmobs.exportSpan(ddSpan) : void 0;
1154
+ state.contexts.set(span.id, {
1155
+ ddSpan,
1156
+ exported
1157
+ });
1158
+ if (typeof ddSpan.finish === "function") ddSpan.finish(endTimeMs);
1159
+ });
1160
+ if (parent) dd_trace.default.scope().activate(parent, runTrace);
1161
+ else runTrace();
1162
+ }
1228
1163
  };
1229
-
1164
+ //#endregion
1230
1165
  exports.DatadogBridge = DatadogBridge;
1231
1166
  exports.DatadogExporter = DatadogExporter;
1232
- //# sourceMappingURL=index.cjs.map
1167
+
1233
1168
  //# sourceMappingURL=index.cjs.map