@mastra/observability 0.0.0-remove-ai-peer-dep-from-evals-20260105220639 → 0.0.0-salesman-20260127182805
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 +469 -3
- package/README.md +34 -79
- package/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/default.d.ts.map +1 -1
- package/dist/exporters/base.d.ts +55 -5
- package/dist/exporters/base.d.ts.map +1 -1
- package/dist/exporters/cloud.d.ts +8 -2
- package/dist/exporters/cloud.d.ts.map +1 -1
- package/dist/exporters/default.d.ts +8 -2
- package/dist/exporters/default.d.ts.map +1 -1
- package/dist/exporters/index.d.ts +3 -2
- package/dist/exporters/index.d.ts.map +1 -1
- package/dist/exporters/span-formatters.d.ts +35 -0
- package/dist/exporters/span-formatters.d.ts.map +1 -0
- package/dist/exporters/tracking.d.ts +470 -0
- package/dist/exporters/tracking.d.ts.map +1 -0
- package/dist/index.cjs +1243 -80
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1241 -81
- package/dist/index.js.map +1 -1
- package/dist/instances/base.d.ts +33 -1
- package/dist/instances/base.d.ts.map +1 -1
- package/dist/model-tracing.d.ts.map +1 -1
- package/dist/spans/base.d.ts.map +1 -1
- package/dist/spans/default.d.ts.map +1 -1
- package/package.json +9 -9
package/dist/index.cjs
CHANGED
|
@@ -4163,12 +4163,19 @@ var observabilityRegistryConfigSchema = external_exports.object({
|
|
|
4163
4163
|
var BaseExporter = class {
|
|
4164
4164
|
/** Mastra logger instance */
|
|
4165
4165
|
logger;
|
|
4166
|
+
/** Base configuration (accessible by subclasses) */
|
|
4167
|
+
baseConfig;
|
|
4166
4168
|
/** Whether this exporter is disabled */
|
|
4167
|
-
|
|
4169
|
+
#disabled = false;
|
|
4170
|
+
/** Public getter for disabled state */
|
|
4171
|
+
get isDisabled() {
|
|
4172
|
+
return this.#disabled;
|
|
4173
|
+
}
|
|
4168
4174
|
/**
|
|
4169
4175
|
* Initialize the base exporter with logger
|
|
4170
4176
|
*/
|
|
4171
4177
|
constructor(config = {}) {
|
|
4178
|
+
this.baseConfig = config;
|
|
4172
4179
|
const logLevel = this.resolveLogLevel(config.logLevel);
|
|
4173
4180
|
this.logger = config.logger ?? new logger.ConsoleLogger({ level: logLevel, name: this.constructor.name });
|
|
4174
4181
|
}
|
|
@@ -4203,20 +4210,62 @@ var BaseExporter = class {
|
|
|
4203
4210
|
* @param reason - Reason why the exporter is disabled
|
|
4204
4211
|
*/
|
|
4205
4212
|
setDisabled(reason) {
|
|
4206
|
-
this
|
|
4213
|
+
this.#disabled = true;
|
|
4207
4214
|
this.logger.warn(`${this.name} disabled: ${reason}`);
|
|
4208
4215
|
}
|
|
4216
|
+
/**
|
|
4217
|
+
* Apply the customSpanFormatter if configured.
|
|
4218
|
+
* This is called automatically by exportTracingEvent before _exportTracingEvent.
|
|
4219
|
+
*
|
|
4220
|
+
* Supports both synchronous and asynchronous formatters. If the formatter
|
|
4221
|
+
* returns a Promise, it will be awaited.
|
|
4222
|
+
*
|
|
4223
|
+
* @param event - The incoming tracing event
|
|
4224
|
+
* @returns The (possibly modified) event to process
|
|
4225
|
+
*/
|
|
4226
|
+
async applySpanFormatter(event) {
|
|
4227
|
+
if (this.baseConfig.customSpanFormatter) {
|
|
4228
|
+
try {
|
|
4229
|
+
const formattedSpan = await this.baseConfig.customSpanFormatter(event.exportedSpan);
|
|
4230
|
+
return {
|
|
4231
|
+
...event,
|
|
4232
|
+
exportedSpan: formattedSpan
|
|
4233
|
+
};
|
|
4234
|
+
} catch (error) {
|
|
4235
|
+
this.logger.error(`${this.name}: Error in customSpanFormatter`, {
|
|
4236
|
+
error,
|
|
4237
|
+
spanId: event.exportedSpan.id,
|
|
4238
|
+
traceId: event.exportedSpan.traceId
|
|
4239
|
+
});
|
|
4240
|
+
}
|
|
4241
|
+
}
|
|
4242
|
+
return event;
|
|
4243
|
+
}
|
|
4209
4244
|
/**
|
|
4210
4245
|
* Export a tracing event
|
|
4211
4246
|
*
|
|
4212
|
-
* This method checks if the exporter is disabled
|
|
4213
|
-
*
|
|
4247
|
+
* This method checks if the exporter is disabled, applies the customSpanFormatter,
|
|
4248
|
+
* then calls _exportTracingEvent.
|
|
4249
|
+
* Subclasses should implement _exportTracingEvent instead of overriding this method.
|
|
4214
4250
|
*/
|
|
4215
4251
|
async exportTracingEvent(event) {
|
|
4216
4252
|
if (this.isDisabled) {
|
|
4217
4253
|
return;
|
|
4218
4254
|
}
|
|
4219
|
-
await this.
|
|
4255
|
+
const processedEvent = await this.applySpanFormatter(event);
|
|
4256
|
+
await this._exportTracingEvent(processedEvent);
|
|
4257
|
+
}
|
|
4258
|
+
/**
|
|
4259
|
+
* Force flush any buffered/queued spans without shutting down the exporter.
|
|
4260
|
+
*
|
|
4261
|
+
* This is useful in serverless environments where you need to ensure spans
|
|
4262
|
+
* are exported before the runtime instance is terminated, while keeping
|
|
4263
|
+
* the exporter active for future requests.
|
|
4264
|
+
*
|
|
4265
|
+
* Default implementation is a no-op. Override to add flush logic.
|
|
4266
|
+
*/
|
|
4267
|
+
async flush() {
|
|
4268
|
+
this.logger.debug(`${this.name} flush called (no-op in base class)`);
|
|
4220
4269
|
}
|
|
4221
4270
|
/**
|
|
4222
4271
|
* Shutdown the exporter and clean up resources
|
|
@@ -4227,21 +4276,990 @@ var BaseExporter = class {
|
|
|
4227
4276
|
this.logger.info(`${this.name} shutdown complete`);
|
|
4228
4277
|
}
|
|
4229
4278
|
};
|
|
4279
|
+
var TraceData = class {
|
|
4280
|
+
/** The vendor-specific root/trace object */
|
|
4281
|
+
#rootSpan;
|
|
4282
|
+
/** The span ID of the root span */
|
|
4283
|
+
#rootSpanId;
|
|
4284
|
+
/** Whether a span with isRootSpan=true has been successfully processed */
|
|
4285
|
+
#rootSpanProcessed;
|
|
4286
|
+
/** Maps eventId to vendor-specific event objects */
|
|
4287
|
+
#events;
|
|
4288
|
+
/** Maps spanId to vendor-specific span objects */
|
|
4289
|
+
#spans;
|
|
4290
|
+
/** Maps spanId to parentSpanId, representing the span hierarchy */
|
|
4291
|
+
#tree;
|
|
4292
|
+
/** Set of span IDs that have started but not yet ended */
|
|
4293
|
+
#activeSpanIds;
|
|
4294
|
+
/** Maps spanId to vendor-specific metadata */
|
|
4295
|
+
#metadata;
|
|
4296
|
+
/** Arbitrary key-value storage for per-trace data */
|
|
4297
|
+
#extraData;
|
|
4298
|
+
/** Events waiting for the root span to be processed */
|
|
4299
|
+
#waitingForRoot;
|
|
4300
|
+
/** Events waiting for specific parent spans, keyed by parentSpanId */
|
|
4301
|
+
#waitingForParent;
|
|
4302
|
+
/** When this trace data was created, used for cap enforcement */
|
|
4303
|
+
createdAt;
|
|
4304
|
+
constructor() {
|
|
4305
|
+
this.#events = /* @__PURE__ */ new Map();
|
|
4306
|
+
this.#spans = /* @__PURE__ */ new Map();
|
|
4307
|
+
this.#activeSpanIds = /* @__PURE__ */ new Set();
|
|
4308
|
+
this.#tree = /* @__PURE__ */ new Map();
|
|
4309
|
+
this.#metadata = /* @__PURE__ */ new Map();
|
|
4310
|
+
this.#extraData = /* @__PURE__ */ new Map();
|
|
4311
|
+
this.#rootSpanProcessed = false;
|
|
4312
|
+
this.#waitingForRoot = [];
|
|
4313
|
+
this.#waitingForParent = /* @__PURE__ */ new Map();
|
|
4314
|
+
this.createdAt = /* @__PURE__ */ new Date();
|
|
4315
|
+
}
|
|
4316
|
+
/**
|
|
4317
|
+
* Check if this trace has a root span registered.
|
|
4318
|
+
* @returns True if addRoot() has been called
|
|
4319
|
+
*/
|
|
4320
|
+
hasRoot() {
|
|
4321
|
+
return !!this.#rootSpanId;
|
|
4322
|
+
}
|
|
4323
|
+
/**
|
|
4324
|
+
* Register the root span for this trace.
|
|
4325
|
+
* @param args.rootId - The span ID of the root span
|
|
4326
|
+
* @param args.rootData - The vendor-specific root object
|
|
4327
|
+
*/
|
|
4328
|
+
addRoot(args) {
|
|
4329
|
+
this.#rootSpanId = args.rootId;
|
|
4330
|
+
this.#rootSpan = args.rootData;
|
|
4331
|
+
this.#rootSpanProcessed = true;
|
|
4332
|
+
}
|
|
4333
|
+
/**
|
|
4334
|
+
* Get the vendor-specific root object.
|
|
4335
|
+
* @returns The root object, or undefined if not yet set
|
|
4336
|
+
*/
|
|
4337
|
+
getRoot() {
|
|
4338
|
+
return this.#rootSpan;
|
|
4339
|
+
}
|
|
4340
|
+
/**
|
|
4341
|
+
* Check if a span with isRootSpan=true has been successfully processed.
|
|
4342
|
+
* Set via addRoot() or markRootSpanProcessed().
|
|
4343
|
+
* @returns True if the root span has been processed
|
|
4344
|
+
*/
|
|
4345
|
+
isRootProcessed() {
|
|
4346
|
+
return this.#rootSpanProcessed;
|
|
4347
|
+
}
|
|
4348
|
+
/**
|
|
4349
|
+
* Mark that the root span has been processed.
|
|
4350
|
+
* Used by exporters with skipBuildRootTask=true where root goes through _buildSpan
|
|
4351
|
+
* instead of _buildRoot.
|
|
4352
|
+
*/
|
|
4353
|
+
markRootSpanProcessed() {
|
|
4354
|
+
this.#rootSpanProcessed = true;
|
|
4355
|
+
}
|
|
4356
|
+
/**
|
|
4357
|
+
* Store an arbitrary value in per-trace storage.
|
|
4358
|
+
* @param key - Storage key
|
|
4359
|
+
* @param value - Value to store
|
|
4360
|
+
*/
|
|
4361
|
+
setExtraValue(key, value) {
|
|
4362
|
+
this.#extraData.set(key, value);
|
|
4363
|
+
}
|
|
4364
|
+
/**
|
|
4365
|
+
* Check if a key exists in per-trace storage.
|
|
4366
|
+
* @param key - Storage key
|
|
4367
|
+
* @returns True if the key exists
|
|
4368
|
+
*/
|
|
4369
|
+
hasExtraValue(key) {
|
|
4370
|
+
return this.#extraData.has(key);
|
|
4371
|
+
}
|
|
4372
|
+
/**
|
|
4373
|
+
* Get a value from per-trace storage.
|
|
4374
|
+
* @param key - Storage key
|
|
4375
|
+
* @returns The stored value, or undefined if not found
|
|
4376
|
+
*/
|
|
4377
|
+
getExtraValue(key) {
|
|
4378
|
+
return this.#extraData.get(key);
|
|
4379
|
+
}
|
|
4380
|
+
// ============================================================================
|
|
4381
|
+
// Early Queue Methods
|
|
4382
|
+
// ============================================================================
|
|
4383
|
+
/**
|
|
4384
|
+
* Add an event to the waiting queue.
|
|
4385
|
+
* @param args.event - The tracing event to queue
|
|
4386
|
+
* @param args.waitingFor - 'root' or a specific parentSpanId
|
|
4387
|
+
* @param args.attempts - Optional: preserve attempts count when re-queuing
|
|
4388
|
+
* @param args.queuedAt - Optional: preserve original queue time when re-queuing
|
|
4389
|
+
*/
|
|
4390
|
+
addToWaitingQueue(args) {
|
|
4391
|
+
const queuedEvent = {
|
|
4392
|
+
event: args.event,
|
|
4393
|
+
waitingFor: args.waitingFor,
|
|
4394
|
+
attempts: args.attempts ?? 0,
|
|
4395
|
+
queuedAt: args.queuedAt ?? /* @__PURE__ */ new Date()
|
|
4396
|
+
};
|
|
4397
|
+
if (args.waitingFor === "root") {
|
|
4398
|
+
this.#waitingForRoot.push(queuedEvent);
|
|
4399
|
+
} else {
|
|
4400
|
+
const queue = this.#waitingForParent.get(args.waitingFor) ?? [];
|
|
4401
|
+
queue.push(queuedEvent);
|
|
4402
|
+
this.#waitingForParent.set(args.waitingFor, queue);
|
|
4403
|
+
}
|
|
4404
|
+
}
|
|
4405
|
+
/**
|
|
4406
|
+
* Get all events waiting for the root span.
|
|
4407
|
+
* Returns a copy of the internal array.
|
|
4408
|
+
*/
|
|
4409
|
+
getEventsWaitingForRoot() {
|
|
4410
|
+
return [...this.#waitingForRoot];
|
|
4411
|
+
}
|
|
4412
|
+
/**
|
|
4413
|
+
* Get all events waiting for a specific parent span.
|
|
4414
|
+
* Returns a copy of the internal array.
|
|
4415
|
+
*/
|
|
4416
|
+
getEventsWaitingFor(args) {
|
|
4417
|
+
return [...this.#waitingForParent.get(args.spanId) ?? []];
|
|
4418
|
+
}
|
|
4419
|
+
/**
|
|
4420
|
+
* Clear the waiting-for-root queue.
|
|
4421
|
+
*/
|
|
4422
|
+
clearWaitingForRoot() {
|
|
4423
|
+
this.#waitingForRoot = [];
|
|
4424
|
+
}
|
|
4425
|
+
/**
|
|
4426
|
+
* Clear the waiting queue for a specific parent span.
|
|
4427
|
+
*/
|
|
4428
|
+
clearWaitingFor(args) {
|
|
4429
|
+
this.#waitingForParent.delete(args.spanId);
|
|
4430
|
+
}
|
|
4431
|
+
/**
|
|
4432
|
+
* Get total count of events in all waiting queues.
|
|
4433
|
+
*/
|
|
4434
|
+
waitingQueueSize() {
|
|
4435
|
+
let count = this.#waitingForRoot.length;
|
|
4436
|
+
for (const queue of this.#waitingForParent.values()) {
|
|
4437
|
+
count += queue.length;
|
|
4438
|
+
}
|
|
4439
|
+
return count;
|
|
4440
|
+
}
|
|
4441
|
+
/**
|
|
4442
|
+
* Get all queued events across all waiting queues.
|
|
4443
|
+
* Used for cleanup and logging orphaned events.
|
|
4444
|
+
* @returns Array of all queued events
|
|
4445
|
+
*/
|
|
4446
|
+
getAllQueuedEvents() {
|
|
4447
|
+
const all = [...this.#waitingForRoot];
|
|
4448
|
+
for (const queue of this.#waitingForParent.values()) {
|
|
4449
|
+
all.push(...queue);
|
|
4450
|
+
}
|
|
4451
|
+
return all;
|
|
4452
|
+
}
|
|
4453
|
+
// ============================================================================
|
|
4454
|
+
// Span Tree Methods
|
|
4455
|
+
// ============================================================================
|
|
4456
|
+
/**
|
|
4457
|
+
* Record the parent-child relationship for a span.
|
|
4458
|
+
* @param args.spanId - The child span ID
|
|
4459
|
+
* @param args.parentSpanId - The parent span ID, or undefined for root spans
|
|
4460
|
+
*/
|
|
4461
|
+
addBranch(args) {
|
|
4462
|
+
this.#tree.set(args.spanId, args.parentSpanId);
|
|
4463
|
+
}
|
|
4464
|
+
/**
|
|
4465
|
+
* Get the parent span ID for a given span.
|
|
4466
|
+
* @param args.spanId - The span ID to look up
|
|
4467
|
+
* @returns The parent span ID, or undefined if root or not found
|
|
4468
|
+
*/
|
|
4469
|
+
getParentId(args) {
|
|
4470
|
+
return this.#tree.get(args.spanId);
|
|
4471
|
+
}
|
|
4472
|
+
// ============================================================================
|
|
4473
|
+
// Span Management Methods
|
|
4474
|
+
// ============================================================================
|
|
4475
|
+
/**
|
|
4476
|
+
* Register a span and mark it as active.
|
|
4477
|
+
* @param args.spanId - The span ID
|
|
4478
|
+
* @param args.spanData - The vendor-specific span object
|
|
4479
|
+
*/
|
|
4480
|
+
addSpan(args) {
|
|
4481
|
+
this.#spans.set(args.spanId, args.spanData);
|
|
4482
|
+
this.#activeSpanIds.add(args.spanId);
|
|
4483
|
+
}
|
|
4484
|
+
/**
|
|
4485
|
+
* Check if a span exists (regardless of active state).
|
|
4486
|
+
* @param args.spanId - The span ID to check
|
|
4487
|
+
* @returns True if the span exists
|
|
4488
|
+
*/
|
|
4489
|
+
hasSpan(args) {
|
|
4490
|
+
return this.#spans.has(args.spanId);
|
|
4491
|
+
}
|
|
4492
|
+
/**
|
|
4493
|
+
* Get a span by ID.
|
|
4494
|
+
* @param args.spanId - The span ID to look up
|
|
4495
|
+
* @returns The vendor-specific span object, or undefined if not found
|
|
4496
|
+
*/
|
|
4497
|
+
getSpan(args) {
|
|
4498
|
+
return this.#spans.get(args.spanId);
|
|
4499
|
+
}
|
|
4500
|
+
/**
|
|
4501
|
+
* Mark a span as ended (no longer active).
|
|
4502
|
+
* @param args.spanId - The span ID to mark as ended
|
|
4503
|
+
*/
|
|
4504
|
+
endSpan(args) {
|
|
4505
|
+
this.#activeSpanIds.delete(args.spanId);
|
|
4506
|
+
}
|
|
4507
|
+
/**
|
|
4508
|
+
* Check if a span is currently active (started but not ended).
|
|
4509
|
+
* @param args.spanId - The span ID to check
|
|
4510
|
+
* @returns True if the span is active
|
|
4511
|
+
*/
|
|
4512
|
+
isActiveSpan(args) {
|
|
4513
|
+
return this.#activeSpanIds.has(args.spanId);
|
|
4514
|
+
}
|
|
4515
|
+
/**
|
|
4516
|
+
* Get the count of currently active spans.
|
|
4517
|
+
* @returns Number of active spans
|
|
4518
|
+
*/
|
|
4519
|
+
activeSpanCount() {
|
|
4520
|
+
return this.#activeSpanIds.size;
|
|
4521
|
+
}
|
|
4522
|
+
/**
|
|
4523
|
+
* Get all active span IDs.
|
|
4524
|
+
* @returns Array of active span IDs
|
|
4525
|
+
*/
|
|
4526
|
+
get activeSpanIds() {
|
|
4527
|
+
return [...this.#activeSpanIds];
|
|
4528
|
+
}
|
|
4529
|
+
// ============================================================================
|
|
4530
|
+
// Event Management Methods
|
|
4531
|
+
// ============================================================================
|
|
4532
|
+
/**
|
|
4533
|
+
* Register an event.
|
|
4534
|
+
* @param args.eventId - The event ID
|
|
4535
|
+
* @param args.eventData - The vendor-specific event object
|
|
4536
|
+
*/
|
|
4537
|
+
addEvent(args) {
|
|
4538
|
+
this.#events.set(args.eventId, args.eventData);
|
|
4539
|
+
}
|
|
4540
|
+
// ============================================================================
|
|
4541
|
+
// Metadata Methods
|
|
4542
|
+
// ============================================================================
|
|
4543
|
+
/**
|
|
4544
|
+
* Store vendor-specific metadata for a span.
|
|
4545
|
+
* Note: This overwrites any existing metadata for the span.
|
|
4546
|
+
* @param args.spanId - The span ID
|
|
4547
|
+
* @param args.metadata - The vendor-specific metadata
|
|
4548
|
+
*/
|
|
4549
|
+
addMetadata(args) {
|
|
4550
|
+
this.#metadata.set(args.spanId, args.metadata);
|
|
4551
|
+
}
|
|
4552
|
+
/**
|
|
4553
|
+
* Get vendor-specific metadata for a span.
|
|
4554
|
+
* @param args.spanId - The span ID
|
|
4555
|
+
* @returns The metadata, or undefined if not found
|
|
4556
|
+
*/
|
|
4557
|
+
getMetadata(args) {
|
|
4558
|
+
return this.#metadata.get(args.spanId);
|
|
4559
|
+
}
|
|
4560
|
+
// ============================================================================
|
|
4561
|
+
// Parent Lookup Methods
|
|
4562
|
+
// ============================================================================
|
|
4563
|
+
/**
|
|
4564
|
+
* Get the parent span or event for a given span.
|
|
4565
|
+
* Looks up in both spans and events maps.
|
|
4566
|
+
* @param args.span - The span to find the parent for
|
|
4567
|
+
* @returns The parent span/event object, or undefined if root or not found
|
|
4568
|
+
*/
|
|
4569
|
+
getParent(args) {
|
|
4570
|
+
const parentId = args.span.parentSpanId;
|
|
4571
|
+
if (parentId) {
|
|
4572
|
+
if (this.#spans.has(parentId)) {
|
|
4573
|
+
return this.#spans.get(parentId);
|
|
4574
|
+
}
|
|
4575
|
+
if (this.#events.has(parentId)) {
|
|
4576
|
+
return this.#events.get(parentId);
|
|
4577
|
+
}
|
|
4578
|
+
}
|
|
4579
|
+
return void 0;
|
|
4580
|
+
}
|
|
4581
|
+
/**
|
|
4582
|
+
* Get the parent span/event or fall back to the root object.
|
|
4583
|
+
* Useful for vendors that attach child spans to either parent spans or the trace root.
|
|
4584
|
+
* @param args.span - The span to find the parent for
|
|
4585
|
+
* @returns The parent span/event, the root object, or undefined
|
|
4586
|
+
*/
|
|
4587
|
+
getParentOrRoot(args) {
|
|
4588
|
+
return this.getParent(args) ?? this.getRoot();
|
|
4589
|
+
}
|
|
4590
|
+
};
|
|
4591
|
+
var DEFAULT_EARLY_QUEUE_MAX_ATTEMPTS = 5;
|
|
4592
|
+
var DEFAULT_EARLY_QUEUE_TTL_MS = 3e4;
|
|
4593
|
+
var DEFAULT_TRACE_CLEANUP_DELAY_MS = 3e4;
|
|
4594
|
+
var DEFAULT_MAX_PENDING_CLEANUP_TRACES = 100;
|
|
4595
|
+
var DEFAULT_MAX_TOTAL_TRACES = 500;
|
|
4596
|
+
var TrackingExporter = class extends BaseExporter {
|
|
4597
|
+
/** Map of traceId to per-trace data container */
|
|
4598
|
+
#traceMap = /* @__PURE__ */ new Map();
|
|
4599
|
+
/** Flag to prevent processing during shutdown */
|
|
4600
|
+
#shutdownStarted = false;
|
|
4601
|
+
/** Flag to prevent concurrent hard cap enforcement */
|
|
4602
|
+
#hardCapEnforcementInProgress = false;
|
|
4603
|
+
/** Map of traceId to scheduled cleanup timeout */
|
|
4604
|
+
#pendingCleanups = /* @__PURE__ */ new Map();
|
|
4605
|
+
// Note: #traceMap maintains insertion order (JS Map spec), so we use
|
|
4606
|
+
// #traceMap.keys() to iterate traces oldest-first for cap enforcement.
|
|
4607
|
+
/** Subclass configuration with resolved values */
|
|
4608
|
+
config;
|
|
4609
|
+
/** Maximum attempts to process a queued event before dropping */
|
|
4610
|
+
#earlyQueueMaxAttempts;
|
|
4611
|
+
/** TTL in milliseconds for queued events */
|
|
4612
|
+
#earlyQueueTTLMs;
|
|
4613
|
+
/** Delay before cleaning up completed traces */
|
|
4614
|
+
#traceCleanupDelayMs;
|
|
4615
|
+
/** Soft cap on traces awaiting cleanup */
|
|
4616
|
+
#maxPendingCleanupTraces;
|
|
4617
|
+
/** Hard cap on total traces (will abort active spans if exceeded) */
|
|
4618
|
+
#maxTotalTraces;
|
|
4619
|
+
constructor(config) {
|
|
4620
|
+
super(config);
|
|
4621
|
+
this.config = config;
|
|
4622
|
+
this.#earlyQueueMaxAttempts = config.earlyQueueMaxAttempts ?? DEFAULT_EARLY_QUEUE_MAX_ATTEMPTS;
|
|
4623
|
+
this.#earlyQueueTTLMs = config.earlyQueueTTLMs ?? DEFAULT_EARLY_QUEUE_TTL_MS;
|
|
4624
|
+
this.#traceCleanupDelayMs = config.traceCleanupDelayMs ?? DEFAULT_TRACE_CLEANUP_DELAY_MS;
|
|
4625
|
+
this.#maxPendingCleanupTraces = config.maxPendingCleanupTraces ?? DEFAULT_MAX_PENDING_CLEANUP_TRACES;
|
|
4626
|
+
this.#maxTotalTraces = config.maxTotalTraces ?? DEFAULT_MAX_TOTAL_TRACES;
|
|
4627
|
+
}
|
|
4628
|
+
// ============================================================================
|
|
4629
|
+
// Early Queue Processing
|
|
4630
|
+
// ============================================================================
|
|
4631
|
+
/**
|
|
4632
|
+
* Schedule async processing of events waiting for root span.
|
|
4633
|
+
* Called after root span is successfully processed.
|
|
4634
|
+
*/
|
|
4635
|
+
#scheduleProcessWaitingForRoot(traceId) {
|
|
4636
|
+
setImmediate(() => {
|
|
4637
|
+
this.#processWaitingForRoot(traceId).catch((error) => {
|
|
4638
|
+
this.logger.error(`${this.name}: Error processing waiting-for-root queue`, { error, traceId });
|
|
4639
|
+
});
|
|
4640
|
+
});
|
|
4641
|
+
}
|
|
4642
|
+
/**
|
|
4643
|
+
* Schedule async processing of events waiting for a specific parent span.
|
|
4644
|
+
* Called after a span/event is successfully created.
|
|
4645
|
+
*/
|
|
4646
|
+
#scheduleProcessWaitingFor(traceId, spanId) {
|
|
4647
|
+
setImmediate(() => {
|
|
4648
|
+
this.#processWaitingFor(traceId, spanId).catch((error) => {
|
|
4649
|
+
this.logger.error(`${this.name}: Error processing waiting queue`, { error, traceId, spanId });
|
|
4650
|
+
});
|
|
4651
|
+
});
|
|
4652
|
+
}
|
|
4653
|
+
/**
|
|
4654
|
+
* Process all events waiting for root span.
|
|
4655
|
+
*/
|
|
4656
|
+
async #processWaitingForRoot(traceId) {
|
|
4657
|
+
if (this.#shutdownStarted) return;
|
|
4658
|
+
const traceData = this.#traceMap.get(traceId);
|
|
4659
|
+
if (!traceData) return;
|
|
4660
|
+
const queue = traceData.getEventsWaitingForRoot();
|
|
4661
|
+
if (queue.length === 0) return;
|
|
4662
|
+
this.logger.debug(`${this.name}: Processing ${queue.length} events waiting for root`, { traceId });
|
|
4663
|
+
const toKeep = [];
|
|
4664
|
+
const now = Date.now();
|
|
4665
|
+
for (const queuedEvent of queue) {
|
|
4666
|
+
if (now - queuedEvent.queuedAt.getTime() > this.#earlyQueueTTLMs) {
|
|
4667
|
+
this.logger.warn(`${this.name}: Dropping event due to TTL expiry`, {
|
|
4668
|
+
traceId,
|
|
4669
|
+
spanId: queuedEvent.event.exportedSpan.id,
|
|
4670
|
+
waitingFor: queuedEvent.waitingFor,
|
|
4671
|
+
queuedAt: queuedEvent.queuedAt,
|
|
4672
|
+
attempts: queuedEvent.attempts
|
|
4673
|
+
});
|
|
4674
|
+
continue;
|
|
4675
|
+
}
|
|
4676
|
+
if (queuedEvent.attempts >= this.#earlyQueueMaxAttempts) {
|
|
4677
|
+
this.logger.warn(`${this.name}: Dropping event due to max attempts`, {
|
|
4678
|
+
traceId,
|
|
4679
|
+
spanId: queuedEvent.event.exportedSpan.id,
|
|
4680
|
+
waitingFor: queuedEvent.waitingFor,
|
|
4681
|
+
attempts: queuedEvent.attempts
|
|
4682
|
+
});
|
|
4683
|
+
continue;
|
|
4684
|
+
}
|
|
4685
|
+
queuedEvent.attempts++;
|
|
4686
|
+
const processed = await this.#tryProcessQueuedEvent(queuedEvent, traceData);
|
|
4687
|
+
if (!processed) {
|
|
4688
|
+
const parentId = queuedEvent.event.exportedSpan.parentSpanId;
|
|
4689
|
+
if (parentId && traceData.isRootProcessed()) {
|
|
4690
|
+
traceData.addToWaitingQueue({
|
|
4691
|
+
event: queuedEvent.event,
|
|
4692
|
+
waitingFor: parentId,
|
|
4693
|
+
attempts: queuedEvent.attempts,
|
|
4694
|
+
queuedAt: queuedEvent.queuedAt
|
|
4695
|
+
});
|
|
4696
|
+
} else {
|
|
4697
|
+
toKeep.push(queuedEvent);
|
|
4698
|
+
}
|
|
4699
|
+
}
|
|
4700
|
+
}
|
|
4701
|
+
traceData.clearWaitingForRoot();
|
|
4702
|
+
for (const event of toKeep) {
|
|
4703
|
+
traceData.addToWaitingQueue({
|
|
4704
|
+
event: event.event,
|
|
4705
|
+
waitingFor: "root",
|
|
4706
|
+
attempts: event.attempts,
|
|
4707
|
+
queuedAt: event.queuedAt
|
|
4708
|
+
});
|
|
4709
|
+
}
|
|
4710
|
+
}
|
|
4711
|
+
/**
|
|
4712
|
+
* Process events waiting for a specific parent span.
|
|
4713
|
+
*/
|
|
4714
|
+
async #processWaitingFor(traceId, spanId) {
|
|
4715
|
+
if (this.#shutdownStarted) return;
|
|
4716
|
+
const traceData = this.#traceMap.get(traceId);
|
|
4717
|
+
if (!traceData) return;
|
|
4718
|
+
const queue = traceData.getEventsWaitingFor({ spanId });
|
|
4719
|
+
if (queue.length === 0) return;
|
|
4720
|
+
this.logger.debug(`${this.name}: Processing ${queue.length} events waiting for span`, { traceId, spanId });
|
|
4721
|
+
const toKeep = [];
|
|
4722
|
+
const now = Date.now();
|
|
4723
|
+
for (const queuedEvent of queue) {
|
|
4724
|
+
if (now - queuedEvent.queuedAt.getTime() > this.#earlyQueueTTLMs) {
|
|
4725
|
+
this.logger.warn(`${this.name}: Dropping event due to TTL expiry`, {
|
|
4726
|
+
traceId,
|
|
4727
|
+
spanId: queuedEvent.event.exportedSpan.id,
|
|
4728
|
+
waitingFor: queuedEvent.waitingFor,
|
|
4729
|
+
queuedAt: queuedEvent.queuedAt,
|
|
4730
|
+
attempts: queuedEvent.attempts
|
|
4731
|
+
});
|
|
4732
|
+
continue;
|
|
4733
|
+
}
|
|
4734
|
+
if (queuedEvent.attempts >= this.#earlyQueueMaxAttempts) {
|
|
4735
|
+
this.logger.warn(`${this.name}: Dropping event due to max attempts`, {
|
|
4736
|
+
traceId,
|
|
4737
|
+
spanId: queuedEvent.event.exportedSpan.id,
|
|
4738
|
+
waitingFor: queuedEvent.waitingFor,
|
|
4739
|
+
attempts: queuedEvent.attempts
|
|
4740
|
+
});
|
|
4741
|
+
continue;
|
|
4742
|
+
}
|
|
4743
|
+
queuedEvent.attempts++;
|
|
4744
|
+
const processed = await this.#tryProcessQueuedEvent(queuedEvent, traceData);
|
|
4745
|
+
if (!processed) {
|
|
4746
|
+
toKeep.push(queuedEvent);
|
|
4747
|
+
}
|
|
4748
|
+
}
|
|
4749
|
+
traceData.clearWaitingFor({ spanId });
|
|
4750
|
+
for (const event of toKeep) {
|
|
4751
|
+
traceData.addToWaitingQueue({
|
|
4752
|
+
event: event.event,
|
|
4753
|
+
waitingFor: spanId,
|
|
4754
|
+
attempts: event.attempts,
|
|
4755
|
+
queuedAt: event.queuedAt
|
|
4756
|
+
});
|
|
4757
|
+
}
|
|
4758
|
+
}
|
|
4759
|
+
/**
|
|
4760
|
+
* Try to process a queued event.
|
|
4761
|
+
* Returns true if successfully processed, false if still waiting for dependencies.
|
|
4762
|
+
*/
|
|
4763
|
+
async #tryProcessQueuedEvent(queuedEvent, traceData) {
|
|
4764
|
+
const { event } = queuedEvent;
|
|
4765
|
+
const { exportedSpan } = event;
|
|
4766
|
+
const method = this.getMethod(event);
|
|
4767
|
+
try {
|
|
4768
|
+
switch (method) {
|
|
4769
|
+
case "handleEventSpan": {
|
|
4770
|
+
traceData.addBranch({ spanId: exportedSpan.id, parentSpanId: exportedSpan.parentSpanId });
|
|
4771
|
+
const eventData = await this._buildEvent({ span: exportedSpan, traceData });
|
|
4772
|
+
if (eventData) {
|
|
4773
|
+
if (!this.skipCachingEventSpans) {
|
|
4774
|
+
traceData.addEvent({ eventId: exportedSpan.id, eventData });
|
|
4775
|
+
}
|
|
4776
|
+
this.#scheduleProcessWaitingFor(exportedSpan.traceId, exportedSpan.id);
|
|
4777
|
+
return true;
|
|
4778
|
+
}
|
|
4779
|
+
return false;
|
|
4780
|
+
}
|
|
4781
|
+
case "handleSpanStart": {
|
|
4782
|
+
traceData.addBranch({ spanId: exportedSpan.id, parentSpanId: exportedSpan.parentSpanId });
|
|
4783
|
+
const spanData = await this._buildSpan({ span: exportedSpan, traceData });
|
|
4784
|
+
if (spanData) {
|
|
4785
|
+
traceData.addSpan({ spanId: exportedSpan.id, spanData });
|
|
4786
|
+
if (exportedSpan.isRootSpan) {
|
|
4787
|
+
traceData.markRootSpanProcessed();
|
|
4788
|
+
}
|
|
4789
|
+
this.#scheduleProcessWaitingFor(exportedSpan.traceId, exportedSpan.id);
|
|
4790
|
+
return true;
|
|
4791
|
+
}
|
|
4792
|
+
return false;
|
|
4793
|
+
}
|
|
4794
|
+
case "handleSpanUpdate": {
|
|
4795
|
+
await this._updateSpan({ span: exportedSpan, traceData });
|
|
4796
|
+
return true;
|
|
4797
|
+
}
|
|
4798
|
+
case "handleSpanEnd": {
|
|
4799
|
+
traceData.endSpan({ spanId: exportedSpan.id });
|
|
4800
|
+
await this._finishSpan({ span: exportedSpan, traceData });
|
|
4801
|
+
if (traceData.activeSpanCount() === 0) {
|
|
4802
|
+
this.#scheduleCleanup(exportedSpan.traceId);
|
|
4803
|
+
}
|
|
4804
|
+
return true;
|
|
4805
|
+
}
|
|
4806
|
+
default:
|
|
4807
|
+
return false;
|
|
4808
|
+
}
|
|
4809
|
+
} catch (error) {
|
|
4810
|
+
this.logger.error(`${this.name}: Error processing queued event`, { error, event, method });
|
|
4811
|
+
return false;
|
|
4812
|
+
}
|
|
4813
|
+
}
|
|
4814
|
+
// ============================================================================
|
|
4815
|
+
// Delayed Cleanup
|
|
4816
|
+
// ============================================================================
|
|
4817
|
+
/**
|
|
4818
|
+
* Schedule cleanup of trace data after a delay.
|
|
4819
|
+
* Allows late-arriving data to still be processed.
|
|
4820
|
+
*/
|
|
4821
|
+
#scheduleCleanup(traceId) {
|
|
4822
|
+
this.#cancelScheduledCleanup(traceId);
|
|
4823
|
+
this.logger.debug(`${this.name}: Scheduling cleanup in ${this.#traceCleanupDelayMs}ms`, { traceId });
|
|
4824
|
+
const timeout = setTimeout(() => {
|
|
4825
|
+
this.#pendingCleanups.delete(traceId);
|
|
4826
|
+
this.#performCleanup(traceId);
|
|
4827
|
+
}, this.#traceCleanupDelayMs);
|
|
4828
|
+
this.#pendingCleanups.set(traceId, timeout);
|
|
4829
|
+
this.#enforcePendingCleanupCap();
|
|
4830
|
+
}
|
|
4831
|
+
/**
|
|
4832
|
+
* Cancel a scheduled cleanup for a trace.
|
|
4833
|
+
*/
|
|
4834
|
+
#cancelScheduledCleanup(traceId) {
|
|
4835
|
+
const existingTimeout = this.#pendingCleanups.get(traceId);
|
|
4836
|
+
if (existingTimeout) {
|
|
4837
|
+
clearTimeout(existingTimeout);
|
|
4838
|
+
this.#pendingCleanups.delete(traceId);
|
|
4839
|
+
this.logger.debug(`${this.name}: Cancelled scheduled cleanup`, { traceId });
|
|
4840
|
+
}
|
|
4841
|
+
}
|
|
4842
|
+
/**
|
|
4843
|
+
* Perform the actual cleanup of trace data.
|
|
4844
|
+
*/
|
|
4845
|
+
#performCleanup(traceId) {
|
|
4846
|
+
const traceData = this.#traceMap.get(traceId);
|
|
4847
|
+
if (!traceData) return;
|
|
4848
|
+
const orphanedEvents = traceData.getAllQueuedEvents();
|
|
4849
|
+
if (orphanedEvents.length > 0) {
|
|
4850
|
+
this.logger.warn(`${this.name}: Dropping ${orphanedEvents.length} orphaned events on cleanup`, {
|
|
4851
|
+
traceId,
|
|
4852
|
+
orphanedEvents: orphanedEvents.map((e) => ({
|
|
4853
|
+
spanId: e.event.exportedSpan.id,
|
|
4854
|
+
waitingFor: e.waitingFor,
|
|
4855
|
+
attempts: e.attempts,
|
|
4856
|
+
queuedAt: e.queuedAt
|
|
4857
|
+
}))
|
|
4858
|
+
});
|
|
4859
|
+
}
|
|
4860
|
+
this.#traceMap.delete(traceId);
|
|
4861
|
+
this.logger.debug(`${this.name}: Cleaned up trace data`, { traceId });
|
|
4862
|
+
}
|
|
4863
|
+
// ============================================================================
|
|
4864
|
+
// Cap Enforcement
|
|
4865
|
+
// ============================================================================
|
|
4866
|
+
/**
|
|
4867
|
+
* Enforce soft cap on pending cleanup traces.
|
|
4868
|
+
* Only removes traces with activeSpanCount == 0.
|
|
4869
|
+
*/
|
|
4870
|
+
#enforcePendingCleanupCap() {
|
|
4871
|
+
if (this.#pendingCleanups.size <= this.#maxPendingCleanupTraces) {
|
|
4872
|
+
return;
|
|
4873
|
+
}
|
|
4874
|
+
const toRemove = this.#pendingCleanups.size - this.#maxPendingCleanupTraces;
|
|
4875
|
+
this.logger.warn(`${this.name}: Pending cleanup cap exceeded, force-cleaning ${toRemove} traces`, {
|
|
4876
|
+
pendingCount: this.#pendingCleanups.size,
|
|
4877
|
+
cap: this.#maxPendingCleanupTraces
|
|
4878
|
+
});
|
|
4879
|
+
let removed = 0;
|
|
4880
|
+
for (const traceId of this.#traceMap.keys()) {
|
|
4881
|
+
if (removed >= toRemove) break;
|
|
4882
|
+
if (this.#pendingCleanups.has(traceId)) {
|
|
4883
|
+
this.#cancelScheduledCleanup(traceId);
|
|
4884
|
+
this.#performCleanup(traceId);
|
|
4885
|
+
removed++;
|
|
4886
|
+
}
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
/**
|
|
4890
|
+
* Enforce hard cap on total traces.
|
|
4891
|
+
* Will kill even active traces if necessary.
|
|
4892
|
+
* Uses a flag to prevent concurrent executions when called fire-and-forget.
|
|
4893
|
+
*/
|
|
4894
|
+
async #enforceHardCap() {
|
|
4895
|
+
if (this.#traceMap.size <= this.#maxTotalTraces || this.#hardCapEnforcementInProgress) {
|
|
4896
|
+
return;
|
|
4897
|
+
}
|
|
4898
|
+
this.#hardCapEnforcementInProgress = true;
|
|
4899
|
+
try {
|
|
4900
|
+
if (this.#traceMap.size <= this.#maxTotalTraces) {
|
|
4901
|
+
return;
|
|
4902
|
+
}
|
|
4903
|
+
const toRemove = this.#traceMap.size - this.#maxTotalTraces;
|
|
4904
|
+
this.logger.warn(`${this.name}: Total trace cap exceeded, killing ${toRemove} oldest traces`, {
|
|
4905
|
+
traceCount: this.#traceMap.size,
|
|
4906
|
+
cap: this.#maxTotalTraces
|
|
4907
|
+
});
|
|
4908
|
+
const reason = {
|
|
4909
|
+
id: "TRACE_CAP_EXCEEDED",
|
|
4910
|
+
message: "Trace killed due to memory cap enforcement.",
|
|
4911
|
+
domain: "MASTRA_OBSERVABILITY",
|
|
4912
|
+
category: "SYSTEM"
|
|
4913
|
+
};
|
|
4914
|
+
let removed = 0;
|
|
4915
|
+
for (const traceId of [...this.#traceMap.keys()]) {
|
|
4916
|
+
if (removed >= toRemove) break;
|
|
4917
|
+
const traceData = this.#traceMap.get(traceId);
|
|
4918
|
+
if (traceData) {
|
|
4919
|
+
for (const spanId of traceData.activeSpanIds) {
|
|
4920
|
+
const span = traceData.getSpan({ spanId });
|
|
4921
|
+
if (span) {
|
|
4922
|
+
await this._abortSpan({ span, traceData, reason });
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
this.#cancelScheduledCleanup(traceId);
|
|
4926
|
+
this.#performCleanup(traceId);
|
|
4927
|
+
removed++;
|
|
4928
|
+
}
|
|
4929
|
+
}
|
|
4930
|
+
} finally {
|
|
4931
|
+
this.#hardCapEnforcementInProgress = false;
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
// ============================================================================
|
|
4935
|
+
// Lifecycle Hooks (Override in subclass)
|
|
4936
|
+
// ============================================================================
|
|
4937
|
+
/**
|
|
4938
|
+
* Hook called before processing each tracing event.
|
|
4939
|
+
* Override to transform or enrich the event before processing.
|
|
4940
|
+
*
|
|
4941
|
+
* Note: The customSpanFormatter is applied at the BaseExporter level before this hook.
|
|
4942
|
+
* Subclasses can override this to add additional pre-processing logic.
|
|
4943
|
+
*
|
|
4944
|
+
* @param event - The incoming tracing event
|
|
4945
|
+
* @returns The (possibly modified) event to process
|
|
4946
|
+
*/
|
|
4947
|
+
async _preExportTracingEvent(event) {
|
|
4948
|
+
return event;
|
|
4949
|
+
}
|
|
4950
|
+
/**
|
|
4951
|
+
* Hook called after processing each tracing event.
|
|
4952
|
+
* Override to perform post-processing actions like flushing.
|
|
4953
|
+
*/
|
|
4954
|
+
async _postExportTracingEvent() {
|
|
4955
|
+
}
|
|
4956
|
+
// ============================================================================
|
|
4957
|
+
// Behavior Flags (Override in subclass as needed)
|
|
4958
|
+
// ============================================================================
|
|
4959
|
+
/**
|
|
4960
|
+
* If true, skip calling _buildRoot and let root spans go through _buildSpan.
|
|
4961
|
+
* Use when the vendor doesn't have a separate trace/root concept.
|
|
4962
|
+
* @default false
|
|
4963
|
+
*/
|
|
4964
|
+
skipBuildRootTask = false;
|
|
4965
|
+
/**
|
|
4966
|
+
* If true, skip processing span_updated events entirely.
|
|
4967
|
+
* Use when the vendor doesn't support incremental span updates.
|
|
4968
|
+
* @default false
|
|
4969
|
+
*/
|
|
4970
|
+
skipSpanUpdateEvents = false;
|
|
4971
|
+
/**
|
|
4972
|
+
* If true, don't cache event spans in TraceData.
|
|
4973
|
+
* Use when events can't be parents of other spans.
|
|
4974
|
+
* @default false
|
|
4975
|
+
*/
|
|
4976
|
+
skipCachingEventSpans = false;
|
|
4977
|
+
getMethod(event) {
|
|
4978
|
+
if (event.exportedSpan.isEvent) {
|
|
4979
|
+
return "handleEventSpan";
|
|
4980
|
+
}
|
|
4981
|
+
const eventType = event.type;
|
|
4982
|
+
switch (eventType) {
|
|
4983
|
+
case observability.TracingEventType.SPAN_STARTED:
|
|
4984
|
+
return "handleSpanStart";
|
|
4985
|
+
case observability.TracingEventType.SPAN_UPDATED:
|
|
4986
|
+
return "handleSpanUpdate";
|
|
4987
|
+
case observability.TracingEventType.SPAN_ENDED:
|
|
4988
|
+
return "handleSpanEnd";
|
|
4989
|
+
default: {
|
|
4990
|
+
const _exhaustiveCheck = eventType;
|
|
4991
|
+
throw new Error(`Unhandled event type: ${_exhaustiveCheck}`);
|
|
4992
|
+
}
|
|
4993
|
+
}
|
|
4994
|
+
}
|
|
4995
|
+
async _exportTracingEvent(event) {
|
|
4996
|
+
if (this.#shutdownStarted) {
|
|
4997
|
+
return;
|
|
4998
|
+
}
|
|
4999
|
+
const method = this.getMethod(event);
|
|
5000
|
+
if (method == "handleSpanUpdate" && this.skipSpanUpdateEvents) {
|
|
5001
|
+
return;
|
|
5002
|
+
}
|
|
5003
|
+
const traceId = event.exportedSpan.traceId;
|
|
5004
|
+
const traceData = this.getTraceData({ traceId, method });
|
|
5005
|
+
const { exportedSpan } = await this._preExportTracingEvent(event);
|
|
5006
|
+
if (!this.skipBuildRootTask && !traceData.hasRoot()) {
|
|
5007
|
+
if (exportedSpan.isRootSpan) {
|
|
5008
|
+
this.logger.debug(`${this.name}: Building root`, {
|
|
5009
|
+
traceId: exportedSpan.traceId,
|
|
5010
|
+
spanId: exportedSpan.id
|
|
5011
|
+
});
|
|
5012
|
+
const rootData = await this._buildRoot({ span: exportedSpan, traceData });
|
|
5013
|
+
if (rootData) {
|
|
5014
|
+
this.logger.debug(`${this.name}: Adding root`, {
|
|
5015
|
+
traceId: exportedSpan.traceId,
|
|
5016
|
+
spanId: exportedSpan.id
|
|
5017
|
+
});
|
|
5018
|
+
traceData.addRoot({ rootId: exportedSpan.id, rootData });
|
|
5019
|
+
this.#scheduleProcessWaitingForRoot(traceId);
|
|
5020
|
+
}
|
|
5021
|
+
} else {
|
|
5022
|
+
this.logger.debug(`${this.name}: Root does not exist, adding span to waiting queue.`, {
|
|
5023
|
+
traceId: exportedSpan.traceId,
|
|
5024
|
+
spanId: exportedSpan.id
|
|
5025
|
+
});
|
|
5026
|
+
traceData.addToWaitingQueue({ event, waitingFor: "root" });
|
|
5027
|
+
return;
|
|
5028
|
+
}
|
|
5029
|
+
}
|
|
5030
|
+
if (exportedSpan.metadata && this.name in exportedSpan.metadata) {
|
|
5031
|
+
const metadata = exportedSpan.metadata[this.name];
|
|
5032
|
+
this.logger.debug(`${this.name}: Found provider metadata in span`, {
|
|
5033
|
+
traceId: exportedSpan.traceId,
|
|
5034
|
+
spanId: exportedSpan.id,
|
|
5035
|
+
metadata
|
|
5036
|
+
});
|
|
5037
|
+
traceData.addMetadata({ spanId: exportedSpan.id, metadata });
|
|
5038
|
+
}
|
|
5039
|
+
try {
|
|
5040
|
+
switch (method) {
|
|
5041
|
+
case "handleEventSpan": {
|
|
5042
|
+
this.logger.debug(`${this.name}: handling event`, {
|
|
5043
|
+
traceId: exportedSpan.traceId,
|
|
5044
|
+
spanId: exportedSpan.id
|
|
5045
|
+
});
|
|
5046
|
+
traceData.addBranch({ spanId: exportedSpan.id, parentSpanId: exportedSpan.parentSpanId });
|
|
5047
|
+
const eventData = await this._buildEvent({ span: exportedSpan, traceData });
|
|
5048
|
+
if (eventData) {
|
|
5049
|
+
if (!this.skipCachingEventSpans) {
|
|
5050
|
+
this.logger.debug(`${this.name}: adding event to traceData`, {
|
|
5051
|
+
traceId: exportedSpan.traceId,
|
|
5052
|
+
spanId: exportedSpan.id
|
|
5053
|
+
});
|
|
5054
|
+
traceData.addEvent({ eventId: exportedSpan.id, eventData });
|
|
5055
|
+
}
|
|
5056
|
+
this.#scheduleProcessWaitingFor(traceId, exportedSpan.id);
|
|
5057
|
+
} else {
|
|
5058
|
+
const parentId = exportedSpan.parentSpanId;
|
|
5059
|
+
this.logger.debug(`${this.name}: adding event to waiting queue`, {
|
|
5060
|
+
traceId: exportedSpan.traceId,
|
|
5061
|
+
spanId: exportedSpan.id,
|
|
5062
|
+
waitingFor: parentId ?? "root"
|
|
5063
|
+
});
|
|
5064
|
+
traceData.addToWaitingQueue({ event, waitingFor: parentId ?? "root" });
|
|
5065
|
+
}
|
|
5066
|
+
break;
|
|
5067
|
+
}
|
|
5068
|
+
case "handleSpanStart": {
|
|
5069
|
+
this.logger.debug(`${this.name}: handling span start`, {
|
|
5070
|
+
traceId: exportedSpan.traceId,
|
|
5071
|
+
spanId: exportedSpan.id
|
|
5072
|
+
});
|
|
5073
|
+
traceData.addBranch({ spanId: exportedSpan.id, parentSpanId: exportedSpan.parentSpanId });
|
|
5074
|
+
const spanData = await this._buildSpan({ span: exportedSpan, traceData });
|
|
5075
|
+
if (spanData) {
|
|
5076
|
+
this.logger.debug(`${this.name}: adding span to traceData`, {
|
|
5077
|
+
traceId: exportedSpan.traceId,
|
|
5078
|
+
spanId: exportedSpan.id
|
|
5079
|
+
});
|
|
5080
|
+
traceData.addSpan({ spanId: exportedSpan.id, spanData });
|
|
5081
|
+
if (exportedSpan.isRootSpan) {
|
|
5082
|
+
traceData.markRootSpanProcessed();
|
|
5083
|
+
this.#scheduleProcessWaitingForRoot(traceId);
|
|
5084
|
+
}
|
|
5085
|
+
this.#scheduleProcessWaitingFor(traceId, exportedSpan.id);
|
|
5086
|
+
} else {
|
|
5087
|
+
const parentId = exportedSpan.parentSpanId;
|
|
5088
|
+
this.logger.debug(`${this.name}: adding span to waiting queue`, {
|
|
5089
|
+
traceId: exportedSpan.traceId,
|
|
5090
|
+
waitingFor: parentId ?? "root"
|
|
5091
|
+
});
|
|
5092
|
+
traceData.addToWaitingQueue({ event, waitingFor: parentId ?? "root" });
|
|
5093
|
+
}
|
|
5094
|
+
break;
|
|
5095
|
+
}
|
|
5096
|
+
case "handleSpanUpdate":
|
|
5097
|
+
this.logger.debug(`${this.name}: handling span update`, {
|
|
5098
|
+
traceId: exportedSpan.traceId,
|
|
5099
|
+
spanId: exportedSpan.id
|
|
5100
|
+
});
|
|
5101
|
+
await this._updateSpan({ span: exportedSpan, traceData });
|
|
5102
|
+
break;
|
|
5103
|
+
case "handleSpanEnd":
|
|
5104
|
+
this.logger.debug(`${this.name}: handling span end`, {
|
|
5105
|
+
traceId: exportedSpan.traceId,
|
|
5106
|
+
spanId: exportedSpan.id
|
|
5107
|
+
});
|
|
5108
|
+
traceData.endSpan({ spanId: exportedSpan.id });
|
|
5109
|
+
await this._finishSpan({ span: exportedSpan, traceData });
|
|
5110
|
+
if (traceData.activeSpanCount() === 0) {
|
|
5111
|
+
this.#scheduleCleanup(traceId);
|
|
5112
|
+
}
|
|
5113
|
+
break;
|
|
5114
|
+
}
|
|
5115
|
+
} catch (error) {
|
|
5116
|
+
this.logger.error(`${this.name}: exporter error`, { error, event, method });
|
|
5117
|
+
}
|
|
5118
|
+
if (traceData.activeSpanCount() === 0) {
|
|
5119
|
+
this.#scheduleCleanup(traceId);
|
|
5120
|
+
}
|
|
5121
|
+
await this._postExportTracingEvent();
|
|
5122
|
+
}
|
|
5123
|
+
// ============================================================================
|
|
5124
|
+
// Protected Helpers
|
|
5125
|
+
// ============================================================================
|
|
5126
|
+
/**
|
|
5127
|
+
* Get or create the TraceData container for a trace.
|
|
5128
|
+
* Also cancels any pending cleanup since new data has arrived.
|
|
5129
|
+
*
|
|
5130
|
+
* @param args.traceId - The trace ID
|
|
5131
|
+
* @param args.method - The calling method name (for logging)
|
|
5132
|
+
* @returns The TraceData container for this trace
|
|
5133
|
+
*/
|
|
5134
|
+
getTraceData(args) {
|
|
5135
|
+
const { traceId, method } = args;
|
|
5136
|
+
this.#cancelScheduledCleanup(traceId);
|
|
5137
|
+
if (!this.#traceMap.has(traceId)) {
|
|
5138
|
+
this.#traceMap.set(traceId, new TraceData());
|
|
5139
|
+
this.logger.debug(`${this.name}: Created new trace data cache`, {
|
|
5140
|
+
traceId,
|
|
5141
|
+
method
|
|
5142
|
+
});
|
|
5143
|
+
this.#enforceHardCap().catch((error) => {
|
|
5144
|
+
this.logger.error(`${this.name}: Error enforcing hard cap`, { error });
|
|
5145
|
+
});
|
|
5146
|
+
}
|
|
5147
|
+
return this.#traceMap.get(traceId);
|
|
5148
|
+
}
|
|
5149
|
+
/**
|
|
5150
|
+
* Get the current number of traces being tracked.
|
|
5151
|
+
* @returns The trace count
|
|
5152
|
+
*/
|
|
5153
|
+
traceMapSize() {
|
|
5154
|
+
return this.#traceMap.size;
|
|
5155
|
+
}
|
|
5156
|
+
// ============================================================================
|
|
5157
|
+
// Flush and Shutdown Hooks (Override in subclass as needed)
|
|
5158
|
+
// ============================================================================
|
|
5159
|
+
/**
|
|
5160
|
+
* Hook called by flush() to perform vendor-specific flush logic.
|
|
5161
|
+
* Override to send buffered data to the vendor's API.
|
|
5162
|
+
*
|
|
5163
|
+
* Unlike _postShutdown(), this method should NOT release resources,
|
|
5164
|
+
* as the exporter will continue to be used after flushing.
|
|
5165
|
+
*/
|
|
5166
|
+
async _flush() {
|
|
5167
|
+
}
|
|
5168
|
+
/**
|
|
5169
|
+
* Force flush any buffered data without shutting down the exporter.
|
|
5170
|
+
* This is useful in serverless environments where you need to ensure spans
|
|
5171
|
+
* are exported before the runtime instance is terminated.
|
|
5172
|
+
*
|
|
5173
|
+
* Subclasses should override _flush() to implement vendor-specific flush logic.
|
|
5174
|
+
*/
|
|
5175
|
+
async flush() {
|
|
5176
|
+
if (this.isDisabled) {
|
|
5177
|
+
return;
|
|
5178
|
+
}
|
|
5179
|
+
this.logger.debug(`${this.name}: Flushing`);
|
|
5180
|
+
await this._flush();
|
|
5181
|
+
}
|
|
5182
|
+
/**
|
|
5183
|
+
* Hook called at the start of shutdown, before cancelling timers and aborting spans.
|
|
5184
|
+
* Override to perform vendor-specific pre-shutdown tasks.
|
|
5185
|
+
*/
|
|
5186
|
+
async _preShutdown() {
|
|
5187
|
+
}
|
|
5188
|
+
/**
|
|
5189
|
+
* Hook called at the end of shutdown, after all spans are aborted.
|
|
5190
|
+
* Override to perform vendor-specific cleanup (e.g., flushing).
|
|
5191
|
+
*/
|
|
5192
|
+
async _postShutdown() {
|
|
5193
|
+
}
|
|
5194
|
+
/**
|
|
5195
|
+
* Gracefully shut down the exporter.
|
|
5196
|
+
* Cancels all pending cleanup timers, aborts all active spans, and clears state.
|
|
5197
|
+
*/
|
|
5198
|
+
async shutdown() {
|
|
5199
|
+
if (this.isDisabled) {
|
|
5200
|
+
return;
|
|
5201
|
+
}
|
|
5202
|
+
this.#shutdownStarted = true;
|
|
5203
|
+
await this._preShutdown();
|
|
5204
|
+
for (const [traceId, timeout] of this.#pendingCleanups) {
|
|
5205
|
+
clearTimeout(timeout);
|
|
5206
|
+
this.logger.debug(`${this.name}: Cancelled pending cleanup on shutdown`, { traceId });
|
|
5207
|
+
}
|
|
5208
|
+
this.#pendingCleanups.clear();
|
|
5209
|
+
const reason = {
|
|
5210
|
+
id: "SHUTDOWN",
|
|
5211
|
+
message: "Observability is shutting down.",
|
|
5212
|
+
domain: "MASTRA_OBSERVABILITY",
|
|
5213
|
+
category: "SYSTEM"
|
|
5214
|
+
};
|
|
5215
|
+
for (const [traceId, traceData] of this.#traceMap) {
|
|
5216
|
+
const orphanedEvents = traceData.getAllQueuedEvents();
|
|
5217
|
+
if (orphanedEvents.length > 0) {
|
|
5218
|
+
this.logger.warn(`${this.name}: Dropping ${orphanedEvents.length} orphaned events on shutdown`, {
|
|
5219
|
+
traceId,
|
|
5220
|
+
orphanedEvents: orphanedEvents.map((e) => ({
|
|
5221
|
+
spanId: e.event.exportedSpan.id,
|
|
5222
|
+
waitingFor: e.waitingFor,
|
|
5223
|
+
attempts: e.attempts
|
|
5224
|
+
}))
|
|
5225
|
+
});
|
|
5226
|
+
}
|
|
5227
|
+
for (const spanId of traceData.activeSpanIds) {
|
|
5228
|
+
const span = traceData.getSpan({ spanId });
|
|
5229
|
+
if (span) {
|
|
5230
|
+
await this._abortSpan({ span, traceData, reason });
|
|
5231
|
+
}
|
|
5232
|
+
}
|
|
5233
|
+
}
|
|
5234
|
+
this.#traceMap.clear();
|
|
5235
|
+
await this._postShutdown();
|
|
5236
|
+
await super.shutdown();
|
|
5237
|
+
}
|
|
5238
|
+
};
|
|
5239
|
+
|
|
5240
|
+
// src/exporters/span-formatters.ts
|
|
5241
|
+
function chainFormatters(formatters) {
|
|
5242
|
+
return async (span) => {
|
|
5243
|
+
let currentSpan = span;
|
|
5244
|
+
for (const formatter of formatters) {
|
|
5245
|
+
currentSpan = await formatter(currentSpan);
|
|
5246
|
+
}
|
|
5247
|
+
return currentSpan;
|
|
5248
|
+
};
|
|
5249
|
+
}
|
|
4230
5250
|
var CloudExporter = class extends BaseExporter {
|
|
4231
5251
|
name = "mastra-cloud-observability-exporter";
|
|
4232
|
-
|
|
5252
|
+
cloudConfig;
|
|
4233
5253
|
buffer;
|
|
4234
5254
|
flushTimer = null;
|
|
4235
5255
|
constructor(config = {}) {
|
|
4236
5256
|
super(config);
|
|
4237
5257
|
const accessToken = config.accessToken ?? process.env.MASTRA_CLOUD_ACCESS_TOKEN;
|
|
4238
5258
|
if (!accessToken) {
|
|
4239
|
-
this.setDisabled(
|
|
4240
|
-
"MASTRA_CLOUD_ACCESS_TOKEN environment variable not set.\n\u{1F680} Sign up at https://cloud.mastra.ai to see your AI traces online and obtain your access token."
|
|
4241
|
-
);
|
|
5259
|
+
this.setDisabled("MASTRA_CLOUD_ACCESS_TOKEN environment variable not set.");
|
|
4242
5260
|
}
|
|
4243
5261
|
const endpoint = config.endpoint ?? process.env.MASTRA_CLOUD_TRACES_ENDPOINT ?? "https://api.mastra.ai/ai/spans/publish";
|
|
4244
|
-
this.
|
|
5262
|
+
this.cloudConfig = {
|
|
4245
5263
|
logger: this.logger,
|
|
4246
5264
|
logLevel: config.logLevel ?? logger.LogLevel.INFO,
|
|
4247
5265
|
maxBatchSize: config.maxBatchSize ?? 1e3,
|
|
@@ -4299,12 +5317,12 @@ var CloudExporter = class extends BaseExporter {
|
|
|
4299
5317
|
return spanRecord;
|
|
4300
5318
|
}
|
|
4301
5319
|
shouldFlush() {
|
|
4302
|
-
if (this.buffer.totalSize >= this.
|
|
5320
|
+
if (this.buffer.totalSize >= this.cloudConfig.maxBatchSize) {
|
|
4303
5321
|
return true;
|
|
4304
5322
|
}
|
|
4305
5323
|
if (this.buffer.firstEventTime && this.buffer.totalSize > 0) {
|
|
4306
5324
|
const elapsed = Date.now() - this.buffer.firstEventTime.getTime();
|
|
4307
|
-
if (elapsed >= this.
|
|
5325
|
+
if (elapsed >= this.cloudConfig.maxBatchWaitMs) {
|
|
4308
5326
|
return true;
|
|
4309
5327
|
}
|
|
4310
5328
|
}
|
|
@@ -4327,9 +5345,9 @@ var CloudExporter = class extends BaseExporter {
|
|
|
4327
5345
|
this.logger.trackException(mastraError);
|
|
4328
5346
|
this.logger.error("Scheduled flush failed", mastraError);
|
|
4329
5347
|
});
|
|
4330
|
-
}, this.
|
|
5348
|
+
}, this.cloudConfig.maxBatchWaitMs);
|
|
4331
5349
|
}
|
|
4332
|
-
async
|
|
5350
|
+
async flushBuffer() {
|
|
4333
5351
|
if (this.flushTimer) {
|
|
4334
5352
|
clearTimeout(this.flushTimer);
|
|
4335
5353
|
this.flushTimer = null;
|
|
@@ -4339,7 +5357,7 @@ var CloudExporter = class extends BaseExporter {
|
|
|
4339
5357
|
}
|
|
4340
5358
|
const startTime = Date.now();
|
|
4341
5359
|
const spansCopy = [...this.buffer.spans];
|
|
4342
|
-
const flushReason = this.buffer.totalSize >= this.
|
|
5360
|
+
const flushReason = this.buffer.totalSize >= this.cloudConfig.maxBatchSize ? "size" : "time";
|
|
4343
5361
|
this.resetBuffer();
|
|
4344
5362
|
try {
|
|
4345
5363
|
await this.batchUpload(spansCopy);
|
|
@@ -4370,7 +5388,7 @@ var CloudExporter = class extends BaseExporter {
|
|
|
4370
5388
|
*/
|
|
4371
5389
|
async batchUpload(spans) {
|
|
4372
5390
|
const headers = {
|
|
4373
|
-
Authorization: `Bearer ${this.
|
|
5391
|
+
Authorization: `Bearer ${this.cloudConfig.accessToken}`,
|
|
4374
5392
|
"Content-Type": "application/json"
|
|
4375
5393
|
};
|
|
4376
5394
|
const options = {
|
|
@@ -4378,13 +5396,29 @@ var CloudExporter = class extends BaseExporter {
|
|
|
4378
5396
|
headers,
|
|
4379
5397
|
body: JSON.stringify({ spans })
|
|
4380
5398
|
};
|
|
4381
|
-
await utils.fetchWithRetry(this.
|
|
5399
|
+
await utils.fetchWithRetry(this.cloudConfig.endpoint, options, this.cloudConfig.maxRetries);
|
|
4382
5400
|
}
|
|
4383
5401
|
resetBuffer() {
|
|
4384
5402
|
this.buffer.spans = [];
|
|
4385
5403
|
this.buffer.firstEventTime = void 0;
|
|
4386
5404
|
this.buffer.totalSize = 0;
|
|
4387
5405
|
}
|
|
5406
|
+
/**
|
|
5407
|
+
* Force flush any buffered spans without shutting down the exporter.
|
|
5408
|
+
* This is useful in serverless environments where you need to ensure spans
|
|
5409
|
+
* are exported before the runtime instance is terminated.
|
|
5410
|
+
*/
|
|
5411
|
+
async flush() {
|
|
5412
|
+
if (this.isDisabled) {
|
|
5413
|
+
return;
|
|
5414
|
+
}
|
|
5415
|
+
if (this.buffer.totalSize > 0) {
|
|
5416
|
+
this.logger.debug("Flushing buffered events", {
|
|
5417
|
+
bufferedEvents: this.buffer.totalSize
|
|
5418
|
+
});
|
|
5419
|
+
await this.flushBuffer();
|
|
5420
|
+
}
|
|
5421
|
+
}
|
|
4388
5422
|
async shutdown() {
|
|
4389
5423
|
if (this.isDisabled) {
|
|
4390
5424
|
return;
|
|
@@ -4393,27 +5427,22 @@ var CloudExporter = class extends BaseExporter {
|
|
|
4393
5427
|
clearTimeout(this.flushTimer);
|
|
4394
5428
|
this.flushTimer = null;
|
|
4395
5429
|
}
|
|
4396
|
-
|
|
4397
|
-
this.
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
{
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
error$1
|
|
4413
|
-
);
|
|
4414
|
-
this.logger.trackException(mastraError);
|
|
4415
|
-
this.logger.error("Failed to flush remaining events during shutdown", mastraError);
|
|
4416
|
-
}
|
|
5430
|
+
try {
|
|
5431
|
+
await this.flush();
|
|
5432
|
+
} catch (error$1) {
|
|
5433
|
+
const mastraError = new error.MastraError(
|
|
5434
|
+
{
|
|
5435
|
+
id: `CLOUD_EXPORTER_FAILED_TO_FLUSH_REMAINING_EVENTS_DURING_SHUTDOWN`,
|
|
5436
|
+
domain: error.ErrorDomain.MASTRA_OBSERVABILITY,
|
|
5437
|
+
category: error.ErrorCategory.USER,
|
|
5438
|
+
details: {
|
|
5439
|
+
remainingEvents: this.buffer.totalSize
|
|
5440
|
+
}
|
|
5441
|
+
},
|
|
5442
|
+
error$1
|
|
5443
|
+
);
|
|
5444
|
+
this.logger.trackException(mastraError);
|
|
5445
|
+
this.logger.error("Failed to flush remaining events during shutdown", mastraError);
|
|
4417
5446
|
}
|
|
4418
5447
|
this.logger.info("CloudExporter shutdown complete");
|
|
4419
5448
|
}
|
|
@@ -4717,7 +5746,7 @@ var DefaultExporter = class extends BaseExporter {
|
|
|
4717
5746
|
clearTimeout(this.#flushTimer);
|
|
4718
5747
|
}
|
|
4719
5748
|
this.#flushTimer = setTimeout(() => {
|
|
4720
|
-
this.
|
|
5749
|
+
this.flushBuffer().catch((error) => {
|
|
4721
5750
|
this.logger.error("Scheduled flush failed", {
|
|
4722
5751
|
error: error instanceof Error ? error.message : String(error)
|
|
4723
5752
|
});
|
|
@@ -4851,7 +5880,7 @@ var DefaultExporter = class extends BaseExporter {
|
|
|
4851
5880
|
handleBatchWithUpdatesEvent(event) {
|
|
4852
5881
|
this.addToBuffer(event);
|
|
4853
5882
|
if (this.shouldFlush()) {
|
|
4854
|
-
this.
|
|
5883
|
+
this.flushBuffer().catch((error) => {
|
|
4855
5884
|
this.logger.error("Batch flush failed", {
|
|
4856
5885
|
error: error instanceof Error ? error.message : String(error)
|
|
4857
5886
|
});
|
|
@@ -4867,7 +5896,7 @@ var DefaultExporter = class extends BaseExporter {
|
|
|
4867
5896
|
if (event.type === observability.TracingEventType.SPAN_ENDED) {
|
|
4868
5897
|
this.addToBuffer(event);
|
|
4869
5898
|
if (this.shouldFlush()) {
|
|
4870
|
-
this.
|
|
5899
|
+
this.flushBuffer().catch((error) => {
|
|
4871
5900
|
this.logger.error("Batch flush failed", {
|
|
4872
5901
|
error: error instanceof Error ? error.message : String(error)
|
|
4873
5902
|
});
|
|
@@ -4884,9 +5913,9 @@ var DefaultExporter = class extends BaseExporter {
|
|
|
4884
5913
|
return this.#config.retryDelayMs * Math.pow(2, attempt);
|
|
4885
5914
|
}
|
|
4886
5915
|
/**
|
|
4887
|
-
* Flushes the current buffer to storage with retry logic
|
|
5916
|
+
* Flushes the current buffer to storage with retry logic (internal implementation)
|
|
4888
5917
|
*/
|
|
4889
|
-
async
|
|
5918
|
+
async flushBuffer() {
|
|
4890
5919
|
if (!this.#observability) {
|
|
4891
5920
|
this.logger.debug("Cannot flush traces. Observability storage is not initialized");
|
|
4892
5921
|
return;
|
|
@@ -4993,23 +6022,25 @@ var DefaultExporter = class extends BaseExporter {
|
|
|
4993
6022
|
break;
|
|
4994
6023
|
}
|
|
4995
6024
|
}
|
|
6025
|
+
/**
|
|
6026
|
+
* Force flush any buffered spans without shutting down the exporter.
|
|
6027
|
+
* This is useful in serverless environments where you need to ensure spans
|
|
6028
|
+
* are exported before the runtime instance is terminated.
|
|
6029
|
+
*/
|
|
6030
|
+
async flush() {
|
|
6031
|
+
if (this.buffer.totalSize > 0) {
|
|
6032
|
+
this.logger.debug("Flushing buffered events", {
|
|
6033
|
+
bufferedEvents: this.buffer.totalSize
|
|
6034
|
+
});
|
|
6035
|
+
await this.flushBuffer();
|
|
6036
|
+
}
|
|
6037
|
+
}
|
|
4996
6038
|
async shutdown() {
|
|
4997
6039
|
if (this.#flushTimer) {
|
|
4998
6040
|
clearTimeout(this.#flushTimer);
|
|
4999
6041
|
this.#flushTimer = null;
|
|
5000
6042
|
}
|
|
5001
|
-
|
|
5002
|
-
this.logger.info("Flushing remaining events on shutdown", {
|
|
5003
|
-
remainingEvents: this.buffer.totalSize
|
|
5004
|
-
});
|
|
5005
|
-
try {
|
|
5006
|
-
await this.flush();
|
|
5007
|
-
} catch (error) {
|
|
5008
|
-
this.logger.error("Failed to flush remaining events during shutdown", {
|
|
5009
|
-
error: error instanceof Error ? error.message : String(error)
|
|
5010
|
-
});
|
|
5011
|
-
}
|
|
5012
|
-
}
|
|
6043
|
+
await this.flush();
|
|
5013
6044
|
this.logger.info("DefaultExporter shutdown complete");
|
|
5014
6045
|
}
|
|
5015
6046
|
};
|
|
@@ -5362,6 +6393,29 @@ var ModelSpanTracker = class {
|
|
|
5362
6393
|
break;
|
|
5363
6394
|
}
|
|
5364
6395
|
}
|
|
6396
|
+
/**
|
|
6397
|
+
* Handle tool-call-approval chunks.
|
|
6398
|
+
* Creates a span for approval requests so they can be seen in traces for debugging.
|
|
6399
|
+
*/
|
|
6400
|
+
#handleToolApprovalChunk(chunk) {
|
|
6401
|
+
if (chunk.type !== "tool-call-approval") return;
|
|
6402
|
+
const payload = chunk.payload;
|
|
6403
|
+
if (!this.#currentStepSpan) {
|
|
6404
|
+
this.startStep();
|
|
6405
|
+
}
|
|
6406
|
+
const span = this.#currentStepSpan?.createEventSpan({
|
|
6407
|
+
name: `chunk: 'tool-call-approval'`,
|
|
6408
|
+
type: observability.SpanType.MODEL_CHUNK,
|
|
6409
|
+
attributes: {
|
|
6410
|
+
chunkType: "tool-call-approval",
|
|
6411
|
+
sequenceNumber: this.#chunkSequence
|
|
6412
|
+
},
|
|
6413
|
+
output: payload
|
|
6414
|
+
});
|
|
6415
|
+
if (span) {
|
|
6416
|
+
this.#chunkSequence++;
|
|
6417
|
+
}
|
|
6418
|
+
}
|
|
5365
6419
|
/**
|
|
5366
6420
|
* Handle tool-output chunks from sub-agents.
|
|
5367
6421
|
* Consolidates streaming text/reasoning deltas into a single span per tool call.
|
|
@@ -5482,10 +6536,40 @@ var ModelSpanTracker = class {
|
|
|
5482
6536
|
case "step-finish":
|
|
5483
6537
|
this.#endStepSpan(chunk.payload);
|
|
5484
6538
|
break;
|
|
6539
|
+
// Infrastructure chunks - skip creating spans for these
|
|
6540
|
+
// They are either redundant, metadata-only, or error/control flow
|
|
5485
6541
|
case "raw":
|
|
5486
|
-
//
|
|
6542
|
+
// Redundant raw data
|
|
5487
6543
|
case "start":
|
|
6544
|
+
// Stream start marker
|
|
5488
6545
|
case "finish":
|
|
6546
|
+
// Stream finish marker (step-finish already captures this)
|
|
6547
|
+
case "response-metadata":
|
|
6548
|
+
// Response metadata (not semantic content)
|
|
6549
|
+
case "source":
|
|
6550
|
+
// Source references (metadata)
|
|
6551
|
+
case "file":
|
|
6552
|
+
// Binary file data (too large/not semantic)
|
|
6553
|
+
case "error":
|
|
6554
|
+
// Error handling
|
|
6555
|
+
case "abort":
|
|
6556
|
+
// Abort signal
|
|
6557
|
+
case "tripwire":
|
|
6558
|
+
// Processor rejection
|
|
6559
|
+
case "watch":
|
|
6560
|
+
// Internal watch event
|
|
6561
|
+
case "tool-error":
|
|
6562
|
+
// Tool error handling
|
|
6563
|
+
case "tool-call-suspended":
|
|
6564
|
+
// Suspension (not content)
|
|
6565
|
+
case "reasoning-signature":
|
|
6566
|
+
// Signature metadata
|
|
6567
|
+
case "redacted-reasoning":
|
|
6568
|
+
// Redacted content metadata
|
|
6569
|
+
case "step-output":
|
|
6570
|
+
break;
|
|
6571
|
+
case "tool-call-approval":
|
|
6572
|
+
this.#handleToolApprovalChunk(chunk);
|
|
5489
6573
|
break;
|
|
5490
6574
|
case "tool-output":
|
|
5491
6575
|
this.#handleToolOutputChunk(chunk);
|
|
@@ -5500,20 +6584,6 @@ var ModelSpanTracker = class {
|
|
|
5500
6584
|
this.#createEventSpan(chunk.type, cleanPayload);
|
|
5501
6585
|
break;
|
|
5502
6586
|
}
|
|
5503
|
-
// Default: auto-create event span for all other chunk types
|
|
5504
|
-
default: {
|
|
5505
|
-
let outputPayload = chunk.payload;
|
|
5506
|
-
if (outputPayload && typeof outputPayload === "object" && "data" in outputPayload) {
|
|
5507
|
-
const typedPayload = outputPayload;
|
|
5508
|
-
outputPayload = { ...typedPayload };
|
|
5509
|
-
if (typedPayload.data) {
|
|
5510
|
-
outputPayload.size = typeof typedPayload.data === "string" ? typedPayload.data.length : typedPayload.data instanceof Uint8Array ? typedPayload.data.length : void 0;
|
|
5511
|
-
delete outputPayload.data;
|
|
5512
|
-
}
|
|
5513
|
-
}
|
|
5514
|
-
this.#createEventSpan(chunk.type, outputPayload);
|
|
5515
|
-
break;
|
|
5516
|
-
}
|
|
5517
6587
|
}
|
|
5518
6588
|
}
|
|
5519
6589
|
})
|
|
@@ -5714,15 +6784,15 @@ var BaseSpan = class {
|
|
|
5714
6784
|
this.attributes = deepClean(options.attributes, this.deepCleanOptions) || {};
|
|
5715
6785
|
this.metadata = deepClean(options.metadata, this.deepCleanOptions);
|
|
5716
6786
|
this.parent = options.parent;
|
|
5717
|
-
this.startTime = /* @__PURE__ */ new Date();
|
|
6787
|
+
this.startTime = options.startTime ?? /* @__PURE__ */ new Date();
|
|
5718
6788
|
this.observabilityInstance = observabilityInstance;
|
|
5719
6789
|
this.isEvent = options.isEvent ?? false;
|
|
5720
6790
|
this.isInternal = isSpanInternal(this.type, options.tracingPolicy?.internal);
|
|
5721
6791
|
this.traceState = options.traceState;
|
|
5722
6792
|
this.tags = !options.parent && options.tags?.length ? options.tags : void 0;
|
|
5723
|
-
this.entityType = options.entityType;
|
|
5724
|
-
this.entityId = options.entityId;
|
|
5725
|
-
this.entityName = options.entityName;
|
|
6793
|
+
this.entityType = options.entityType ?? options.parent?.entityType;
|
|
6794
|
+
this.entityId = options.entityId ?? options.parent?.entityId;
|
|
6795
|
+
this.entityName = options.entityName ?? options.parent?.entityName;
|
|
5726
6796
|
if (this.isEvent) {
|
|
5727
6797
|
this.output = deepClean(options.output, this.deepCleanOptions);
|
|
5728
6798
|
} else {
|
|
@@ -5771,6 +6841,8 @@ var BaseSpan = class {
|
|
|
5771
6841
|
}
|
|
5772
6842
|
/** Returns a lightweight span ready for export */
|
|
5773
6843
|
exportSpan(includeInternalSpans) {
|
|
6844
|
+
const hideInput = this.traceState?.hideInput ?? false;
|
|
6845
|
+
const hideOutput = this.traceState?.hideOutput ?? false;
|
|
5774
6846
|
return {
|
|
5775
6847
|
id: this.id,
|
|
5776
6848
|
traceId: this.traceId,
|
|
@@ -5783,8 +6855,8 @@ var BaseSpan = class {
|
|
|
5783
6855
|
metadata: this.metadata,
|
|
5784
6856
|
startTime: this.startTime,
|
|
5785
6857
|
endTime: this.endTime,
|
|
5786
|
-
input: this.input,
|
|
5787
|
-
output: this.output,
|
|
6858
|
+
input: hideInput ? void 0 : this.input,
|
|
6859
|
+
output: hideOutput ? void 0 : this.output,
|
|
5788
6860
|
errorInfo: this.errorInfo,
|
|
5789
6861
|
isEvent: this.isEvent,
|
|
5790
6862
|
isRootSpan: this.isRootSpan,
|
|
@@ -5824,6 +6896,14 @@ var DefaultSpan = class extends BaseSpan {
|
|
|
5824
6896
|
traceId;
|
|
5825
6897
|
constructor(options, observabilityInstance) {
|
|
5826
6898
|
super(options, observabilityInstance);
|
|
6899
|
+
if (options.spanId && options.traceId) {
|
|
6900
|
+
this.id = options.spanId;
|
|
6901
|
+
this.traceId = options.traceId;
|
|
6902
|
+
if (options.parentSpanId) {
|
|
6903
|
+
this.parentSpanId = options.parentSpanId;
|
|
6904
|
+
}
|
|
6905
|
+
return;
|
|
6906
|
+
}
|
|
5827
6907
|
const bridge = observabilityInstance.getBridge();
|
|
5828
6908
|
if (bridge && !this.isInternal) {
|
|
5829
6909
|
const bridgeIds = bridge.createSpan(options);
|
|
@@ -6037,11 +7117,26 @@ var BaseObservabilityInstance = class extends base.MastraBase {
|
|
|
6037
7117
|
// ============================================================================
|
|
6038
7118
|
/**
|
|
6039
7119
|
* Start a new span of a specific SpanType
|
|
7120
|
+
*
|
|
7121
|
+
* Sampling Decision:
|
|
7122
|
+
* - For root spans (no parent): Perform sampling check using the configured strategy
|
|
7123
|
+
* - For child spans: Inherit the sampling decision from the parent
|
|
7124
|
+
* - If parent is a NoOpSpan (not sampled), child is also a NoOpSpan
|
|
7125
|
+
* - If parent is a valid span (sampled), child is also sampled
|
|
7126
|
+
*
|
|
7127
|
+
* This ensures trace-level sampling: either all spans in a trace are sampled or none are.
|
|
7128
|
+
* See: https://github.com/mastra-ai/mastra/issues/11504
|
|
6040
7129
|
*/
|
|
6041
7130
|
startSpan(options) {
|
|
6042
7131
|
const { customSamplerOptions, requestContext, metadata, tracingOptions, ...rest } = options;
|
|
6043
|
-
if (
|
|
6044
|
-
|
|
7132
|
+
if (options.parent) {
|
|
7133
|
+
if (!options.parent.isValid) {
|
|
7134
|
+
return new NoOpSpan({ ...rest, metadata }, this);
|
|
7135
|
+
}
|
|
7136
|
+
} else {
|
|
7137
|
+
if (!this.shouldSample(customSamplerOptions)) {
|
|
7138
|
+
return new NoOpSpan({ ...rest, metadata }, this);
|
|
7139
|
+
}
|
|
6045
7140
|
}
|
|
6046
7141
|
let traceState;
|
|
6047
7142
|
if (options.parent) {
|
|
@@ -6053,8 +7148,12 @@ var BaseObservabilityInstance = class extends base.MastraBase {
|
|
|
6053
7148
|
const mergedMetadata = metadata || tracingMetadata ? { ...metadata, ...tracingMetadata } : void 0;
|
|
6054
7149
|
const enrichedMetadata = this.extractMetadataFromRequestContext(requestContext, mergedMetadata, traceState);
|
|
6055
7150
|
const tags = !options.parent ? tracingOptions?.tags : void 0;
|
|
7151
|
+
const traceId = !options.parent ? options.traceId ?? tracingOptions?.traceId : options.traceId;
|
|
7152
|
+
const parentSpanId = !options.parent ? options.parentSpanId ?? tracingOptions?.parentSpanId : options.parentSpanId;
|
|
6056
7153
|
const span = this.createSpan({
|
|
6057
7154
|
...rest,
|
|
7155
|
+
traceId,
|
|
7156
|
+
parentSpanId,
|
|
6058
7157
|
metadata: enrichedMetadata,
|
|
6059
7158
|
traceState,
|
|
6060
7159
|
tags
|
|
@@ -6067,6 +7166,37 @@ var BaseObservabilityInstance = class extends base.MastraBase {
|
|
|
6067
7166
|
}
|
|
6068
7167
|
return span;
|
|
6069
7168
|
}
|
|
7169
|
+
/**
|
|
7170
|
+
* Rebuild a span from exported data for lifecycle operations.
|
|
7171
|
+
* Used by durable execution engines (e.g., Inngest) to end/update spans
|
|
7172
|
+
* that were created in a previous durable operation.
|
|
7173
|
+
*
|
|
7174
|
+
* The rebuilt span:
|
|
7175
|
+
* - Does NOT emit SPAN_STARTED (assumes original span already did)
|
|
7176
|
+
* - Can have end(), update(), error() called on it
|
|
7177
|
+
* - Will emit SPAN_ENDED or SPAN_UPDATED when those methods are called
|
|
7178
|
+
*
|
|
7179
|
+
* @param cached - The exported span data to rebuild from
|
|
7180
|
+
* @returns A span that can have lifecycle methods called on it
|
|
7181
|
+
*/
|
|
7182
|
+
rebuildSpan(cached) {
|
|
7183
|
+
const span = this.createSpan({
|
|
7184
|
+
name: cached.name,
|
|
7185
|
+
type: cached.type,
|
|
7186
|
+
traceId: cached.traceId,
|
|
7187
|
+
spanId: cached.id,
|
|
7188
|
+
parentSpanId: cached.parentSpanId,
|
|
7189
|
+
startTime: cached.startTime instanceof Date ? cached.startTime : new Date(cached.startTime),
|
|
7190
|
+
input: cached.input,
|
|
7191
|
+
attributes: cached.attributes,
|
|
7192
|
+
metadata: cached.metadata,
|
|
7193
|
+
entityType: cached.entityType,
|
|
7194
|
+
entityId: cached.entityId,
|
|
7195
|
+
entityName: cached.entityName
|
|
7196
|
+
});
|
|
7197
|
+
this.wireSpanLifecycle(span);
|
|
7198
|
+
return span;
|
|
7199
|
+
}
|
|
6070
7200
|
// ============================================================================
|
|
6071
7201
|
// Configuration Management
|
|
6072
7202
|
// ============================================================================
|
|
@@ -6169,11 +7299,15 @@ var BaseObservabilityInstance = class extends base.MastraBase {
|
|
|
6169
7299
|
const configuredKeys = this.config.requestContextKeys ?? [];
|
|
6170
7300
|
const additionalKeys = tracingOptions?.requestContextKeys ?? [];
|
|
6171
7301
|
const allKeys = [...configuredKeys, ...additionalKeys];
|
|
6172
|
-
|
|
7302
|
+
const hideInput = tracingOptions?.hideInput;
|
|
7303
|
+
const hideOutput = tracingOptions?.hideOutput;
|
|
7304
|
+
if (allKeys.length === 0 && !hideInput && !hideOutput) {
|
|
6173
7305
|
return void 0;
|
|
6174
7306
|
}
|
|
6175
7307
|
return {
|
|
6176
|
-
requestContextKeys: allKeys
|
|
7308
|
+
requestContextKeys: allKeys,
|
|
7309
|
+
...hideInput !== void 0 && { hideInput },
|
|
7310
|
+
...hideOutput !== void 0 && { hideOutput }
|
|
6177
7311
|
};
|
|
6178
7312
|
}
|
|
6179
7313
|
/**
|
|
@@ -6304,6 +7438,29 @@ var BaseObservabilityInstance = class extends base.MastraBase {
|
|
|
6304
7438
|
this.logger.debug(`[Observability] Initialization started [name=${this.name}]`);
|
|
6305
7439
|
this.logger.info(`[Observability] Initialized successfully [name=${this.name}]`);
|
|
6306
7440
|
}
|
|
7441
|
+
/**
|
|
7442
|
+
* Force flush any buffered/queued spans from all exporters and the bridge
|
|
7443
|
+
* without shutting down the observability instance.
|
|
7444
|
+
*
|
|
7445
|
+
* This is useful in serverless environments (like Vercel's fluid compute) where
|
|
7446
|
+
* you need to ensure all spans are exported before the runtime instance is
|
|
7447
|
+
* terminated, while keeping the observability system active for future requests.
|
|
7448
|
+
*/
|
|
7449
|
+
async flush() {
|
|
7450
|
+
this.logger.debug(`[Observability] Flush started [name=${this.name}]`);
|
|
7451
|
+
const flushPromises = [...this.exporters.map((e) => e.flush())];
|
|
7452
|
+
if (this.config.bridge) {
|
|
7453
|
+
flushPromises.push(this.config.bridge.flush());
|
|
7454
|
+
}
|
|
7455
|
+
const results = await Promise.allSettled(flushPromises);
|
|
7456
|
+
results.forEach((result, index) => {
|
|
7457
|
+
if (result.status === "rejected") {
|
|
7458
|
+
const targetName = index < this.exporters.length ? this.exporters[index]?.name : "bridge";
|
|
7459
|
+
this.logger.error(`[Observability] Flush error [target=${targetName}]`, result.reason);
|
|
7460
|
+
}
|
|
7461
|
+
});
|
|
7462
|
+
this.logger.debug(`[Observability] Flush completed [name=${this.name}]`);
|
|
7463
|
+
}
|
|
6307
7464
|
/**
|
|
6308
7465
|
* Shutdown Observability and clean up resources
|
|
6309
7466
|
*/
|
|
@@ -6613,6 +7770,9 @@ var Observability = class extends base.MastraBase {
|
|
|
6613
7770
|
}
|
|
6614
7771
|
}
|
|
6615
7772
|
if (config.default?.enabled) {
|
|
7773
|
+
console.warn(
|
|
7774
|
+
'[Mastra Observability] The "default: { enabled: true }" configuration is deprecated and will be removed in a future version. Please use explicit configs with DefaultExporter, CloudExporter, and SensitiveDataFilter instead. See https://mastra.ai/docs/observability/tracing/overview for the recommended configuration.'
|
|
7775
|
+
);
|
|
6616
7776
|
const defaultInstance = new DefaultObservabilityInstance({
|
|
6617
7777
|
serviceName: "mastra",
|
|
6618
7778
|
name: "default",
|
|
@@ -6716,7 +7876,10 @@ exports.Observability = Observability;
|
|
|
6716
7876
|
exports.SamplingStrategyType = SamplingStrategyType;
|
|
6717
7877
|
exports.SensitiveDataFilter = SensitiveDataFilter;
|
|
6718
7878
|
exports.TestExporter = TestExporter;
|
|
7879
|
+
exports.TraceData = TraceData;
|
|
7880
|
+
exports.TrackingExporter = TrackingExporter;
|
|
6719
7881
|
exports.buildTracingOptions = buildTracingOptions;
|
|
7882
|
+
exports.chainFormatters = chainFormatters;
|
|
6720
7883
|
exports.deepClean = deepClean;
|
|
6721
7884
|
exports.getExternalParentId = getExternalParentId;
|
|
6722
7885
|
exports.mergeSerializationOptions = mergeSerializationOptions;
|