@mastra/observability 0.0.0-agent-error-handling-20251023180025 → 0.0.0-allow-to-pass-a-mastra-url-instance-20251105224938
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/CHANGELOG.md +47 -3
- package/README.md +99 -0
- package/dist/config.d.ts +75 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/default.d.ts +29 -0
- package/dist/default.d.ts.map +1 -0
- package/dist/exporters/base.d.ts +111 -0
- package/dist/exporters/base.d.ts.map +1 -0
- package/dist/exporters/cloud.d.ts +30 -0
- package/dist/exporters/cloud.d.ts.map +1 -0
- package/dist/exporters/console.d.ts +10 -0
- package/dist/exporters/console.d.ts.map +1 -0
- package/dist/exporters/default.d.ts +89 -0
- package/dist/exporters/default.d.ts.map +1 -0
- package/dist/exporters/index.d.ts +9 -0
- package/dist/exporters/index.d.ts.map +1 -0
- package/dist/index.cjs +2098 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2084 -0
- package/dist/index.js.map +1 -1
- package/dist/instances/base.d.ts +106 -0
- package/dist/instances/base.d.ts.map +1 -0
- package/dist/instances/default.d.ts +8 -0
- package/dist/instances/default.d.ts.map +1 -0
- package/dist/instances/index.d.ts +6 -0
- package/dist/instances/index.d.ts.map +1 -0
- package/dist/model-tracing.d.ts +48 -0
- package/dist/model-tracing.d.ts.map +1 -0
- package/dist/registry.d.ts +49 -0
- package/dist/registry.d.ts.map +1 -0
- package/dist/span_processors/index.d.ts +5 -0
- package/dist/span_processors/index.d.ts.map +1 -0
- package/dist/span_processors/sensitive-data-filter.d.ts +85 -0
- package/dist/span_processors/sensitive-data-filter.d.ts.map +1 -0
- package/dist/spans/base.d.ts +71 -0
- package/dist/spans/base.d.ts.map +1 -0
- package/dist/spans/default.d.ts +13 -0
- package/dist/spans/default.d.ts.map +1 -0
- package/dist/spans/index.d.ts +7 -0
- package/dist/spans/index.d.ts.map +1 -0
- package/dist/spans/no-op.d.ts +15 -0
- package/dist/spans/no-op.d.ts.map +1 -0
- package/package.json +12 -7
package/dist/index.js
CHANGED
|
@@ -1,3 +1,2087 @@
|
|
|
1
|
+
import { MastraBase } from '@mastra/core/base';
|
|
2
|
+
import { ConsoleLogger, LogLevel, RegisteredLogger } from '@mastra/core/logger';
|
|
3
|
+
import { MastraError, ErrorCategory, ErrorDomain } from '@mastra/core/error';
|
|
4
|
+
import { TracingEventType, SpanType, InternalSpans } from '@mastra/core/observability';
|
|
5
|
+
import { fetchWithRetry, getNestedValue, setNestedValue } from '@mastra/core/utils';
|
|
6
|
+
import { TransformStream } from 'stream/web';
|
|
1
7
|
|
|
8
|
+
// src/default.ts
|
|
9
|
+
|
|
10
|
+
// src/config.ts
|
|
11
|
+
var SamplingStrategyType = /* @__PURE__ */ ((SamplingStrategyType2) => {
|
|
12
|
+
SamplingStrategyType2["ALWAYS"] = "always";
|
|
13
|
+
SamplingStrategyType2["NEVER"] = "never";
|
|
14
|
+
SamplingStrategyType2["RATIO"] = "ratio";
|
|
15
|
+
SamplingStrategyType2["CUSTOM"] = "custom";
|
|
16
|
+
return SamplingStrategyType2;
|
|
17
|
+
})(SamplingStrategyType || {});
|
|
18
|
+
var BaseExporter = class {
|
|
19
|
+
/** Mastra logger instance */
|
|
20
|
+
logger;
|
|
21
|
+
/** Whether this exporter is disabled */
|
|
22
|
+
isDisabled = false;
|
|
23
|
+
/**
|
|
24
|
+
* Initialize the base exporter with logger
|
|
25
|
+
*/
|
|
26
|
+
constructor(config = {}) {
|
|
27
|
+
const logLevel = this.resolveLogLevel(config.logLevel);
|
|
28
|
+
this.logger = config.logger ?? new ConsoleLogger({ level: logLevel, name: this.constructor.name });
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Set the logger for the exporter (called by Mastra/ObservabilityInstance during initialization)
|
|
32
|
+
*/
|
|
33
|
+
__setLogger(logger) {
|
|
34
|
+
this.logger = logger;
|
|
35
|
+
this.logger.debug(`Logger updated for exporter [name=${this.name}]`);
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Convert string log level to LogLevel enum
|
|
39
|
+
*/
|
|
40
|
+
resolveLogLevel(logLevel) {
|
|
41
|
+
if (!logLevel) {
|
|
42
|
+
return LogLevel.INFO;
|
|
43
|
+
}
|
|
44
|
+
if (typeof logLevel === "number") {
|
|
45
|
+
return logLevel;
|
|
46
|
+
}
|
|
47
|
+
const logLevelMap = {
|
|
48
|
+
debug: LogLevel.DEBUG,
|
|
49
|
+
info: LogLevel.INFO,
|
|
50
|
+
warn: LogLevel.WARN,
|
|
51
|
+
error: LogLevel.ERROR
|
|
52
|
+
};
|
|
53
|
+
return logLevelMap[logLevel] ?? LogLevel.INFO;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Mark the exporter as disabled and log a message
|
|
57
|
+
*
|
|
58
|
+
* @param reason - Reason why the exporter is disabled
|
|
59
|
+
*/
|
|
60
|
+
setDisabled(reason) {
|
|
61
|
+
this.isDisabled = true;
|
|
62
|
+
this.logger.warn(`${this.name} disabled: ${reason}`);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Export a tracing event
|
|
66
|
+
*
|
|
67
|
+
* This method checks if the exporter is disabled before calling _exportEvent.
|
|
68
|
+
* Subclasses should implement _exportEvent instead of overriding this method.
|
|
69
|
+
*/
|
|
70
|
+
async exportTracingEvent(event) {
|
|
71
|
+
if (this.isDisabled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
await this._exportTracingEvent(event);
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Shutdown the exporter and clean up resources
|
|
78
|
+
*
|
|
79
|
+
* Default implementation just logs. Override to add custom cleanup.
|
|
80
|
+
*/
|
|
81
|
+
async shutdown() {
|
|
82
|
+
this.logger.info(`${this.name} shutdown complete`);
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
var CloudExporter = class extends BaseExporter {
|
|
86
|
+
name = "mastra-cloud-observability-exporter";
|
|
87
|
+
config;
|
|
88
|
+
buffer;
|
|
89
|
+
flushTimer = null;
|
|
90
|
+
constructor(config = {}) {
|
|
91
|
+
super(config);
|
|
92
|
+
const accessToken = config.accessToken ?? process.env.MASTRA_CLOUD_ACCESS_TOKEN;
|
|
93
|
+
if (!accessToken) {
|
|
94
|
+
this.setDisabled(
|
|
95
|
+
"MASTRA_CLOUD_ACCESS_TOKEN environment variable not set. \u{1F680} Sign up for Mastra Cloud at https://cloud.mastra.ai to see your traces online and obtain your access token."
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
const endpoint = config.endpoint ?? process.env.MASTRA_CLOUD_TRACES_ENDPOINT ?? "https://api.mastra.ai/ai/spans/publish";
|
|
99
|
+
this.config = {
|
|
100
|
+
logger: this.logger,
|
|
101
|
+
logLevel: config.logLevel ?? LogLevel.INFO,
|
|
102
|
+
maxBatchSize: config.maxBatchSize ?? 1e3,
|
|
103
|
+
maxBatchWaitMs: config.maxBatchWaitMs ?? 5e3,
|
|
104
|
+
maxRetries: config.maxRetries ?? 3,
|
|
105
|
+
accessToken: accessToken || "",
|
|
106
|
+
endpoint
|
|
107
|
+
};
|
|
108
|
+
this.buffer = {
|
|
109
|
+
spans: [],
|
|
110
|
+
totalSize: 0
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
async _exportTracingEvent(event) {
|
|
114
|
+
if (event.type !== TracingEventType.SPAN_ENDED) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
this.addToBuffer(event);
|
|
118
|
+
if (this.shouldFlush()) {
|
|
119
|
+
this.flush().catch((error) => {
|
|
120
|
+
this.logger.error("Batch flush failed", {
|
|
121
|
+
error: error instanceof Error ? error.message : String(error)
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
} else if (this.buffer.totalSize === 1) {
|
|
125
|
+
this.scheduleFlush();
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
addToBuffer(event) {
|
|
129
|
+
if (this.buffer.totalSize === 0) {
|
|
130
|
+
this.buffer.firstEventTime = /* @__PURE__ */ new Date();
|
|
131
|
+
}
|
|
132
|
+
const spanRecord = this.formatSpan(event.exportedSpan);
|
|
133
|
+
this.buffer.spans.push(spanRecord);
|
|
134
|
+
this.buffer.totalSize++;
|
|
135
|
+
}
|
|
136
|
+
formatSpan(span) {
|
|
137
|
+
const spanRecord = {
|
|
138
|
+
traceId: span.traceId,
|
|
139
|
+
spanId: span.id,
|
|
140
|
+
parentSpanId: span.parentSpanId ?? null,
|
|
141
|
+
name: span.name,
|
|
142
|
+
spanType: span.type,
|
|
143
|
+
attributes: span.attributes ?? null,
|
|
144
|
+
metadata: span.metadata ?? null,
|
|
145
|
+
startedAt: span.startTime,
|
|
146
|
+
endedAt: span.endTime ?? null,
|
|
147
|
+
input: span.input ?? null,
|
|
148
|
+
output: span.output ?? null,
|
|
149
|
+
error: span.errorInfo,
|
|
150
|
+
isEvent: span.isEvent,
|
|
151
|
+
createdAt: /* @__PURE__ */ new Date(),
|
|
152
|
+
updatedAt: null
|
|
153
|
+
};
|
|
154
|
+
return spanRecord;
|
|
155
|
+
}
|
|
156
|
+
shouldFlush() {
|
|
157
|
+
if (this.buffer.totalSize >= this.config.maxBatchSize) {
|
|
158
|
+
return true;
|
|
159
|
+
}
|
|
160
|
+
if (this.buffer.firstEventTime && this.buffer.totalSize > 0) {
|
|
161
|
+
const elapsed = Date.now() - this.buffer.firstEventTime.getTime();
|
|
162
|
+
if (elapsed >= this.config.maxBatchWaitMs) {
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
scheduleFlush() {
|
|
169
|
+
if (this.flushTimer) {
|
|
170
|
+
clearTimeout(this.flushTimer);
|
|
171
|
+
}
|
|
172
|
+
this.flushTimer = setTimeout(() => {
|
|
173
|
+
this.flush().catch((error) => {
|
|
174
|
+
const mastraError = new MastraError(
|
|
175
|
+
{
|
|
176
|
+
id: `CLOUD_EXPORTER_FAILED_TO_SCHEDULE_FLUSH`,
|
|
177
|
+
domain: ErrorDomain.MASTRA_OBSERVABILITY,
|
|
178
|
+
category: ErrorCategory.USER
|
|
179
|
+
},
|
|
180
|
+
error
|
|
181
|
+
);
|
|
182
|
+
this.logger.trackException(mastraError);
|
|
183
|
+
this.logger.error("Scheduled flush failed", mastraError);
|
|
184
|
+
});
|
|
185
|
+
}, this.config.maxBatchWaitMs);
|
|
186
|
+
}
|
|
187
|
+
async flush() {
|
|
188
|
+
if (this.flushTimer) {
|
|
189
|
+
clearTimeout(this.flushTimer);
|
|
190
|
+
this.flushTimer = null;
|
|
191
|
+
}
|
|
192
|
+
if (this.buffer.totalSize === 0) {
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
const startTime = Date.now();
|
|
196
|
+
const spansCopy = [...this.buffer.spans];
|
|
197
|
+
const flushReason = this.buffer.totalSize >= this.config.maxBatchSize ? "size" : "time";
|
|
198
|
+
this.resetBuffer();
|
|
199
|
+
try {
|
|
200
|
+
await this.batchUpload(spansCopy);
|
|
201
|
+
const elapsed = Date.now() - startTime;
|
|
202
|
+
this.logger.debug("Batch flushed successfully", {
|
|
203
|
+
batchSize: spansCopy.length,
|
|
204
|
+
flushReason,
|
|
205
|
+
durationMs: elapsed
|
|
206
|
+
});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
const mastraError = new MastraError(
|
|
209
|
+
{
|
|
210
|
+
id: `CLOUD_EXPORTER_FAILED_TO_BATCH_UPLOAD`,
|
|
211
|
+
domain: ErrorDomain.MASTRA_OBSERVABILITY,
|
|
212
|
+
category: ErrorCategory.USER,
|
|
213
|
+
details: {
|
|
214
|
+
droppedBatchSize: spansCopy.length
|
|
215
|
+
}
|
|
216
|
+
},
|
|
217
|
+
error
|
|
218
|
+
);
|
|
219
|
+
this.logger.trackException(mastraError);
|
|
220
|
+
this.logger.error("Batch upload failed after all retries, dropping batch", mastraError);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Uploads spans to cloud API using fetchWithRetry for all retry logic
|
|
225
|
+
*/
|
|
226
|
+
async batchUpload(spans) {
|
|
227
|
+
const headers = {
|
|
228
|
+
Authorization: `Bearer ${this.config.accessToken}`,
|
|
229
|
+
"Content-Type": "application/json"
|
|
230
|
+
};
|
|
231
|
+
const options = {
|
|
232
|
+
method: "POST",
|
|
233
|
+
headers,
|
|
234
|
+
body: JSON.stringify({ spans })
|
|
235
|
+
};
|
|
236
|
+
await fetchWithRetry(this.config.endpoint, options, this.config.maxRetries);
|
|
237
|
+
}
|
|
238
|
+
resetBuffer() {
|
|
239
|
+
this.buffer.spans = [];
|
|
240
|
+
this.buffer.firstEventTime = void 0;
|
|
241
|
+
this.buffer.totalSize = 0;
|
|
242
|
+
}
|
|
243
|
+
async shutdown() {
|
|
244
|
+
if (this.isDisabled) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
if (this.flushTimer) {
|
|
248
|
+
clearTimeout(this.flushTimer);
|
|
249
|
+
this.flushTimer = null;
|
|
250
|
+
}
|
|
251
|
+
if (this.buffer.totalSize > 0) {
|
|
252
|
+
this.logger.info("Flushing remaining events on shutdown", {
|
|
253
|
+
remainingEvents: this.buffer.totalSize
|
|
254
|
+
});
|
|
255
|
+
try {
|
|
256
|
+
await this.flush();
|
|
257
|
+
} catch (error) {
|
|
258
|
+
const mastraError = new MastraError(
|
|
259
|
+
{
|
|
260
|
+
id: `CLOUD_EXPORTER_FAILED_TO_FLUSH_REMAINING_EVENTS_DURING_SHUTDOWN`,
|
|
261
|
+
domain: ErrorDomain.MASTRA_OBSERVABILITY,
|
|
262
|
+
category: ErrorCategory.USER,
|
|
263
|
+
details: {
|
|
264
|
+
remainingEvents: this.buffer.totalSize
|
|
265
|
+
}
|
|
266
|
+
},
|
|
267
|
+
error
|
|
268
|
+
);
|
|
269
|
+
this.logger.trackException(mastraError);
|
|
270
|
+
this.logger.error("Failed to flush remaining events during shutdown", mastraError);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
this.logger.info("CloudExporter shutdown complete");
|
|
274
|
+
}
|
|
275
|
+
};
|
|
276
|
+
var ConsoleExporter = class extends BaseExporter {
|
|
277
|
+
name = "tracing-console-exporter";
|
|
278
|
+
constructor(config = {}) {
|
|
279
|
+
super(config);
|
|
280
|
+
}
|
|
281
|
+
async _exportTracingEvent(event) {
|
|
282
|
+
const span = event.exportedSpan;
|
|
283
|
+
const formatAttributes = (attributes) => {
|
|
284
|
+
try {
|
|
285
|
+
return JSON.stringify(attributes, null, 2);
|
|
286
|
+
} catch (error) {
|
|
287
|
+
const errMsg = error instanceof Error ? error.message : "Unknown formatting error";
|
|
288
|
+
return `[Unable to serialize attributes: ${errMsg}]`;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
const formatDuration = (startTime, endTime) => {
|
|
292
|
+
if (!endTime) return "N/A";
|
|
293
|
+
const duration = endTime.getTime() - startTime.getTime();
|
|
294
|
+
return `${duration}ms`;
|
|
295
|
+
};
|
|
296
|
+
switch (event.type) {
|
|
297
|
+
case TracingEventType.SPAN_STARTED:
|
|
298
|
+
this.logger.info(`\u{1F680} SPAN_STARTED`);
|
|
299
|
+
this.logger.info(` Type: ${span.type}`);
|
|
300
|
+
this.logger.info(` Name: ${span.name}`);
|
|
301
|
+
this.logger.info(` ID: ${span.id}`);
|
|
302
|
+
this.logger.info(` Trace ID: ${span.traceId}`);
|
|
303
|
+
if (span.input !== void 0) {
|
|
304
|
+
this.logger.info(` Input: ${formatAttributes(span.input)}`);
|
|
305
|
+
}
|
|
306
|
+
this.logger.info(` Attributes: ${formatAttributes(span.attributes)}`);
|
|
307
|
+
this.logger.info("\u2500".repeat(80));
|
|
308
|
+
break;
|
|
309
|
+
case TracingEventType.SPAN_ENDED:
|
|
310
|
+
const duration = formatDuration(span.startTime, span.endTime);
|
|
311
|
+
this.logger.info(`\u2705 SPAN_ENDED`);
|
|
312
|
+
this.logger.info(` Type: ${span.type}`);
|
|
313
|
+
this.logger.info(` Name: ${span.name}`);
|
|
314
|
+
this.logger.info(` ID: ${span.id}`);
|
|
315
|
+
this.logger.info(` Duration: ${duration}`);
|
|
316
|
+
this.logger.info(` Trace ID: ${span.traceId}`);
|
|
317
|
+
if (span.input !== void 0) {
|
|
318
|
+
this.logger.info(` Input: ${formatAttributes(span.input)}`);
|
|
319
|
+
}
|
|
320
|
+
if (span.output !== void 0) {
|
|
321
|
+
this.logger.info(` Output: ${formatAttributes(span.output)}`);
|
|
322
|
+
}
|
|
323
|
+
if (span.errorInfo) {
|
|
324
|
+
this.logger.info(` Error: ${formatAttributes(span.errorInfo)}`);
|
|
325
|
+
}
|
|
326
|
+
this.logger.info(` Attributes: ${formatAttributes(span.attributes)}`);
|
|
327
|
+
this.logger.info("\u2500".repeat(80));
|
|
328
|
+
break;
|
|
329
|
+
case TracingEventType.SPAN_UPDATED:
|
|
330
|
+
this.logger.info(`\u{1F4DD} SPAN_UPDATED`);
|
|
331
|
+
this.logger.info(` Type: ${span.type}`);
|
|
332
|
+
this.logger.info(` Name: ${span.name}`);
|
|
333
|
+
this.logger.info(` ID: ${span.id}`);
|
|
334
|
+
this.logger.info(` Trace ID: ${span.traceId}`);
|
|
335
|
+
if (span.input !== void 0) {
|
|
336
|
+
this.logger.info(` Input: ${formatAttributes(span.input)}`);
|
|
337
|
+
}
|
|
338
|
+
if (span.output !== void 0) {
|
|
339
|
+
this.logger.info(` Output: ${formatAttributes(span.output)}`);
|
|
340
|
+
}
|
|
341
|
+
if (span.errorInfo) {
|
|
342
|
+
this.logger.info(` Error: ${formatAttributes(span.errorInfo)}`);
|
|
343
|
+
}
|
|
344
|
+
this.logger.info(` Updated Attributes: ${formatAttributes(span.attributes)}`);
|
|
345
|
+
this.logger.info("\u2500".repeat(80));
|
|
346
|
+
break;
|
|
347
|
+
default:
|
|
348
|
+
this.logger.warn(`Tracing event type not implemented: ${event.type}`);
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
async shutdown() {
|
|
352
|
+
this.logger.info("ConsoleExporter shutdown");
|
|
353
|
+
}
|
|
354
|
+
};
|
|
355
|
+
function resolveTracingStorageStrategy(config, storage, logger) {
|
|
356
|
+
if (config.strategy && config.strategy !== "auto") {
|
|
357
|
+
const hints = storage.tracingStrategy;
|
|
358
|
+
if (hints.supported.includes(config.strategy)) {
|
|
359
|
+
return config.strategy;
|
|
360
|
+
}
|
|
361
|
+
logger.warn("User-specified tracing strategy not supported by storage adapter, falling back to auto-selection", {
|
|
362
|
+
userStrategy: config.strategy,
|
|
363
|
+
storageAdapter: storage.constructor.name,
|
|
364
|
+
supportedStrategies: hints.supported,
|
|
365
|
+
fallbackStrategy: hints.preferred
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
return storage.tracingStrategy.preferred;
|
|
369
|
+
}
|
|
370
|
+
var DefaultExporter = class extends BaseExporter {
|
|
371
|
+
name = "mastra-default-observability-exporter";
|
|
372
|
+
#storage;
|
|
373
|
+
#config;
|
|
374
|
+
#resolvedStrategy;
|
|
375
|
+
buffer;
|
|
376
|
+
#flushTimer = null;
|
|
377
|
+
// Track all spans that have been created, persists across flushes
|
|
378
|
+
allCreatedSpans = /* @__PURE__ */ new Set();
|
|
379
|
+
constructor(config = {}) {
|
|
380
|
+
super(config);
|
|
381
|
+
if (config === void 0) {
|
|
382
|
+
config = {};
|
|
383
|
+
}
|
|
384
|
+
this.#config = {
|
|
385
|
+
...config,
|
|
386
|
+
maxBatchSize: config.maxBatchSize ?? 1e3,
|
|
387
|
+
maxBufferSize: config.maxBufferSize ?? 1e4,
|
|
388
|
+
maxBatchWaitMs: config.maxBatchWaitMs ?? 5e3,
|
|
389
|
+
maxRetries: config.maxRetries ?? 4,
|
|
390
|
+
retryDelayMs: config.retryDelayMs ?? 500,
|
|
391
|
+
strategy: config.strategy ?? "auto"
|
|
392
|
+
};
|
|
393
|
+
this.buffer = {
|
|
394
|
+
creates: [],
|
|
395
|
+
updates: [],
|
|
396
|
+
insertOnly: [],
|
|
397
|
+
seenSpans: /* @__PURE__ */ new Set(),
|
|
398
|
+
spanSequences: /* @__PURE__ */ new Map(),
|
|
399
|
+
completedSpans: /* @__PURE__ */ new Set(),
|
|
400
|
+
outOfOrderCount: 0,
|
|
401
|
+
totalSize: 0
|
|
402
|
+
};
|
|
403
|
+
this.#resolvedStrategy = "batch-with-updates";
|
|
404
|
+
}
|
|
405
|
+
#strategyInitialized = false;
|
|
406
|
+
/**
|
|
407
|
+
* Initialize the exporter (called after all dependencies are ready)
|
|
408
|
+
*/
|
|
409
|
+
init(options) {
|
|
410
|
+
this.#storage = options.mastra?.getStorage();
|
|
411
|
+
if (!this.#storage) {
|
|
412
|
+
this.logger.warn("DefaultExporter disabled: Storage not available. Traces will not be persisted.");
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
this.initializeStrategy(this.#storage);
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Initialize the resolved strategy once storage is available
|
|
419
|
+
*/
|
|
420
|
+
initializeStrategy(storage) {
|
|
421
|
+
if (this.#strategyInitialized) return;
|
|
422
|
+
this.#resolvedStrategy = resolveTracingStorageStrategy(this.#config, storage, this.logger);
|
|
423
|
+
this.#strategyInitialized = true;
|
|
424
|
+
this.logger.debug("tracing storage exporter initialized", {
|
|
425
|
+
strategy: this.#resolvedStrategy,
|
|
426
|
+
source: this.#config.strategy !== "auto" ? "user" : "auto",
|
|
427
|
+
storageAdapter: storage.constructor.name,
|
|
428
|
+
maxBatchSize: this.#config.maxBatchSize,
|
|
429
|
+
maxBatchWaitMs: this.#config.maxBatchWaitMs
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Builds a unique span key for tracking
|
|
434
|
+
*/
|
|
435
|
+
buildSpanKey(traceId, spanId) {
|
|
436
|
+
return `${traceId}:${spanId}`;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Gets the next sequence number for a span
|
|
440
|
+
*/
|
|
441
|
+
getNextSequence(spanKey) {
|
|
442
|
+
const current = this.buffer.spanSequences.get(spanKey) || 0;
|
|
443
|
+
const next = current + 1;
|
|
444
|
+
this.buffer.spanSequences.set(spanKey, next);
|
|
445
|
+
return next;
|
|
446
|
+
}
|
|
447
|
+
/**
|
|
448
|
+
* Handles out-of-order span updates by logging and skipping
|
|
449
|
+
*/
|
|
450
|
+
handleOutOfOrderUpdate(event) {
|
|
451
|
+
this.logger.warn("Out-of-order span update detected - skipping event", {
|
|
452
|
+
spanId: event.exportedSpan.id,
|
|
453
|
+
traceId: event.exportedSpan.traceId,
|
|
454
|
+
spanName: event.exportedSpan.name,
|
|
455
|
+
eventType: event.type
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Adds an event to the appropriate buffer based on strategy
|
|
460
|
+
*/
|
|
461
|
+
addToBuffer(event) {
|
|
462
|
+
const spanKey = this.buildSpanKey(event.exportedSpan.traceId, event.exportedSpan.id);
|
|
463
|
+
if (this.buffer.totalSize === 0) {
|
|
464
|
+
this.buffer.firstEventTime = /* @__PURE__ */ new Date();
|
|
465
|
+
}
|
|
466
|
+
switch (event.type) {
|
|
467
|
+
case TracingEventType.SPAN_STARTED:
|
|
468
|
+
if (this.#resolvedStrategy === "batch-with-updates") {
|
|
469
|
+
const createRecord = this.buildCreateRecord(event.exportedSpan);
|
|
470
|
+
this.buffer.creates.push(createRecord);
|
|
471
|
+
this.buffer.seenSpans.add(spanKey);
|
|
472
|
+
this.allCreatedSpans.add(spanKey);
|
|
473
|
+
}
|
|
474
|
+
break;
|
|
475
|
+
case TracingEventType.SPAN_UPDATED:
|
|
476
|
+
if (this.#resolvedStrategy === "batch-with-updates") {
|
|
477
|
+
if (this.allCreatedSpans.has(spanKey)) {
|
|
478
|
+
this.buffer.updates.push({
|
|
479
|
+
traceId: event.exportedSpan.traceId,
|
|
480
|
+
spanId: event.exportedSpan.id,
|
|
481
|
+
updates: this.buildUpdateRecord(event.exportedSpan),
|
|
482
|
+
sequenceNumber: this.getNextSequence(spanKey)
|
|
483
|
+
});
|
|
484
|
+
} else {
|
|
485
|
+
this.handleOutOfOrderUpdate(event);
|
|
486
|
+
this.buffer.outOfOrderCount++;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
break;
|
|
490
|
+
case TracingEventType.SPAN_ENDED:
|
|
491
|
+
if (this.#resolvedStrategy === "batch-with-updates") {
|
|
492
|
+
if (this.allCreatedSpans.has(spanKey)) {
|
|
493
|
+
this.buffer.updates.push({
|
|
494
|
+
traceId: event.exportedSpan.traceId,
|
|
495
|
+
spanId: event.exportedSpan.id,
|
|
496
|
+
updates: this.buildUpdateRecord(event.exportedSpan),
|
|
497
|
+
sequenceNumber: this.getNextSequence(spanKey)
|
|
498
|
+
});
|
|
499
|
+
this.buffer.completedSpans.add(spanKey);
|
|
500
|
+
} else if (event.exportedSpan.isEvent) {
|
|
501
|
+
const createRecord = this.buildCreateRecord(event.exportedSpan);
|
|
502
|
+
this.buffer.creates.push(createRecord);
|
|
503
|
+
this.buffer.seenSpans.add(spanKey);
|
|
504
|
+
this.allCreatedSpans.add(spanKey);
|
|
505
|
+
this.buffer.completedSpans.add(spanKey);
|
|
506
|
+
} else {
|
|
507
|
+
this.handleOutOfOrderUpdate(event);
|
|
508
|
+
this.buffer.outOfOrderCount++;
|
|
509
|
+
}
|
|
510
|
+
} else if (this.#resolvedStrategy === "insert-only") {
|
|
511
|
+
const createRecord = this.buildCreateRecord(event.exportedSpan);
|
|
512
|
+
this.buffer.insertOnly.push(createRecord);
|
|
513
|
+
this.buffer.completedSpans.add(spanKey);
|
|
514
|
+
this.allCreatedSpans.add(spanKey);
|
|
515
|
+
}
|
|
516
|
+
break;
|
|
517
|
+
}
|
|
518
|
+
this.buffer.totalSize = this.buffer.creates.length + this.buffer.updates.length + this.buffer.insertOnly.length;
|
|
519
|
+
}
|
|
520
|
+
/**
|
|
521
|
+
* Checks if buffer should be flushed based on size or time triggers
|
|
522
|
+
*/
|
|
523
|
+
shouldFlush() {
|
|
524
|
+
if (this.buffer.totalSize >= this.#config.maxBufferSize) {
|
|
525
|
+
return true;
|
|
526
|
+
}
|
|
527
|
+
if (this.buffer.totalSize >= this.#config.maxBatchSize) {
|
|
528
|
+
return true;
|
|
529
|
+
}
|
|
530
|
+
if (this.buffer.firstEventTime && this.buffer.totalSize > 0) {
|
|
531
|
+
const elapsed = Date.now() - this.buffer.firstEventTime.getTime();
|
|
532
|
+
if (elapsed >= this.#config.maxBatchWaitMs) {
|
|
533
|
+
return true;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
return false;
|
|
537
|
+
}
|
|
538
|
+
/**
|
|
539
|
+
* Resets the buffer after successful flush
|
|
540
|
+
*/
|
|
541
|
+
resetBuffer(completedSpansToCleanup = /* @__PURE__ */ new Set()) {
|
|
542
|
+
this.buffer.creates = [];
|
|
543
|
+
this.buffer.updates = [];
|
|
544
|
+
this.buffer.insertOnly = [];
|
|
545
|
+
this.buffer.seenSpans.clear();
|
|
546
|
+
this.buffer.spanSequences.clear();
|
|
547
|
+
this.buffer.completedSpans.clear();
|
|
548
|
+
this.buffer.outOfOrderCount = 0;
|
|
549
|
+
this.buffer.firstEventTime = void 0;
|
|
550
|
+
this.buffer.totalSize = 0;
|
|
551
|
+
for (const spanKey of completedSpansToCleanup) {
|
|
552
|
+
this.allCreatedSpans.delete(spanKey);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
/**
|
|
556
|
+
* Schedules a flush using setTimeout
|
|
557
|
+
*/
|
|
558
|
+
scheduleFlush() {
|
|
559
|
+
if (this.#flushTimer) {
|
|
560
|
+
clearTimeout(this.#flushTimer);
|
|
561
|
+
}
|
|
562
|
+
this.#flushTimer = setTimeout(() => {
|
|
563
|
+
this.flush().catch((error) => {
|
|
564
|
+
this.logger.error("Scheduled flush failed", {
|
|
565
|
+
error: error instanceof Error ? error.message : String(error)
|
|
566
|
+
});
|
|
567
|
+
});
|
|
568
|
+
}, this.#config.maxBatchWaitMs);
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* Serializes span attributes to storage record format
|
|
572
|
+
* Handles all Span types and their specific attributes
|
|
573
|
+
*/
|
|
574
|
+
serializeAttributes(span) {
|
|
575
|
+
if (!span.attributes) {
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
return JSON.parse(
|
|
580
|
+
JSON.stringify(span.attributes, (_key, value) => {
|
|
581
|
+
if (value instanceof Date) {
|
|
582
|
+
return value.toISOString();
|
|
583
|
+
}
|
|
584
|
+
if (typeof value === "object" && value !== null) {
|
|
585
|
+
return value;
|
|
586
|
+
}
|
|
587
|
+
return value;
|
|
588
|
+
})
|
|
589
|
+
);
|
|
590
|
+
} catch (error) {
|
|
591
|
+
this.logger.warn("Failed to serialize span attributes, storing as null", {
|
|
592
|
+
spanId: span.id,
|
|
593
|
+
spanType: span.type,
|
|
594
|
+
error: error instanceof Error ? error.message : String(error)
|
|
595
|
+
});
|
|
596
|
+
return null;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
buildCreateRecord(span) {
|
|
600
|
+
return {
|
|
601
|
+
traceId: span.traceId,
|
|
602
|
+
spanId: span.id,
|
|
603
|
+
parentSpanId: span.parentSpanId ?? null,
|
|
604
|
+
name: span.name,
|
|
605
|
+
scope: null,
|
|
606
|
+
spanType: span.type,
|
|
607
|
+
attributes: this.serializeAttributes(span),
|
|
608
|
+
metadata: span.metadata ?? null,
|
|
609
|
+
links: null,
|
|
610
|
+
startedAt: span.startTime,
|
|
611
|
+
endedAt: span.endTime ?? null,
|
|
612
|
+
input: span.input,
|
|
613
|
+
output: span.output,
|
|
614
|
+
error: span.errorInfo,
|
|
615
|
+
isEvent: span.isEvent
|
|
616
|
+
};
|
|
617
|
+
}
|
|
618
|
+
buildUpdateRecord(span) {
|
|
619
|
+
return {
|
|
620
|
+
name: span.name,
|
|
621
|
+
scope: null,
|
|
622
|
+
attributes: this.serializeAttributes(span),
|
|
623
|
+
metadata: span.metadata ?? null,
|
|
624
|
+
links: null,
|
|
625
|
+
endedAt: span.endTime ?? null,
|
|
626
|
+
input: span.input,
|
|
627
|
+
output: span.output,
|
|
628
|
+
error: span.errorInfo
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
/**
|
|
632
|
+
* Handles realtime strategy - processes each event immediately
|
|
633
|
+
*/
|
|
634
|
+
async handleRealtimeEvent(event, storage) {
|
|
635
|
+
const span = event.exportedSpan;
|
|
636
|
+
const spanKey = this.buildSpanKey(span.traceId, span.id);
|
|
637
|
+
if (span.isEvent) {
|
|
638
|
+
if (event.type === TracingEventType.SPAN_ENDED) {
|
|
639
|
+
await storage.createSpan(this.buildCreateRecord(event.exportedSpan));
|
|
640
|
+
} else {
|
|
641
|
+
this.logger.warn(`Tracing event type not implemented for event spans: ${event.type}`);
|
|
642
|
+
}
|
|
643
|
+
} else {
|
|
644
|
+
switch (event.type) {
|
|
645
|
+
case TracingEventType.SPAN_STARTED:
|
|
646
|
+
await storage.createSpan(this.buildCreateRecord(event.exportedSpan));
|
|
647
|
+
this.allCreatedSpans.add(spanKey);
|
|
648
|
+
break;
|
|
649
|
+
case TracingEventType.SPAN_UPDATED:
|
|
650
|
+
await storage.updateSpan({
|
|
651
|
+
traceId: span.traceId,
|
|
652
|
+
spanId: span.id,
|
|
653
|
+
updates: this.buildUpdateRecord(span)
|
|
654
|
+
});
|
|
655
|
+
break;
|
|
656
|
+
case TracingEventType.SPAN_ENDED:
|
|
657
|
+
await storage.updateSpan({
|
|
658
|
+
traceId: span.traceId,
|
|
659
|
+
spanId: span.id,
|
|
660
|
+
updates: this.buildUpdateRecord(span)
|
|
661
|
+
});
|
|
662
|
+
this.allCreatedSpans.delete(spanKey);
|
|
663
|
+
break;
|
|
664
|
+
default:
|
|
665
|
+
this.logger.warn(`Tracing event type not implemented for span spans: ${event.type}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
/**
|
|
670
|
+
* Handles batch-with-updates strategy - buffers events and processes in batches
|
|
671
|
+
*/
|
|
672
|
+
handleBatchWithUpdatesEvent(event) {
|
|
673
|
+
this.addToBuffer(event);
|
|
674
|
+
if (this.shouldFlush()) {
|
|
675
|
+
this.flush().catch((error) => {
|
|
676
|
+
this.logger.error("Batch flush failed", {
|
|
677
|
+
error: error instanceof Error ? error.message : String(error)
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
} else if (this.buffer.totalSize === 1) {
|
|
681
|
+
this.scheduleFlush();
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
/**
|
|
685
|
+
* Handles insert-only strategy - only processes SPAN_ENDED events in batches
|
|
686
|
+
*/
|
|
687
|
+
handleInsertOnlyEvent(event) {
|
|
688
|
+
if (event.type === TracingEventType.SPAN_ENDED) {
|
|
689
|
+
this.addToBuffer(event);
|
|
690
|
+
if (this.shouldFlush()) {
|
|
691
|
+
this.flush().catch((error) => {
|
|
692
|
+
this.logger.error("Batch flush failed", {
|
|
693
|
+
error: error instanceof Error ? error.message : String(error)
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
} else if (this.buffer.totalSize === 1) {
|
|
697
|
+
this.scheduleFlush();
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
/**
|
|
702
|
+
* Calculates retry delay using exponential backoff
|
|
703
|
+
*/
|
|
704
|
+
calculateRetryDelay(attempt) {
|
|
705
|
+
return this.#config.retryDelayMs * Math.pow(2, attempt);
|
|
706
|
+
}
|
|
707
|
+
/**
|
|
708
|
+
* Flushes the current buffer to storage with retry logic
|
|
709
|
+
*/
|
|
710
|
+
async flush() {
|
|
711
|
+
if (!this.#storage) {
|
|
712
|
+
this.logger.debug("Cannot flush traces. Mastra storage is not initialized");
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (this.#flushTimer) {
|
|
716
|
+
clearTimeout(this.#flushTimer);
|
|
717
|
+
this.#flushTimer = null;
|
|
718
|
+
}
|
|
719
|
+
if (this.buffer.totalSize === 0) {
|
|
720
|
+
return;
|
|
721
|
+
}
|
|
722
|
+
const startTime = Date.now();
|
|
723
|
+
const flushReason = this.buffer.totalSize >= this.#config.maxBufferSize ? "overflow" : this.buffer.totalSize >= this.#config.maxBatchSize ? "size" : "time";
|
|
724
|
+
const bufferCopy = {
|
|
725
|
+
creates: [...this.buffer.creates],
|
|
726
|
+
updates: [...this.buffer.updates],
|
|
727
|
+
insertOnly: [...this.buffer.insertOnly],
|
|
728
|
+
seenSpans: new Set(this.buffer.seenSpans),
|
|
729
|
+
spanSequences: new Map(this.buffer.spanSequences),
|
|
730
|
+
completedSpans: new Set(this.buffer.completedSpans),
|
|
731
|
+
outOfOrderCount: this.buffer.outOfOrderCount,
|
|
732
|
+
firstEventTime: this.buffer.firstEventTime,
|
|
733
|
+
totalSize: this.buffer.totalSize
|
|
734
|
+
};
|
|
735
|
+
this.resetBuffer();
|
|
736
|
+
await this.flushWithRetries(this.#storage, bufferCopy, 0);
|
|
737
|
+
const elapsed = Date.now() - startTime;
|
|
738
|
+
this.logger.debug("Batch flushed", {
|
|
739
|
+
strategy: this.#resolvedStrategy,
|
|
740
|
+
batchSize: bufferCopy.totalSize,
|
|
741
|
+
flushReason,
|
|
742
|
+
durationMs: elapsed,
|
|
743
|
+
outOfOrderCount: bufferCopy.outOfOrderCount > 0 ? bufferCopy.outOfOrderCount : void 0
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* Attempts to flush with exponential backoff retry logic
|
|
748
|
+
*/
|
|
749
|
+
async flushWithRetries(storage, buffer, attempt) {
|
|
750
|
+
try {
|
|
751
|
+
if (this.#resolvedStrategy === "batch-with-updates") {
|
|
752
|
+
if (buffer.creates.length > 0) {
|
|
753
|
+
await storage.batchCreateSpans({ records: buffer.creates });
|
|
754
|
+
}
|
|
755
|
+
if (buffer.updates.length > 0) {
|
|
756
|
+
const sortedUpdates = buffer.updates.sort((a, b) => {
|
|
757
|
+
const spanCompare = this.buildSpanKey(a.traceId, a.spanId).localeCompare(
|
|
758
|
+
this.buildSpanKey(b.traceId, b.spanId)
|
|
759
|
+
);
|
|
760
|
+
if (spanCompare !== 0) return spanCompare;
|
|
761
|
+
return a.sequenceNumber - b.sequenceNumber;
|
|
762
|
+
});
|
|
763
|
+
await storage.batchUpdateSpans({ records: sortedUpdates });
|
|
764
|
+
}
|
|
765
|
+
} else if (this.#resolvedStrategy === "insert-only") {
|
|
766
|
+
if (buffer.insertOnly.length > 0) {
|
|
767
|
+
await storage.batchCreateSpans({ records: buffer.insertOnly });
|
|
768
|
+
}
|
|
769
|
+
}
|
|
770
|
+
for (const spanKey of buffer.completedSpans) {
|
|
771
|
+
this.allCreatedSpans.delete(spanKey);
|
|
772
|
+
}
|
|
773
|
+
} catch (error) {
|
|
774
|
+
if (attempt < this.#config.maxRetries) {
|
|
775
|
+
const retryDelay = this.calculateRetryDelay(attempt);
|
|
776
|
+
this.logger.warn("Batch flush failed, retrying", {
|
|
777
|
+
attempt: attempt + 1,
|
|
778
|
+
maxRetries: this.#config.maxRetries,
|
|
779
|
+
nextRetryInMs: retryDelay,
|
|
780
|
+
error: error instanceof Error ? error.message : String(error)
|
|
781
|
+
});
|
|
782
|
+
await new Promise((resolve) => setTimeout(resolve, retryDelay));
|
|
783
|
+
return this.flushWithRetries(storage, buffer, attempt + 1);
|
|
784
|
+
} else {
|
|
785
|
+
this.logger.error("Batch flush failed after all retries, dropping batch", {
|
|
786
|
+
finalAttempt: attempt + 1,
|
|
787
|
+
maxRetries: this.#config.maxRetries,
|
|
788
|
+
droppedBatchSize: buffer.totalSize,
|
|
789
|
+
error: error instanceof Error ? error.message : String(error)
|
|
790
|
+
});
|
|
791
|
+
for (const spanKey of buffer.completedSpans) {
|
|
792
|
+
this.allCreatedSpans.delete(spanKey);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
async _exportTracingEvent(event) {
|
|
798
|
+
if (!this.#storage) {
|
|
799
|
+
this.logger.debug("Cannot store traces. Mastra storage is not initialized");
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (!this.#strategyInitialized) {
|
|
803
|
+
this.initializeStrategy(this.#storage);
|
|
804
|
+
}
|
|
805
|
+
switch (this.#resolvedStrategy) {
|
|
806
|
+
case "realtime":
|
|
807
|
+
await this.handleRealtimeEvent(event, this.#storage);
|
|
808
|
+
break;
|
|
809
|
+
case "batch-with-updates":
|
|
810
|
+
this.handleBatchWithUpdatesEvent(event);
|
|
811
|
+
break;
|
|
812
|
+
case "insert-only":
|
|
813
|
+
this.handleInsertOnlyEvent(event);
|
|
814
|
+
break;
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async shutdown() {
|
|
818
|
+
if (this.#flushTimer) {
|
|
819
|
+
clearTimeout(this.#flushTimer);
|
|
820
|
+
this.#flushTimer = null;
|
|
821
|
+
}
|
|
822
|
+
if (this.buffer.totalSize > 0) {
|
|
823
|
+
this.logger.info("Flushing remaining events on shutdown", {
|
|
824
|
+
remainingEvents: this.buffer.totalSize
|
|
825
|
+
});
|
|
826
|
+
try {
|
|
827
|
+
await this.flush();
|
|
828
|
+
} catch (error) {
|
|
829
|
+
this.logger.error("Failed to flush remaining events during shutdown", {
|
|
830
|
+
error: error instanceof Error ? error.message : String(error)
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
this.logger.info("DefaultExporter shutdown complete");
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
var ModelSpanTracker = class {
|
|
838
|
+
#modelSpan;
|
|
839
|
+
#currentStepSpan;
|
|
840
|
+
#currentChunkSpan;
|
|
841
|
+
#accumulator = {};
|
|
842
|
+
#stepIndex = 0;
|
|
843
|
+
#chunkSequence = 0;
|
|
844
|
+
constructor(modelSpan) {
|
|
845
|
+
this.#modelSpan = modelSpan;
|
|
846
|
+
}
|
|
847
|
+
/**
|
|
848
|
+
* Get the tracing context for creating child spans.
|
|
849
|
+
* Returns the current step span if active, otherwise the model span.
|
|
850
|
+
*/
|
|
851
|
+
getTracingContext() {
|
|
852
|
+
return {
|
|
853
|
+
currentSpan: this.#currentStepSpan ?? this.#modelSpan
|
|
854
|
+
};
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* Report an error on the generation span
|
|
858
|
+
*/
|
|
859
|
+
reportGenerationError(options) {
|
|
860
|
+
this.#modelSpan?.error(options);
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* End the generation span
|
|
864
|
+
*/
|
|
865
|
+
endGeneration(options) {
|
|
866
|
+
this.#modelSpan?.end(options);
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* Update the generation span
|
|
870
|
+
*/
|
|
871
|
+
updateGeneration(options) {
|
|
872
|
+
this.#modelSpan?.update(options);
|
|
873
|
+
}
|
|
874
|
+
/**
|
|
875
|
+
* Start a new Model execution step
|
|
876
|
+
*/
|
|
877
|
+
#startStepSpan(payload) {
|
|
878
|
+
this.#currentStepSpan = this.#modelSpan?.createChildSpan({
|
|
879
|
+
name: `step: ${this.#stepIndex}`,
|
|
880
|
+
type: SpanType.MODEL_STEP,
|
|
881
|
+
attributes: {
|
|
882
|
+
stepIndex: this.#stepIndex,
|
|
883
|
+
...payload?.messageId ? { messageId: payload.messageId } : {},
|
|
884
|
+
...payload?.warnings?.length ? { warnings: payload.warnings } : {}
|
|
885
|
+
},
|
|
886
|
+
input: payload?.request
|
|
887
|
+
});
|
|
888
|
+
this.#chunkSequence = 0;
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* End the current Model execution step with token usage, finish reason, output, and metadata
|
|
892
|
+
*/
|
|
893
|
+
#endStepSpan(payload) {
|
|
894
|
+
if (!this.#currentStepSpan) return;
|
|
895
|
+
const output = payload.output;
|
|
896
|
+
const { usage, ...otherOutput } = output;
|
|
897
|
+
const stepResult = payload.stepResult;
|
|
898
|
+
const metadata = payload.metadata;
|
|
899
|
+
const cleanMetadata = metadata ? { ...metadata } : void 0;
|
|
900
|
+
if (cleanMetadata?.request) {
|
|
901
|
+
delete cleanMetadata.request;
|
|
902
|
+
}
|
|
903
|
+
this.#currentStepSpan.end({
|
|
904
|
+
output: otherOutput,
|
|
905
|
+
attributes: {
|
|
906
|
+
usage,
|
|
907
|
+
isContinued: stepResult.isContinued,
|
|
908
|
+
finishReason: stepResult.reason,
|
|
909
|
+
warnings: stepResult.warnings
|
|
910
|
+
},
|
|
911
|
+
metadata: {
|
|
912
|
+
...cleanMetadata
|
|
913
|
+
}
|
|
914
|
+
});
|
|
915
|
+
this.#currentStepSpan = void 0;
|
|
916
|
+
this.#stepIndex++;
|
|
917
|
+
}
|
|
918
|
+
/**
|
|
919
|
+
* Create a new chunk span (for multi-part chunks like text-start/delta/end)
|
|
920
|
+
*/
|
|
921
|
+
#startChunkSpan(chunkType, initialData) {
|
|
922
|
+
if (!this.#currentStepSpan) {
|
|
923
|
+
this.#startStepSpan();
|
|
924
|
+
}
|
|
925
|
+
this.#currentChunkSpan = this.#currentStepSpan?.createChildSpan({
|
|
926
|
+
name: `chunk: '${chunkType}'`,
|
|
927
|
+
type: SpanType.MODEL_CHUNK,
|
|
928
|
+
attributes: {
|
|
929
|
+
chunkType,
|
|
930
|
+
sequenceNumber: this.#chunkSequence
|
|
931
|
+
}
|
|
932
|
+
});
|
|
933
|
+
this.#accumulator = initialData || {};
|
|
934
|
+
}
|
|
935
|
+
/**
|
|
936
|
+
* Append string content to a specific field in the accumulator
|
|
937
|
+
*/
|
|
938
|
+
#appendToAccumulator(field, text) {
|
|
939
|
+
if (this.#accumulator[field] === void 0) {
|
|
940
|
+
this.#accumulator[field] = text;
|
|
941
|
+
} else {
|
|
942
|
+
this.#accumulator[field] += text;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/**
|
|
946
|
+
* End the current chunk span.
|
|
947
|
+
* Safe to call multiple times - will no-op if span already ended.
|
|
948
|
+
*/
|
|
949
|
+
#endChunkSpan(output) {
|
|
950
|
+
if (!this.#currentChunkSpan) return;
|
|
951
|
+
this.#currentChunkSpan.end({
|
|
952
|
+
output: output !== void 0 ? output : this.#accumulator
|
|
953
|
+
});
|
|
954
|
+
this.#currentChunkSpan = void 0;
|
|
955
|
+
this.#accumulator = {};
|
|
956
|
+
this.#chunkSequence++;
|
|
957
|
+
}
|
|
958
|
+
/**
|
|
959
|
+
* Create an event span (for single chunks like tool-call)
|
|
960
|
+
*/
|
|
961
|
+
#createEventSpan(chunkType, output) {
|
|
962
|
+
if (!this.#currentStepSpan) {
|
|
963
|
+
this.#startStepSpan();
|
|
964
|
+
}
|
|
965
|
+
const span = this.#currentStepSpan?.createEventSpan({
|
|
966
|
+
name: `chunk: '${chunkType}'`,
|
|
967
|
+
type: SpanType.MODEL_CHUNK,
|
|
968
|
+
attributes: {
|
|
969
|
+
chunkType,
|
|
970
|
+
sequenceNumber: this.#chunkSequence
|
|
971
|
+
},
|
|
972
|
+
output
|
|
973
|
+
});
|
|
974
|
+
if (span) {
|
|
975
|
+
this.#chunkSequence++;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
/**
|
|
979
|
+
* Check if there is currently an active chunk span
|
|
980
|
+
*/
|
|
981
|
+
#hasActiveChunkSpan() {
|
|
982
|
+
return !!this.#currentChunkSpan;
|
|
983
|
+
}
|
|
984
|
+
/**
|
|
985
|
+
* Get the current accumulator value
|
|
986
|
+
*/
|
|
987
|
+
#getAccumulator() {
|
|
988
|
+
return this.#accumulator;
|
|
989
|
+
}
|
|
990
|
+
/**
|
|
991
|
+
* Handle text chunk spans (text-start/delta/end)
|
|
992
|
+
*/
|
|
993
|
+
#handleTextChunk(chunk) {
|
|
994
|
+
switch (chunk.type) {
|
|
995
|
+
case "text-start":
|
|
996
|
+
this.#startChunkSpan("text");
|
|
997
|
+
break;
|
|
998
|
+
case "text-delta":
|
|
999
|
+
this.#appendToAccumulator("text", chunk.payload.text);
|
|
1000
|
+
break;
|
|
1001
|
+
case "text-end": {
|
|
1002
|
+
this.#endChunkSpan();
|
|
1003
|
+
break;
|
|
1004
|
+
}
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Handle reasoning chunk spans (reasoning-start/delta/end)
|
|
1009
|
+
*/
|
|
1010
|
+
#handleReasoningChunk(chunk) {
|
|
1011
|
+
switch (chunk.type) {
|
|
1012
|
+
case "reasoning-start":
|
|
1013
|
+
this.#startChunkSpan("reasoning");
|
|
1014
|
+
break;
|
|
1015
|
+
case "reasoning-delta":
|
|
1016
|
+
this.#appendToAccumulator("text", chunk.payload.text);
|
|
1017
|
+
break;
|
|
1018
|
+
case "reasoning-end": {
|
|
1019
|
+
this.#endChunkSpan();
|
|
1020
|
+
break;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
/**
|
|
1025
|
+
* Handle tool call chunk spans (tool-call-input-streaming-start/delta/end, tool-call)
|
|
1026
|
+
*/
|
|
1027
|
+
#handleToolCallChunk(chunk) {
|
|
1028
|
+
switch (chunk.type) {
|
|
1029
|
+
case "tool-call-input-streaming-start":
|
|
1030
|
+
this.#startChunkSpan("tool-call", {
|
|
1031
|
+
toolName: chunk.payload.toolName,
|
|
1032
|
+
toolCallId: chunk.payload.toolCallId
|
|
1033
|
+
});
|
|
1034
|
+
break;
|
|
1035
|
+
case "tool-call-delta":
|
|
1036
|
+
this.#appendToAccumulator("toolInput", chunk.payload.argsTextDelta);
|
|
1037
|
+
break;
|
|
1038
|
+
case "tool-call-input-streaming-end":
|
|
1039
|
+
case "tool-call": {
|
|
1040
|
+
const acc = this.#getAccumulator();
|
|
1041
|
+
let toolInput;
|
|
1042
|
+
try {
|
|
1043
|
+
toolInput = acc.toolInput ? JSON.parse(acc.toolInput) : {};
|
|
1044
|
+
} catch {
|
|
1045
|
+
toolInput = acc.toolInput;
|
|
1046
|
+
}
|
|
1047
|
+
this.#endChunkSpan({
|
|
1048
|
+
toolName: acc.toolName,
|
|
1049
|
+
toolCallId: acc.toolCallId,
|
|
1050
|
+
toolInput
|
|
1051
|
+
});
|
|
1052
|
+
break;
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
/**
|
|
1057
|
+
* Handle object chunk spans (object, object-result)
|
|
1058
|
+
*/
|
|
1059
|
+
#handleObjectChunk(chunk) {
|
|
1060
|
+
switch (chunk.type) {
|
|
1061
|
+
case "object":
|
|
1062
|
+
if (!this.#hasActiveChunkSpan()) {
|
|
1063
|
+
this.#startChunkSpan("object");
|
|
1064
|
+
}
|
|
1065
|
+
break;
|
|
1066
|
+
case "object-result":
|
|
1067
|
+
this.#endChunkSpan(chunk.object);
|
|
1068
|
+
break;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Wraps a stream with model tracing transform to track MODEL_STEP and MODEL_CHUNK spans.
|
|
1073
|
+
*
|
|
1074
|
+
* This should be added to the stream pipeline to automatically
|
|
1075
|
+
* create MODEL_STEP and MODEL_CHUNK spans for each semantic unit in the stream.
|
|
1076
|
+
*/
|
|
1077
|
+
wrapStream(stream) {
|
|
1078
|
+
return stream.pipeThrough(
|
|
1079
|
+
new TransformStream({
|
|
1080
|
+
transform: (chunk, controller) => {
|
|
1081
|
+
controller.enqueue(chunk);
|
|
1082
|
+
switch (chunk.type) {
|
|
1083
|
+
case "text-start":
|
|
1084
|
+
case "text-delta":
|
|
1085
|
+
case "text-end":
|
|
1086
|
+
this.#handleTextChunk(chunk);
|
|
1087
|
+
break;
|
|
1088
|
+
case "tool-call-input-streaming-start":
|
|
1089
|
+
case "tool-call-delta":
|
|
1090
|
+
case "tool-call-input-streaming-end":
|
|
1091
|
+
case "tool-call":
|
|
1092
|
+
this.#handleToolCallChunk(chunk);
|
|
1093
|
+
break;
|
|
1094
|
+
case "reasoning-start":
|
|
1095
|
+
case "reasoning-delta":
|
|
1096
|
+
case "reasoning-end":
|
|
1097
|
+
this.#handleReasoningChunk(chunk);
|
|
1098
|
+
break;
|
|
1099
|
+
case "object":
|
|
1100
|
+
case "object-result":
|
|
1101
|
+
this.#handleObjectChunk(chunk);
|
|
1102
|
+
break;
|
|
1103
|
+
case "step-start":
|
|
1104
|
+
this.#startStepSpan(chunk.payload);
|
|
1105
|
+
break;
|
|
1106
|
+
case "step-finish":
|
|
1107
|
+
this.#endStepSpan(chunk.payload);
|
|
1108
|
+
break;
|
|
1109
|
+
case "raw":
|
|
1110
|
+
// Skip raw chunks as they're redundant
|
|
1111
|
+
case "start":
|
|
1112
|
+
case "finish":
|
|
1113
|
+
break;
|
|
1114
|
+
// Default: auto-create event span for all other chunk types
|
|
1115
|
+
default: {
|
|
1116
|
+
let outputPayload = chunk.payload;
|
|
1117
|
+
if (outputPayload && typeof outputPayload === "object" && "data" in outputPayload) {
|
|
1118
|
+
const typedPayload = outputPayload;
|
|
1119
|
+
outputPayload = { ...typedPayload };
|
|
1120
|
+
if (typedPayload.data) {
|
|
1121
|
+
outputPayload.size = typeof typedPayload.data === "string" ? typedPayload.data.length : typedPayload.data instanceof Uint8Array ? typedPayload.data.length : void 0;
|
|
1122
|
+
delete outputPayload.data;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
this.#createEventSpan(chunk.type, outputPayload);
|
|
1126
|
+
break;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
})
|
|
1131
|
+
);
|
|
1132
|
+
}
|
|
1133
|
+
};
|
|
1134
|
+
|
|
1135
|
+
// src/spans/base.ts
|
|
1136
|
+
function isSpanInternal(spanType, flags) {
|
|
1137
|
+
if (flags === void 0 || flags === InternalSpans.NONE) {
|
|
1138
|
+
return false;
|
|
1139
|
+
}
|
|
1140
|
+
switch (spanType) {
|
|
1141
|
+
// Workflow-related spans
|
|
1142
|
+
case SpanType.WORKFLOW_RUN:
|
|
1143
|
+
case SpanType.WORKFLOW_STEP:
|
|
1144
|
+
case SpanType.WORKFLOW_CONDITIONAL:
|
|
1145
|
+
case SpanType.WORKFLOW_CONDITIONAL_EVAL:
|
|
1146
|
+
case SpanType.WORKFLOW_PARALLEL:
|
|
1147
|
+
case SpanType.WORKFLOW_LOOP:
|
|
1148
|
+
case SpanType.WORKFLOW_SLEEP:
|
|
1149
|
+
case SpanType.WORKFLOW_WAIT_EVENT:
|
|
1150
|
+
return (flags & InternalSpans.WORKFLOW) !== 0;
|
|
1151
|
+
// Agent-related spans
|
|
1152
|
+
case SpanType.AGENT_RUN:
|
|
1153
|
+
return (flags & InternalSpans.AGENT) !== 0;
|
|
1154
|
+
// Tool-related spans
|
|
1155
|
+
case SpanType.TOOL_CALL:
|
|
1156
|
+
case SpanType.MCP_TOOL_CALL:
|
|
1157
|
+
return (flags & InternalSpans.TOOL) !== 0;
|
|
1158
|
+
// Model-related spans
|
|
1159
|
+
case SpanType.MODEL_GENERATION:
|
|
1160
|
+
case SpanType.MODEL_STEP:
|
|
1161
|
+
case SpanType.MODEL_CHUNK:
|
|
1162
|
+
return (flags & InternalSpans.MODEL) !== 0;
|
|
1163
|
+
// Default: never internal
|
|
1164
|
+
default:
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
}
|
|
1168
|
+
var BaseSpan = class {
|
|
1169
|
+
name;
|
|
1170
|
+
type;
|
|
1171
|
+
attributes;
|
|
1172
|
+
parent;
|
|
1173
|
+
startTime;
|
|
1174
|
+
endTime;
|
|
1175
|
+
isEvent;
|
|
1176
|
+
isInternal;
|
|
1177
|
+
observabilityInstance;
|
|
1178
|
+
input;
|
|
1179
|
+
output;
|
|
1180
|
+
errorInfo;
|
|
1181
|
+
metadata;
|
|
1182
|
+
traceState;
|
|
1183
|
+
/** Parent span ID (for root spans that are children of external spans) */
|
|
1184
|
+
parentSpanId;
|
|
1185
|
+
constructor(options, observabilityInstance) {
|
|
1186
|
+
this.name = options.name;
|
|
1187
|
+
this.type = options.type;
|
|
1188
|
+
this.attributes = deepClean(options.attributes) || {};
|
|
1189
|
+
this.metadata = deepClean(options.metadata);
|
|
1190
|
+
this.parent = options.parent;
|
|
1191
|
+
this.startTime = /* @__PURE__ */ new Date();
|
|
1192
|
+
this.observabilityInstance = observabilityInstance;
|
|
1193
|
+
this.isEvent = options.isEvent ?? false;
|
|
1194
|
+
this.isInternal = isSpanInternal(this.type, options.tracingPolicy?.internal);
|
|
1195
|
+
this.traceState = options.traceState;
|
|
1196
|
+
if (this.isEvent) {
|
|
1197
|
+
this.output = deepClean(options.output);
|
|
1198
|
+
} else {
|
|
1199
|
+
this.input = deepClean(options.input);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
createChildSpan(options) {
|
|
1203
|
+
return this.observabilityInstance.startSpan({ ...options, parent: this, isEvent: false });
|
|
1204
|
+
}
|
|
1205
|
+
createEventSpan(options) {
|
|
1206
|
+
return this.observabilityInstance.startSpan({ ...options, parent: this, isEvent: true });
|
|
1207
|
+
}
|
|
1208
|
+
/**
|
|
1209
|
+
* Create a ModelSpanTracker for this span (only works if this is a MODEL_GENERATION span)
|
|
1210
|
+
* Returns undefined for non-MODEL_GENERATION spans
|
|
1211
|
+
*/
|
|
1212
|
+
createTracker() {
|
|
1213
|
+
if (this.type !== SpanType.MODEL_GENERATION) {
|
|
1214
|
+
return void 0;
|
|
1215
|
+
}
|
|
1216
|
+
return new ModelSpanTracker(this);
|
|
1217
|
+
}
|
|
1218
|
+
/** Returns `TRUE` if the span is the root span of a trace */
|
|
1219
|
+
get isRootSpan() {
|
|
1220
|
+
return !this.parent;
|
|
1221
|
+
}
|
|
1222
|
+
/** Get the closest parent spanId that isn't an internal span */
|
|
1223
|
+
getParentSpanId(includeInternalSpans) {
|
|
1224
|
+
if (!this.parent) {
|
|
1225
|
+
return this.parentSpanId;
|
|
1226
|
+
}
|
|
1227
|
+
if (includeInternalSpans) return this.parent.id;
|
|
1228
|
+
if (this.parent.isInternal) return this.parent.getParentSpanId(includeInternalSpans);
|
|
1229
|
+
return this.parent.id;
|
|
1230
|
+
}
|
|
1231
|
+
/** Find the closest parent span of a specific type by walking up the parent chain */
|
|
1232
|
+
findParent(spanType) {
|
|
1233
|
+
let current = this.parent;
|
|
1234
|
+
while (current) {
|
|
1235
|
+
if (current.type === spanType) {
|
|
1236
|
+
return current;
|
|
1237
|
+
}
|
|
1238
|
+
current = current.parent;
|
|
1239
|
+
}
|
|
1240
|
+
return void 0;
|
|
1241
|
+
}
|
|
1242
|
+
/** Returns a lightweight span ready for export */
|
|
1243
|
+
exportSpan(includeInternalSpans) {
|
|
1244
|
+
return {
|
|
1245
|
+
id: this.id,
|
|
1246
|
+
traceId: this.traceId,
|
|
1247
|
+
name: this.name,
|
|
1248
|
+
type: this.type,
|
|
1249
|
+
attributes: this.attributes,
|
|
1250
|
+
metadata: this.metadata,
|
|
1251
|
+
startTime: this.startTime,
|
|
1252
|
+
endTime: this.endTime,
|
|
1253
|
+
input: this.input,
|
|
1254
|
+
output: this.output,
|
|
1255
|
+
errorInfo: this.errorInfo,
|
|
1256
|
+
isEvent: this.isEvent,
|
|
1257
|
+
isRootSpan: this.isRootSpan,
|
|
1258
|
+
parentSpanId: this.getParentSpanId(includeInternalSpans)
|
|
1259
|
+
};
|
|
1260
|
+
}
|
|
1261
|
+
get externalTraceId() {
|
|
1262
|
+
return this.isValid ? this.traceId : void 0;
|
|
1263
|
+
}
|
|
1264
|
+
};
|
|
1265
|
+
var DEFAULT_KEYS_TO_STRIP = /* @__PURE__ */ new Set([
|
|
1266
|
+
"logger",
|
|
1267
|
+
"experimental_providerMetadata",
|
|
1268
|
+
"providerMetadata",
|
|
1269
|
+
"steps",
|
|
1270
|
+
"tracingContext"
|
|
1271
|
+
]);
|
|
1272
|
+
function deepClean(value, options = {}, _seen = /* @__PURE__ */ new WeakSet(), _depth = 0) {
|
|
1273
|
+
const { keysToStrip = DEFAULT_KEYS_TO_STRIP, maxDepth = 10 } = options;
|
|
1274
|
+
if (_depth > maxDepth) {
|
|
1275
|
+
return "[MaxDepth]";
|
|
1276
|
+
}
|
|
1277
|
+
if (value === null || typeof value !== "object") {
|
|
1278
|
+
try {
|
|
1279
|
+
JSON.stringify(value);
|
|
1280
|
+
return value;
|
|
1281
|
+
} catch (error) {
|
|
1282
|
+
return `[${error instanceof Error ? error.message : String(error)}]`;
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
if (_seen.has(value)) {
|
|
1286
|
+
return "[Circular]";
|
|
1287
|
+
}
|
|
1288
|
+
_seen.add(value);
|
|
1289
|
+
if (Array.isArray(value)) {
|
|
1290
|
+
return value.map((item) => deepClean(item, options, _seen, _depth + 1));
|
|
1291
|
+
}
|
|
1292
|
+
const cleaned = {};
|
|
1293
|
+
for (const [key, val] of Object.entries(value)) {
|
|
1294
|
+
if (keysToStrip.has(key)) {
|
|
1295
|
+
continue;
|
|
1296
|
+
}
|
|
1297
|
+
try {
|
|
1298
|
+
cleaned[key] = deepClean(val, options, _seen, _depth + 1);
|
|
1299
|
+
} catch (error) {
|
|
1300
|
+
cleaned[key] = `[${error instanceof Error ? error.message : String(error)}]`;
|
|
1301
|
+
}
|
|
1302
|
+
}
|
|
1303
|
+
return cleaned;
|
|
1304
|
+
}
|
|
1305
|
+
var DefaultSpan = class extends BaseSpan {
|
|
1306
|
+
id;
|
|
1307
|
+
traceId;
|
|
1308
|
+
constructor(options, observabilityInstance) {
|
|
1309
|
+
super(options, observabilityInstance);
|
|
1310
|
+
this.id = generateSpanId();
|
|
1311
|
+
if (options.parent) {
|
|
1312
|
+
this.traceId = options.parent.traceId;
|
|
1313
|
+
} else if (options.traceId) {
|
|
1314
|
+
if (isValidTraceId(options.traceId)) {
|
|
1315
|
+
this.traceId = options.traceId;
|
|
1316
|
+
} else {
|
|
1317
|
+
console.error(
|
|
1318
|
+
`[Mastra Tracing] Invalid traceId: must be 1-32 hexadecimal characters, got "${options.traceId}". Generating new trace ID.`
|
|
1319
|
+
);
|
|
1320
|
+
this.traceId = generateTraceId();
|
|
1321
|
+
}
|
|
1322
|
+
} else {
|
|
1323
|
+
this.traceId = generateTraceId();
|
|
1324
|
+
}
|
|
1325
|
+
if (!options.parent && options.parentSpanId) {
|
|
1326
|
+
if (isValidSpanId(options.parentSpanId)) {
|
|
1327
|
+
this.parentSpanId = options.parentSpanId;
|
|
1328
|
+
} else {
|
|
1329
|
+
console.error(
|
|
1330
|
+
`[Mastra Tracing] Invalid parentSpanId: must be 1-16 hexadecimal characters, got "${options.parentSpanId}". Ignoring parent span ID.`
|
|
1331
|
+
);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
end(options) {
|
|
1336
|
+
if (this.isEvent) {
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
this.endTime = /* @__PURE__ */ new Date();
|
|
1340
|
+
if (options?.output !== void 0) {
|
|
1341
|
+
this.output = deepClean(options.output);
|
|
1342
|
+
}
|
|
1343
|
+
if (options?.attributes) {
|
|
1344
|
+
this.attributes = { ...this.attributes, ...deepClean(options.attributes) };
|
|
1345
|
+
}
|
|
1346
|
+
if (options?.metadata) {
|
|
1347
|
+
this.metadata = { ...this.metadata, ...deepClean(options.metadata) };
|
|
1348
|
+
}
|
|
1349
|
+
}
|
|
1350
|
+
error(options) {
|
|
1351
|
+
if (this.isEvent) {
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
const { error, endSpan = true, attributes, metadata } = options;
|
|
1355
|
+
this.errorInfo = error instanceof MastraError ? {
|
|
1356
|
+
id: error.id,
|
|
1357
|
+
details: error.details,
|
|
1358
|
+
category: error.category,
|
|
1359
|
+
domain: error.domain,
|
|
1360
|
+
message: error.message
|
|
1361
|
+
} : {
|
|
1362
|
+
message: error.message
|
|
1363
|
+
};
|
|
1364
|
+
if (attributes) {
|
|
1365
|
+
this.attributes = { ...this.attributes, ...deepClean(attributes) };
|
|
1366
|
+
}
|
|
1367
|
+
if (metadata) {
|
|
1368
|
+
this.metadata = { ...this.metadata, ...deepClean(metadata) };
|
|
1369
|
+
}
|
|
1370
|
+
if (endSpan) {
|
|
1371
|
+
this.end();
|
|
1372
|
+
} else {
|
|
1373
|
+
this.update({});
|
|
1374
|
+
}
|
|
1375
|
+
}
|
|
1376
|
+
update(options) {
|
|
1377
|
+
if (this.isEvent) {
|
|
1378
|
+
return;
|
|
1379
|
+
}
|
|
1380
|
+
if (options.input !== void 0) {
|
|
1381
|
+
this.input = deepClean(options.input);
|
|
1382
|
+
}
|
|
1383
|
+
if (options.output !== void 0) {
|
|
1384
|
+
this.output = deepClean(options.output);
|
|
1385
|
+
}
|
|
1386
|
+
if (options.attributes) {
|
|
1387
|
+
this.attributes = { ...this.attributes, ...deepClean(options.attributes) };
|
|
1388
|
+
}
|
|
1389
|
+
if (options.metadata) {
|
|
1390
|
+
this.metadata = { ...this.metadata, ...deepClean(options.metadata) };
|
|
1391
|
+
}
|
|
1392
|
+
}
|
|
1393
|
+
get isValid() {
|
|
1394
|
+
return true;
|
|
1395
|
+
}
|
|
1396
|
+
async export() {
|
|
1397
|
+
return JSON.stringify({
|
|
1398
|
+
spanId: this.id,
|
|
1399
|
+
traceId: this.traceId,
|
|
1400
|
+
startTime: this.startTime,
|
|
1401
|
+
endTime: this.endTime,
|
|
1402
|
+
attributes: this.attributes,
|
|
1403
|
+
metadata: this.metadata
|
|
1404
|
+
});
|
|
1405
|
+
}
|
|
1406
|
+
};
|
|
1407
|
+
function generateSpanId() {
|
|
1408
|
+
const bytes = new Uint8Array(8);
|
|
1409
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
1410
|
+
crypto.getRandomValues(bytes);
|
|
1411
|
+
} else {
|
|
1412
|
+
for (let i = 0; i < 8; i++) {
|
|
1413
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1417
|
+
}
|
|
1418
|
+
function generateTraceId() {
|
|
1419
|
+
const bytes = new Uint8Array(16);
|
|
1420
|
+
if (typeof crypto !== "undefined" && crypto.getRandomValues) {
|
|
1421
|
+
crypto.getRandomValues(bytes);
|
|
1422
|
+
} else {
|
|
1423
|
+
for (let i = 0; i < 16; i++) {
|
|
1424
|
+
bytes[i] = Math.floor(Math.random() * 256);
|
|
1425
|
+
}
|
|
1426
|
+
}
|
|
1427
|
+
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
1428
|
+
}
|
|
1429
|
+
function isValidTraceId(traceId) {
|
|
1430
|
+
return /^[0-9a-f]{1,32}$/i.test(traceId);
|
|
1431
|
+
}
|
|
1432
|
+
function isValidSpanId(spanId) {
|
|
1433
|
+
return /^[0-9a-f]{1,16}$/i.test(spanId);
|
|
1434
|
+
}
|
|
1435
|
+
|
|
1436
|
+
// src/spans/no-op.ts
|
|
1437
|
+
var NoOpSpan = class extends BaseSpan {
|
|
1438
|
+
id;
|
|
1439
|
+
traceId;
|
|
1440
|
+
constructor(options, observabilityInstance) {
|
|
1441
|
+
super(options, observabilityInstance);
|
|
1442
|
+
this.id = "no-op";
|
|
1443
|
+
this.traceId = "no-op-trace";
|
|
1444
|
+
}
|
|
1445
|
+
end(_options) {
|
|
1446
|
+
}
|
|
1447
|
+
error(_options) {
|
|
1448
|
+
}
|
|
1449
|
+
update(_options) {
|
|
1450
|
+
}
|
|
1451
|
+
get isValid() {
|
|
1452
|
+
return false;
|
|
1453
|
+
}
|
|
1454
|
+
};
|
|
1455
|
+
|
|
1456
|
+
// src/instances/base.ts
|
|
1457
|
+
var BaseObservabilityInstance = class extends MastraBase {
|
|
1458
|
+
config;
|
|
1459
|
+
constructor(config) {
|
|
1460
|
+
super({ component: RegisteredLogger.OBSERVABILITY, name: config.serviceName });
|
|
1461
|
+
this.config = {
|
|
1462
|
+
serviceName: config.serviceName,
|
|
1463
|
+
name: config.name,
|
|
1464
|
+
sampling: config.sampling ?? { type: "always" /* ALWAYS */ },
|
|
1465
|
+
exporters: config.exporters ?? [],
|
|
1466
|
+
spanOutputProcessors: config.spanOutputProcessors ?? [],
|
|
1467
|
+
includeInternalSpans: config.includeInternalSpans ?? false,
|
|
1468
|
+
requestContextKeys: config.requestContextKeys ?? []
|
|
1469
|
+
};
|
|
1470
|
+
}
|
|
1471
|
+
/**
|
|
1472
|
+
* Override setLogger to add Observability specific initialization log
|
|
1473
|
+
* and propagate logger to exporters
|
|
1474
|
+
*/
|
|
1475
|
+
__setLogger(logger) {
|
|
1476
|
+
super.__setLogger(logger);
|
|
1477
|
+
this.exporters.forEach((exporter) => {
|
|
1478
|
+
if (typeof exporter.__setLogger === "function") {
|
|
1479
|
+
exporter.__setLogger(logger);
|
|
1480
|
+
}
|
|
1481
|
+
});
|
|
1482
|
+
this.logger.debug(
|
|
1483
|
+
`[Observability] Initialized [service=${this.config.serviceName}] [instance=${this.config.name}] [sampling=${this.config.sampling.type}]`
|
|
1484
|
+
);
|
|
1485
|
+
}
|
|
1486
|
+
// ============================================================================
|
|
1487
|
+
// Protected getters for clean config access
|
|
1488
|
+
// ============================================================================
|
|
1489
|
+
get exporters() {
|
|
1490
|
+
return this.config.exporters || [];
|
|
1491
|
+
}
|
|
1492
|
+
get spanOutputProcessors() {
|
|
1493
|
+
return this.config.spanOutputProcessors || [];
|
|
1494
|
+
}
|
|
1495
|
+
// ============================================================================
|
|
1496
|
+
// Public API - Single type-safe span creation method
|
|
1497
|
+
// ============================================================================
|
|
1498
|
+
/**
|
|
1499
|
+
* Start a new span of a specific SpanType
|
|
1500
|
+
*/
|
|
1501
|
+
startSpan(options) {
|
|
1502
|
+
const { customSamplerOptions, requestContext, metadata, tracingOptions, ...rest } = options;
|
|
1503
|
+
if (!this.shouldSample(customSamplerOptions)) {
|
|
1504
|
+
return new NoOpSpan({ ...rest, metadata }, this);
|
|
1505
|
+
}
|
|
1506
|
+
let traceState;
|
|
1507
|
+
if (options.parent) {
|
|
1508
|
+
traceState = options.parent.traceState;
|
|
1509
|
+
} else {
|
|
1510
|
+
traceState = this.computeTraceState(tracingOptions);
|
|
1511
|
+
}
|
|
1512
|
+
const enrichedMetadata = this.extractMetadataFromRequestContext(requestContext, metadata, traceState);
|
|
1513
|
+
const span = this.createSpan({
|
|
1514
|
+
...rest,
|
|
1515
|
+
metadata: enrichedMetadata,
|
|
1516
|
+
traceState
|
|
1517
|
+
});
|
|
1518
|
+
if (span.isEvent) {
|
|
1519
|
+
this.emitSpanEnded(span);
|
|
1520
|
+
} else {
|
|
1521
|
+
this.wireSpanLifecycle(span);
|
|
1522
|
+
this.emitSpanStarted(span);
|
|
1523
|
+
}
|
|
1524
|
+
return span;
|
|
1525
|
+
}
|
|
1526
|
+
// ============================================================================
|
|
1527
|
+
// Configuration Management
|
|
1528
|
+
// ============================================================================
|
|
1529
|
+
/**
|
|
1530
|
+
* Get current configuration
|
|
1531
|
+
*/
|
|
1532
|
+
getConfig() {
|
|
1533
|
+
return { ...this.config };
|
|
1534
|
+
}
|
|
1535
|
+
// ============================================================================
|
|
1536
|
+
// Plugin Access
|
|
1537
|
+
// ============================================================================
|
|
1538
|
+
/**
|
|
1539
|
+
* Get all exporters
|
|
1540
|
+
*/
|
|
1541
|
+
getExporters() {
|
|
1542
|
+
return [...this.exporters];
|
|
1543
|
+
}
|
|
1544
|
+
/**
|
|
1545
|
+
* Get all span output processors
|
|
1546
|
+
*/
|
|
1547
|
+
getSpanOutputProcessors() {
|
|
1548
|
+
return [...this.spanOutputProcessors];
|
|
1549
|
+
}
|
|
1550
|
+
/**
|
|
1551
|
+
* Get the logger instance (for exporters and other components)
|
|
1552
|
+
*/
|
|
1553
|
+
getLogger() {
|
|
1554
|
+
return this.logger;
|
|
1555
|
+
}
|
|
1556
|
+
// ============================================================================
|
|
1557
|
+
// Span Lifecycle Management
|
|
1558
|
+
// ============================================================================
|
|
1559
|
+
/**
|
|
1560
|
+
* Automatically wires up Observability lifecycle events for any span
|
|
1561
|
+
* This ensures all spans emit events regardless of implementation
|
|
1562
|
+
*/
|
|
1563
|
+
wireSpanLifecycle(span) {
|
|
1564
|
+
if (!this.config.includeInternalSpans && span.isInternal) {
|
|
1565
|
+
return;
|
|
1566
|
+
}
|
|
1567
|
+
const originalEnd = span.end.bind(span);
|
|
1568
|
+
const originalUpdate = span.update.bind(span);
|
|
1569
|
+
span.end = (options) => {
|
|
1570
|
+
if (span.isEvent) {
|
|
1571
|
+
this.logger.warn(`End event is not available on event spans`);
|
|
1572
|
+
return;
|
|
1573
|
+
}
|
|
1574
|
+
originalEnd(options);
|
|
1575
|
+
this.emitSpanEnded(span);
|
|
1576
|
+
};
|
|
1577
|
+
span.update = (options) => {
|
|
1578
|
+
if (span.isEvent) {
|
|
1579
|
+
this.logger.warn(`Update() is not available on event spans`);
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
originalUpdate(options);
|
|
1583
|
+
this.emitSpanUpdated(span);
|
|
1584
|
+
};
|
|
1585
|
+
}
|
|
1586
|
+
// ============================================================================
|
|
1587
|
+
// Utility Methods
|
|
1588
|
+
// ============================================================================
|
|
1589
|
+
/**
|
|
1590
|
+
* Check if a trace should be sampled
|
|
1591
|
+
*/
|
|
1592
|
+
shouldSample(options) {
|
|
1593
|
+
const { sampling } = this.config;
|
|
1594
|
+
switch (sampling.type) {
|
|
1595
|
+
case "always" /* ALWAYS */:
|
|
1596
|
+
return true;
|
|
1597
|
+
case "never" /* NEVER */:
|
|
1598
|
+
return false;
|
|
1599
|
+
case "ratio" /* RATIO */:
|
|
1600
|
+
if (sampling.probability === void 0 || sampling.probability < 0 || sampling.probability > 1) {
|
|
1601
|
+
this.logger.warn(
|
|
1602
|
+
`Invalid sampling probability: ${sampling.probability}. Expected value between 0 and 1. Defaulting to no sampling.`
|
|
1603
|
+
);
|
|
1604
|
+
return false;
|
|
1605
|
+
}
|
|
1606
|
+
return Math.random() < sampling.probability;
|
|
1607
|
+
case "custom" /* CUSTOM */:
|
|
1608
|
+
return sampling.sampler(options);
|
|
1609
|
+
default:
|
|
1610
|
+
throw new Error(`Sampling strategy type not implemented: ${sampling.type}`);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
/**
|
|
1614
|
+
* Compute TraceState for a new trace based on configured and per-request keys
|
|
1615
|
+
*/
|
|
1616
|
+
computeTraceState(tracingOptions) {
|
|
1617
|
+
const configuredKeys = this.config.requestContextKeys ?? [];
|
|
1618
|
+
const additionalKeys = tracingOptions?.requestContextKeys ?? [];
|
|
1619
|
+
const allKeys = [...configuredKeys, ...additionalKeys];
|
|
1620
|
+
if (allKeys.length === 0) {
|
|
1621
|
+
return void 0;
|
|
1622
|
+
}
|
|
1623
|
+
return {
|
|
1624
|
+
requestContextKeys: allKeys
|
|
1625
|
+
};
|
|
1626
|
+
}
|
|
1627
|
+
/**
|
|
1628
|
+
* Extract metadata from RequestContext using TraceState
|
|
1629
|
+
*/
|
|
1630
|
+
extractMetadataFromRequestContext(requestContext, explicitMetadata, traceState) {
|
|
1631
|
+
if (!requestContext || !traceState || traceState.requestContextKeys.length === 0) {
|
|
1632
|
+
return explicitMetadata;
|
|
1633
|
+
}
|
|
1634
|
+
const extracted = this.extractKeys(requestContext, traceState.requestContextKeys);
|
|
1635
|
+
if (Object.keys(extracted).length === 0 && !explicitMetadata) {
|
|
1636
|
+
return void 0;
|
|
1637
|
+
}
|
|
1638
|
+
return {
|
|
1639
|
+
...extracted,
|
|
1640
|
+
...explicitMetadata
|
|
1641
|
+
// Explicit metadata always wins
|
|
1642
|
+
};
|
|
1643
|
+
}
|
|
1644
|
+
/**
|
|
1645
|
+
* Extract specific keys from RequestContext
|
|
1646
|
+
*/
|
|
1647
|
+
extractKeys(requestContext, keys) {
|
|
1648
|
+
const result = {};
|
|
1649
|
+
for (const key of keys) {
|
|
1650
|
+
const parts = key.split(".");
|
|
1651
|
+
const rootKey = parts[0];
|
|
1652
|
+
const value = requestContext.get(rootKey);
|
|
1653
|
+
if (value !== void 0) {
|
|
1654
|
+
if (parts.length > 1) {
|
|
1655
|
+
const nestedPath = parts.slice(1).join(".");
|
|
1656
|
+
const nestedValue = getNestedValue(value, nestedPath);
|
|
1657
|
+
if (nestedValue !== void 0) {
|
|
1658
|
+
setNestedValue(result, key, nestedValue);
|
|
1659
|
+
}
|
|
1660
|
+
} else {
|
|
1661
|
+
setNestedValue(result, key, value);
|
|
1662
|
+
}
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
return result;
|
|
1666
|
+
}
|
|
1667
|
+
/**
|
|
1668
|
+
* Process a span through all output processors
|
|
1669
|
+
*/
|
|
1670
|
+
processSpan(span) {
|
|
1671
|
+
for (const processor of this.spanOutputProcessors) {
|
|
1672
|
+
if (!span) {
|
|
1673
|
+
break;
|
|
1674
|
+
}
|
|
1675
|
+
try {
|
|
1676
|
+
span = processor.process(span);
|
|
1677
|
+
} catch (error) {
|
|
1678
|
+
this.logger.error(`[Observability] Processor error [name=${processor.name}]`, error);
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1681
|
+
return span;
|
|
1682
|
+
}
|
|
1683
|
+
// ============================================================================
|
|
1684
|
+
// Event-driven Export Methods
|
|
1685
|
+
// ============================================================================
|
|
1686
|
+
getSpanForExport(span) {
|
|
1687
|
+
if (!span.isValid) return void 0;
|
|
1688
|
+
if (span.isInternal && !this.config.includeInternalSpans) return void 0;
|
|
1689
|
+
const processedSpan = this.processSpan(span);
|
|
1690
|
+
return processedSpan?.exportSpan(this.config.includeInternalSpans);
|
|
1691
|
+
}
|
|
1692
|
+
/**
|
|
1693
|
+
* Emit a span started event
|
|
1694
|
+
*/
|
|
1695
|
+
emitSpanStarted(span) {
|
|
1696
|
+
const exportedSpan = this.getSpanForExport(span);
|
|
1697
|
+
if (exportedSpan) {
|
|
1698
|
+
this.exportTracingEvent({ type: TracingEventType.SPAN_STARTED, exportedSpan }).catch((error) => {
|
|
1699
|
+
this.logger.error("[Observability] Failed to export span_started event", error);
|
|
1700
|
+
});
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
/**
|
|
1704
|
+
* Emit a span ended event (called automatically when spans end)
|
|
1705
|
+
*/
|
|
1706
|
+
emitSpanEnded(span) {
|
|
1707
|
+
const exportedSpan = this.getSpanForExport(span);
|
|
1708
|
+
if (exportedSpan) {
|
|
1709
|
+
this.exportTracingEvent({ type: TracingEventType.SPAN_ENDED, exportedSpan }).catch((error) => {
|
|
1710
|
+
this.logger.error("[Observability] Failed to export span_ended event", error);
|
|
1711
|
+
});
|
|
1712
|
+
}
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Emit a span updated event
|
|
1716
|
+
*/
|
|
1717
|
+
emitSpanUpdated(span) {
|
|
1718
|
+
const exportedSpan = this.getSpanForExport(span);
|
|
1719
|
+
if (exportedSpan) {
|
|
1720
|
+
this.exportTracingEvent({ type: TracingEventType.SPAN_UPDATED, exportedSpan }).catch((error) => {
|
|
1721
|
+
this.logger.error("[Observability] Failed to export span_updated event", error);
|
|
1722
|
+
});
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
/**
|
|
1726
|
+
* Export tracing event through all exporters (realtime mode)
|
|
1727
|
+
*/
|
|
1728
|
+
async exportTracingEvent(event) {
|
|
1729
|
+
const exportPromises = this.exporters.map(async (exporter) => {
|
|
1730
|
+
try {
|
|
1731
|
+
if (exporter.exportTracingEvent) {
|
|
1732
|
+
await exporter.exportTracingEvent(event);
|
|
1733
|
+
this.logger.debug(`[Observability] Event exported [exporter=${exporter.name}] [type=${event.type}]`);
|
|
1734
|
+
}
|
|
1735
|
+
} catch (error) {
|
|
1736
|
+
this.logger.error(`[Observability] Export error [exporter=${exporter.name}]`, error);
|
|
1737
|
+
}
|
|
1738
|
+
});
|
|
1739
|
+
await Promise.allSettled(exportPromises);
|
|
1740
|
+
}
|
|
1741
|
+
// ============================================================================
|
|
1742
|
+
// Lifecycle Management
|
|
1743
|
+
// ============================================================================
|
|
1744
|
+
/**
|
|
1745
|
+
* Initialize Observability (called by Mastra during component registration)
|
|
1746
|
+
*/
|
|
1747
|
+
init() {
|
|
1748
|
+
this.logger.debug(`[Observability] Initialization started [name=${this.name}]`);
|
|
1749
|
+
this.logger.info(`[Observability] Initialized successfully [name=${this.name}]`);
|
|
1750
|
+
}
|
|
1751
|
+
/**
|
|
1752
|
+
* Shutdown Observability and clean up resources
|
|
1753
|
+
*/
|
|
1754
|
+
async shutdown() {
|
|
1755
|
+
this.logger.debug(`[Observability] Shutdown started [name=${this.name}]`);
|
|
1756
|
+
const shutdownPromises = [
|
|
1757
|
+
...this.exporters.map((e) => e.shutdown()),
|
|
1758
|
+
...this.spanOutputProcessors.map((p) => p.shutdown())
|
|
1759
|
+
];
|
|
1760
|
+
await Promise.allSettled(shutdownPromises);
|
|
1761
|
+
this.logger.info(`[Observability] Shutdown completed [name=${this.name}]`);
|
|
1762
|
+
}
|
|
1763
|
+
};
|
|
1764
|
+
|
|
1765
|
+
// src/instances/default.ts
|
|
1766
|
+
var DefaultObservabilityInstance = class extends BaseObservabilityInstance {
|
|
1767
|
+
constructor(config) {
|
|
1768
|
+
super(config);
|
|
1769
|
+
}
|
|
1770
|
+
createSpan(options) {
|
|
1771
|
+
return new DefaultSpan(options, this);
|
|
1772
|
+
}
|
|
1773
|
+
};
|
|
1774
|
+
|
|
1775
|
+
// src/registry.ts
|
|
1776
|
+
var ObservabilityRegistry = class {
|
|
1777
|
+
#instances = /* @__PURE__ */ new Map();
|
|
1778
|
+
#defaultInstance;
|
|
1779
|
+
#configSelector;
|
|
1780
|
+
/**
|
|
1781
|
+
* Register a tracing instance
|
|
1782
|
+
*/
|
|
1783
|
+
register(name, instance, isDefault = false) {
|
|
1784
|
+
if (this.#instances.has(name)) {
|
|
1785
|
+
throw new Error(`Tracing instance '${name}' already registered`);
|
|
1786
|
+
}
|
|
1787
|
+
this.#instances.set(name, instance);
|
|
1788
|
+
if (isDefault || !this.#defaultInstance) {
|
|
1789
|
+
this.#defaultInstance = instance;
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
/**
|
|
1793
|
+
* Get a tracing instance by name
|
|
1794
|
+
*/
|
|
1795
|
+
get(name) {
|
|
1796
|
+
return this.#instances.get(name);
|
|
1797
|
+
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Get the default tracing instance
|
|
1800
|
+
*/
|
|
1801
|
+
getDefault() {
|
|
1802
|
+
return this.#defaultInstance;
|
|
1803
|
+
}
|
|
1804
|
+
/**
|
|
1805
|
+
* Set the tracing selector function
|
|
1806
|
+
*/
|
|
1807
|
+
setSelector(selector) {
|
|
1808
|
+
this.#configSelector = selector;
|
|
1809
|
+
}
|
|
1810
|
+
/**
|
|
1811
|
+
* Get the selected tracing instance based on context
|
|
1812
|
+
*/
|
|
1813
|
+
getSelected(options) {
|
|
1814
|
+
if (this.#configSelector) {
|
|
1815
|
+
const selected = this.#configSelector(options, this.#instances);
|
|
1816
|
+
if (selected && this.#instances.has(selected)) {
|
|
1817
|
+
return this.#instances.get(selected);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
return this.#defaultInstance;
|
|
1821
|
+
}
|
|
1822
|
+
/**
|
|
1823
|
+
* Unregister a tracing instance
|
|
1824
|
+
*/
|
|
1825
|
+
unregister(name) {
|
|
1826
|
+
const instance = this.#instances.get(name);
|
|
1827
|
+
const deleted = this.#instances.delete(name);
|
|
1828
|
+
if (deleted && instance === this.#defaultInstance) {
|
|
1829
|
+
const next = this.#instances.values().next();
|
|
1830
|
+
this.#defaultInstance = next.done ? void 0 : next.value;
|
|
1831
|
+
}
|
|
1832
|
+
return deleted;
|
|
1833
|
+
}
|
|
1834
|
+
/**
|
|
1835
|
+
* Shutdown all instances and clear the registry
|
|
1836
|
+
*/
|
|
1837
|
+
async shutdown() {
|
|
1838
|
+
const shutdownPromises = Array.from(this.#instances.values()).map((instance) => instance.shutdown());
|
|
1839
|
+
await Promise.allSettled(shutdownPromises);
|
|
1840
|
+
this.#instances.clear();
|
|
1841
|
+
this.#instances.clear();
|
|
1842
|
+
this.#defaultInstance = void 0;
|
|
1843
|
+
this.#configSelector = void 0;
|
|
1844
|
+
}
|
|
1845
|
+
/**
|
|
1846
|
+
* Clear all instances without shutdown
|
|
1847
|
+
*/
|
|
1848
|
+
clear() {
|
|
1849
|
+
this.#instances.clear();
|
|
1850
|
+
this.#defaultInstance = void 0;
|
|
1851
|
+
this.#configSelector = void 0;
|
|
1852
|
+
}
|
|
1853
|
+
/**
|
|
1854
|
+
* list all registered instances
|
|
1855
|
+
*/
|
|
1856
|
+
list() {
|
|
1857
|
+
return new Map(this.#instances);
|
|
1858
|
+
}
|
|
1859
|
+
};
|
|
1860
|
+
|
|
1861
|
+
// src/span_processors/sensitive-data-filter.ts
|
|
1862
|
+
var SensitiveDataFilter = class {
|
|
1863
|
+
name = "sensitive-data-filter";
|
|
1864
|
+
sensitiveFields;
|
|
1865
|
+
redactionToken;
|
|
1866
|
+
redactionStyle;
|
|
1867
|
+
constructor(options = {}) {
|
|
1868
|
+
this.sensitiveFields = (options.sensitiveFields || [
|
|
1869
|
+
"password",
|
|
1870
|
+
"token",
|
|
1871
|
+
"secret",
|
|
1872
|
+
"key",
|
|
1873
|
+
"apikey",
|
|
1874
|
+
"auth",
|
|
1875
|
+
"authorization",
|
|
1876
|
+
"bearer",
|
|
1877
|
+
"bearertoken",
|
|
1878
|
+
"jwt",
|
|
1879
|
+
"credential",
|
|
1880
|
+
"clientsecret",
|
|
1881
|
+
"privatekey",
|
|
1882
|
+
"refresh",
|
|
1883
|
+
"ssn"
|
|
1884
|
+
]).map((f) => this.normalizeKey(f));
|
|
1885
|
+
this.redactionToken = options.redactionToken ?? "[REDACTED]";
|
|
1886
|
+
this.redactionStyle = options.redactionStyle ?? "full";
|
|
1887
|
+
}
|
|
1888
|
+
/**
|
|
1889
|
+
* Process a span by filtering sensitive data across its key fields.
|
|
1890
|
+
* Fields processed: attributes, metadata, input, output, errorInfo.
|
|
1891
|
+
*
|
|
1892
|
+
* @param span - The input span to filter
|
|
1893
|
+
* @returns A new span with sensitive values redacted
|
|
1894
|
+
*/
|
|
1895
|
+
process(span) {
|
|
1896
|
+
span.attributes = this.tryFilter(span.attributes);
|
|
1897
|
+
span.metadata = this.tryFilter(span.metadata);
|
|
1898
|
+
span.input = this.tryFilter(span.input);
|
|
1899
|
+
span.output = this.tryFilter(span.output);
|
|
1900
|
+
span.errorInfo = this.tryFilter(span.errorInfo);
|
|
1901
|
+
return span;
|
|
1902
|
+
}
|
|
1903
|
+
/**
|
|
1904
|
+
* Recursively filter objects/arrays for sensitive keys.
|
|
1905
|
+
* Handles circular references by replacing with a marker.
|
|
1906
|
+
*/
|
|
1907
|
+
deepFilter(obj, seen = /* @__PURE__ */ new WeakSet()) {
|
|
1908
|
+
if (obj === null || typeof obj !== "object") {
|
|
1909
|
+
return obj;
|
|
1910
|
+
}
|
|
1911
|
+
if (seen.has(obj)) {
|
|
1912
|
+
return "[Circular Reference]";
|
|
1913
|
+
}
|
|
1914
|
+
seen.add(obj);
|
|
1915
|
+
if (Array.isArray(obj)) {
|
|
1916
|
+
return obj.map((item) => this.deepFilter(item, seen));
|
|
1917
|
+
}
|
|
1918
|
+
const filtered = {};
|
|
1919
|
+
for (const key of Object.keys(obj)) {
|
|
1920
|
+
const normKey = this.normalizeKey(key);
|
|
1921
|
+
if (this.isSensitive(normKey)) {
|
|
1922
|
+
if (obj[key] && typeof obj[key] === "object") {
|
|
1923
|
+
filtered[key] = this.deepFilter(obj[key], seen);
|
|
1924
|
+
} else {
|
|
1925
|
+
filtered[key] = this.redactValue(obj[key]);
|
|
1926
|
+
}
|
|
1927
|
+
} else {
|
|
1928
|
+
filtered[key] = this.deepFilter(obj[key], seen);
|
|
1929
|
+
}
|
|
1930
|
+
}
|
|
1931
|
+
return filtered;
|
|
1932
|
+
}
|
|
1933
|
+
tryFilter(value) {
|
|
1934
|
+
try {
|
|
1935
|
+
return this.deepFilter(value);
|
|
1936
|
+
} catch {
|
|
1937
|
+
return { error: { processor: this.name } };
|
|
1938
|
+
}
|
|
1939
|
+
}
|
|
1940
|
+
/**
|
|
1941
|
+
* Normalize keys by lowercasing and stripping non-alphanumeric characters.
|
|
1942
|
+
* Ensures consistent matching for variants like "api-key", "api_key", "Api Key".
|
|
1943
|
+
*/
|
|
1944
|
+
normalizeKey(key) {
|
|
1945
|
+
return key.toLowerCase().replace(/[^a-z0-9]/g, "");
|
|
1946
|
+
}
|
|
1947
|
+
/**
|
|
1948
|
+
* Check whether a normalized key exactly matches any sensitive field.
|
|
1949
|
+
* Both key and sensitive fields are normalized by removing all non-alphanumeric
|
|
1950
|
+
* characters and converting to lowercase before comparison.
|
|
1951
|
+
*
|
|
1952
|
+
* Examples:
|
|
1953
|
+
* - "api_key", "api-key", "ApiKey" all normalize to "apikey" → MATCHES "apikey"
|
|
1954
|
+
* - "promptTokens", "prompt_tokens" normalize to "prompttokens" → DOES NOT MATCH "token"
|
|
1955
|
+
*/
|
|
1956
|
+
isSensitive(normalizedKey) {
|
|
1957
|
+
return this.sensitiveFields.some((sensitiveField) => {
|
|
1958
|
+
return normalizedKey === sensitiveField;
|
|
1959
|
+
});
|
|
1960
|
+
}
|
|
1961
|
+
/**
|
|
1962
|
+
* Redact a sensitive value.
|
|
1963
|
+
* - Full style: replaces with a fixed token.
|
|
1964
|
+
* - Partial style: shows 3 chars at start and end, hides the middle.
|
|
1965
|
+
*
|
|
1966
|
+
* Non-string values are converted to strings before partial redaction.
|
|
1967
|
+
*/
|
|
1968
|
+
redactValue(value) {
|
|
1969
|
+
if (this.redactionStyle === "full") {
|
|
1970
|
+
return this.redactionToken;
|
|
1971
|
+
}
|
|
1972
|
+
const str = String(value);
|
|
1973
|
+
const len = str.length;
|
|
1974
|
+
if (len <= 6) {
|
|
1975
|
+
return this.redactionToken;
|
|
1976
|
+
}
|
|
1977
|
+
return str.slice(0, 3) + "\u2026" + str.slice(len - 3);
|
|
1978
|
+
}
|
|
1979
|
+
async shutdown() {
|
|
1980
|
+
}
|
|
1981
|
+
};
|
|
1982
|
+
|
|
1983
|
+
// src/default.ts
|
|
1984
|
+
function isInstance(obj) {
|
|
1985
|
+
return obj instanceof BaseObservabilityInstance;
|
|
1986
|
+
}
|
|
1987
|
+
var Observability = class extends MastraBase {
|
|
1988
|
+
#registry = new ObservabilityRegistry();
|
|
1989
|
+
constructor(config) {
|
|
1990
|
+
super({
|
|
1991
|
+
component: RegisteredLogger.OBSERVABILITY,
|
|
1992
|
+
name: "Observability"
|
|
1993
|
+
});
|
|
1994
|
+
if (config === void 0) {
|
|
1995
|
+
config = {};
|
|
1996
|
+
}
|
|
1997
|
+
if (config.default?.enabled && config.configs?.["default"]) {
|
|
1998
|
+
throw new Error(
|
|
1999
|
+
"Cannot use 'default' as a custom config name when default tracing is enabled. Please rename your custom config to avoid conflicts."
|
|
2000
|
+
);
|
|
2001
|
+
}
|
|
2002
|
+
if (config.default?.enabled) {
|
|
2003
|
+
const defaultInstance = new DefaultObservabilityInstance({
|
|
2004
|
+
serviceName: "mastra",
|
|
2005
|
+
name: "default",
|
|
2006
|
+
sampling: { type: "always" /* ALWAYS */ },
|
|
2007
|
+
exporters: [new DefaultExporter(), new CloudExporter()],
|
|
2008
|
+
spanOutputProcessors: [new SensitiveDataFilter()]
|
|
2009
|
+
});
|
|
2010
|
+
this.#registry.register("default", defaultInstance, true);
|
|
2011
|
+
}
|
|
2012
|
+
if (config.configs) {
|
|
2013
|
+
const instances = Object.entries(config.configs);
|
|
2014
|
+
instances.forEach(([name, tracingDef], index) => {
|
|
2015
|
+
const instance = isInstance(tracingDef) ? tracingDef : new DefaultObservabilityInstance({ ...tracingDef, name });
|
|
2016
|
+
const isDefault = !config.default?.enabled && index === 0;
|
|
2017
|
+
this.#registry.register(name, instance, isDefault);
|
|
2018
|
+
});
|
|
2019
|
+
}
|
|
2020
|
+
if (config.configSelector) {
|
|
2021
|
+
this.#registry.setSelector(config.configSelector);
|
|
2022
|
+
}
|
|
2023
|
+
}
|
|
2024
|
+
setMastraContext(options) {
|
|
2025
|
+
const instances = this.listInstances();
|
|
2026
|
+
const { mastra } = options;
|
|
2027
|
+
instances.forEach((instance) => {
|
|
2028
|
+
const config = instance.getConfig();
|
|
2029
|
+
const exporters = instance.getExporters();
|
|
2030
|
+
exporters.forEach((exporter) => {
|
|
2031
|
+
if ("init" in exporter && typeof exporter.init === "function") {
|
|
2032
|
+
try {
|
|
2033
|
+
exporter.init({ mastra, config });
|
|
2034
|
+
} catch (error) {
|
|
2035
|
+
this.logger?.warn("Failed to initialize observability exporter", {
|
|
2036
|
+
exporterName: exporter.name,
|
|
2037
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2038
|
+
});
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
});
|
|
2042
|
+
});
|
|
2043
|
+
}
|
|
2044
|
+
setLogger(options) {
|
|
2045
|
+
super.__setLogger(options.logger);
|
|
2046
|
+
this.listInstances().forEach((instance) => {
|
|
2047
|
+
instance.__setLogger(options.logger);
|
|
2048
|
+
});
|
|
2049
|
+
}
|
|
2050
|
+
getSelectedInstance(options) {
|
|
2051
|
+
return this.#registry.getSelected(options);
|
|
2052
|
+
}
|
|
2053
|
+
/**
|
|
2054
|
+
* Registry management methods
|
|
2055
|
+
*/
|
|
2056
|
+
registerInstance(name, instance, isDefault = false) {
|
|
2057
|
+
this.#registry.register(name, instance, isDefault);
|
|
2058
|
+
}
|
|
2059
|
+
getInstance(name) {
|
|
2060
|
+
return this.#registry.get(name);
|
|
2061
|
+
}
|
|
2062
|
+
getDefaultInstance() {
|
|
2063
|
+
return this.#registry.getDefault();
|
|
2064
|
+
}
|
|
2065
|
+
listInstances() {
|
|
2066
|
+
return this.#registry.list();
|
|
2067
|
+
}
|
|
2068
|
+
unregisterInstance(name) {
|
|
2069
|
+
return this.#registry.unregister(name);
|
|
2070
|
+
}
|
|
2071
|
+
hasInstance(name) {
|
|
2072
|
+
return !!this.#registry.get(name);
|
|
2073
|
+
}
|
|
2074
|
+
setConfigSelector(selector) {
|
|
2075
|
+
this.#registry.setSelector(selector);
|
|
2076
|
+
}
|
|
2077
|
+
clear() {
|
|
2078
|
+
this.#registry.clear();
|
|
2079
|
+
}
|
|
2080
|
+
async shutdown() {
|
|
2081
|
+
await this.#registry.shutdown();
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
|
|
2085
|
+
export { BaseExporter, BaseObservabilityInstance, BaseSpan, CloudExporter, ConsoleExporter, DefaultExporter, DefaultObservabilityInstance, DefaultSpan, ModelSpanTracker, NoOpSpan, Observability, SamplingStrategyType, SensitiveDataFilter, deepClean };
|
|
2
2086
|
//# sourceMappingURL=index.js.map
|
|
3
2087
|
//# sourceMappingURL=index.js.map
|