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