@mastra/observability 1.16.6-alpha.3 → 1.16.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -5178,15 +5178,15 @@ function isAuthFailureError(error) {
5178
5178
  function isAuthFailureStatus(status) {
5179
5179
  return AUTH_FAILURE_STATUSES.has(status);
5180
5180
  }
5181
- async function fetchWithAuthFailureHandling(url, options, maxRetries) {
5181
+ async function fetchWithAuthFailureHandling(url, options, maxRetries, callerShouldRetry) {
5182
5182
  let authFailureStatus;
5183
5183
  try {
5184
- await fetchWithRetry(url, options, maxRetries, { shouldRetryResponse: (response) => {
5184
+ return await fetchWithRetry(url, options, maxRetries, { shouldRetryResponse: (response) => {
5185
5185
  if (isAuthFailureStatus(response.status)) {
5186
5186
  authFailureStatus = response.status;
5187
5187
  return false;
5188
5188
  }
5189
- return true;
5189
+ return callerShouldRetry?.(response) ?? true;
5190
5190
  } });
5191
5191
  } catch (error) {
5192
5192
  if (authFailureStatus !== void 0) throw new AuthFailureError(authFailureStatus, error);
@@ -6316,6 +6316,25 @@ const SIGNAL_PUBLISH_SUFFIXES = {
6316
6316
  feedback: "/feedback/publish"
6317
6317
  };
6318
6318
  const DEFAULT_PLATFORM_SPAN_FILTER = (span) => span.type !== SpanType.MODEL_CHUNK;
6319
+ const QUOTA_EXCEEDED_STATUS = 402;
6320
+ const OBSERVABILITY_STATUS_HEADER = "x-mastra-observability";
6321
+ const OBSERVABILITY_DISABLED_VALUE = "disabled";
6322
+ const OBSERVABILITY_RETRY_AFTER_HEADER = "x-mastra-observability-retry-after";
6323
+ const DEFAULT_QUOTA_PROBE_INTERVAL_SECONDS = 300;
6324
+ const MAX_QUOTA_PROBE_INTERVAL_SECONDS = Math.floor(2147483647 / 1e3);
6325
+ function isObservabilityDisabled(response) {
6326
+ return response.headers.get(OBSERVABILITY_STATUS_HEADER) === OBSERVABILITY_DISABLED_VALUE;
6327
+ }
6328
+ function isQuotaExceededResponse(response) {
6329
+ return response.status === QUOTA_EXCEEDED_STATUS || isObservabilityDisabled(response);
6330
+ }
6331
+ function parseQuotaRetryAfterSeconds(response) {
6332
+ const raw = response.headers.get(OBSERVABILITY_RETRY_AFTER_HEADER);
6333
+ if (!raw || !/^\d+$/.test(raw.trim())) return DEFAULT_QUOTA_PROBE_INTERVAL_SECONDS;
6334
+ const parsed = Number.parseInt(raw, 10);
6335
+ if (!Number.isFinite(parsed) || parsed <= 0) return DEFAULT_QUOTA_PROBE_INTERVAL_SECONDS;
6336
+ return Math.min(parsed, MAX_QUOTA_PROBE_INTERVAL_SECONDS);
6337
+ }
6319
6338
  const SIGNAL_PUBLISH_SEGMENTS = {
6320
6339
  traces: "spans",
6321
6340
  logs: "logs",
@@ -6405,6 +6424,10 @@ var MastraPlatformExporter = class extends BaseExporter {
6405
6424
  buffer;
6406
6425
  flushTimer = null;
6407
6426
  inFlightFlushes = /* @__PURE__ */ new Set();
6427
+ quotaPaused = false;
6428
+ quotaProbeTimer = null;
6429
+ quotaProbeIntervalSeconds = DEFAULT_QUOTA_PROBE_INTERVAL_SECONDS;
6430
+ shuttingDown = false;
6408
6431
  constructor(config = {}) {
6409
6432
  super(config);
6410
6433
  if (config.projectId !== void 0 && !VALID_PROJECT_ID.test(config.projectId)) throw createInvalidProjectIdError(config.projectId);
@@ -6453,31 +6476,31 @@ var MastraPlatformExporter = class extends BaseExporter {
6453
6476
  async _exportTracingEvent(event) {
6454
6477
  if (event.type !== TracingEventType.SPAN_ENDED) return;
6455
6478
  if (!DEFAULT_PLATFORM_SPAN_FILTER(event.exportedSpan)) return;
6456
- if (this.authFailureCooldown.dropEventIfCoolingDown()) return;
6479
+ if (this.quotaPaused || this.authFailureCooldown.dropEventIfCoolingDown()) return;
6457
6480
  this.addToBuffer(event);
6458
6481
  await this.handleBufferedEvent();
6459
6482
  }
6460
6483
  async onLogEvent(event) {
6461
6484
  if (this.isDisabled) return;
6462
- if (this.authFailureCooldown.dropEventIfCoolingDown()) return;
6485
+ if (this.quotaPaused || this.authFailureCooldown.dropEventIfCoolingDown()) return;
6463
6486
  this.addLogToBuffer(event);
6464
6487
  await this.handleBufferedEvent();
6465
6488
  }
6466
6489
  async onMetricEvent(event) {
6467
6490
  if (this.isDisabled) return;
6468
- if (this.authFailureCooldown.dropEventIfCoolingDown()) return;
6491
+ if (this.quotaPaused || this.authFailureCooldown.dropEventIfCoolingDown()) return;
6469
6492
  this.addMetricToBuffer(event);
6470
6493
  await this.handleBufferedEvent();
6471
6494
  }
6472
6495
  async onScoreEvent(event) {
6473
6496
  if (this.isDisabled) return;
6474
- if (this.authFailureCooldown.dropEventIfCoolingDown()) return;
6497
+ if (this.quotaPaused || this.authFailureCooldown.dropEventIfCoolingDown()) return;
6475
6498
  this.addScoreToBuffer(event);
6476
6499
  await this.handleBufferedEvent();
6477
6500
  }
6478
6501
  async onFeedbackEvent(event) {
6479
6502
  if (this.isDisabled) return;
6480
- if (this.authFailureCooldown.dropEventIfCoolingDown()) return;
6503
+ if (this.quotaPaused || this.authFailureCooldown.dropEventIfCoolingDown()) return;
6481
6504
  this.addFeedbackToBuffer(event);
6482
6505
  await this.handleBufferedEvent();
6483
6506
  }
@@ -6567,6 +6590,10 @@ var MastraPlatformExporter = class extends BaseExporter {
6567
6590
  this.flushTimer = null;
6568
6591
  }
6569
6592
  if (this.buffer.totalSize === 0) return;
6593
+ if (this.quotaPaused) {
6594
+ this.resetBuffer();
6595
+ return;
6596
+ }
6570
6597
  if (this.authFailureCooldown.dropEventsIfCoolingDown(this.buffer.totalSize)) {
6571
6598
  this.resetBuffer();
6572
6599
  return;
@@ -6616,11 +6643,16 @@ var MastraPlatformExporter = class extends BaseExporter {
6616
6643
  /**
6617
6644
  * Uploads a signal batch to the configured Mastra Observability API using fetchWithRetry.
6618
6645
  */
6619
- async batchUpload(signal, records) {
6620
- const headers = {
6646
+ buildPublishHeaders() {
6647
+ return {
6621
6648
  Authorization: `Bearer ${this.platformConfig.accessToken}`,
6622
6649
  "Content-Type": "application/json"
6623
6650
  };
6651
+ }
6652
+ buildPublishBody(signal, records) {
6653
+ return JSON.stringify({ [SIGNAL_PUBLISH_SEGMENTS[signal]]: records });
6654
+ }
6655
+ async batchUpload(signal, records) {
6624
6656
  const endpointMap = {
6625
6657
  traces: this.platformConfig.tracesEndpoint,
6626
6658
  logs: this.platformConfig.logsEndpoint,
@@ -6630,10 +6662,85 @@ var MastraPlatformExporter = class extends BaseExporter {
6630
6662
  };
6631
6663
  const options = {
6632
6664
  method: "POST",
6633
- headers,
6634
- body: JSON.stringify({ [SIGNAL_PUBLISH_SEGMENTS[signal]]: records })
6665
+ headers: this.buildPublishHeaders(),
6666
+ body: this.buildPublishBody(signal, records)
6635
6667
  };
6636
- await fetchWithAuthFailureHandling(endpointMap[signal], options, this.platformConfig.maxRetries);
6668
+ let quotaResponse;
6669
+ let response;
6670
+ try {
6671
+ response = await fetchWithAuthFailureHandling(endpointMap[signal], options, this.platformConfig.maxRetries, (res) => {
6672
+ if (isQuotaExceededResponse(res)) {
6673
+ quotaResponse = res;
6674
+ return false;
6675
+ }
6676
+ return true;
6677
+ });
6678
+ } catch (error) {
6679
+ if (quotaResponse) {
6680
+ this.enterQuotaPause(parseQuotaRetryAfterSeconds(quotaResponse));
6681
+ return;
6682
+ }
6683
+ throw error;
6684
+ }
6685
+ if (isObservabilityDisabled(response)) this.enterQuotaPause(parseQuotaRetryAfterSeconds(response));
6686
+ }
6687
+ /**
6688
+ * Enter the quota-exhausted paused state: drop buffered events, stop the
6689
+ * flush timer, and start probing until the collector stops rejecting with
6690
+ * 402. The gate is per-org, so a signal on any publish route pauses all
6691
+ * five signal types.
6692
+ */
6693
+ enterQuotaPause(retryAfterSeconds) {
6694
+ this.quotaProbeIntervalSeconds = retryAfterSeconds;
6695
+ if (this.quotaPaused) return;
6696
+ this.quotaPaused = true;
6697
+ if (this.flushTimer) {
6698
+ clearTimeout(this.flushTimer);
6699
+ this.flushTimer = null;
6700
+ }
6701
+ this.resetBuffer();
6702
+ this.logger.warn(`Mastra observability paused: quota exhausted, dropping telemetry and probing every ${retryAfterSeconds}s`);
6703
+ this.scheduleQuotaProbe();
6704
+ }
6705
+ scheduleQuotaProbe() {
6706
+ if (this.shuttingDown) return;
6707
+ if (this.quotaProbeTimer) clearTimeout(this.quotaProbeTimer);
6708
+ this.quotaProbeTimer = setTimeout(() => {
6709
+ this.quotaProbeTimer = null;
6710
+ this.probeQuotaStatus();
6711
+ }, this.quotaProbeIntervalSeconds * 1e3);
6712
+ this.quotaProbeTimer.unref?.();
6713
+ }
6714
+ /**
6715
+ * Send an empty spans batch as a free probe. The collector treats an empty
6716
+ * batch as a no-op, and an exhausted org still gets the 402 on it. A 402
6717
+ * throws out of fetchWithRetry without the Response, so the retry predicate
6718
+ * captures it to keep the retry-after hint.
6719
+ */
6720
+ async probeQuotaStatus() {
6721
+ if (this.shuttingDown || !this.quotaPaused) return;
6722
+ let quotaResponse;
6723
+ try {
6724
+ const response = await fetchWithRetry(this.platformConfig.tracesEndpoint, {
6725
+ method: "POST",
6726
+ headers: this.buildPublishHeaders(),
6727
+ body: this.buildPublishBody("traces", [])
6728
+ }, 1, { shouldRetryResponse: (res) => {
6729
+ if (isQuotaExceededResponse(res)) quotaResponse = res;
6730
+ return false;
6731
+ } });
6732
+ if (this.shuttingDown) return;
6733
+ if (!isObservabilityDisabled(response)) {
6734
+ this.quotaPaused = false;
6735
+ this.quotaProbeIntervalSeconds = DEFAULT_QUOTA_PROBE_INTERVAL_SECONDS;
6736
+ this.logger.warn("Mastra observability resumed: quota restored, exports re-enabled");
6737
+ return;
6738
+ }
6739
+ this.quotaProbeIntervalSeconds = parseQuotaRetryAfterSeconds(response);
6740
+ } catch {
6741
+ if (quotaResponse) this.quotaProbeIntervalSeconds = parseQuotaRetryAfterSeconds(quotaResponse);
6742
+ }
6743
+ this.scheduleQuotaProbe();
6637
6744
  }
6638
6745
  async flushSignalBatch(signal, records) {
6639
6746
  if (records.length === 0) return {
@@ -6701,11 +6808,16 @@ var MastraPlatformExporter = class extends BaseExporter {
6701
6808
  }
6702
6809
  }
6703
6810
  async shutdown() {
6811
+ this.shuttingDown = true;
6704
6812
  if (this.isDisabled) return;
6705
6813
  if (this.flushTimer) {
6706
6814
  clearTimeout(this.flushTimer);
6707
6815
  this.flushTimer = null;
6708
6816
  }
6817
+ if (this.quotaProbeTimer) {
6818
+ clearTimeout(this.quotaProbeTimer);
6819
+ this.quotaProbeTimer = null;
6820
+ }
6709
6821
  try {
6710
6822
  await this.flush();
6711
6823
  } catch (error) {