@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/CHANGELOG.md +12 -0
- package/dist/core/database/acquisitionMetrics.d.ts +23 -0
- package/dist/core/database/baseRepository.d.ts +6 -0
- package/dist/core/database/postgresReadOnlyCollector.d.ts +7 -0
- package/dist/core/database/transaction.d.ts +11 -1
- package/dist/core/queue/failedJobMetrics.d.ts +17 -0
- package/dist/core/queue/queueMetrics.d.ts +4 -0
- package/dist/core/tracing/health.d.ts +27 -0
- package/dist/core/tracing/otel.d.ts +2 -0
- package/dist/core/tracing/tracingMiddleware.d.ts +2 -0
- package/dist/entries/audit/exportAuditLogs.js +80 -5
- package/dist/entries/auth/scimAuthMiddleware.js +80 -5
- package/dist/entries/http/response.js +3 -0
- package/dist/entries/jobs/exportAuditLogsJob.js +80 -5
- package/dist/entries/queue/queueMetrics.js +1018 -7
- package/dist/entries/tenant/databaseTenantContext.js +80 -5
- package/dist/entries/tenant/tenantDatabaseScope.js +80 -5
- package/dist/entries/tracing/tracingMiddleware.js +188 -5
- package/dist/framework/public-api.d.ts +7 -0
- package/dist/index.js +474 -98
- package/package.json +2 -1
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),
|
|
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
|
-
|
|
743
|
-
|
|
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/
|
|
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
|
-
|
|
9225
|
-
|
|
9226
|
-
|
|
9227
|
-
|
|
9228
|
-
|
|
9229
|
-
|
|
9230
|
-
|
|
9231
|
-
|
|
9232
|
-
|
|
9233
|
-
|
|
9234
|
-
if (
|
|
9235
|
-
throw new Error("
|
|
9236
|
-
|
|
9237
|
-
|
|
9238
|
-
|
|
9239
|
-
|
|
9240
|
-
|
|
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
|
-
|
|
9246
|
-
|
|
9247
|
-
|
|
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(
|
|
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
|
-
|
|
14771
|
-
|
|
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,
|