@getstrata/core 1.1.9 → 2.0.0
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 +15 -0
- package/dist/core/database/transaction.d.ts +2 -1
- package/dist/core/database/transactionControl.d.ts +5 -0
- package/dist/core/http/loginThrottleMiddleware.d.ts +3 -0
- package/dist/core/http/metricsMiddleware.d.ts +1 -2
- package/dist/core/http/requestMetaContext.d.ts +2 -0
- package/dist/core/http/throttleMiddleware.d.ts +11 -2
- package/dist/core/metrics/prometheus.d.ts +11 -1
- package/dist/core/queue/jobRunner.d.ts +3 -1
- package/dist/core/queue/redisQueue.d.ts +20 -3
- package/dist/core/tenant/databaseTenantContext.d.ts +5 -1
- package/dist/entries/audit/exportAuditLogs.js +522 -62
- package/dist/entries/auth/basicAuthGuard.js +245 -219
- package/dist/entries/auth/oauth/samlProvider.js +185 -161
- package/dist/entries/auth/oneTimeToken.js +130 -127
- package/dist/entries/auth/passwordLogin.js +185 -180
- package/dist/entries/auth/saml/samlServiceProvider.js +185 -161
- package/dist/entries/auth/scimAuthMiddleware.js +525 -11
- package/dist/entries/database/migrations.js +132 -129
- package/dist/entries/database/schema.js +58 -39
- package/dist/entries/http/metricsMiddleware.js +124 -27
- package/dist/entries/http/response.js +107 -0
- package/dist/entries/http/routeMiddleware.js +40 -0
- package/dist/entries/http/throttleMiddleware.js +124 -11
- package/dist/entries/jobs/exportAuditLogsJob.js +522 -62
- package/dist/entries/metrics/prometheus.js +71 -11
- package/dist/entries/queue/createAppQueue.js +215 -21
- package/dist/entries/queue/jobRunner.js +3 -2
- package/dist/entries/queue/publicQueue.js +215 -21
- package/dist/entries/queue/queueMetrics.js +215 -21
- package/dist/entries/queue/redisQueue.js +223 -22
- package/dist/entries/tenant/databaseTenantContext.js +477 -16
- package/dist/entries/tenant/tenantDatabaseScope.js +523 -9
- package/dist/framework/public-api.d.ts +2 -2
- package/dist/index.js +954 -457
- package/package.json +1 -1
|
@@ -26,14 +26,6 @@ function createAsyncContextStore(key) {
|
|
|
26
26
|
}
|
|
27
27
|
var init_asyncContextStore = () => {};
|
|
28
28
|
|
|
29
|
-
// ../../src/core/database/migrations/runner.ts
|
|
30
|
-
import { readdir } from "fs/promises";
|
|
31
|
-
import { join } from "path";
|
|
32
|
-
import { pathToFileURL } from "url";
|
|
33
|
-
|
|
34
|
-
// ../../src/core/database/dialect.ts
|
|
35
|
-
init_asyncContextStore();
|
|
36
|
-
|
|
37
29
|
// ../../src/core/database/schema/driver.ts
|
|
38
30
|
function normalizeConnectionName(connection) {
|
|
39
31
|
const normalized = connection.trim().toLowerCase();
|
|
@@ -73,6 +65,7 @@ function resolveDatabaseDriver(options = {}) {
|
|
|
73
65
|
}
|
|
74
66
|
return "pgsql";
|
|
75
67
|
}
|
|
68
|
+
var init_driver = () => {};
|
|
76
69
|
|
|
77
70
|
// ../../src/core/database/dialect.ts
|
|
78
71
|
function assertSafeIdentifier(identifier) {
|
|
@@ -84,133 +77,143 @@ function assertSafeIdentifier(identifier) {
|
|
|
84
77
|
function quoteIdentifierFor(dialect, column) {
|
|
85
78
|
return dialect.quoteIdentifier(column);
|
|
86
79
|
}
|
|
87
|
-
var postgresDialect = {
|
|
88
|
-
driver: "pgsql",
|
|
89
|
-
placeholder(index) {
|
|
90
|
-
return `$${index}`;
|
|
91
|
-
},
|
|
92
|
-
quoteIdentifier(identifier) {
|
|
93
|
-
return `"${assertSafeIdentifier(identifier)}"`;
|
|
94
|
-
},
|
|
95
|
-
nowExpression() {
|
|
96
|
-
return "NOW()";
|
|
97
|
-
},
|
|
98
|
-
timestampValue(value) {
|
|
99
|
-
return value.toISOString();
|
|
100
|
-
},
|
|
101
|
-
returningClause(columns) {
|
|
102
|
-
return ` RETURNING ${columns}`;
|
|
103
|
-
},
|
|
104
|
-
ilikeOperator() {
|
|
105
|
-
return "ILIKE";
|
|
106
|
-
},
|
|
107
|
-
nullsLastSuffix() {
|
|
108
|
-
return " NULLS LAST";
|
|
109
|
-
},
|
|
110
|
-
castToText(expression) {
|
|
111
|
-
return `${expression}::text`;
|
|
112
|
-
},
|
|
113
|
-
upsertSuffix(conflictColumns, updateColumns) {
|
|
114
|
-
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
115
|
-
if (updateColumns.length === 0) {
|
|
116
|
-
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
117
|
-
}
|
|
118
|
-
const assignments = updateColumns.map((column) => {
|
|
119
|
-
const quoted = quoteIdentifierFor(this, column);
|
|
120
|
-
return `${quoted} = excluded.${quoted}`;
|
|
121
|
-
}).join(", ");
|
|
122
|
-
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
123
|
-
}
|
|
124
|
-
};
|
|
125
|
-
var mysqlDialect = {
|
|
126
|
-
driver: "mysql",
|
|
127
|
-
placeholder() {
|
|
128
|
-
return "?";
|
|
129
|
-
},
|
|
130
|
-
quoteIdentifier(identifier) {
|
|
131
|
-
return `\`${assertSafeIdentifier(identifier)}\``;
|
|
132
|
-
},
|
|
133
|
-
nowExpression() {
|
|
134
|
-
return "CURRENT_TIMESTAMP";
|
|
135
|
-
},
|
|
136
|
-
timestampValue(value) {
|
|
137
|
-
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
138
|
-
},
|
|
139
|
-
returningClause() {
|
|
140
|
-
return "";
|
|
141
|
-
},
|
|
142
|
-
ilikeOperator() {
|
|
143
|
-
return "LIKE";
|
|
144
|
-
},
|
|
145
|
-
nullsLastSuffix() {
|
|
146
|
-
return "";
|
|
147
|
-
},
|
|
148
|
-
castToText(expression) {
|
|
149
|
-
return `CAST(${expression} AS CHAR)`;
|
|
150
|
-
},
|
|
151
|
-
upsertSuffix(conflictColumns, updateColumns) {
|
|
152
|
-
if (updateColumns.length === 0) {
|
|
153
|
-
const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
|
|
154
|
-
return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
|
|
155
|
-
}
|
|
156
|
-
const assignments = updateColumns.map((column) => {
|
|
157
|
-
const quoted = quoteIdentifierFor(this, column);
|
|
158
|
-
return `${quoted} = VALUES(${quoted})`;
|
|
159
|
-
}).join(", ");
|
|
160
|
-
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
161
|
-
}
|
|
162
|
-
};
|
|
163
|
-
var sqliteDialect = {
|
|
164
|
-
driver: "sqlite",
|
|
165
|
-
placeholder() {
|
|
166
|
-
return "?";
|
|
167
|
-
},
|
|
168
|
-
quoteIdentifier(identifier) {
|
|
169
|
-
return `"${assertSafeIdentifier(identifier)}"`;
|
|
170
|
-
},
|
|
171
|
-
nowExpression() {
|
|
172
|
-
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
173
|
-
},
|
|
174
|
-
timestampValue(value) {
|
|
175
|
-
return value.toISOString();
|
|
176
|
-
},
|
|
177
|
-
returningClause(columns) {
|
|
178
|
-
return ` RETURNING ${columns}`;
|
|
179
|
-
},
|
|
180
|
-
ilikeOperator() {
|
|
181
|
-
return "LIKE";
|
|
182
|
-
},
|
|
183
|
-
nullsLastSuffix() {
|
|
184
|
-
return "";
|
|
185
|
-
},
|
|
186
|
-
castToText(expression) {
|
|
187
|
-
return `CAST(${expression} AS TEXT)`;
|
|
188
|
-
},
|
|
189
|
-
upsertSuffix(conflictColumns, updateColumns) {
|
|
190
|
-
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
191
|
-
if (updateColumns.length === 0) {
|
|
192
|
-
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
193
|
-
}
|
|
194
|
-
const assignments = updateColumns.map((column) => {
|
|
195
|
-
const quoted = quoteIdentifierFor(this, column);
|
|
196
|
-
return `${quoted} = excluded.${quoted}`;
|
|
197
|
-
}).join(", ");
|
|
198
|
-
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
var dialects = {
|
|
202
|
-
pgsql: postgresDialect,
|
|
203
|
-
mysql: mysqlDialect,
|
|
204
|
-
sqlite: sqliteDialect
|
|
205
|
-
};
|
|
206
|
-
var dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
|
|
207
|
-
var dialectOverride = null;
|
|
208
80
|
function dialectFor(driver) {
|
|
209
81
|
return dialects[driver];
|
|
210
82
|
}
|
|
211
83
|
function currentSqlDialect() {
|
|
212
84
|
return dialectContext.getStore() ?? dialectOverride ?? dialectFor(resolveDatabaseDriver());
|
|
213
85
|
}
|
|
86
|
+
var postgresDialect, mysqlDialect, sqliteDialect, dialects, dialectContext, dialectOverride = null;
|
|
87
|
+
var init_dialect = __esm(() => {
|
|
88
|
+
init_asyncContextStore();
|
|
89
|
+
init_driver();
|
|
90
|
+
postgresDialect = {
|
|
91
|
+
driver: "pgsql",
|
|
92
|
+
placeholder(index) {
|
|
93
|
+
return `$${index}`;
|
|
94
|
+
},
|
|
95
|
+
quoteIdentifier(identifier) {
|
|
96
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
97
|
+
},
|
|
98
|
+
nowExpression() {
|
|
99
|
+
return "NOW()";
|
|
100
|
+
},
|
|
101
|
+
timestampValue(value) {
|
|
102
|
+
return value.toISOString();
|
|
103
|
+
},
|
|
104
|
+
returningClause(columns) {
|
|
105
|
+
return ` RETURNING ${columns}`;
|
|
106
|
+
},
|
|
107
|
+
ilikeOperator() {
|
|
108
|
+
return "ILIKE";
|
|
109
|
+
},
|
|
110
|
+
nullsLastSuffix() {
|
|
111
|
+
return " NULLS LAST";
|
|
112
|
+
},
|
|
113
|
+
castToText(expression) {
|
|
114
|
+
return `${expression}::text`;
|
|
115
|
+
},
|
|
116
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
117
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
118
|
+
if (updateColumns.length === 0) {
|
|
119
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
120
|
+
}
|
|
121
|
+
const assignments = updateColumns.map((column) => {
|
|
122
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
123
|
+
return `${quoted} = excluded.${quoted}`;
|
|
124
|
+
}).join(", ");
|
|
125
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
mysqlDialect = {
|
|
129
|
+
driver: "mysql",
|
|
130
|
+
placeholder() {
|
|
131
|
+
return "?";
|
|
132
|
+
},
|
|
133
|
+
quoteIdentifier(identifier) {
|
|
134
|
+
return `\`${assertSafeIdentifier(identifier)}\``;
|
|
135
|
+
},
|
|
136
|
+
nowExpression() {
|
|
137
|
+
return "CURRENT_TIMESTAMP";
|
|
138
|
+
},
|
|
139
|
+
timestampValue(value) {
|
|
140
|
+
return value.toISOString().slice(0, 19).replace("T", " ");
|
|
141
|
+
},
|
|
142
|
+
returningClause() {
|
|
143
|
+
return "";
|
|
144
|
+
},
|
|
145
|
+
ilikeOperator() {
|
|
146
|
+
return "LIKE";
|
|
147
|
+
},
|
|
148
|
+
nullsLastSuffix() {
|
|
149
|
+
return "";
|
|
150
|
+
},
|
|
151
|
+
castToText(expression) {
|
|
152
|
+
return `CAST(${expression} AS CHAR)`;
|
|
153
|
+
},
|
|
154
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
155
|
+
if (updateColumns.length === 0) {
|
|
156
|
+
const anchor = quoteIdentifierFor(this, conflictColumns[0] ?? "");
|
|
157
|
+
return ` ON DUPLICATE KEY UPDATE ${anchor} = ${anchor}`;
|
|
158
|
+
}
|
|
159
|
+
const assignments = updateColumns.map((column) => {
|
|
160
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
161
|
+
return `${quoted} = VALUES(${quoted})`;
|
|
162
|
+
}).join(", ");
|
|
163
|
+
return ` ON DUPLICATE KEY UPDATE ${assignments}`;
|
|
164
|
+
}
|
|
165
|
+
};
|
|
166
|
+
sqliteDialect = {
|
|
167
|
+
driver: "sqlite",
|
|
168
|
+
placeholder() {
|
|
169
|
+
return "?";
|
|
170
|
+
},
|
|
171
|
+
quoteIdentifier(identifier) {
|
|
172
|
+
return `"${assertSafeIdentifier(identifier)}"`;
|
|
173
|
+
},
|
|
174
|
+
nowExpression() {
|
|
175
|
+
return "strftime('%Y-%m-%dT%H:%M:%fZ', 'now')";
|
|
176
|
+
},
|
|
177
|
+
timestampValue(value) {
|
|
178
|
+
return value.toISOString();
|
|
179
|
+
},
|
|
180
|
+
returningClause(columns) {
|
|
181
|
+
return ` RETURNING ${columns}`;
|
|
182
|
+
},
|
|
183
|
+
ilikeOperator() {
|
|
184
|
+
return "LIKE";
|
|
185
|
+
},
|
|
186
|
+
nullsLastSuffix() {
|
|
187
|
+
return "";
|
|
188
|
+
},
|
|
189
|
+
castToText(expression) {
|
|
190
|
+
return `CAST(${expression} AS TEXT)`;
|
|
191
|
+
},
|
|
192
|
+
upsertSuffix(conflictColumns, updateColumns) {
|
|
193
|
+
const target = conflictColumns.map((column) => quoteIdentifierFor(this, column)).join(", ");
|
|
194
|
+
if (updateColumns.length === 0) {
|
|
195
|
+
return ` ON CONFLICT (${target}) DO NOTHING`;
|
|
196
|
+
}
|
|
197
|
+
const assignments = updateColumns.map((column) => {
|
|
198
|
+
const quoted = quoteIdentifierFor(this, column);
|
|
199
|
+
return `${quoted} = excluded.${quoted}`;
|
|
200
|
+
}).join(", ");
|
|
201
|
+
return ` ON CONFLICT (${target}) DO UPDATE SET ${assignments}`;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
dialects = {
|
|
205
|
+
pgsql: postgresDialect,
|
|
206
|
+
mysql: mysqlDialect,
|
|
207
|
+
sqlite: sqliteDialect
|
|
208
|
+
};
|
|
209
|
+
dialectContext = createAsyncContextStore("@getstrata/sqlDialect");
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
// ../../src/core/database/migrations/runner.ts
|
|
213
|
+
init_dialect();
|
|
214
|
+
import { readdir } from "fs/promises";
|
|
215
|
+
import { join } from "path";
|
|
216
|
+
import { pathToFileURL } from "url";
|
|
214
217
|
|
|
215
218
|
// ../../src/core/database/migrations/advisoryLock.ts
|
|
216
219
|
var MIGRATION_LOCK_KEY = 42424242;
|
|
@@ -1,4 +1,57 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __esm = (fn, res, err) => () => {
|
|
3
|
+
if (fn)
|
|
4
|
+
try {
|
|
5
|
+
res = fn(fn = 0);
|
|
6
|
+
} catch (e) {
|
|
7
|
+
err = [e];
|
|
8
|
+
}
|
|
9
|
+
if (err)
|
|
10
|
+
throw err[0];
|
|
11
|
+
return res;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// ../../src/core/database/schema/driver.ts
|
|
15
|
+
function normalizeConnectionName(connection) {
|
|
16
|
+
const normalized = connection.trim().toLowerCase();
|
|
17
|
+
if (normalized === "pgsql" || normalized === "postgres" || normalized === "postgresql") {
|
|
18
|
+
return "pgsql";
|
|
19
|
+
}
|
|
20
|
+
if (normalized === "mysql" || normalized === "mariadb") {
|
|
21
|
+
return "mysql";
|
|
22
|
+
}
|
|
23
|
+
if (normalized === "sqlite") {
|
|
24
|
+
return "sqlite";
|
|
25
|
+
}
|
|
26
|
+
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
27
|
+
}
|
|
28
|
+
function resolveDriverFromUrl(url) {
|
|
29
|
+
const normalized = url.trim().toLowerCase();
|
|
30
|
+
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
31
|
+
return "pgsql";
|
|
32
|
+
}
|
|
33
|
+
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
34
|
+
return "mysql";
|
|
35
|
+
}
|
|
36
|
+
if (normalized.startsWith("sqlite:")) {
|
|
37
|
+
return "sqlite";
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
function resolveDatabaseDriver(options = {}) {
|
|
42
|
+
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
43
|
+
if (connection) {
|
|
44
|
+
return normalizeConnectionName(connection);
|
|
45
|
+
}
|
|
46
|
+
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
47
|
+
const fromUrl = resolveDriverFromUrl(url);
|
|
48
|
+
if (fromUrl) {
|
|
49
|
+
return fromUrl;
|
|
50
|
+
}
|
|
51
|
+
return "pgsql";
|
|
52
|
+
}
|
|
53
|
+
var init_driver = () => {};
|
|
54
|
+
|
|
2
55
|
// ../../src/core/database/schema/columnDefinition.ts
|
|
3
56
|
class ColumnDefinition {
|
|
4
57
|
name;
|
|
@@ -220,45 +273,10 @@ class Blueprint {
|
|
|
220
273
|
});
|
|
221
274
|
}
|
|
222
275
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return "pgsql";
|
|
228
|
-
}
|
|
229
|
-
if (normalized === "mysql" || normalized === "mariadb") {
|
|
230
|
-
return "mysql";
|
|
231
|
-
}
|
|
232
|
-
if (normalized === "sqlite") {
|
|
233
|
-
return "sqlite";
|
|
234
|
-
}
|
|
235
|
-
throw new Error(`Unsupported DB_CONNECTION: ${connection}`);
|
|
236
|
-
}
|
|
237
|
-
function resolveDriverFromUrl(url) {
|
|
238
|
-
const normalized = url.trim().toLowerCase();
|
|
239
|
-
if (normalized.startsWith("postgres://") || normalized.startsWith("postgresql://") || normalized.startsWith("postgres:")) {
|
|
240
|
-
return "pgsql";
|
|
241
|
-
}
|
|
242
|
-
if (normalized.startsWith("mysql://") || normalized.startsWith("mysql:")) {
|
|
243
|
-
return "mysql";
|
|
244
|
-
}
|
|
245
|
-
if (normalized.startsWith("sqlite:")) {
|
|
246
|
-
return "sqlite";
|
|
247
|
-
}
|
|
248
|
-
return null;
|
|
249
|
-
}
|
|
250
|
-
function resolveDatabaseDriver(options = {}) {
|
|
251
|
-
const connection = options.connection ?? process.env.DB_CONNECTION;
|
|
252
|
-
if (connection) {
|
|
253
|
-
return normalizeConnectionName(connection);
|
|
254
|
-
}
|
|
255
|
-
const url = options.url ?? process.env.DATABASE_URL ?? "";
|
|
256
|
-
const fromUrl = resolveDriverFromUrl(url);
|
|
257
|
-
if (fromUrl) {
|
|
258
|
-
return fromUrl;
|
|
259
|
-
}
|
|
260
|
-
return "pgsql";
|
|
261
|
-
}
|
|
276
|
+
|
|
277
|
+
// ../../src/core/database/schema/index.ts
|
|
278
|
+
init_driver();
|
|
279
|
+
|
|
262
280
|
// ../../src/core/database/schema/errors.ts
|
|
263
281
|
class UnsupportedSchemaFeatureError extends Error {
|
|
264
282
|
constructor(feature, driver) {
|
|
@@ -1084,6 +1102,7 @@ function grammarForDriver(driver) {
|
|
|
1084
1102
|
}
|
|
1085
1103
|
}
|
|
1086
1104
|
// ../../src/core/database/schema/schema.ts
|
|
1105
|
+
init_driver();
|
|
1087
1106
|
class SchemaBuilder {
|
|
1088
1107
|
#driver;
|
|
1089
1108
|
#statements = [];
|
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
var __esm = (fn, res, err) => () => {
|
|
3
|
+
if (fn)
|
|
4
|
+
try {
|
|
5
|
+
res = fn(fn = 0);
|
|
6
|
+
} catch (e) {
|
|
7
|
+
err = [e];
|
|
8
|
+
}
|
|
9
|
+
if (err)
|
|
10
|
+
throw err[0];
|
|
11
|
+
return res;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
// ../../src/core/runtime/asyncContextStore.ts
|
|
15
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
16
|
+
function createAsyncContextStore(key) {
|
|
17
|
+
const symbol = Symbol.for(key);
|
|
18
|
+
const globalRecord = globalThis;
|
|
19
|
+
const existing = globalRecord[symbol];
|
|
20
|
+
if (existing) {
|
|
21
|
+
return existing;
|
|
22
|
+
}
|
|
23
|
+
const store = new AsyncLocalStorage;
|
|
24
|
+
globalRecord[symbol] = store;
|
|
25
|
+
return store;
|
|
26
|
+
}
|
|
27
|
+
var init_asyncContextStore = () => {};
|
|
28
|
+
|
|
2
29
|
// ../../src/core/metrics/prometheus.ts
|
|
30
|
+
var HTTP_DURATION_BUCKETS_MS = [5, 10, 25, 50, 100, 250, 500, 1000, 2500, 5000, 1e4];
|
|
31
|
+
function emptyHistogram() {
|
|
32
|
+
return {
|
|
33
|
+
cumulative: HTTP_DURATION_BUCKETS_MS.map(() => 0),
|
|
34
|
+
sum: 0,
|
|
35
|
+
count: 0
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
3
39
|
class PrometheusRegistry {
|
|
40
|
+
maxSeries;
|
|
41
|
+
seriesLabels = new Map;
|
|
42
|
+
constructor(maxSeries = 4096) {
|
|
43
|
+
this.maxSeries = maxSeries;
|
|
44
|
+
if (!Number.isSafeInteger(maxSeries) || maxSeries < 1) {
|
|
45
|
+
throw new Error("Metrics maxSeries must be a positive safe integer.");
|
|
46
|
+
}
|
|
47
|
+
}
|
|
4
48
|
httpRequestsTotal = new Map;
|
|
5
49
|
httpRequestDurationMs = new Map;
|
|
6
50
|
incrementHttpRequest(labels) {
|
|
@@ -8,10 +52,20 @@ class PrometheusRegistry {
|
|
|
8
52
|
this.httpRequestsTotal.set(key, (this.httpRequestsTotal.get(key) ?? 0) + 1);
|
|
9
53
|
}
|
|
10
54
|
observeHttpDuration(labels, durationMs) {
|
|
55
|
+
if (!Number.isFinite(durationMs) || durationMs < 0)
|
|
56
|
+
return;
|
|
11
57
|
const key = this.metricKey(labels);
|
|
12
|
-
const
|
|
13
|
-
|
|
14
|
-
|
|
58
|
+
const histogram = this.httpRequestDurationMs.get(key) ?? emptyHistogram();
|
|
59
|
+
histogram.count += 1;
|
|
60
|
+
histogram.sum += durationMs;
|
|
61
|
+
for (let index = 0;index < HTTP_DURATION_BUCKETS_MS.length; index += 1) {
|
|
62
|
+
const bound = HTTP_DURATION_BUCKETS_MS[index];
|
|
63
|
+
if (bound !== undefined && durationMs <= bound) {
|
|
64
|
+
const count = histogram.cumulative[index] ?? 0;
|
|
65
|
+
histogram.cumulative[index] = count + 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
this.httpRequestDurationMs.set(key, histogram);
|
|
15
69
|
}
|
|
16
70
|
renderMetrics() {
|
|
17
71
|
const lines = [
|
|
@@ -21,16 +75,22 @@ class PrometheusRegistry {
|
|
|
21
75
|
for (const [key, value] of this.httpRequestsTotal) {
|
|
22
76
|
lines.push(`http_requests_total{${key}} ${value}`);
|
|
23
77
|
}
|
|
24
|
-
lines.push("# HELP
|
|
25
|
-
for (const [key,
|
|
26
|
-
|
|
27
|
-
|
|
78
|
+
lines.push("# HELP http_request_duration_ms HTTP request duration in milliseconds.", "# TYPE http_request_duration_ms histogram");
|
|
79
|
+
for (const [key, histogram] of this.httpRequestDurationMs) {
|
|
80
|
+
for (let index = 0;index < HTTP_DURATION_BUCKETS_MS.length; index += 1) {
|
|
81
|
+
const bound = HTTP_DURATION_BUCKETS_MS[index];
|
|
82
|
+
lines.push(`http_request_duration_ms_bucket{${key},le="${bound}"} ${histogram.cumulative[index] ?? 0}`);
|
|
83
|
+
}
|
|
84
|
+
lines.push(`http_request_duration_ms_bucket{${key},le="+Inf"} ${histogram.count}`);
|
|
85
|
+
lines.push(`http_request_duration_ms_sum{${key}} ${histogram.sum}`);
|
|
86
|
+
lines.push(`http_request_duration_ms_count{${key}} ${histogram.count}`);
|
|
28
87
|
}
|
|
29
88
|
return `${lines.join(`
|
|
30
89
|
`)}
|
|
31
90
|
`;
|
|
32
91
|
}
|
|
33
92
|
resetForTests() {
|
|
93
|
+
this.seriesLabels.clear();
|
|
34
94
|
this.httpRequestsTotal.clear();
|
|
35
95
|
this.httpRequestDurationMs.clear();
|
|
36
96
|
}
|
|
@@ -40,9 +100,10 @@ class PrometheusRegistry {
|
|
|
40
100
|
let totalRequests = 0;
|
|
41
101
|
for (const [key, count] of this.httpRequestsTotal) {
|
|
42
102
|
totalRequests += count;
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
103
|
+
const labels = this.seriesLabels.get(key);
|
|
104
|
+
if (!labels)
|
|
105
|
+
continue;
|
|
106
|
+
const { method, path, status } = labels;
|
|
46
107
|
byStatus[status] = (byStatus[status] ?? 0) + count;
|
|
47
108
|
const pathKey = `${method} ${path}`;
|
|
48
109
|
const existing = pathCounts.get(pathKey);
|
|
@@ -59,33 +120,69 @@ class PrometheusRegistry {
|
|
|
59
120
|
topPaths
|
|
60
121
|
};
|
|
61
122
|
}
|
|
123
|
+
getStorageStats() {
|
|
124
|
+
return {
|
|
125
|
+
series: this.seriesLabels.size,
|
|
126
|
+
histograms: this.httpRequestDurationMs.size,
|
|
127
|
+
bucketCounters: this.httpRequestDurationMs.size * HTTP_DURATION_BUCKETS_MS.length
|
|
128
|
+
};
|
|
129
|
+
}
|
|
62
130
|
metricKey(labels) {
|
|
63
|
-
|
|
131
|
+
const normalized = {
|
|
132
|
+
method: /^(GET|HEAD|POST|PUT|PATCH|DELETE|CONNECT|OPTIONS|TRACE)$/.test(labels.method) ? labels.method : "OTHER",
|
|
133
|
+
path: labels.path.length <= 512 ? labels.path : "__overflow__",
|
|
134
|
+
status: /^[1-5][0-9]{2}$/.test(labels.status) ? labels.status : "000"
|
|
135
|
+
};
|
|
136
|
+
const escapeLabel = (value) => value.replaceAll("\\", "\\\\").replaceAll(`
|
|
137
|
+
`, "\\n").replaceAll('"', "\\\"");
|
|
138
|
+
const encode = (value) => `method="${value.method}",path="${escapeLabel(value.path)}",status="${value.status}"`;
|
|
139
|
+
let key = encode(normalized);
|
|
140
|
+
if (!this.seriesLabels.has(key)) {
|
|
141
|
+
if (this.seriesLabels.size >= this.maxSeries) {
|
|
142
|
+
const overflow = { method: "OTHER", path: "__overflow__", status: "000" };
|
|
143
|
+
key = encode(overflow);
|
|
144
|
+
this.seriesLabels.set(key, overflow);
|
|
145
|
+
} else {
|
|
146
|
+
this.seriesLabels.set(key, normalized);
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
return key;
|
|
64
150
|
}
|
|
65
151
|
}
|
|
66
152
|
var prometheusRegistry = new PrometheusRegistry;
|
|
67
153
|
|
|
68
|
-
// ../../src/core/http/
|
|
69
|
-
|
|
70
|
-
|
|
154
|
+
// ../../src/core/http/requestMetaContext.ts
|
|
155
|
+
init_asyncContextStore();
|
|
156
|
+
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
157
|
+
function runWithRequestMeta(meta, callback) {
|
|
158
|
+
return requestMetaContext.run(meta, callback);
|
|
159
|
+
}
|
|
160
|
+
function currentRequestMeta() {
|
|
161
|
+
return requestMetaContext.getStore() ?? {
|
|
162
|
+
ipAddress: null,
|
|
163
|
+
userAgent: null
|
|
164
|
+
};
|
|
71
165
|
}
|
|
166
|
+
|
|
167
|
+
// ../../src/core/http/metricsMiddleware.ts
|
|
72
168
|
function createMetricsMiddleware() {
|
|
73
169
|
return async (request, next) => {
|
|
74
170
|
const startedAt = performance.now();
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
171
|
+
let response;
|
|
172
|
+
try {
|
|
173
|
+
response = await next();
|
|
174
|
+
return response;
|
|
175
|
+
} finally {
|
|
176
|
+
const labels = {
|
|
177
|
+
method: request.method,
|
|
178
|
+
path: currentRequestMeta().routeTemplate ?? "__unmatched__",
|
|
179
|
+
status: String(response?.status ?? 500)
|
|
180
|
+
};
|
|
181
|
+
prometheusRegistry.incrementHttpRequest(labels);
|
|
182
|
+
prometheusRegistry.observeHttpDuration(labels, performance.now() - startedAt);
|
|
183
|
+
}
|
|
86
184
|
};
|
|
87
185
|
}
|
|
88
186
|
export {
|
|
89
|
-
createMetricsMiddleware
|
|
90
|
-
normalizeMetricPath
|
|
187
|
+
createMetricsMiddleware
|
|
91
188
|
};
|