@getstrata/core 2.2.8 → 2.2.10

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
@@ -687,6 +687,111 @@ var init_deferredModelEvents = __esm(() => {
687
687
  deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
688
688
  });
689
689
 
690
+ // ../../src/core/database/acquisitionMetrics.ts
691
+ function createTransactionAcquisitionMetrics() {
692
+ const state = {
693
+ inflight: 0,
694
+ outcomes: outcomes.map((outcome) => ({
695
+ outcome,
696
+ count: 0,
697
+ sumSeconds: 0,
698
+ buckets: ACQUISITION_BUCKETS_SECONDS.map(() => 0)
699
+ }))
700
+ };
701
+ const metrics = {
702
+ [metricsBrand]: true,
703
+ snapshot: () => ({
704
+ inflight: state.inflight,
705
+ outcomes: state.outcomes.map((histogram) => ({
706
+ ...histogram,
707
+ buckets: [...histogram.buckets]
708
+ }))
709
+ })
710
+ };
711
+ states.set(metrics, state);
712
+ return metrics;
713
+ }
714
+ async function observeTransactionAcquisition(metrics, acquire, signal) {
715
+ const state = states.get(metrics);
716
+ if (!state)
717
+ throw new TypeError("Transaction acquisition metrics must be created by the framework factory.");
718
+ const started = performance.now();
719
+ state.inflight++;
720
+ let outcome = "acquired";
721
+ try {
722
+ return await acquire();
723
+ } catch (error) {
724
+ outcome = signal?.aborted ? "aborted" : "failed";
725
+ throw error;
726
+ } finally {
727
+ state.inflight--;
728
+ const elapsed = Math.max(0, performance.now() - started) / 1000;
729
+ const histogram = state.outcomes[outcomes.indexOf(outcome)];
730
+ if (histogram) {
731
+ histogram.count++;
732
+ histogram.sumSeconds += elapsed;
733
+ for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
734
+ const bound = ACQUISITION_BUCKETS_SECONDS[index];
735
+ if (bound !== undefined && elapsed <= bound)
736
+ histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
737
+ }
738
+ }
739
+ }
740
+ }
741
+ function renderTransactionAcquisitionMetrics(snapshot) {
742
+ const invalid = () => new TypeError("Invalid transaction acquisition metrics.");
743
+ if (!Number.isSafeInteger(snapshot.inflight) || snapshot.inflight < 0 || snapshot.outcomes.length !== outcomes.length)
744
+ throw invalid();
745
+ const name = "strata_database_transaction_acquisition_duration_seconds";
746
+ const lines = [
747
+ "# HELP strata_database_acquisition_collector_success Whether transaction acquisition observation succeeded.",
748
+ "# TYPE strata_database_acquisition_collector_success gauge",
749
+ "strata_database_acquisition_collector_success 1",
750
+ "# HELP strata_database_transaction_acquisition_inflight Native transaction checkouts awaiting settlement in this process.",
751
+ "# TYPE strata_database_transaction_acquisition_inflight gauge",
752
+ `strata_database_transaction_acquisition_inflight ${snapshot.inflight}`,
753
+ `# HELP ${name} Native checkout duration, including pool wait and connection establishment, for explicitly observed transactions.`,
754
+ `# TYPE ${name} histogram`
755
+ ];
756
+ for (let i = 0;i < outcomes.length; i++) {
757
+ const histogram = snapshot.outcomes[i];
758
+ if (!histogram || histogram.outcome !== outcomes[i] || !Number.isSafeInteger(histogram.count) || histogram.count < 0 || !Number.isFinite(histogram.sumSeconds) || histogram.sumSeconds < 0 || histogram.buckets.length !== ACQUISITION_BUCKETS_SECONDS.length)
759
+ throw invalid();
760
+ let previous = 0;
761
+ for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
762
+ const value = histogram.buckets[index];
763
+ if (value === undefined || !Number.isSafeInteger(value) || value < previous || value > histogram.count)
764
+ throw invalid();
765
+ previous = value;
766
+ lines.push(`${name}_bucket{outcome="${histogram.outcome}",le="${ACQUISITION_BUCKETS_SECONDS[index]}"} ${value}`);
767
+ }
768
+ lines.push(`${name}_bucket{outcome="${histogram.outcome}",le="+Inf"} ${histogram.count}`, `${name}_count{outcome="${histogram.outcome}"} ${histogram.count}`, `${name}_sum{outcome="${histogram.outcome}"} ${histogram.sumSeconds}`);
769
+ }
770
+ return `${lines.join(`
771
+ `)}
772
+ `;
773
+ }
774
+ var ACQUISITION_BUCKETS_SECONDS, outcomes, metricsBrand, states;
775
+ var init_acquisitionMetrics = __esm(() => {
776
+ ACQUISITION_BUCKETS_SECONDS = [
777
+ 0.001,
778
+ 0.005,
779
+ 0.01,
780
+ 0.025,
781
+ 0.05,
782
+ 0.1,
783
+ 0.25,
784
+ 0.5,
785
+ 1,
786
+ 2.5,
787
+ 5,
788
+ 10
789
+ ];
790
+ outcomes = ["acquired", "failed", "aborted"];
791
+ metricsBrand = Symbol("transaction acquisition metrics");
792
+ states = new WeakMap;
793
+ });
794
+
690
795
  // ../../src/core/database/connection.ts
