@mastra/observability 1.21.1-alpha.2 → 1.22.0-alpha.3

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;
@@ -3371,8 +3397,10 @@ var BaseSpan = class BaseSpan {
3371
3397
  if (this.type !== SpanType.MODEL_STEP) return value;
3372
3398
  try {
3373
3399
  const prepared = { ...value };
3400
+ const gatewayRouting = readGatewayRouting(prepared.providerMetadata);
3374
3401
  delete prepared.providerMetadata;
3375
3402
  delete prepared.experimental_providerMetadata;
3403
+ if (gatewayRouting !== void 0) prepared.providerMetadata = { gateway: { routing: gatewayRouting } };
3376
3404
  return prepared;
3377
3405
  } catch {
3378
3406
  return value;
@@ -4029,6 +4057,8 @@ var EventBuffer = class {
4029
4057
  #creates = [];
4030
4058
  #updates = [];
4031
4059
  #allCreatedSpans = /* @__PURE__ */ new Set();
4060
+ /** Spans whose SPAN_STARTED this buffer has seen but whose create isn't confirmed yet */
4061
+ #pendingCreateSpans = /* @__PURE__ */ new Set();
4032
4062
  #firstEventTime;
4033
4063
  #storageStrategy;
4034
4064
  #maxRetries;
@@ -4079,8 +4109,11 @@ var EventBuffer = class {
4079
4109
  case TracingEventType.SPAN_STARTED:
4080
4110
  switch (this.#storageStrategy) {
4081
4111
  case "realtime":
4082
- case "event-sourced":
4083
- case "batch-with-updates": this.pushCreate(event);
4112
+ case "batch-with-updates":
4113
+ if (!event.exportedSpan.isEvent) this.#pendingCreateSpans.add(this.exportedSpanKey(event.exportedSpan));
4114
+ this.pushCreate(event);
4115
+ break;
4116
+ case "event-sourced": this.pushCreate(event);
4084
4117
  }
4085
4118
  break;
4086
4119
  case TracingEventType.SPAN_UPDATED:
@@ -4149,23 +4182,40 @@ var EventBuffer = class {
4149
4182
  buildSpanKey(span) {
4150
4183
  return `${span.traceId}:${span.spanId}`;
4151
4184
  }
4185
+ exportedSpanKey(span) {
4186
+ return this.buildSpanKey({
4187
+ traceId: span.traceId,
4188
+ spanId: span.id
4189
+ });
4190
+ }
4152
4191
  /** Track successfully created spans so updates can verify span existence before flushing. */
4153
4192
  addCreatedSpans(args) {
4154
4193
  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));
4194
+ for (const createRecord of args.records) if (!createRecord.isEvent) {
4195
+ const key = this.buildSpanKey(createRecord);
4196
+ this.#pendingCreateSpans.delete(key);
4197
+ this.#allCreatedSpans.add(key);
4198
+ }
4199
+ }
4200
+ /** Stop waiting on creates that were dropped, so their updates aren't held back for them. */
4201
+ forgetDroppedCreates(events) {
4202
+ for (const event of events) if (event.type === TracingEventType.SPAN_STARTED) this.#pendingCreateSpans.delete(this.exportedSpanKey(event.exportedSpan));
4156
4203
  }
4157
4204
  /** Check whether a span's create record has already been flushed to storage. */
4158
4205
  spanExists(span) {
4159
- return this.#allCreatedSpans?.has(this.buildSpanKey({
4160
- traceId: span.traceId,
4161
- spanId: span.id
4162
- }));
4206
+ return this.#allCreatedSpans.has(this.exportedSpanKey(span));
4207
+ }
4208
+ /** Check whether this buffer saw the span's SPAN_STARTED but its create hasn't been confirmed yet. */
4209
+ isAwaitingCreate(span) {
4210
+ return this.#pendingCreateSpans.has(this.exportedSpanKey(span));
4163
4211
  }
4164
4212
  /** Remove completed spans from tracking after their SPAN_ENDED updates are flushed. */
4165
4213
  endFinishedSpans(args) {
4166
4214
  if (this.#storageStrategy === "event-sourced" || this.#storageStrategy === "insert-only") return;
4167
4215
  args.records.forEach((r) => {
4168
- this.#allCreatedSpans.delete(this.buildSpanKey(r));
4216
+ const key = this.buildSpanKey(r);
4217
+ this.#allCreatedSpans.delete(key);
4218
+ this.#pendingCreateSpans.delete(key);
4169
4219
  });
4170
4220
  }
4171
4221
  };
@@ -4607,6 +4657,21 @@ var DefaultExporter = class extends BaseExporter {
4607
4657
  this.logger.info("DefaultExporter shutdown complete");
4608
4658
  }
4609
4659
  };
4660
+ // src/exporters/span-previews.ts
4661
+ const buildSpanPreviews = coreObservability.buildSpanPreviews;
4662
+ /** Older core versions retain their existing storage preview fallback. */
4663
+ function withSpanPreviews(span) {
4664
+ if (!coreFeatures.has("observability-span-previews")) return span;
4665
+ try {
4666
+ const previews = buildSpanPreviews?.(span);
4667
+ return previews ? {
4668
+ ...span,
4669
+ ...previews
4670
+ } : span;
4671
+ } catch {
4672
+ return span;
4673
+ }
4674
+ }
4610
4675
  // src/exporters/mastra-storage.ts
