@getstrata/core 2.2.9 → 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 {
@@ -745,9 +850,13 @@ async function runInTransaction(operation, options = {}) {
745
850
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
746
851
  }
747
852
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
748
- if (signal && !pool.reserve)
749
- throw new Error("Active database connection does not support cancellable transaction acquisition.");
750
- const reserved = signal ? await pool.reserve?.({ signal }) : undefined;
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
+ }
751
860
  const connection = reserved ?? pool;
752
861
  try {
753
862
  signal?.throwIfAborted();
@@ -765,10 +874,12 @@ var transactionFrame, outerFrames;
765
874
  var init_transaction = __esm(() => {
766
875
  init_deferredModelEvents();
767
876
  init_asyncContextStore();
877
+ init_acquisitionMetrics();
768
878
  init_connection();
769
879
  init_connectionContext();
770
880
  init_repositoryConnection();
771
881
  init_transactionControl();
882
+ init_acquisitionMetrics();
772
883
  transactionFrame = createAsyncContextStore("@getstrata/transactionFrame");
773
884
  outerFrames = new WeakMap;
774
885
  });
@@ -4123,6 +4234,10 @@ var CACHE_TAGS = {
4123
4234
  careers: "careers",
4124
4235
  offers: "offers"
4125
4236
  };
4237
+
4238
+ // ../../src/framework/public-api.ts
4239
+ init_acquisitionMetrics();
4240
+
4126
4241
  // ../../src/core/database/baseRepository.ts
4127
4242
  init_eventBus();
4128
4243
 
@@ -9181,10 +9296,75 @@ init_databaseTenantContext();
9181
9296
  init_tenantContext();
9182
9297
  init_tenantDatabaseScope();
9183
9298
 
9184
- // ../../src/core/events/outbox/metrics.ts
9299
+ // ../../src/core/database/postgresReadOnlyCollector.ts
9300
+ init_connectionContext();
9185
9301
  init_dialect();
9186
9302
  init_repositoryConnection();
9187
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();
9188
9368
  init_databaseTenantContext();
9189
9369
  var specifications = [
9190
9370
  { state: "pending_due", status: "pending", column: "available_at", predicate: "<=", age: true },
@@ -9234,84 +9414,29 @@ UNION ALL
9234
9414
  function createOutboxMetricsCollector(options = {}) {
9235
9415
  const timeoutMs = options.timeoutMs ?? 1000;
9236
9416
  const sampleLimit = options.sampleLimit ?? 500;
9237
- if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1 || timeoutMs > 5000)
9238
- throw new TypeError("Outbox metrics timeout must be between 1 and 5000 milliseconds.");
9239
9417
  if (!Number.isSafeInteger(sampleLimit) || sampleLimit < 1 || sampleLimit > 1000)
9240
9418
  throw new TypeError("Outbox metrics sample limit must be between 1 and 1000.");
9241
- let active;
9242
- let closed = false;
9243
- return {
9244
- async collect() {
9245
- if (closed)
9246
- throw new Error("Outbox metrics collector is closed.");
9247
- if (hasActiveTransaction())
9248
- throw new Error("Outbox metrics require an independent transaction.");
9249
- if (currentSqlDialect().driver !== "pgsql")
9250
- throw new Error("Outbox metrics currently require Postgres.");
9251
- if (active)
9252
- throw new Error("Outbox metrics collection is still settling.");
9253
- const deadline = performance.now() + timeoutMs;
9254
- let expired = false;
9255
- const acquisition = new AbortController;
9256
- let timer;
9257
- const remaining = () => {
9258
- const value = Math.floor(deadline - performance.now());
9259
- if (expired || value < 1)
9260
- throw new Error("Outbox metrics collection timed out.");
9261
- 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
9262
9436
  };
9263
- const work = runInTransaction(async () => {
9264
- await repositoryConnection.unsafe("SELECT set_config('statement_timeout', $1, true)", [`${remaining()}ms`]);
9265
- await repositoryConnection.unsafe("SET TRANSACTION READ ONLY");
9266
- return runWithMigrationBypass(async () => {
9267
- remaining();
9268
- const rows = await repositoryConnection.unsafe(snapshotQuery, [sampleLimit + 1]);
9269
- remaining();
9270
- if (rows.length !== specifications.length)
9271
- throw new Error("Invalid outbox metrics response.");
9272
- const states = specifications.map((spec) => {
9273
- const row = rows.find((value) => value.state === spec.state);
9274
- const count = Number(row?.count);
9275
- const lateness = row?.lateness_ms == null ? null : Number(row.lateness_ms);
9276
- if (!Number.isSafeInteger(count) || count < 0 || count > sampleLimit + 1 || spec.age && (lateness === null || !Number.isFinite(lateness) || lateness < 0))
9277
- throw new Error("Invalid outbox metrics values.");
9278
- return {
9279
- state: spec.state,
9280
- count: Math.min(count, sampleLimit),
9281
- capped: count > sampleLimit,
9282
- actionableLatenessSeconds: lateness === null ? null : lateness / 1000
9283
- };
9284
- });
9285
- return { sampleLimit, states };
9286
- });
9287
- }, { acquisitionSignal: acquisition.signal });
9288
- active = work;
9289
- work.finally(() => {
9290
- if (active === work)
9291
- active = undefined;
9292
- }).catch(() => {});
9293
- try {
9294
- return await Promise.race([
9295
- work,
9296
- new Promise((_, reject) => {
9297
- timer = setTimeout(() => {
9298
- expired = true;
9299
- const error = new Error("Outbox metrics collection timed out.");
9300
- acquisition.abort(error);
9301
- reject(error);
9302
- }, timeoutMs);
9303
- })
9304
- ]);
9305
- } finally {
9306
- if (timer !== undefined)
9307
- clearTimeout(timer);
9308
- }
9309
- },
9310
- async close() {
9311
- closed = true;
9312
- await active?.catch(() => {});
9313
- }
9314
- };
9437
+ });
9438
+ return { sampleLimit, states };
9439
+ }));
9315
9440
  }