691
796
  function createDatabaseConnection(source) {
692
797
  return {
@@ -730,28 +835,51 @@ async function runInSavepoint(connection, operation) {
730
835
  function supportsTransactions(connection) {
731
836
  return typeof connection.begin === "function";
732
837
  }
733
- async function runInTransaction(operation) {
838
+ async function runInTransaction(operation, options = {}) {
839
+ const signal = options.acquisitionSignal;
840
+ signal?.throwIfAborted();
841
+ const admittedOperation = (connection) => {
842
+ signal?.throwIfAborted();
843
+ return operation(connection);
844
+ };
734
845
  const pool = resolveRepositoryConnection();
735
846
  if (hasActiveDatabaseConnection()) {
736
- return await runInSavepoint(getActiveDatabaseConnection(pool), operation);
847
+ return await runInSavepoint(getActiveDatabaseConnection(pool), admittedOperation);
737
848
  }
738
849
  if (!supportsTransactions(pool)) {
739
850
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
740
851
  }
741
852
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
742
- return await pool.begin(async (transaction) => {
743
- return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await operation(createDatabaseConnection(transaction))))));
744
- });
853
+ let reserved;
854
+ if (signal || options.acquisitionMetrics) {
855
+ if (!pool.reserve)
856
+ throw new Error(signal ? "Active database connection does not support cancellable transaction acquisition." : "Active database connection does not support observable transaction acquisition.");
857
+ const reserve = pool.reserve.bind(pool);
858
+ reserved = options.acquisitionMetrics ? await observeTransactionAcquisition(options.acquisitionMetrics, () => reserve({ signal }), signal) : await reserve({ signal });
859
+ }
860
+ const connection = reserved ?? pool;
861
+ try {
862
+ signal?.throwIfAborted();
863
+ if (!supportsTransactions(connection))
864
+ throw new Error("Reserved database connection does not support transactions.");
865
+ return await connection.begin(async (transaction) => {
866
+ return await runWithDatabaseConnection(transaction, () => transactionFrame.run({ connection: transaction, childActive: false }, () => runWithTransactionScope(async () => commitOrRollbackScope(await admittedOperation(createDatabaseConnection(transaction))))));
867
+ });
868
+ } finally {
869
+ await reserved?.release();
870
+ }
745
871
  }));
746
872
  }
747
873
  var transactionFrame, outerFrames;
748
874
  var init_transaction = __esm(() => {
749
875
  init_deferredModelEvents();
750
876
  init_asyncContextStore();
877
+ init_acquisitionMetrics();
751
878
  init_connection();
752
879
  init_connectionContext();
753
880
  init_repositoryConnection();
754
881
  init_transactionControl();
882
+ init_acquisitionMetrics();
755
883
  transactionFrame = createAsyncContextStore("@getstrata/transactionFrame");
756
884
  outerFrames = new WeakMap;
757
885
  });
@@ -4106,6 +4234,10 @@ var CACHE_TAGS = {
4106
4234
  careers: "careers",
4107
4235
  offers: "offers"
4108
4236
  };
4237
+
4238
+ // ../../src/framework/public-api.ts
4239
+ init_acquisitionMetrics();
4240
+
4109
4241
  // ../../src/core/database/baseRepository.ts
4110
4242
  init_eventBus();
4111
4243
 
