@mastra/observability 1.21.1-alpha.2 → 1.22.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import { createRequire } from "module";
2
2
  import { MastraBase } from "@mastra/core/base";
3
3
  import { ErrorCategory, ErrorDomain, MastraError } from "@mastra/core/error";
4
4
  import { ConsoleLogger, LogLevel, RegisteredLogger } from "@mastra/core/logger";
5
+ import * as coreObservability from "@mastra/core/observability";
5
6
  import { DEFAULT_BLOCKED_LABELS, EntityType, InternalSpans, SpanType, TracingEventType, noOpLoggerContext } from "@mastra/core/observability";
6
7
  import { fetchWithRetry, getNestedValue, setNestedValue } from "@mastra/core/utils";
7
8
  import { z } from "zod/v4";
@@ -2712,8 +2713,20 @@ var ModelSpanTracker = class {
2712
2713
  * feature flag, or when called without an active step span. Auto-invoked from
2713
2714
  * chunk handlers as a safety net; explicit callers get the most accurate
2714
2715
  * start time.
2716
+ *
2717
+ * `payload.modelId` / `payload.provider` name the model the caller is about
2718
+ * to invoke and are recorded on MODEL_STEP, also when a fallback attempt
2719
+ * reuses the step's open inference span. The safety net never records them:
2720
+ * a replayed step-start chunk names the model of the original call.
2715
2721
  */
2716
2722
  startInference(payload) {
2723
+ if (payload?.modelId && payload.provider) this.#currentStepSpan?.update({ attributes: {
2724
+ model: payload.modelId,
2725
+ provider: payload.provider
2726
+ } });
2727
+ this.#openInference(payload);
2728
+ }
2729
+ #openInference(payload) {
2717
2730
  if (!supportsModelInference()) return;
