@getstrata/core 2.2.9 → 2.2.11

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.
@@ -129,6 +129,9 @@ function unwrapTransactionRollback(error, depth = 0) {
129
129
  }
130
130
  return unwrapTransactionRollback(error.cause, depth + 1);
131
131
  }
132
+ function hasActiveTransaction() {
133
+ return hasActiveDatabaseConnection() && (transactionScopes.getStore()?.rollback.length ?? 0) > 0;
134
+ }
132
135
  function requestTransactionRollback() {
133
136
  const state = transactionScopes.getStore();
134
137
  const index = state ? state.rollback.length - 1 : -1;
@@ -344,6 +347,55 @@ var init_deferredModelEvents = __esm(() => {
344
347
  deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
345
348
  });
346
349
 
350
+ // ../../src/core/database/acquisitionMetrics.ts
351
+ async function observeTransactionAcquisition(metrics, acquire, signal) {
352
+ const state = states.get(metrics);
353
+ if (!state)
354
+ throw new TypeError("Transaction acquisition metrics must be created by the framework factory.");
355
+ const started = performance.now();
356
+ state.inflight++;
357
+ let outcome = "acquired";
358
+ try {
359
+ return await acquire();
360
+ } catch (error) {
361
+ outcome = signal?.aborted ? "aborted" : "failed";
362
+ throw error;
363
+ } finally {
364
+ state.inflight--;
365
+ const elapsed = Math.max(0, performance.now() - started) / 1000;
366
+ const histogram = state.outcomes[outcomes.indexOf(outcome)];
367
+ if (histogram) {
368
+ histogram.count++;
369
+ histogram.sumSeconds += elapsed;
370
+ for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
371
+ const bound = ACQUISITION_BUCKETS_SECONDS[index];
372
+ if (bound !== undefined && elapsed <= bound)
373
+ histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
374
+ }
375
+ }
376
+ }
377
+ }
378
+ var ACQUISITION_BUCKETS_SECONDS, outcomes, metricsBrand, states;
379
+ var init_acquisitionMetrics = __esm(() => {
380
+ ACQUISITION_BUCKETS_SECONDS = [
381
+ 0.001,
382
+ 0.005,
383
+ 0.01,
384
+ 0.025,
385
+ 0.05,
386
+ 0.1,
387
+ 0.25,
388
+ 0.5,
389
+ 1,
390
+ 2.5,
391
+ 5,
392
+ 10
393
+ ];
394
+ outcomes = ["acquired", "failed", "aborted"];
395
+ metricsBrand = Symbol("transaction acquisition metrics");
396
+ states = new WeakMap;
397
+ });
398
+
347
399
  // ../../src/core/database/connection.ts