@@ -9164,10 +9296,75 @@ init_databaseTenantContext();
9164
9296
  init_tenantContext();
9165
9297
  init_tenantDatabaseScope();
9166
9298
 
9167
- // ../../src/core/events/outbox/metrics.ts
9299
+ // ../../src/core/database/postgresReadOnlyCollector.ts
9300
+ init_connectionContext();
9168
9301
  init_dialect();
9169
9302
  init_repositoryConnection();
9170
9303
  init_transaction();
9304
+ function createPostgresReadOnlyCollector(name, timeoutMs, collect) {
9305
+ if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 5000)
9306
+ throw new TypeError(`${name} timeout must be between 1 and 5000 milliseconds.`);
9307
+ let active;
9308
+ let closed = false;
9309
+ return {
9310
+ async collect() {
9311
+ if (closed)
9312
+ throw new Error(`${name} collector is closed.`);
9313
+ if (hasActiveTransaction() || hasActiveDatabaseConnection())
9314
+ throw new Error(`${name} require an independent transaction.`);
9315
+ if (currentSqlDialect().driver !== "pgsql")
9316
+ throw new Error(`${name} currently require Postgres.`);
9317
+ if (active)
9318
+ throw new Error(`${name} collection is still settling.`);
9319
+ const deadline = performance.now() + timeoutMs;
9320
+ let expired = false;
9321
+ const acquisition = new AbortController;
9322
+ let timer;
9323
+ const remaining = () => {
9324
+ const value = Math.floor(deadline - performance.now());
9325
+ if (expired || value < 1)
9326
+ throw new Error(`${name} collection timed out.`);
9327
+ return value;
9328
+ };
9329
+ const work = runInTransaction(async () => {
9330
+ await repositoryConnection.unsafe("SELECT set_config('statement_timeout', $1, true)", [`${remaining()}ms`]);
9331
+ await repositoryConnection.unsafe("SET TRANSACTION READ ONLY");
9332
+ remaining();
9333
+ const result = await collect(remaining);
9334
+ remaining();
9335
+ return result;
9336
+ }, { acquisitionSignal: acquisition.signal });
9337
+ active = work;
9338
+ work.finally(() => {
9339
+ if (active === work)
9340
+ active = undefined;
9341
+ }).catch(() => {});
9342
+ try {
9343
+ return await Promise.race([
9344
+ work,
9345
+ new Promise((_, reject) => {
9346
+ timer = setTimeout(() => {
9347
+ expired = true;
9348
+ const error = new Error(`${name} collection timed out.`);
9349
+ acquisition.abort(error);
9350
+ reject(error);
9351
+ }, timeoutMs);
9352
+ })
9353
+ ]);
9354
+ } finally {
9355
+ if (timer !== undefined)
9356
+ clearTimeout(timer);
9357
+ }
9358
+ },
9359
+ async close() {
9360
+ closed = true;
9361
+ await active?.catch(() => {});
9362
+ }
9363
+ };
9364
+ }
9365
+
9366
+ // ../../src/core/events/outbox/metrics.ts
9367
+ init_repositoryConnection();
9171
9368
  init_databaseTenantContext();