9316
9441
  function renderOutboxMetrics(snapshot) {
9317
9442
  if (!Number.isSafeInteger(snapshot.sampleLimit) || snapshot.sampleLimit < 1 || snapshot.sampleLimit > 1000 || snapshot.states.length !== specifications.length)
@@ -12876,6 +13001,67 @@ class Notification {
12876
13001
  return null;
12877
13002
  }
12878
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
+ }
12879
13065
  // ../../src/core/queue/jobRegistry.ts
12880
13066
  class JobRegistry {
12881
13067
  factories = new Map;
@@ -12970,22 +13156,6 @@ class AsyncQueue {
12970
13156
  function createQueue(driver) {
12971
13157
  return driver === "async" ? new AsyncQueue : new SyncQueue;
12972
13158
  }
12973
- // ../../src/core/queue/failedJobTable.ts
12974
- var failedJobTable = defineTable({
12975
- name: "failed_job",
12976
- primaryKey: "id",
12977
- columns: ["id", "job_name", "job_id", "payload", "exception", "failed_at"],
12978
- defaultOrderBy: { column: "failed_at", direction: "DESC" }
12979
- });
12980
-
12981
- // ../../src/core/queue/failedJobRepository.ts
12982
- class FailedJobRepository extends BaseRepository {
12983
- constructor() {
12984
- super(failedJobTable);
12985
- }
12986
- }
12987
- var failedJobRepository_default = FailedJobRepository;
12988
-
12989
13159
  // ../../src/core/queue/failedJobService.ts
12990
13160
  class FailedJobService {
12991
13161
  repository;
@@ -14688,6 +14858,171 @@ function createTenantMiddleware(options = {}) {
14688
14858
  }
14689
14859
  };
14690
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
+ }
14691
15026
  // ../../src/core/tracing/traceContext.ts
14692
15027
  init_asyncContextStore();
14693
15028
  var traceContextStorage = createAsyncContextStore("@getstrata/traceContext");
@@ -14706,10 +15041,10 @@ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-ho
14706
15041
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
14707
15042
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
14708
15043
  import { resourceFromAttributes } from "@opentelemetry/resources";
15044
+ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace";
14709
15045
  import {
14710
15046
  AlwaysOffSampler,
14711
15047
  AlwaysOnSampler,
14712
- BatchSpanProcessor,
14713
15048
  ParentBasedSampler,
14714
15049
  TraceIdRatioBasedSampler
14715
15050
  } from "@opentelemetry/sdk-trace-base";
@@ -14763,11 +15098,14 @@ function createTracingRuntime(options = {}) {
14763
15098
  throw new TypeError("Invalid tracing collector endpoint.");
14764
15099
  }
14765
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);
14766
15102
  const provider = new NodeTracerProvider({
14767
15103
  resource: resourceFromAttributes({ "service.name": options.serviceName ?? otelServiceName() }),
14768
15104
  sampler,
14769
15105
  spanProcessors: exporter ? [
14770
- new BatchSpanProcessor(exporter, {
15106
+ new BatchSpanProcessor({
15107
+ exporter: health.wrap(exporter),
15108
+ selfObsMeterProvider: health.meterProvider,
14771
15109
  maxQueueSize: queueSize,
14772
15110
  maxExportBatchSize: batchSize,
14773
15111
  scheduledDelayMillis: delay,
@@ -14787,8 +15125,21 @@ function createTracingRuntime(options = {}) {
14787
15125
  tracer: provider.getTracer("@getstrata/http"),
14788
15126
  propagator: new W3CTraceContextPropagator,
14789
15127
  run: (parent, callback) => context.with(parent, callback),
14790
- forceFlush: () => provider.forceFlush(),
14791
- 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
+ })
14792
15143
  };
14793
15144
  }
14794
15145
  var runtimeKey = Symbol.for("@getstrata/otel-runtime");
@@ -15408,6 +15759,7 @@ export {
15408
15759
  createCsrfTokenCookie,
15409
15760
  createDatabaseConnection,
15410
15761
  createDatabaseQueryProxy,
15762
+ createFailedJobMetricsCollector,
15411
15763
  createFailedJobService,
15412
15764
  createFlashMiddleware,
15413
15765
  createLoginThrottleMiddleware,
@@ -15444,6 +15796,7 @@ export {
15444
15796
  createTracingMiddleware,
15445
15797
  createTracingRuntime,
15446
15798
  createTrackedJob,
15799
+ createTransactionAcquisitionMetrics,
15447
15800
  createValidateSignatureMiddleware,
15448
15801
  createdResponse,
15449
15802
  currentAuthUser,
@@ -15592,9 +15945,12 @@ export {
15592
15945
  registerModelRepository,
15593
15946
  registerNamedConnection,
15594
15947
  registerShutdownHandler,
15948
+ renderFailedJobMetrics,
15595
15949
  renderKernelErrorChrome,
15596
15950
  renderMarkdownMail,
15597
15951
  renderOutboxMetrics,
15952
+ renderTracingMetrics,
15953
+ renderTransactionAcquisitionMetrics,
15598
15954
  renderWebErrorHtml,
15599
15955
  repositoryConnection,
15600
15956
  requestIdMiddleware,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@getstrata/core",
3
- "version": "2.2.9",
3
+ "version": "2.2.10",
4
4
  "description": "Strata Bun framework public API",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -970,6 +970,7 @@
970
970
  "@opentelemetry/core": "2.12.0",
971
971
  "@opentelemetry/exporter-trace-otlp-http": "0.223.0",
972
972
  "@opentelemetry/resources": "2.12.0",
973
+ "@opentelemetry/sdk-trace": "2.12.0",
973
974
  "@opentelemetry/sdk-trace-base": "2.12.0",
974
975
  "@opentelemetry/sdk-trace-node": "2.12.0"
975
976
  }