4611
4676
  /**
4612
4677
  * Resolves the final tracing storage strategy based on config and observability store hints
@@ -4810,6 +4875,7 @@ var MastraStorageExporter = class extends BaseExporter {
4810
4875
  return { failed: true };
4811
4876
  }
4812
4877
  const dropped = this.#eventBuffer.reAddCreates(events);
4878
+ this.#eventBuffer.forgetDroppedCreates(dropped);
4813
4879
  const retryAttempt = this.retryAttempt(events);
4814
4880
  this.logStorageFailure(signal, events, retryAttempt, error);
4815
4881
  this.emitDrop(signal, "retry-exhausted", dropped.length, error);
@@ -4820,7 +4886,15 @@ var MastraStorageExporter = class extends BaseExporter {
4820
4886
  }
4821
4887
  }
4822
4888
  /**
4823
- * Flush span update/end events, deferring any whose span hasn't been created yet.
4889
+ * Flush span update/end events.
4890
+ *
4891
+ * When the store supports conditional updates, only spans this exporter started
4892
+ * and hasn't confirmed the create for are deferred. Everything else, including
4893
+ * spans another exporter instance started (such as a durable agent ending spans
4894
+ * it rebuilt after a crash), goes straight to storage, and updates that didn't
4895
+ * apply are counted as drops. Without conditional updates, updates for spans this
4896
+ * exporter hasn't created are deferred until their create is flushed.
4897
+ *
4824
4898
  * When `isEnd` is true, successfully flushed spans are removed from tracking.
4825
4899
  */
4826
4900
  async flushSpanUpdates(events, deferredUpdates, isEnd) {
@@ -4830,10 +4904,12 @@ var MastraStorageExporter = class extends BaseExporter {
4830
4904
  this.emitDrop("tracing", "unsupported-storage", events.length);
4831
4905
  return { failed: true };
4832
4906
  }
4907
+ const storage = this.#observabilityStorage;
4908
+ const batchUpdateUnendedSpans = typeof storage.batchUpdateUnendedSpans === "function" ? storage.batchUpdateUnendedSpans.bind(storage) : void 0;
4833
4909
  const partials = [];
4834
4910
  for (const event of events) {
4835
4911
  const span = event.exportedSpan;
4836
- if (this.#eventBuffer.spanExists(span)) partials.push({
4912
+ if (batchUpdateUnendedSpans ? !this.#eventBuffer.isAwaitingCreate(span) : this.#eventBuffer.spanExists(span)) partials.push({
4837
4913
  traceId: span.traceId,
4838
4914
  spanId: span.id,
4839
4915
  updates: buildUpdateSpanRecord(span)
@@ -4842,7 +4918,10 @@ var MastraStorageExporter = class extends BaseExporter {
4842
4918
  }
4843
4919
  if (partials.length === 0) return FLUSH_SUCCEEDED;
4844
4920
  try {
4845
- await this.#observabilityStorage.batchUpdateSpans({ records: partials });
4921
+ if (batchUpdateUnendedSpans) {
4922
+ const { updated } = await batchUpdateUnendedSpans({ records: partials });
4923
+ this.emitNotApplied(partials, updated);
4924
+ } else await storage.batchUpdateSpans({ records: partials });
4846
4925
  if (isEnd) this.#eventBuffer.endFinishedSpans({ records: partials });
4847
4926
  return FLUSH_SUCCEEDED;
4848
4927
  } catch (error) {
@@ -4866,6 +4945,27 @@ var MastraStorageExporter = class extends BaseExporter {
4866
4945
  }
4867
4946
  }
4868
4947
  /**
4948
+ * Reports conditional span updates that storage didn't apply: the span wasn't
4949
+ * stored, or it had already ended. Ended spans are immutable, so these are dropped.
4950
+ */
4951
+ emitNotApplied(partials, updated) {
4952
+ const appliedCounts = /* @__PURE__ */ new Map();
4953
+ for (const span of updated) {
4954
+ const key = `${span.traceId}:${span.spanId}`;
4955
+ appliedCounts.set(key, (appliedCounts.get(key) ?? 0) + 1);
4956
+ }
4957
+ let notApplied = 0;
4958
+ for (const partial of partials) {
4959
+ const key = `${partial.traceId}:${partial.spanId}`;
4960
+ const remaining = appliedCounts.get(key) ?? 0;
4961
+ if (remaining > 0) appliedCounts.set(key, remaining - 1);
4962
+ else notApplied++;
4963
+ }
4964
+ if (notApplied === 0) return;
4965
+ this.logger.debug("Span updates not applied: span not stored or already ended", { count: notApplied });
4966
+ this.emitDrop("tracing", "span-not-updatable", notApplied);
4967
+ }
4968
+ /**
4869
4969
  * Runs flushBuffer() and tracks its promise so flush() can wait for writes
4870
4970
  * that a timer or size trigger started on their own.
4871
4971
  */
@@ -4975,7 +5075,10 @@ var MastraStorageExporter = class extends BaseExporter {
4975
5075
  this.logger.debug("Cannot store traces. Observability storage is not initialized");
4976
5076
  return;
4977
5077
  }
4978
- this.#eventBuffer.addEvent(event);
5078
+ this.#eventBuffer.addEvent({
5079
+ ...event,
5080
+ exportedSpan: withSpanPreviews(event.exportedSpan)
5081
+ });
4979
5082
  await this.handleBatchedFlush();
4980
5083
  }
4981
5084
  /**
@@ -8079,7 +8182,7 @@ var MastraPlatformExporter = class extends BaseExporter {
8079
8182
  }
8080
8183
  formatSpan(span) {
8081
8184
  return {
8082
- ...span,
8185
+ ...withSpanPreviews(span),
8083
8186
  spanId: span.id,
8084
8187
  spanType: span.type,
8085
8188
  startedAt: span.startTime,