9172
9369
  var specifications = [
9173
9370
  { state: "pending_due", status: "pending", column: "available_at", predicate: "<=", age: true },
@@ -9217,81 +9414,29 @@ UNION ALL
9217
9414
  function createOutboxMetricsCollector(options = {}) {
9218
9415
  const timeoutMs = options.timeoutMs ?? 1000;
9219
9416
  const sampleLimit = options.sampleLimit ?? 500;
9220
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 5000)
9221
- throw new TypeError("Outbox metrics timeout must be between 1 and 5000 milliseconds.");
9222
9417
  if (!Number.isSafeInteger(sampleLimit) || sampleLimit < 1 || sampleLimit > 1000)
9223
9418
  throw new TypeError("Outbox metrics sample limit must be between 1 and 1000.");
9224
- let active;
9225
- let closed = false;
9226
- return {
9227
- async collect() {
9228
- if (closed)
9229
- throw new Error("Outbox metrics collector is closed.");
9230
- if (hasActiveTransaction())
9231
- throw new Error("Outbox metrics require an independent transaction.");
9232
- if (currentSqlDialect().driver !== "pgsql")
9233
- throw new Error("Outbox metrics currently require Postgres.");
9234
- if (active)
9235
- throw new Error("Outbox metrics collection is still settling.");
9236
- const deadline = performance.now() + timeoutMs;
9237
- let expired = false;
9238
- let timer;
9239
- const remaining = () => {
9240
- const value = Math.floor(deadline - performance.now());
9241
- if (expired || value < 1)
9242
- throw new Error("Outbox metrics collection timed out.");
9243
- return value;
9419
+ return createPostgresReadOnlyCollector("Outbox metrics", timeoutMs, (remaining) => runWithMigrationBypass(async () => {
9420
+ remaining();
9421
+ const rows = await repositoryConnection.unsafe(snapshotQuery, [sampleLimit + 1]);
9422
+ remaining();
9423
+ if (rows.length !== specifications.length)
9424
+ throw new Error("Invalid outbox metrics response.");
9425
+ const states = specifications.map((spec) => {
9426
+ const row = rows.find((value) => value.state === spec.state);
9427
+ const count = Number(row?.count);
9428
+ const lateness = row?.lateness_ms == null ? null : Number(row.lateness_ms);
9429
+ if (!Number.isSafeInteger(count) || count < 0 || count > sampleLimit + 1 || spec.age && (lateness === null || !Number.isFinite(lateness) || lateness < 0))
9430
+ throw new Error("Invalid outbox metrics values.");
9431
+ return {
9432
+ state: spec.state,
9433
+ count: Math.min(count, sampleLimit),
9434
+ capped: count > sampleLimit,
9435
+ actionableLatenessSeconds: lateness === null ? null : lateness / 1000
9244
9436
  };
9245
- const work = runInTransaction(async () => {
9246
- await repositoryConnection.unsafe("SELECT set_config('statement_timeout', $1, true)", [`${remaining()}ms`]);
9247
- await repositoryConnection.unsafe("SET TRANSACTION READ ONLY");
9248
- return runWithMigrationBypass(async () => {
9249
- remaining();
9250
- const rows = await repositoryConnection.unsafe(snapshotQuery, [sampleLimit + 1]);
9251
- remaining();
9252
- if (rows.length !== specifications.length)
9253
- throw new Error("Invalid outbox metrics response.");
9254
- const states = specifications.map((spec) => {
9255
- const row = rows.find((value) => value.state === spec.state);
9256
- const count = Number(row?.count);
9257
- const lateness = row?.lateness_ms == null ? null : Number(row.lateness_ms);
9258
- if (!Number.isSafeInteger(count) || count < 0 || count > sampleLimit + 1 || spec.age && (lateness === null || !Number.isFinite(lateness) || lateness < 0))
9259
- throw new Error("Invalid outbox metrics values.");
9260
- return {
9261
- state: spec.state,
9262
- count: Math.min(count, sampleLimit),
9263
- capped: count > sampleLimit,
9264
- actionableLatenessSeconds: lateness === null ? null : lateness / 1000
9265
- };
9266
- });
9267
- return { sampleLimit, states };
9268
- });
9269
- });
9270
- active = work;
9271
- work.finally(() => {
9272
- if (active === work)
9273
- active = undefined;
9274
- }).catch(() => {});
9275
- try {
9276
- return await Promise.race([
9277
- work,
9278
- new Promise((_, reject) => {
9279
- timer = setTimeout(() => {
9280
- expired = true;
9281
- reject(new Error("Outbox metrics collection timed out."));
9282
- }, timeoutMs);
9283
- })
9284
- ]);
9285
- } finally {
9286
- if (timer !== undefined)
9287
- clearTimeout(timer);
9288
- }
9289
- },
9290
- async close() {
9291
- closed = true;
9292
- await active?.catch(() => {});
9293
- }
9294
- };
9437
+ });
9438
+ return { sampleLimit, states };
9439
+ }));
9295
9440
  }
9296
9441
  function renderOutboxMetrics(snapshot) {
9297
9442
  if (!Number.isSafeInteger(snapshot.sampleLimit) || snapshot.sampleLimit < 1 || snapshot.sampleLimit > 1000 || snapshot.states.length !== specifications.length)
@@ -12856,6 +13001,67 @@ class Notification {
12856
13001
  return null;
12857
13002
  }
12858
13003
  }
13004
+ // ../../src/core/queue/failedJobMetrics.ts
13005
+ init_repositoryConnection();
13006
+
13007
+ // ../../src/core/queue/failedJobTable.ts
13008
+ var failedJobTable = defineTable({
13009
+ name: "failed_job",
13010
+ primaryKey: "id",
13011
+ columns: ["id", "job_name", "job_id", "payload", "exception", "failed_at"],
13012
+ defaultOrderBy: { column: "failed_at", direction: "DESC" }
13013
+ });
13014
+
13015
+ // ../../src/core/queue/failedJobRepository.ts
13016
+ class FailedJobRepository extends BaseRepository {
13017
+ constructor() {
13018
+ super(failedJobTable);
13019
+ }
13020
+ }
13021
+ var failedJobRepository_default = FailedJobRepository;
13022
+
13023
+ // ../../src/core/queue/failedJobMetrics.ts
13024
+ var schemaQuery = `SELECT c.relkind = 'r' AND c.relpersistence = 'p' AND NOT c.relrowsecurity AND EXISTS (
13025
+ SELECT 1 FROM pg_index i JOIN pg_attribute a ON a.attrelid = i.indrelid AND a.attnum = i.indkey[0]
13026
+ WHERE i.indrelid = c.oid AND i.indisprimary AND i.indisvalid AND i.indisready
13027
+ AND i.indnkeyatts = 1 AND a.attname = 'id' AND NOT a.attisdropped
13028
+ AND a.atttypid IN ('int2'::regtype, 'int4'::regtype, 'int8'::regtype)
13029
+ ) AS supported FROM pg_class c WHERE c.oid = 'failed_job'::regclass`;
13030
+ function createFailedJobMetricsCollector(options = {}) {
13031
+ const sampleLimit = options.sampleLimit ?? 500;
13032
+ if (!Number.isSafeInteger(sampleLimit) || sampleLimit < 1 || sampleLimit > 1000)
13033
+ throw new TypeError("Failed-job metrics sample limit must be between 1 and 1000.");
13034
+ const repository = new failedJobRepository_default;
13035
+ return createPostgresReadOnlyCollector("Failed-job metrics", options.timeoutMs ?? 1000, async (remaining) => {
13036
+ await repositoryConnection.unsafe('LOCK TABLE "failed_job" IN ACCESS SHARE MODE');
13037
+ const schema = await repositoryConnection.unsafe(schemaQuery);
13038
+ if (schema.length !== 1 || schema[0]?.supported !== true)
13039
+ throw new Error("Failed-job metrics require the standard global table with an ID primary key and no RLS.");
13040
+ remaining();
13041
+ const rows = await repository.query().orderBy({ column: "id", direction: "ASC" }).project(["id"], sampleLimit + 1);
13042
+ if (rows.length > sampleLimit + 1 || rows.some((row) => row.id == null))
13043
+ throw new Error("Invalid failed-job metrics response.");
13044
+ return {
13045
+ sampleLimit,
13046
+ count: Math.min(rows.length, sampleLimit),
13047
+ capped: rows.length > sampleLimit
13048
+ };
13049
+ });
13050
+ }
13051
+ function renderFailedJobMetrics(snapshot) {
13052
+ if (!Number.isSafeInteger(snapshot.sampleLimit) || snapshot.sampleLimit < 1 || snapshot.sampleLimit > 1000 || !Number.isSafeInteger(snapshot.count) || snapshot.count < 0 || snapshot.count > snapshot.sampleLimit || typeof snapshot.capped !== "boolean" || snapshot.capped && snapshot.count !== snapshot.sampleLimit)
13053
+ throw new TypeError("Invalid failed-job metrics snapshot.");
13054
+ return `# HELP strata_queue_failed_job_collector_success Whether failed-job collection succeeded.
13055
+ # TYPE strata_queue_failed_job_collector_success gauge
13056
+ strata_queue_failed_job_collector_success 1
13057
+ # HELP strata_queue_failed_jobs_sample Capped retained failed-job count; a lower bound when capped.
13058
+ # TYPE strata_queue_failed_jobs_sample gauge
13059
+ strata_queue_failed_jobs_sample ${snapshot.count}
13060
+ # HELP strata_queue_failed_jobs_sample_capped Whether the failed-job sample is truncated.
13061
+ # TYPE strata_queue_failed_jobs_sample_capped gauge
13062
+ strata_queue_failed_jobs_sample_capped ${Number(snapshot.capped)}
13063
+ `;
13064
+ }
12859
13065
  // ../../src/core/queue/jobRegistry.ts
12860
13066
  class JobRegistry {
12861
13067
  factories = new Map;
@@ -12950,22 +13156,6 @@ class AsyncQueue {
12950
13156
  function createQueue(driver) {
12951
13157
  return driver === "async" ? new AsyncQueue : new SyncQueue;
12952
13158
  }
12953
- // ../../src/core/queue/failedJobTable.ts
12954
- var failedJobTable = defineTable({
12955
- name: "failed_job",
12956
- primaryKey: "id",
12957
- columns: ["id", "job_name", "job_id", "payload", "exception", "failed_at"],
12958
- defaultOrderBy: { column: "failed_at", direction: "DESC" }
12959
- });
12960
-
12961
- // ../../src/core/queue/failedJobRepository.ts
12962
- class FailedJobRepository extends BaseRepository {
12963
- constructor() {
12964
- super(failedJobTable);
12965
- }
12966
- }
12967
- var failedJobRepository_default = FailedJobRepository;
12968
-
12969
13159
  // ../../src/core/queue/failedJobService.ts
12970
13160
  class FailedJobService {
12971
13161
  repository;
@@ -14668,6 +14858,171 @@ function createTenantMiddleware(options = {}) {
14668
14858
  }
14669
14859
  };
14670
14860
  }
14861
+ // ../../src/core/tracing/health.ts
14862
+ import {
14863
+ createNoopMeter
14864
+ } from "@opentelemetry/api";
14865
+ import { ExportResultCode } from "@opentelemetry/core";
14866
+ function observeExportDeadline(state, timeoutMillis) {
14867
+ const deadline = setTimeout(() => {
14868
+ state.exportDeadlineExceededBatches++;
14869
+ }, timeoutMillis);
14870
+ deadline.unref();
14871
+ return deadline;
14872
+ }
14873
+ function createTracingHealth(enabled, exporterConfigured, timeoutMillis) {
14874
+ const state = {
14875
+ enabled,
14876
+ exporterConfigured,
14877
+ active: true,
14878
+ queueSize: null,
14879
+ queueCapacity: null,
14880
+ droppedSpans: 0,
14881
+ exportedBatches: 0,
14882
+ failedBatches: 0,
14883
+ exportedSpans: 0,
14884
+ failedSpans: 0,
14885
+ exportDeadlineExceededBatches: 0,
14886
+ flushFailures: 0,
14887
+ shutdownFailures: 0
14888
+ };
14889
+ let exportQueueFullSpans = 0;
14890
+ let queueCallback;
14891
+ const noop = createNoopMeter();
14892
+ const meter = {
14893
+ createGauge: noop.createGauge.bind(noop),
14894
+ createHistogram: noop.createHistogram.bind(noop),
14895
+ createObservableGauge: noop.createObservableGauge.bind(noop),
14896
+ createObservableCounter: noop.createObservableCounter.bind(noop),
14897
+ addBatchObservableCallback: noop.addBatchObservableCallback.bind(noop),
14898
+ removeBatchObservableCallback: noop.removeBatchObservableCallback.bind(noop),
14899
+ createCounter(name, options) {
14900
+ if (name !== "otel.sdk.processor.span.processed")
14901
+ return noop.createCounter(name, options);
14902
+ return {
14903
+ add(value, attributes) {
14904
+ if (attributes?.["error.type"] === "queue_full")
14905
+ state.droppedSpans += value;
14906
+ }
14907
+ };
14908
+ },
14909
+ createUpDownCounter(name, options) {
14910
+ if (name !== "otel.sdk.processor.span.queue.capacity")
14911
+ return noop.createUpDownCounter(name, options);
14912
+ return {
14913
+ add(value) {
14914
+ state.queueCapacity = (state.queueCapacity ?? 0) + value;
14915
+ }
14916
+ };
14917
+ },
14918
+ createObservableUpDownCounter(name, options) {
14919
+ if (name !== "otel.sdk.processor.span.queue.size")
14920
+ return noop.createObservableUpDownCounter(name, options);
14921
+ return {
14922
+ addCallback(callback) {
14923
+ queueCallback = callback;
14924
+ },
14925
+ removeCallback(callback) {
14926
+ if (queueCallback === callback)
14927
+ queueCallback = undefined;
14928
+ }
14929
+ };
14930
+ }
14931
+ };
14932
+ const meterProvider = { getMeter: () => meter };
14933
+ return {
14934
+ state,
14935
+ meterProvider,
14936
+ snapshot() {
14937
+ let queueSize = null;
14938
+ queueCallback?.({
14939
+ observe(value) {
14940
+ queueSize = value;
14941
+ }
14942
+ });
14943
+ return {
14944
+ ...state,
14945
+ queueSize,
14946
+ droppedSpans: Math.max(0, state.droppedSpans - exportQueueFullSpans)
14947
+ };
14948
+ },
14949
+ wrap(exporter) {
14950
+ return {
14951
+ export(spans, callback) {
14952
+ const count = spans.length;
14953
+ let completed = false;
14954
+ const deadline = observeExportDeadline(state, timeoutMillis);
14955
+ const finish = (success) => {
14956
+ completed = true;
14957
+ clearTimeout(deadline);
14958
+ if (success) {
14959
+ state.exportedBatches++;
14960
+ state.exportedSpans += count;
14961
+ } else {
14962
+ state.failedBatches++;
14963
+ state.failedSpans += count;
14964
+ }
14965
+ };
14966
+ try {
14967
+ exporter.export(spans, (result) => {
14968
+ if (completed)
14969
+ return;
14970
+ finish(result.code === ExportResultCode.SUCCESS);
14971
+ if (result.error?.name === "queue_full")
14972
+ exportQueueFullSpans += count;
14973
+ callback(result);
14974
+ });
14975
+ } catch (error) {
14976
+ if (!completed)
14977
+ finish(false);
14978
+ throw error;
14979
+ }
14980
+ },
14981
+ shutdown: () => exporter.shutdown(),
14982
+ ...exporter.forceFlush ? { forceFlush: exporter.forceFlush.bind(exporter) } : {}
14983
+ };
14984
+ }
14985
+ };
14986
+ }
14987
+ function renderTracingMetrics(snapshot) {
14988
+ for (const key of ["enabled", "exporterConfigured", "active"])
14989
+ if (typeof snapshot[key] !== "boolean")
14990
+ throw new TypeError("Invalid tracing metrics.");
14991
+ const values = {
14992
+ strata_tracing_enabled: Number(snapshot.enabled),
14993
+ strata_tracing_exporter_configured: Number(snapshot.exporterConfigured),
14994
+ strata_tracing_active: Number(snapshot.active),
14995
+ strata_tracing_queue_size: snapshot.queueSize,
14996
+ strata_tracing_queue_capacity: snapshot.queueCapacity,
14997
+ strata_tracing_dropped_spans_total: snapshot.droppedSpans,
14998
+ strata_tracing_exported_batches_total: snapshot.exportedBatches,
14999
+ strata_tracing_failed_batches_total: snapshot.failedBatches,
15000
+ strata_tracing_exported_spans_total: snapshot.exportedSpans,
15001
+ strata_tracing_failed_spans_total: snapshot.failedSpans,
15002
+ strata_tracing_export_deadline_exceeded_batches_total: snapshot.exportDeadlineExceededBatches,
15003
+ strata_tracing_flush_failures_total: snapshot.flushFailures,
15004
+ strata_tracing_shutdown_failures_total: snapshot.shutdownFailures
15005
+ };
15006
+ let output = `# HELP strata_tracing_collector_success Whether tracing observation succeeded.
15007
+ # TYPE strata_tracing_collector_success gauge
15008
+ strata_tracing_collector_success 1
15009
+ `;
15010
+ for (const [name, value] of Object.entries(values)) {
15011
+ if (value === null) {
15012
+ if (name === "strata_tracing_queue_size" || name === "strata_tracing_queue_capacity")
15013
+ continue;
15014
+ throw new TypeError("Invalid tracing metrics.");
15015
+ }
15016
+ if (!Number.isSafeInteger(value) || value < 0)
15017
+ throw new TypeError("Invalid tracing metrics.");
15018
+ const type = name.endsWith("_total") ? "counter" : "gauge";
15019
+ output += `# HELP ${name} Process-local tracing ${name.slice(15).replaceAll("_", " ")}.
15020
+ # TYPE ${name} ${type}
15021
+ ${name} ${value}
15022
+ `;
15023
+ }
15024
+ return output;
15025
+ }
14671
15026
  // ../../src/core/tracing/traceContext.ts
14672
15027
  init_asyncContextStore();
14673
15028
  var traceContextStorage = createAsyncContextStore("@getstrata/traceContext");
@@ -14686,10 +15041,10 @@ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-ho
14686
15041
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
14687
15042
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
14688
15043
  import { resourceFromAttributes } from "@opentelemetry/resources";
15044
+ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace";
14689
15045
  import {
14690
15046
  AlwaysOffSampler,
14691
15047
  AlwaysOnSampler,
14692
- BatchSpanProcessor,
14693
15048
  ParentBasedSampler,
14694
15049
  TraceIdRatioBasedSampler
14695
15050
  } from "@opentelemetry/sdk-trace-base";
@@ -14743,11 +15098,14 @@ function createTracingRuntime(options = {}) {
14743
15098
  throw new TypeError("Invalid tracing collector endpoint.");
14744
15099
  }
14745
15100
  const exporter = disabled ? undefined : options.exporter ?? (endpoint ? new OTLPTraceExporter({ url: endpoint, timeoutMillis: timeout, concurrencyLimit: 1 }) : undefined);
15101
+ const health = createTracingHealth(!disabled, exporter !== undefined, timeout);
14746
15102
  const provider = new NodeTracerProvider({
14747
15103
  resource: resourceFromAttributes({ "service.name": options.serviceName ?? otelServiceName() }),
14748
15104
  sampler,
14749
15105
  spanProcessors: exporter ? [
14750
- new BatchSpanProcessor(exporter, {
15106
+ new BatchSpanProcessor({
15107
+ exporter: health.wrap(exporter),
15108
+ selfObsMeterProvider: health.meterProvider,
14751
15109
  maxQueueSize: queueSize,
14752
15110
  maxExportBatchSize: batchSize,
14753
15111
  scheduledDelayMillis: delay,
@@ -14767,8 +15125,21 @@ function createTracingRuntime(options = {}) {
14767
15125
  tracer: provider.getTracer("@getstrata/http"),
14768
15126
  propagator: new W3CTraceContextPropagator,
14769
15127
  run: (parent, callback) => context.with(parent, callback),
14770
- forceFlush: () => provider.forceFlush(),
14771
- shutdown: () => shutdown ??= provider.shutdown()
15128
+ metrics: () => health.snapshot(),
15129
+ async forceFlush() {
15130
+ try {
15131
+ await provider.forceFlush();
15132
+ } catch (error) {
15133
+ health.state.flushFailures++;
15134
+ throw error;
15135
+ }
15136
+ },
15137
+ shutdown: () => shutdown ??= provider.shutdown().catch((error) => {
15138
+ health.state.shutdownFailures++;
15139
+ throw error;
15140
+ }).finally(() => {
15141
+ health.state.active = false;
15142
+ })
14772
15143
  };
14773
15144
  }
14774
15145
  var runtimeKey = Symbol.for("@getstrata/otel-runtime");
@@ -15388,6 +15759,7 @@ export {
15388
15759
  createCsrfTokenCookie,
15389
15760
  createDatabaseConnection,
15390
15761
  createDatabaseQueryProxy,
15762
+ createFailedJobMetricsCollector,
15391
15763
  createFailedJobService,
15392
15764
  createFlashMiddleware,
15393
15765
  createLoginThrottleMiddleware,
@@ -15424,6 +15796,7 @@ export {
15424
15796
  createTracingMiddleware,
15425
15797
  createTracingRuntime,
15426
15798
  createTrackedJob,
15799
+ createTransactionAcquisitionMetrics,
15427
15800
  createValidateSignatureMiddleware,
15428
15801
  createdResponse,
15429
15802
  currentAuthUser,
@@ -15572,9 +15945,12 @@ export {
15572
15945
  registerModelRepository,
15573
15946
  registerNamedConnection,
15574
15947
  registerShutdownHandler,
15948
+ renderFailedJobMetrics,
15575
15949
  renderKernelErrorChrome,
15576
15950
  renderMarkdownMail,
15577
15951
  renderOutboxMetrics,
15952
+ renderTracingMetrics,
15953
+ renderTransactionAcquisitionMetrics,
15578
15954
  renderWebErrorHtml,
15579
15955
  repositoryConnection,
15580
15956
  requestIdMiddleware,