348
400
  function createDatabaseConnection(source) {
349
401
  return {
@@ -712,9 +764,13 @@ async function runInTransaction(operation, options = {}) {
712
764
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
713
765
  }
714
766
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
715
- if (signal && !pool.reserve)
716
- throw new Error("Active database connection does not support cancellable transaction acquisition.");
717
- const reserved = signal ? await pool.reserve?.({ signal }) : undefined;
767
+ let reserved;
768
+ if (signal || options.acquisitionMetrics) {
769
+ if (!pool.reserve)
770
+ throw new Error(signal ? "Active database connection does not support cancellable transaction acquisition." : "Active database connection does not support observable transaction acquisition.");
771
+ const reserve = pool.reserve.bind(pool);
772
+ reserved = options.acquisitionMetrics ? await observeTransactionAcquisition(options.acquisitionMetrics, () => reserve({ signal }), signal) : await reserve({ signal });
773
+ }
718
774
  const connection = reserved ?? pool;
719
775
  try {
720
776
  signal?.throwIfAborted();
@@ -732,10 +788,12 @@ var transactionFrame, outerFrames;
732
788
  var init_transaction = __esm(() => {
733
789
  init_deferredModelEvents();
734
790
  init_asyncContextStore();
791
+ init_acquisitionMetrics();
735
792
  init_connection();
736
793
  init_connectionContext();
737
794
  init_repositoryConnection();
738
795
  init_transactionControl();
796
+ init_acquisitionMetrics();
739
797
  transactionFrame = createAsyncContextStore("@getstrata/transactionFrame");
740
798
  outerFrames = new WeakMap;
741
799
  });
@@ -52,6 +52,9 @@ function unwrapTransactionRollback(error, depth = 0) {
52
52
  }
53
53
  return unwrapTransactionRollback(error.cause, depth + 1);
54
54
  }
55
+ function hasActiveTransaction() {
56
+ return hasActiveDatabaseConnection() && (transactionScopes.getStore()?.rollback.length ?? 0) > 0;
57
+ }
55
58
  function requestTransactionRollback() {
56
59
  const state = transactionScopes.getStore();
57
60
  const index = state ? state.rollback.length - 1 : -1;
@@ -278,6 +281,55 @@ var init_deferredModelEvents = __esm(() => {
278
281
  deferredModelEvents = createAsyncContextStore("@getstrata/deferredModelEvents");
279
282
  });
280
283
 
284
+ // ../../src/core/database/acquisitionMetrics.ts
285
+ async function observeTransactionAcquisition(metrics, acquire, signal) {
286
+ const state = states.get(metrics);
287
+ if (!state)
288
+ throw new TypeError("Transaction acquisition metrics must be created by the framework factory.");
289
+ const started = performance.now();
290
+ state.inflight++;
291
+ let outcome = "acquired";
292
+ try {
293
+ return await acquire();
294
+ } catch (error) {
295
+ outcome = signal?.aborted ? "aborted" : "failed";
296
+ throw error;
297
+ } finally {
298
+ state.inflight--;
299
+ const elapsed = Math.max(0, performance.now() - started) / 1000;
300
+ const histogram = state.outcomes[outcomes.indexOf(outcome)];
301
+ if (histogram) {
302
+ histogram.count++;
303
+ histogram.sumSeconds += elapsed;
304
+ for (let index = 0;index < ACQUISITION_BUCKETS_SECONDS.length; index++) {
305
+ const bound = ACQUISITION_BUCKETS_SECONDS[index];
306
+ if (bound !== undefined && elapsed <= bound)
307
+ histogram.buckets[index] = (histogram.buckets[index] ?? 0) + 1;
308
+ }
309
+ }
310
+ }
311
+ }
312
+ var ACQUISITION_BUCKETS_SECONDS, outcomes, metricsBrand, states;
313
+ var init_acquisitionMetrics = __esm(() => {
314
+ ACQUISITION_BUCKETS_SECONDS = [
315
+ 0.001,
316
+ 0.005,
317
+ 0.01,
318
+ 0.025,
319
+ 0.05,
320
+ 0.1,
321
+ 0.25,
322
+ 0.5,
323
+ 1,
324
+ 2.5,
325
+ 5,
326
+ 10
327
+ ];
328
+ outcomes = ["acquired", "failed", "aborted"];
329
+ metricsBrand = Symbol("transaction acquisition metrics");
330
+ states = new WeakMap;
331
+ });
332
+
281
333
  // ../../src/core/database/connection.ts
282
334
  function createDatabaseConnection(source) {
283
335
  return {
@@ -646,9 +698,13 @@ async function runInTransaction(operation, options = {}) {
646
698
  throw new Error("Active database connection does not support transactions. Bind a client with begin() via bindDatabaseConnection.");
647
699
  }
648
700
  return await settleTransaction(() => runWithDeferredModelEvents(async () => {
649
- if (signal && !pool.reserve)
650
- throw new Error("Active database connection does not support cancellable transaction acquisition.");
651
- const reserved = signal ? await pool.reserve?.({ signal }) : undefined;
701
+ let reserved;
702
+ if (signal || options.acquisitionMetrics) {
703
+ if (!pool.reserve)
704
+ throw new Error(signal ? "Active database connection does not support cancellable transaction acquisition." : "Active database connection does not support observable transaction acquisition.");
705
+ const reserve = pool.reserve.bind(pool);
706
+ reserved = options.acquisitionMetrics ? await observeTransactionAcquisition(options.acquisitionMetrics, () => reserve({ signal }), signal) : await reserve({ signal });
707
+ }
652
708
  const connection = reserved ?? pool;
653
709
  try {
654
710
  signal?.throwIfAborted();
@@ -666,10 +722,12 @@ var transactionFrame, outerFrames;
666
722
  var init_transaction = __esm(() => {
667
723
  init_deferredModelEvents();
668
724
  init_asyncContextStore();
725
+ init_acquisitionMetrics();
669
726
  init_connection();
670
727
  init_connectionContext();
671
728
  init_repositoryConnection();
672
729
  init_transactionControl();
730
+ init_acquisitionMetrics();
673
731
  transactionFrame = createAsyncContextStore("@getstrata/transactionFrame");
674
732
  outerFrames = new WeakMap;
675
733
  });
@@ -48,10 +48,10 @@ import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-ho
48
48
  import { W3CTraceContextPropagator } from "@opentelemetry/core";
49
49
  import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
50
50
  import { resourceFromAttributes } from "@opentelemetry/resources";
51
+ import { BatchSpanProcessor } from "@opentelemetry/sdk-trace";
51
52
  import {
52
53
  AlwaysOffSampler,
53
54
  AlwaysOnSampler,
54
- BatchSpanProcessor,
55
55
  ParentBasedSampler,
56
56
  TraceIdRatioBasedSampler
57
57
  } from "@opentelemetry/sdk-trace-base";
@@ -143,6 +143,172 @@ function sdkClientClassName() {
143
143
  return fromName ? `${fromName}Client` : "AppClient";
144
144
  }
145
145
 
146
+ // ../../src/core/tracing/health.ts
147
+ import {
148
+ createNoopMeter
149
+ } from "@opentelemetry/api";
150
+ import { ExportResultCode } from "@opentelemetry/core";
151
+ function observeExportDeadline(state, timeoutMillis) {
152
+ const deadline = setTimeout(() => {
153
+ state.exportDeadlineExceededBatches++;
154
+ }, timeoutMillis);
155
+ deadline.unref();
156
+ return deadline;
157
+ }
158
+ function createTracingHealth(enabled, exporterConfigured, timeoutMillis) {
159
+ const state = {
160
+ enabled,
161
+ exporterConfigured,
162
+ active: true,
163
+ queueSize: null,
164
+ queueCapacity: null,
165
+ droppedSpans: 0,
166
+ exportedBatches: 0,
167
+ failedBatches: 0,
168
+ exportedSpans: 0,
169
+ failedSpans: 0,
170
+ exportDeadlineExceededBatches: 0,
171
+ flushFailures: 0,
172
+ shutdownFailures: 0
173
+ };
174
+ let exportQueueFullSpans = 0;
175
+ let queueCallback;
176
+ const noop = createNoopMeter();
177
+ const meter = {
178
+ createGauge: noop.createGauge.bind(noop),
179
+ createHistogram: noop.createHistogram.bind(noop),
180
+ createObservableGauge: noop.createObservableGauge.bind(noop),
181
+ createObservableCounter: noop.createObservableCounter.bind(noop),
182
+ addBatchObservableCallback: noop.addBatchObservableCallback.bind(noop),
183
+ removeBatchObservableCallback: noop.removeBatchObservableCallback.bind(noop),
184
+ createCounter(name, options) {
185
+ if (name !== "otel.sdk.processor.span.processed")
186
+ return noop.createCounter(name, options);
187
+ return {
188
+ add(value, attributes) {
189
+ if (attributes?.["error.type"] === "queue_full")
190
+ state.droppedSpans += value;
191
+ }
192
+ };
193
+ },
194
+ createUpDownCounter(name, options) {
195
+ if (name !== "otel.sdk.processor.span.queue.capacity")
196
+ return noop.createUpDownCounter(name, options);
197
+ return {
198
+ add(value) {
199
+ state.queueCapacity = (state.queueCapacity ?? 0) + value;
200
+ }
201
+ };
202
+ },
203
+ createObservableUpDownCounter(name, options) {
204
+ if (name !== "otel.sdk.processor.span.queue.size")
205
+ return noop.createObservableUpDownCounter(name, options);
206
+ return {
207
+ addCallback(callback) {
208
+ queueCallback = callback;
209
+ },
210
+ removeCallback(callback) {
211
+ if (queueCallback === callback)
212
+ queueCallback = undefined;
213
+ }
214
+ };
215
+ }
216
+ };
217
+ const meterProvider = { getMeter: () => meter };
218
+ return {
219
+ state,
220
+ meterProvider,
221
+ snapshot() {
222
+ let queueSize = null;
223
+ queueCallback?.({
224
+ observe(value) {
225
+ queueSize = value;
226
+ }
227
+ });
228
+ return {
229
+ ...state,
230
+ queueSize,
231
+ droppedSpans: Math.max(0, state.droppedSpans - exportQueueFullSpans)
232
+ };
233
+ },
234
+ wrap(exporter) {
235
+ return {
236
+ export(spans, callback) {
237
+ const count = spans.length;
238
+ let completed = false;
239
+ const deadline = observeExportDeadline(state, timeoutMillis);
240
+ const finish = (success) => {
241
+ completed = true;
242
+ clearTimeout(deadline);
243
+ if (success) {
244
+ state.exportedBatches++;
245
+ state.exportedSpans += count;
246
+ } else {
247
+ state.failedBatches++;
248
+ state.failedSpans += count;
249
+ }
250
+ };
251
+ try {
252
+ exporter.export(spans, (result) => {
253
+ if (completed)
254
+ return;
255
+ finish(result.code === ExportResultCode.SUCCESS);
256
+ if (result.error?.name === "queue_full")
257
+ exportQueueFullSpans += count;
258
+ callback(result);
259
+ });
260
+ } catch (error) {
261
+ if (!completed)
262
+ finish(false);
263
+ throw error;
264
+ }
265
+ },
266
+ shutdown: () => exporter.shutdown(),
267
+ ...exporter.forceFlush ? { forceFlush: exporter.forceFlush.bind(exporter) } : {}
268
+ };
269
+ }
270
+ };
271
+ }
272
+ function renderTracingMetrics(snapshot) {
273
+ for (const key of ["enabled", "exporterConfigured", "active"])
274
+ if (typeof snapshot[key] !== "boolean")
275
+ throw new TypeError("Invalid tracing metrics.");
276
+ const values = {
277
+ strata_tracing_enabled: Number(snapshot.enabled),
278
+ strata_tracing_exporter_configured: Number(snapshot.exporterConfigured),
279
+ strata_tracing_active: Number(snapshot.active),
280
+ strata_tracing_queue_size: snapshot.queueSize,
281
+ strata_tracing_queue_capacity: snapshot.queueCapacity,
282
+ strata_tracing_dropped_spans_total: snapshot.droppedSpans,
283
+ strata_tracing_exported_batches_total: snapshot.exportedBatches,
284
+ strata_tracing_failed_batches_total: snapshot.failedBatches,
285
+ strata_tracing_exported_spans_total: snapshot.exportedSpans,
286
+ strata_tracing_failed_spans_total: snapshot.failedSpans,
287
+ strata_tracing_export_deadline_exceeded_batches_total: snapshot.exportDeadlineExceededBatches,
288
+ strata_tracing_flush_failures_total: snapshot.flushFailures,
289
+ strata_tracing_shutdown_failures_total: snapshot.shutdownFailures
290
+ };
291
+ let output = `# HELP strata_tracing_collector_success Whether tracing observation succeeded.
292
+ # TYPE strata_tracing_collector_success gauge
293
+ strata_tracing_collector_success 1
294
+ `;
295
+ for (const [name, value] of Object.entries(values)) {
296
+ if (value === null) {
297
+ if (name === "strata_tracing_queue_size" || name === "strata_tracing_queue_capacity")
298
+ continue;
299
+ throw new TypeError("Invalid tracing metrics.");
300
+ }
301
+ if (!Number.isSafeInteger(value) || value < 0)
302
+ throw new TypeError("Invalid tracing metrics.");
303
+ const type = name.endsWith("_total") ? "counter" : "gauge";
304
+ output += `# HELP ${name} Process-local tracing ${name.slice(15).replaceAll("_", " ")}.
305
+ # TYPE ${name} ${type}
306
+ ${name} ${value}
307
+ `;
308
+ }
309
+ return output;
310
+ }
311
+
146
312
  // ../../src/core/tracing/otel.ts
147
313
  function numberOption(value, fallback, name, max = 1e6) {
148
314
  const result = value ?? fallback;
@@ -193,11 +359,14 @@ function createTracingRuntime(options = {}) {
193
359
  throw new TypeError("Invalid tracing collector endpoint.");
194
360
  }
195
361
  const exporter = disabled ? undefined : options.exporter ?? (endpoint ? new OTLPTraceExporter({ url: endpoint, timeoutMillis: timeout, concurrencyLimit: 1 }) : undefined);
362
+ const health = createTracingHealth(!disabled, exporter !== undefined, timeout);
196
363
  const provider = new NodeTracerProvider({
197
364
  resource: resourceFromAttributes({ "service.name": options.serviceName ?? otelServiceName() }),
198
365
  sampler,
199
366
  spanProcessors: exporter ? [
200
- new BatchSpanProcessor(exporter, {
367
+ new BatchSpanProcessor({
368
+ exporter: health.wrap(exporter),
369
+ selfObsMeterProvider: health.meterProvider,
201
370
  maxQueueSize: queueSize,
202
371
  maxExportBatchSize: batchSize,
203
372
  scheduledDelayMillis: delay,
@@ -217,8 +386,21 @@ function createTracingRuntime(options = {}) {
217
386
  tracer: provider.getTracer("@getstrata/http"),
218
387
  propagator: new W3CTraceContextPropagator,
219
388
  run: (parent, callback) => context.with(parent, callback),
220
- forceFlush: () => provider.forceFlush(),
221
- shutdown: () => shutdown ??= provider.shutdown()
389
+ metrics: () => health.snapshot(),
390
+ async forceFlush() {
391
+ try {
392
+ await provider.forceFlush();
393
+ } catch (error) {
394
+ health.state.flushFailures++;
395
+ throw error;
396
+ }
397
+ },
398
+ shutdown: () => shutdown ??= provider.shutdown().catch((error) => {
399
+ health.state.shutdownFailures++;
400
+ throw error;
401
+ }).finally(() => {
402
+ health.state.active = false;
403
+ })
222
404
  };
223
405
  }
224
406
  var runtimeKey = Symbol.for("@getstrata/otel-runtime");
@@ -319,5 +501,6 @@ export {
319
501
  acquireTracingRuntime,
320
502
  createTracingMiddleware,
321
503
  createTracingRuntime,
322
- getTracingRuntime
504
+ getTracingRuntime,
505
+ renderTracingMetrics
323
506
  };
@@ -32,6 +32,8 @@ export { CACHE_TAGS } from "../core/cache/tags.ts";
32
32
  export { ConfigStore, createServiceToken, ServiceContainer, type ServiceToken, } from "../core/contracts/container.ts";
33
33
  export type { ServiceProvider } from "../core/contracts/di.ts";
34
34
  export { resolveService } from "../core/contracts/di.ts";
35
+ export type { TransactionAcquisitionMetrics, TransactionAcquisitionSnapshot, } from "../core/database/acquisitionMetrics.ts";
36
+ export { createTransactionAcquisitionMetrics, renderTransactionAcquisitionMetrics, } from "../core/database/acquisitionMetrics.ts";
35
37
  export type { DatabaseConnection } from "../core/database/baseRepository.ts";
36
38
  export { default as BaseRepository } from "../core/database/baseRepository.ts";
37
39
  export { bindDatabaseConnection, getBoundDatabaseConnection, resetBoundDatabaseConnection, } from "../core/database/bindConnection.ts";
@@ -128,6 +130,8 @@ export { PrometheusRegistry, prometheusRegistry } from "../core/metrics/promethe
128
130
  export type { DatabaseNotificationPayload, DatabaseNotificationStore, MailNotificationMessage, Notifiable, NotificationChannelName, } from "../core/notifications/index.ts";
129
131
  export { createNotificationDispatcher, Notification, NotificationDispatcher, } from "../core/notifications/index.ts";
130
132
  export type { CursorPaginatedResult, KeysetColumn, KeysetCursor, KeysetOptions, PaginatedResult, PaginationMeta, } from "../core/pagination/index.ts";
133
+ export type { FailedJobMetricsCollector, FailedJobMetricsOptions, FailedJobMetricsSnapshot, } from "../core/queue/failedJobMetrics.ts";
134
+ export { createFailedJobMetricsCollector, renderFailedJobMetrics, } from "../core/queue/failedJobMetrics.ts";
131
135
  export type { Queue, QueuePriority } from "../core/queue/index.ts";
132
136
  export { AsyncQueue, createQueue, Job, SyncQueue } from "../core/queue/index.ts";
133
137
  export { JobRegistry } from "../core/queue/jobRegistry.ts";
@@ -155,6 +159,8 @@ export { currentTenant, currentTenantId, runWithTenant, tenantContext, } from ".
155
159
  export { isInsideTenantDatabaseScope, runWithTenantDatabase, } from "../core/tenant/tenantDatabaseScope.ts";
156
160
  export type { PublicTenancyOptions, TenantMiddlewareOptions, TenantResolver, } from "../core/tenant/tenantMiddleware.ts";
157
161
  export { auditChecksum, createTenantMiddleware, DEFAULT_TENANT, normalizeTenantHostname, resolveUserTenantId, } from "../core/tenant/tenantMiddleware.ts";
162
+ export type { TracingMetricsSnapshot } from "../core/tracing/health.ts";
163
+ export { renderTracingMetrics } from "../core/tracing/health.ts";
158
164
  export type { TraceContext } from "../core/tracing/traceContext.ts";
159
165
  export { currentTraceId, runWithTraceContext, traceContextStorage, } from "../core/tracing/traceContext.ts";
160
166
  export type { TracingOptions, TracingRuntime } from "../core/tracing/tracingMiddleware.ts";