2718
2731
  if (!this.#currentStepSpan || this.#currentInferenceSpan) return;
2719
2732
  this.#inferenceCompletionStartTime = void 0;
@@ -2838,7 +2851,7 @@ var ModelSpanTracker = class {
2838
2851
  */
2839
2852
  #ensureStepAndInference() {
2840
2853
  if (!this.#currentStepSpan) this.startStep();
2841
- if (!this.#currentInferenceSpan) this.startInference();
2854
+ if (!this.#currentInferenceSpan) this.#openInference();
2842
2855
  }
2843
2856
  /**
2844
2857
  * Create a new chunk span (for multi-part chunks like text-start/delta/end)
@@ -3038,7 +3051,7 @@ var ModelSpanTracker = class {
3038
3051
  this.#inModelResponse = true;
3039
3052
  if (this.#currentStepSpan) this.updateStep(chunk.payload);
3040
3053
  else this.startStep(chunk.payload);
3041
- if (!this.#currentInferenceSpan) this.startInference(chunk.payload);
3054
+ if (!this.#currentInferenceSpan) this.#openInference(chunk.payload);
3042
3055
  break;
3043
3056
  case "step-finish":
3044
3057
  this.#inModelResponse = false;
@@ -3244,6 +3257,19 @@ function getExternalParentId(options) {
3244
3257
  if (parent.isInternal || parent.isExcluded) return parent.getParentSpanId(false);
3245
3258
  return parent.id;
3246
3259
  }
3260
+ /**
3261
+ * Vercel AI Gateway reports which provider served a step, and any provider
3262
+ * attempts that failed first, only in `providerMetadata.gateway.routing`.
3263
+ * Read it defensively so a throwing getter never skips the strip below.
3264
+ */
3265
+ function readGatewayRouting(providerMetadata) {
3266
+ try {
3267
+ const gateway = isPlainRecord(providerMetadata) ? providerMetadata.gateway : void 0;
3268
+ return isPlainRecord(gateway) ? gateway.routing : void 0;
3269
+ } catch {
3270
+ return;
3271
+ }
3272
+ }
3247
3273
  var BaseSpan = class BaseSpan {
3248
3274
  name;
3249
3275
  type;
@@ -3273,6 +3299,11 @@ var BaseSpan = class BaseSpan {
3273
3299
  parentSpanId;
3274
3300
  /** Parent from an external tracing system (ambient OTel / dd-trace span) that Mastra did not create; carried for external correlation, not part of Mastra's own parent/child linkage */
3275
3301
  externalParentSpanId;
3302
+ /** Native span fields (see `SpanData`); carried as supplied and exported only when set */
3303
+ resumedFromSpanId;
3304
+ outcome;
3305
+ stopReason;
3306
+ stopReasonContext;
3276
3307
  /** Deep clean options for serialization */
3277
3308
  deepCleanOptions;
3278
3309
  /**
@@ -3339,6 +3370,10 @@ var BaseSpan = class BaseSpan {
3339
3370
  }
3340
3371
  this.tags = !options.parent && options.tags?.length ? options.tags : void 0;
3341
3372
  this.links = validLinks(options.links);
3373
+ this.resumedFromSpanId = options.resumedFromSpanId;
3374
+ this.outcome = options.outcome;
3375
+ this.stopReason = options.stopReason;
3376
+ this.stopReasonContext = deepClean(options.stopReasonContext, this.deepCleanOptions);
3342
3377
  const entityParent = this.getParentSpan(false);
3343
3378
  this.entityType = options.entityType ?? entityParent?.entityType;
3344
3379
  this.entityId = options.entityId ?? entityParent?.entityId;
@@ -3371,8 +3406,10 @@ var BaseSpan = class BaseSpan {
3371
3406
  if (this.type !== SpanType.MODEL_STEP) return value;
3372
3407
  try {
3373
3408
  const prepared = { ...value };
3409
+ const gatewayRouting = readGatewayRouting(prepared.providerMetadata);
3374
3410
  delete prepared.providerMetadata;
3375
3411
  delete prepared.experimental_providerMetadata;
3412
+ if (gatewayRouting !== void 0) prepared.providerMetadata = { gateway: { routing: gatewayRouting } };
3376
3413
  return prepared;
3377
3414
  } catch {
3378
3415
  return value;
@@ -3605,6 +3642,10 @@ var BaseSpan = class BaseSpan {
3605
3642
  ...definedLineage(correlation),
3606
3643
  parentSpanId: this.getParentSpanId(includeInternalSpans),
3607
3644
  externalParentSpanId: this.getExportedExternalParentSpanId(includeInternalSpans),
3645
+ ...this.resumedFromSpanId !== void 0 ? { resumedFromSpanId: this.resumedFromSpanId } : {},
3646
+ ...this.outcome !== void 0 ? { outcome: this.outcome } : {},
3647
+ ...this.stopReason !== void 0 ? { stopReason: this.stopReason } : {},
3648
+ ...this.stopReasonContext !== void 0 ? { stopReasonContext: this.stopReasonContext } : {},
3608
3649
  ...nestedUnderParent ? { nestedUnderParent: true } : {},
3609
3650
  ...links ? { links } : {},
3610
3651
  ...this.isRootSpan && !nestedUnderParent && this.tags?.length ? { tags: this.tags } : {}
@@ -3833,7 +3874,7 @@ function isValidSpanId(spanId) {
3833
3874
  function getOrCreateTraceId(options) {
3834
3875
  if (options.traceId) {
3835
3876
  if (isValidTraceId(options.traceId)) return options.traceId;
3836
- else console.error(`[Mastra Tracing] Invalid traceId: must be 1-32 hexadecimal characters, got "${options.traceId}". Generating new trace ID.`);
3877
+ console.error(`[Mastra Tracing] Invalid traceId: must be 1-32 hexadecimal characters, got "${options.traceId}". Generating new trace ID.`);
3837
3878
  }
3838
3879
  return generateTraceId();
3839
3880
  }
@@ -4029,6 +4070,8 @@ var EventBuffer = class {
4029
4070
  #creates = [];
4030
4071
  #updates = [];
4031
4072
  #allCreatedSpans = /* @__PURE__ */ new Set();
4073
+ /** Spans whose SPAN_STARTED this buffer has seen but whose create isn't confirmed yet */
4074
+ #pendingCreateSpans = /* @__PURE__ */ new Set();
4032
4075
  #firstEventTime;
4033
4076
  #storageStrategy;
4034
4077
  #maxRetries;
@@ -4079,8 +4122,11 @@ var EventBuffer = class {
4079
4122
  case TracingEventType.SPAN_STARTED:
4080
4123
  switch (this.#storageStrategy) {
4081
4124
  case "realtime":
4082
- case "event-sourced":
4083
- case "batch-with-updates": this.pushCreate(event);
4125
+ case "batch-with-updates":
4126
+ if (!event.exportedSpan.isEvent) this.#pendingCreateSpans.add(this.exportedSpanKey(event.exportedSpan));
4127
+ this.pushCreate(event);
4128
+ break;
4129
+ case "event-sourced": this.pushCreate(event);
4084
4130
  }
4085
4131
  break;
4086
4132
  case TracingEventType.SPAN_UPDATED:
@@ -4149,23 +4195,40 @@ var EventBuffer = class {
4149
4195
  buildSpanKey(span) {
4150
4196
  return `${span.traceId}:${span.spanId}`;
4151
4197
  }
4198
+ exportedSpanKey(span) {
4199
+ return this.buildSpanKey({
4200
+ traceId: span.traceId,
4201
+ spanId: span.id
4202
+ });
4203
+ }
4152
4204
  /** Track successfully created spans so updates can verify span existence before flushing. */
4153
4205
  addCreatedSpans(args) {
4154
4206
  if (this.#storageStrategy === "event-sourced" || this.#storageStrategy === "insert-only") return;
4155
- for (const createRecord of args.records) if (!createRecord.isEvent) this.#allCreatedSpans.add(this.buildSpanKey(createRecord));
4207
+ for (const createRecord of args.records) if (!createRecord.isEvent) {
4208
+ const key = this.buildSpanKey(createRecord);
4209
+ this.#pendingCreateSpans.delete(key);
4210
+ this.#allCreatedSpans.add(key);
4211
+ }
4212
+ }
4213
+ /** Stop waiting on creates that were dropped, so their updates aren't held back for them. */
4214
+ forgetDroppedCreates(events) {
4215
+ for (const event of events) if (event.type === TracingEventType.SPAN_STARTED) this.#pendingCreateSpans.delete(this.exportedSpanKey(event.exportedSpan));
4156
4216
  }
4157
4217
  /** Check whether a span's create record has already been flushed to storage. */
4158
4218
  spanExists(span) {
4159
- return this.#allCreatedSpans?.has(this.buildSpanKey({
4160
- traceId: span.traceId,
4161
- spanId: span.id
4162
- }));
4219
+ return this.#allCreatedSpans.has(this.exportedSpanKey(span));
4220
+ }
4221
+ /** Check whether this buffer saw the span's SPAN_STARTED but its create hasn't been confirmed yet. */
4222
+ isAwaitingCreate(span) {
4223
+ return this.#pendingCreateSpans.has(this.exportedSpanKey(span));
4163
4224
  }
4164
4225
  /** Remove completed spans from tracking after their SPAN_ENDED updates are flushed. */
4165
4226
  endFinishedSpans(args) {
4166
4227
  if (this.#storageStrategy === "event-sourced" || this.#storageStrategy === "insert-only") return;
4167
4228
  args.records.forEach((r) => {
4168
- this.#allCreatedSpans.delete(this.buildSpanKey(r));
4229
+ const key = this.buildSpanKey(r);
4230
+ this.#allCreatedSpans.delete(key);
4231
+ this.#pendingCreateSpans.delete(key);
4169
4232
  });
4170
4233
  }
4171
4234
  };
@@ -4607,6 +4670,21 @@ var DefaultExporter = class extends BaseExporter {
4607
4670
  this.logger.info("DefaultExporter shutdown complete");
4608
4671
  }
4609
4672
  };
4673
+ // src/exporters/span-previews.ts
4674
+ const buildSpanPreviews = coreObservability.buildSpanPreviews;
4675
+ /** Older core versions retain their existing storage preview fallback. */
4676
+ function withSpanPreviews(span) {
4677
+ if (!coreFeatures.has("observability-span-previews")) return span;
4678
+ try {
4679
+ const previews = buildSpanPreviews?.(span);
4680
+ return previews ? {
4681
+ ...span,
4682
+ ...previews
4683
+ } : span;
4684
+ } catch {
4685
+ return span;
4686
+ }
4687
+ }
4610
4688
  // src/exporters/mastra-storage.ts
4611
4689
  /**
4612
4690
  * Resolves the final tracing storage strategy based on config and observability store hints
@@ -4810,6 +4888,7 @@ var MastraStorageExporter = class extends BaseExporter {
4810
4888
  return { failed: true };
4811
4889
  }
4812
4890
  const dropped = this.#eventBuffer.reAddCreates(events);
4891
+ this.#eventBuffer.forgetDroppedCreates(dropped);
4813
4892
  const retryAttempt = this.retryAttempt(events);
4814
4893
  this.logStorageFailure(signal, events, retryAttempt, error);
4815
4894
  this.emitDrop(signal, "retry-exhausted", dropped.length, error);
@@ -4820,7 +4899,15 @@ var MastraStorageExporter = class extends BaseExporter {
4820
4899
  }
4821
4900
  }
4822
4901
  /**
4823
- * Flush span update/end events, deferring any whose span hasn't been created yet.
4902
+ * Flush span update/end events.
4903
+ *
4904
+ * When the store supports conditional updates, only spans this exporter started
4905
+ * and hasn't confirmed the create for are deferred. Everything else, including
4906
+ * spans another exporter instance started (such as a durable agent ending spans
4907
+ * it rebuilt after a crash), goes straight to storage, and updates that didn't
4908
+ * apply are counted as drops. Without conditional updates, updates for spans this
4909
+ * exporter hasn't created are deferred until their create is flushed.
4910
+ *
4824
4911
  * When `isEnd` is true, successfully flushed spans are removed from tracking.
4825
4912
  */
4826
4913
  async flushSpanUpdates(events, deferredUpdates, isEnd) {
@@ -4830,10 +4917,12 @@ var MastraStorageExporter = class extends BaseExporter {
4830
4917
  this.emitDrop("tracing", "unsupported-storage", events.length);
4831
4918
  return { failed: true };
4832
4919
  }
4920
+ const storage = this.#observabilityStorage;
4921
+ const batchUpdateUnendedSpans = typeof storage.batchUpdateUnendedSpans === "function" ? storage.batchUpdateUnendedSpans.bind(storage) : void 0;
4833
4922
  const partials = [];
4834
4923
  for (const event of events) {
4835
4924
  const span = event.exportedSpan;
4836
- if (this.#eventBuffer.spanExists(span)) partials.push({
4925
+ if (batchUpdateUnendedSpans ? !this.#eventBuffer.isAwaitingCreate(span) : this.#eventBuffer.spanExists(span)) partials.push({
4837
4926
  traceId: span.traceId,
4838
4927
  spanId: span.id,
4839
4928
  updates: buildUpdateSpanRecord(span)
@@ -4842,7 +4931,10 @@ var MastraStorageExporter = class extends BaseExporter {
4842
4931
  }
4843
4932
  if (partials.length === 0) return FLUSH_SUCCEEDED;
4844
4933
  try {
4845
- await this.#observabilityStorage.batchUpdateSpans({ records: partials });
4934
+ if (batchUpdateUnendedSpans) {
4935
+ const { updated } = await batchUpdateUnendedSpans({ records: partials });
4936
+ this.emitNotApplied(partials, updated);
4937
+ } else await storage.batchUpdateSpans({ records: partials });
4846
4938
  if (isEnd) this.#eventBuffer.endFinishedSpans({ records: partials });
4847
4939
  return FLUSH_SUCCEEDED;
4848
4940
  } catch (error) {
@@ -4866,6 +4958,27 @@ var MastraStorageExporter = class extends BaseExporter {
4866
4958
  }
4867
4959
  }
4868
4960
  /**
4961
+ * Reports conditional span updates that storage didn't apply: the span wasn't
4962
+ * stored, or it had already ended. Ended spans are immutable, so these are dropped.
4963
+ */
4964
+ emitNotApplied(partials, updated) {
4965
+ const appliedCounts = /* @__PURE__ */ new Map();
4966
+ for (const span of updated) {
4967
+ const key = `${span.traceId}:${span.spanId}`;
4968
+ appliedCounts.set(key, (appliedCounts.get(key) ?? 0) + 1);
4969
+ }
4970
+ let notApplied = 0;
4971
+ for (const partial of partials) {
4972
+ const key = `${partial.traceId}:${partial.spanId}`;
4973
+ const remaining = appliedCounts.get(key) ?? 0;
4974
+ if (remaining > 0) appliedCounts.set(key, remaining - 1);
4975
+ else notApplied++;
4976
+ }
4977
+ if (notApplied === 0) return;
4978
+ this.logger.debug("Span updates not applied: span not stored or already ended", { count: notApplied });
4979
+ this.emitDrop("tracing", "span-not-updatable", notApplied);
4980
+ }
4981
+ /**
4869
4982
  * Runs flushBuffer() and tracks its promise so flush() can wait for writes
4870
4983
  * that a timer or size trigger started on their own.
4871
4984
  */
@@ -4975,7 +5088,10 @@ var MastraStorageExporter = class extends BaseExporter {
4975
5088
  this.logger.debug("Cannot store traces. Observability storage is not initialized");
4976
5089
  return;
4977
5090
  }
4978
- this.#eventBuffer.addEvent(event);
5091
+ this.#eventBuffer.addEvent({
5092
+ ...event,
5093
+ exportedSpan: withSpanPreviews(event.exportedSpan)
5094
+ });
4979
5095
  await this.handleBatchedFlush();
4980
5096
  }
4981
5097
  /**
@@ -5227,6 +5343,10 @@ var BaseObservabilityInstance = class extends MastraBase {
5227
5343
  spanId: cached.id,
5228
5344
  parentSpanId: cached.parentSpanId,
5229
5345
  externalParentSpanId: cached.externalParentSpanId,
5346
+ resumedFromSpanId: cached.resumedFromSpanId,
5347
+ outcome: cached.outcome,
5348
+ stopReason: cached.stopReason,
5349
+ stopReasonContext: cached.stopReasonContext,
5230
5350
  links: cached.links,
5231
5351
  startTime: cached.startTime instanceof Date ? cached.startTime : new Date(cached.startTime),
5232
5352
  input: cached.input,
@@ -8079,7 +8199,7 @@ var MastraPlatformExporter = class extends BaseExporter {
8079
8199
  }
8080
8200
  formatSpan(span) {
8081
8201
  return {
8082
- ...span,
8202
+ ...withSpanPreviews(span),
8083
8203
  spanId: span.id,
8084
8204
  spanType: span.type,
8085
8205
  startedAt: span.startTime,