@getstrata/core 2.1.0 → 2.2.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 +33 -0
- package/dist/core/cache/createCacheStore.d.ts +3 -1
- package/dist/core/cache/redisCacheStore.d.ts +21 -8
- package/dist/core/cache/repository.d.ts +3 -2
- package/dist/core/cache/store.d.ts +4 -1
- package/dist/core/cache/taggedCache.d.ts +1 -1
- package/dist/core/contracts/di.d.ts +4 -2
- package/dist/core/contracts/serviceTokens.d.ts +6 -1
- package/dist/core/database/baseRepository.d.ts +0 -1
- package/dist/core/database/index.d.ts +1 -1
- package/dist/core/database/model.d.ts +51 -22
- package/dist/core/database/repositoryQuery.d.ts +16 -1
- package/dist/core/database/types.d.ts +25 -2
- package/dist/core/database/whereBuilder.d.ts +5 -5
- package/dist/core/events/deferredModelEvents.d.ts +2 -1
- package/dist/core/http/loginThrottleMiddleware.d.ts +6 -5
- package/dist/core/http/memoryThrottleMiddleware.d.ts +11 -3
- package/dist/core/http/scimThrottleMiddleware.d.ts +12 -3
- package/dist/core/http/throttleMiddleware.d.ts +24 -6
- package/dist/core/lifecycle/gracefulShutdown.d.ts +33 -5
- package/dist/core/queue/index.d.ts +7 -0
- package/dist/core/queue/redisQueue.d.ts +1 -0
- package/dist/core/queue/resilientQueue.d.ts +2 -2
- package/dist/core/runtime/applicationRegistry.d.ts +2 -1
- package/dist/core/runtime/boundedThrottleStore.d.ts +29 -0
- package/dist/core/scheduler/leases.d.ts +40 -0
- package/dist/core/scheduler/schedule.d.ts +29 -5
- package/dist/core/tenant/tenantContext.d.ts +5 -4
- package/dist/core/tenant/tenantMiddleware.d.ts +7 -1
- package/dist/core/tracing/otel.d.ts +28 -26
- package/dist/core/tracing/tracingMiddleware.d.ts +4 -1
- package/dist/entries/audit/exportAuditLogs.js +14 -2
- package/dist/entries/auth/basicAuthGuard.js +80 -0
- package/dist/entries/auth/jwtGuard.js +80 -0
- package/dist/entries/auth/scimAuthMiddleware.js +17 -7
- package/dist/entries/auth/sessionGuard.js +80 -0
- package/dist/entries/cache/createCacheStore.js +365 -119
- package/dist/entries/cache/repository.js +5 -0
- package/dist/entries/contracts/serviceTokens.js +83 -0
- package/dist/entries/database/factory.js +0 -3
- package/dist/entries/database/model.js +1 -2486
- package/dist/entries/database/repositoryQuery.js +67 -9
- package/dist/entries/http/bodySizeLimitMiddleware.js +76 -10
- package/dist/entries/http/memoryThrottleMiddleware.js +276 -21
- package/dist/entries/http/scimThrottleMiddleware.js +576 -54
- package/dist/entries/http/throttleMiddleware.js +65 -16
- package/dist/entries/jobs/exportAuditLogsJob.js +44 -7
- package/dist/entries/jobs/invalidateCacheTagsJob.js +30 -5
- package/dist/entries/queue/createAppQueue.js +67 -6
- package/dist/entries/queue/publicQueue.js +67 -6
- package/dist/entries/queue/queueMetrics.js +67 -6
- package/dist/entries/queue/redisQueue.js +3 -0
- package/dist/entries/queue.js +30 -5
- package/dist/entries/scheduler/schedule.js +1 -72
- package/dist/entries/tenant/databaseTenantContext.js +14 -2
- package/dist/entries/tenant/tenantDatabaseScope.js +14 -2
- package/dist/entries/tracing/tracingMiddleware.js +186 -454
- package/dist/framework/public-api.d.ts +13 -9
- package/dist/index.js +1745 -633
- package/dist/types/services.d.ts +3 -3
- package/package.json +17 -8
|
@@ -26,6 +26,37 @@ function createAsyncContextStore(key) {
|
|
|
26
26
|
}
|
|
27
27
|
var init_asyncContextStore = () => {};
|
|
28
28
|
|
|
29
|
+
// ../../src/core/tracing/tracingMiddleware.ts
|
|
30
|
+
import { ROOT_CONTEXT, SpanKind, SpanStatusCode, trace } from "@opentelemetry/api";
|
|
31
|
+
|
|
32
|
+
// ../../src/core/http/requestMetaContext.ts
|
|
33
|
+
init_asyncContextStore();
|
|
34
|
+
var requestMetaContext = createAsyncContextStore("@getstrata/requestMetaContext");
|
|
35
|
+
function runWithRequestMeta(meta, callback) {
|
|
36
|
+
return requestMetaContext.run(meta, callback);
|
|
37
|
+
}
|
|
38
|
+
function currentRequestMeta() {
|
|
39
|
+
return requestMetaContext.getStore() ?? {
|
|
40
|
+
ipAddress: null,
|
|
41
|
+
userAgent: null
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ../../src/core/tracing/otel.ts
|
|
46
|
+
import { context } from "@opentelemetry/api";
|
|
47
|
+
import { AsyncLocalStorageContextManager } from "@opentelemetry/context-async-hooks";
|
|
48
|
+
import { W3CTraceContextPropagator } from "@opentelemetry/core";
|
|
49
|
+
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
|
|
50
|
+
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
51
|
+
import {
|
|
52
|
+
AlwaysOffSampler,
|
|
53
|
+
AlwaysOnSampler,
|
|
54
|
+
BatchSpanProcessor,
|
|
55
|
+
ParentBasedSampler,
|
|
56
|
+
TraceIdRatioBasedSampler
|
|
57
|
+
} from "@opentelemetry/sdk-trace-base";
|
|
58
|
+
import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
|
|
59
|
+
|
|
29
60
|
// ../../src/core/runtime/appEnv.ts
|
|
30
61
|
var NON_PRODUCTION_APP_ENVS = new Set(["local", "development", "dev", "test", "testing", "ci"]);
|
|
31
62
|
function normalizeEnvValue(value) {
|
|
@@ -46,9 +77,6 @@ function envFlagEnabled(value) {
|
|
|
46
77
|
return value === "true";
|
|
47
78
|
}
|
|
48
79
|
|
|
49
|
-
// ../../src/core/tracing/otel.ts
|
|
50
|
-
import { randomBytes } from "crypto";
|
|
51
|
-
|
|
52
80
|
// ../../src/core/runtime/appKeyPrefix.ts
|
|
53
81
|
function appKeyPrefix() {
|
|
54
82
|
return process.env.APP_KEY_PREFIX?.trim() || "strata";
|
|
@@ -115,422 +143,114 @@ function sdkClientClassName() {
|
|
|
115
143
|
return fromName ? `${fromName}Client` : "AppClient";
|
|
116
144
|
}
|
|
117
145
|
|
|
118
|
-
// ../../src/core/
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
"
|
|
127
|
-
|
|
128
|
-
])
|
|
129
|
-
|
|
130
|
-
const
|
|
131
|
-
if (
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
if (
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
return true;
|
|
157
|
-
}
|
|
158
|
-
if (a === 172 && b >= 16 && b <= 31) {
|
|
159
|
-
return true;
|
|
160
|
-
}
|
|
161
|
-
if (a === 192 && b === 168) {
|
|
162
|
-
return true;
|
|
163
|
-
}
|
|
164
|
-
if (a === 100 && b >= 64 && b <= 127) {
|
|
165
|
-
return true;
|
|
166
|
-
}
|
|
167
|
-
if (a === 198 && (b === 18 || b === 19)) {
|
|
168
|
-
return true;
|
|
169
|
-
}
|
|
170
|
-
return false;
|
|
171
|
-
}
|
|
172
|
-
function expandIpv6(hostname) {
|
|
173
|
-
const trimmed = hostname.trim().toLowerCase();
|
|
174
|
-
const unbracketed = trimmed.startsWith("[") && trimmed.endsWith("]") ? trimmed.slice(1, -1) : trimmed;
|
|
175
|
-
if (unbracketed.includes("%")) {
|
|
176
|
-
return null;
|
|
177
|
-
}
|
|
178
|
-
const mapped = unbracketed.match(/^::ffff:([0-9a-fx.]+)$/iu);
|
|
179
|
-
if (mapped?.[1]) {
|
|
180
|
-
const octets = parseDottedIpv4(mapped[1]);
|
|
181
|
-
if (!octets) {
|
|
182
|
-
return null;
|
|
183
|
-
}
|
|
184
|
-
return [
|
|
185
|
-
0,
|
|
186
|
-
0,
|
|
187
|
-
0,
|
|
188
|
-
0,
|
|
189
|
-
0,
|
|
190
|
-
65535,
|
|
191
|
-
octets[0] << 8 | octets[1],
|
|
192
|
-
octets[2] << 8 | octets[3]
|
|
193
|
-
];
|
|
194
|
-
}
|
|
195
|
-
const halves = unbracketed.split("::");
|
|
196
|
-
if (halves.length > 2) {
|
|
197
|
-
return null;
|
|
198
|
-
}
|
|
199
|
-
const parseGroup = (part) => {
|
|
200
|
-
if (!/^[0-9a-f]{1,4}$/u.test(part)) {
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
return Number.parseInt(part, 16);
|
|
204
|
-
};
|
|
205
|
-
if (halves.length === 1) {
|
|
206
|
-
const groups = unbracketed.split(":");
|
|
207
|
-
if (groups.length !== 8) {
|
|
208
|
-
return null;
|
|
209
|
-
}
|
|
210
|
-
const values = groups.map(parseGroup);
|
|
211
|
-
return values.every((value) => value !== null) ? values : null;
|
|
212
|
-
}
|
|
213
|
-
const head = halves[0] ? halves[0].split(":").filter(Boolean) : [];
|
|
214
|
-
const tail = halves[1] ? halves[1].split(":").filter(Boolean) : [];
|
|
215
|
-
if (head.length + tail.length > 8) {
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
const values = [];
|
|
219
|
-
for (const part of head) {
|
|
220
|
-
const value = parseGroup(part);
|
|
221
|
-
if (value === null) {
|
|
222
|
-
return null;
|
|
223
|
-
}
|
|
224
|
-
values.push(value);
|
|
225
|
-
}
|
|
226
|
-
while (values.length < 8 - tail.length) {
|
|
227
|
-
values.push(0);
|
|
228
|
-
}
|
|
229
|
-
for (const part of tail) {
|
|
230
|
-
const value = parseGroup(part);
|
|
231
|
-
if (value === null) {
|
|
232
|
-
return null;
|
|
233
|
-
}
|
|
234
|
-
values.push(value);
|
|
235
|
-
}
|
|
236
|
-
return values.length === 8 ? values : null;
|
|
237
|
-
}
|
|
238
|
-
function isBlockedIpv6(hostname) {
|
|
239
|
-
const groups = expandIpv6(hostname);
|
|
240
|
-
if (!groups) {
|
|
241
|
-
return hostname.includes(":");
|
|
242
|
-
}
|
|
243
|
-
const first = groups[0] ?? 0;
|
|
244
|
-
const second = groups[1] ?? 0;
|
|
245
|
-
if (groups.every((group) => group === 0)) {
|
|
246
|
-
return true;
|
|
247
|
-
}
|
|
248
|
-
if (groups.slice(0, 7).every((group) => group === 0) && groups[7] === 1) {
|
|
249
|
-
return true;
|
|
250
|
-
}
|
|
251
|
-
if ((first & 65472) === 65152) {
|
|
252
|
-
return true;
|
|
253
|
-
}
|
|
254
|
-
if ((first & 65024) === 64512) {
|
|
255
|
-
return true;
|
|
256
|
-
}
|
|
257
|
-
if ((first & 65280) === 65280) {
|
|
258
|
-
return true;
|
|
259
|
-
}
|
|
260
|
-
if (first === 0 && second === 0 && groups[2] === 0 && groups[3] === 0 && groups[4] === 0 && groups[5] === 65535) {
|
|
261
|
-
const a = (groups[6] ?? 0) >> 8;
|
|
262
|
-
const b = (groups[6] ?? 0) & 255;
|
|
263
|
-
const c = (groups[7] ?? 0) >> 8;
|
|
264
|
-
const d = (groups[7] ?? 0) & 255;
|
|
265
|
-
return isBlockedIpv4Octets([a, b, c, d]);
|
|
266
|
-
}
|
|
267
|
-
return false;
|
|
268
|
-
}
|
|
269
|
-
function isBlockedHostname(hostname) {
|
|
270
|
-
const normalized = hostname.trim().toLowerCase();
|
|
271
|
-
if (normalized.length === 0) {
|
|
272
|
-
return true;
|
|
273
|
-
}
|
|
274
|
-
if (BLOCKED_HOSTNAMES.has(normalized)) {
|
|
275
|
-
return true;
|
|
276
|
-
}
|
|
277
|
-
if (normalized.endsWith(".local") || normalized.endsWith(".internal")) {
|
|
278
|
-
return true;
|
|
279
|
-
}
|
|
280
|
-
if (normalized.includes(":")) {
|
|
281
|
-
return isBlockedIpv6(normalized);
|
|
282
|
-
}
|
|
283
|
-
if (/^\d+$/u.test(normalized)) {
|
|
284
|
-
return true;
|
|
146
|
+
// ../../src/core/tracing/otel.ts
|
|
147
|
+
function numberOption(value, fallback, name, max = 1e6) {
|
|
148
|
+
const result = value ?? fallback;
|
|
149
|
+
if (!Number.isInteger(result) || result < 1 || result > max)
|
|
150
|
+
throw new TypeError(`Invalid tracing ${name}.`);
|
|
151
|
+
return result;
|
|
152
|
+
}
|
|
153
|
+
function ensureContextManager() {
|
|
154
|
+
const key = Symbol.for("@getstrata/otel-context-installed");
|
|
155
|
+
const state = globalThis;
|
|
156
|
+
if (state[key])
|
|
157
|
+
return;
|
|
158
|
+
const manager = new AsyncLocalStorageContextManager().enable();
|
|
159
|
+
if (!context.setGlobalContextManager(manager))
|
|
160
|
+
manager.disable();
|
|
161
|
+
state[key] = true;
|
|
162
|
+
}
|
|
163
|
+
function createTracingRuntime(options = {}) {
|
|
164
|
+
ensureContextManager();
|
|
165
|
+
const disabled = process.env.OTEL_SDK_DISABLED === "true";
|
|
166
|
+
const samplerName = options.sampleRatio === undefined ? process.env.OTEL_TRACES_SAMPLER ?? "parentbased_traceidratio" : "parentbased_traceidratio";
|
|
167
|
+
const rootName = samplerName.replace(/^parentbased_/, "");
|
|
168
|
+
if (!["always_on", "always_off", "traceidratio"].includes(rootName))
|
|
169
|
+
throw new TypeError("Unsupported tracing sampler.");
|
|
170
|
+
const ratio = options.sampleRatio ?? Number(process.env.OTEL_TRACES_SAMPLER_ARG ?? "1");
|
|
171
|
+
if (!Number.isFinite(ratio) || ratio < 0 || ratio > 1)
|
|
172
|
+
throw new TypeError("Invalid tracing sampleRatio.");
|
|
173
|
+
const rootSampler = rootName === "always_on" ? new AlwaysOnSampler : rootName === "always_off" ? new AlwaysOffSampler : new TraceIdRatioBasedSampler(ratio);
|
|
174
|
+
const sampler = disabled ? new AlwaysOffSampler : samplerName.startsWith("parentbased_") ? new ParentBasedSampler({ root: rootSampler }) : rootSampler;
|
|
175
|
+
const queueSize = numberOption(options.maxQueueSize, 2048, "maxQueueSize");
|
|
176
|
+
const batchSize = numberOption(options.maxExportBatchSize, Math.min(512, queueSize), "maxExportBatchSize", queueSize);
|
|
177
|
+
const timeout = numberOption(options.exportTimeoutMillis, 5000, "exportTimeoutMillis", 60000);
|
|
178
|
+
const delay = numberOption(options.scheduledDelayMillis, 5000, "scheduledDelayMillis", 60000);
|
|
179
|
+
let endpoint = options.endpoint ?? process.env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT?.trim();
|
|
180
|
+
if (!endpoint) {
|
|
181
|
+
const base = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
182
|
+
if (base)
|
|
183
|
+
endpoint = base.endsWith("/v1/traces") ? base : `${base.replace(/\/$/, "")}/v1/traces`;
|
|
285
184
|
}
|
|
286
|
-
if (
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
return true;
|
|
185
|
+
if (endpoint) {
|
|
186
|
+
let url;
|
|
187
|
+
try {
|
|
188
|
+
url = new URL(endpoint);
|
|
189
|
+
} catch {
|
|
190
|
+
throw new TypeError("Invalid tracing collector endpoint.");
|
|
293
191
|
}
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
}
|
|
317
|
-
if (!resolved.allowPrivate && isBlockedHostname(parsed.hostname)) {
|
|
318
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
319
|
-
}
|
|
320
|
-
return parsed;
|
|
321
|
-
}
|
|
322
|
-
function isBlockedIpAddress(address) {
|
|
323
|
-
return isBlockedHostname(address.trim().toLowerCase());
|
|
324
|
-
}
|
|
325
|
-
function isLiteralIpHostname(hostname) {
|
|
326
|
-
return parseCanonicalDottedDecimal(hostname) !== null || expandIpv6(hostname) !== null;
|
|
327
|
-
}
|
|
328
|
-
function pinUrlToAddress(url, address) {
|
|
329
|
-
const pinned = new URL(url.toString());
|
|
330
|
-
const ipv6 = expandIpv6(address.replace(/^\[|\]$/gu, ""));
|
|
331
|
-
pinned.hostname = ipv6 ? `[${address.replace(/^\[|\]$/gu, "")}]` : address;
|
|
332
|
-
return pinned;
|
|
333
|
-
}
|
|
334
|
-
async function resolveSafeOutboundTarget(rawUrl, options = {}) {
|
|
335
|
-
const resolvedOptions = productionSafeOutboundOptions(options);
|
|
336
|
-
const parsed = assertSafeOutboundUrl(rawUrl, resolvedOptions);
|
|
337
|
-
if (resolvedOptions.resolveDns === false || resolvedOptions.allowPrivate) {
|
|
338
|
-
return { url: parsed, addresses: [] };
|
|
339
|
-
}
|
|
340
|
-
const hostname = parsed.hostname.trim().toLowerCase();
|
|
341
|
-
if (isLiteralIpHostname(hostname)) {
|
|
342
|
-
return { url: parsed, addresses: [hostname] };
|
|
343
|
-
}
|
|
344
|
-
let results;
|
|
345
|
-
try {
|
|
346
|
-
results = await dnsLookup(hostname, { all: true, verbatim: true });
|
|
347
|
-
} catch {
|
|
348
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
349
|
-
}
|
|
350
|
-
if (results.length === 0 || results.some((result) => isBlockedIpAddress(result.address))) {
|
|
351
|
-
throw new BadRequestError("Webhook URL targets a blocked host.");
|
|
352
|
-
}
|
|
353
|
-
return { url: parsed, addresses: results.map((result) => result.address) };
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// ../../src/core/security/safeFetch.ts
|
|
357
|
-
var DEFAULT_FETCH_TIMEOUT_MS = 1e4;
|
|
358
|
-
function untilAborted(work, signal) {
|
|
359
|
-
signal.throwIfAborted();
|
|
360
|
-
return new Promise((resolve, reject) => {
|
|
361
|
-
const onAbort = () => reject(signal.reason);
|
|
362
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
363
|
-
work.then(resolve, reject).finally(() => signal.removeEventListener("abort", onAbort));
|
|
364
|
-
});
|
|
365
|
-
}
|
|
366
|
-
function preserveResponseMetadata(response, original) {
|
|
367
|
-
Object.defineProperties(response, {
|
|
368
|
-
url: { value: original.url },
|
|
369
|
-
redirected: { value: original.redirected },
|
|
370
|
-
type: { value: original.type }
|
|
192
|
+
if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash)
|
|
193
|
+
throw new TypeError("Invalid tracing collector endpoint.");
|
|
194
|
+
}
|
|
195
|
+
const exporter = disabled ? undefined : options.exporter ?? (endpoint ? new OTLPTraceExporter({ url: endpoint, timeoutMillis: timeout, concurrencyLimit: 1 }) : undefined);
|
|
196
|
+
const provider = new NodeTracerProvider({
|
|
197
|
+
resource: resourceFromAttributes({ "service.name": options.serviceName ?? otelServiceName() }),
|
|
198
|
+
sampler,
|
|
199
|
+
spanProcessors: exporter ? [
|
|
200
|
+
new BatchSpanProcessor(exporter, {
|
|
201
|
+
maxQueueSize: queueSize,
|
|
202
|
+
maxExportBatchSize: batchSize,
|
|
203
|
+
scheduledDelayMillis: delay,
|
|
204
|
+
exportTimeoutMillis: timeout
|
|
205
|
+
})
|
|
206
|
+
] : [],
|
|
207
|
+
spanLimits: {
|
|
208
|
+
attributeCountLimit: 16,
|
|
209
|
+
attributeValueLengthLimit: 256,
|
|
210
|
+
eventCountLimit: 8,
|
|
211
|
+
linkCountLimit: 8
|
|
212
|
+
},
|
|
213
|
+
forceFlushTimeoutMillis: timeout
|
|
371
214
|
});
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
return response;
|
|
380
|
-
}
|
|
381
|
-
const reader = response.body.getReader();
|
|
382
|
-
let stopped = false;
|
|
383
|
-
let bodyController;
|
|
384
|
-
const stop = (reason, errorBody) => {
|
|
385
|
-
if (stopped)
|
|
386
|
-
return;
|
|
387
|
-
stopped = true;
|
|
388
|
-
release();
|
|
389
|
-
signal.removeEventListener("abort", onAbort);
|
|
390
|
-
if (errorBody)
|
|
391
|
-
bodyController.error(reason);
|
|
392
|
-
reader.cancel(reason).catch(() => {}).finally(() => reader.releaseLock());
|
|
215
|
+
let shutdown;
|
|
216
|
+
return {
|
|
217
|
+
tracer: provider.getTracer("@getstrata/http"),
|
|
218
|
+
propagator: new W3CTraceContextPropagator,
|
|
219
|
+
run: (parent, callback) => context.with(parent, callback),
|
|
220
|
+
forceFlush: () => provider.forceFlush(),
|
|
221
|
+
shutdown: () => shutdown ??= provider.shutdown()
|
|
393
222
|
};
|
|
394
|
-
const onAbort = () => stop(signal.reason, true);
|
|
395
|
-
const body = new ReadableStream({
|
|
396
|
-
start(controller) {
|
|
397
|
-
bodyController = controller;
|
|
398
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
399
|
-
if (signal.aborted)
|
|
400
|
-
onAbort();
|
|
401
|
-
},
|
|
402
|
-
async pull(controller) {
|
|
403
|
-
try {
|
|
404
|
-
const result = await untilAborted(reader.read(), signal);
|
|
405
|
-
if (stopped)
|
|
406
|
-
return;
|
|
407
|
-
if (result.done) {
|
|
408
|
-
stopped = true;
|
|
409
|
-
release();
|
|
410
|
-
signal.removeEventListener("abort", onAbort);
|
|
411
|
-
reader.releaseLock();
|
|
412
|
-
controller.close();
|
|
413
|
-
} else {
|
|
414
|
-
controller.enqueue(result.value);
|
|
415
|
-
}
|
|
416
|
-
} catch (error) {
|
|
417
|
-
stop(error, true);
|
|
418
|
-
}
|
|
419
|
-
},
|
|
420
|
-
cancel(reason) {
|
|
421
|
-
stop(reason, false);
|
|
422
|
-
}
|
|
423
|
-
}, { highWaterMark: 0 });
|
|
424
|
-
return preserveResponseMetadata(new Response(body, {
|
|
425
|
-
status: response.status,
|
|
426
|
-
statusText: response.statusText,
|
|
427
|
-
headers: response.headers
|
|
428
|
-
}), response);
|
|
429
223
|
}
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const
|
|
443
|
-
const
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
const fetchUrl = address ? pinUrlToAddress(current.url, address).toString() : current.url.toString();
|
|
457
|
-
const headers = new Headers(init.headers);
|
|
458
|
-
if (address && !headers.has("host"))
|
|
459
|
-
headers.set("Host", current.url.host);
|
|
460
|
-
const fetchInit = {
|
|
461
|
-
...init,
|
|
462
|
-
headers,
|
|
463
|
-
signal: controller.signal,
|
|
464
|
-
redirect: "manual"
|
|
465
|
-
};
|
|
466
|
-
if (address)
|
|
467
|
-
fetchInit.tls = { serverName: current.url.hostname };
|
|
468
|
-
const response = await untilAborted(fetch(fetchUrl, fetchInit), controller.signal);
|
|
469
|
-
if (response.status >= 300 && response.status < 400) {
|
|
470
|
-
const location = response.headers.get("location");
|
|
471
|
-
if (location && redirectCount < maxRedirects) {
|
|
472
|
-
if (response.body)
|
|
473
|
-
await untilAborted(response.body.cancel(), controller.signal);
|
|
474
|
-
current = await untilAborted(resolveSafeOutboundTarget(new URL(location, current.url).toString(), urlOptions), controller.signal);
|
|
475
|
-
redirectCount++;
|
|
476
|
-
continue;
|
|
477
|
-
}
|
|
224
|
+
var runtimeKey = Symbol.for("@getstrata/otel-runtime");
|
|
225
|
+
function runtimeState() {
|
|
226
|
+
const state = globalThis;
|
|
227
|
+
state[runtimeKey] ??= { owners: 0 };
|
|
228
|
+
return state[runtimeKey];
|
|
229
|
+
}
|
|
230
|
+
function getTracingRuntime() {
|
|
231
|
+
const state = runtimeState();
|
|
232
|
+
state.runtime ??= createTracingRuntime();
|
|
233
|
+
return state.runtime;
|
|
234
|
+
}
|
|
235
|
+
function acquireTracingRuntime() {
|
|
236
|
+
const state = runtimeState();
|
|
237
|
+
const runtime = getTracingRuntime();
|
|
238
|
+
state.owners++;
|
|
239
|
+
let released = false;
|
|
240
|
+
return {
|
|
241
|
+
flush: () => runtime.forceFlush(),
|
|
242
|
+
async release() {
|
|
243
|
+
if (released)
|
|
244
|
+
return;
|
|
245
|
+
released = true;
|
|
246
|
+
if (--state.owners === 0) {
|
|
247
|
+
if (state.runtime === runtime)
|
|
248
|
+
state.runtime = undefined;
|
|
249
|
+
await runtime.shutdown();
|
|
478
250
|
}
|
|
479
|
-
return withBodyDeadline(response, controller.signal, release);
|
|
480
251
|
}
|
|
481
|
-
} catch (error) {
|
|
482
|
-
controller.abort(error);
|
|
483
|
-
release();
|
|
484
|
-
throw error;
|
|
485
|
-
}
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
// ../../src/core/tracing/otel.ts
|
|
489
|
-
function randomHex(bytes) {
|
|
490
|
-
return randomBytes(bytes).toString("hex");
|
|
491
|
-
}
|
|
492
|
-
function createSpan(input) {
|
|
493
|
-
const spanId = randomHex(8);
|
|
494
|
-
return {
|
|
495
|
-
traceId: input.traceId,
|
|
496
|
-
spanId,
|
|
497
|
-
name: input.name,
|
|
498
|
-
startTimeUnixNano: String(Math.floor(input.startedAt * 1e6)),
|
|
499
|
-
endTimeUnixNano: String(Math.floor(input.endedAt * 1e6)),
|
|
500
|
-
attributes: Object.entries(input.attributes ?? {}).map(([key, value]) => ({
|
|
501
|
-
key,
|
|
502
|
-
value: { stringValue: value }
|
|
503
|
-
})),
|
|
504
|
-
status: { code: 1 }
|
|
505
252
|
};
|
|
506
253
|
}
|
|
507
|
-
async function exportOtelSpan(span) {
|
|
508
|
-
const endpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT?.trim();
|
|
509
|
-
if (!endpoint) {
|
|
510
|
-
return;
|
|
511
|
-
}
|
|
512
|
-
const serviceName = otelServiceName();
|
|
513
|
-
const url = endpoint.endsWith("/v1/traces") ? endpoint : `${endpoint.replace(/\/$/, "")}/v1/traces`;
|
|
514
|
-
await safeFetch(url, {
|
|
515
|
-
method: "POST",
|
|
516
|
-
headers: { "content-type": "application/json" },
|
|
517
|
-
body: JSON.stringify({
|
|
518
|
-
resourceSpans: [
|
|
519
|
-
{
|
|
520
|
-
resource: {
|
|
521
|
-
attributes: [{ key: "service.name", value: { stringValue: serviceName } }]
|
|
522
|
-
},
|
|
523
|
-
scopeSpans: [{ spans: [span] }]
|
|
524
|
-
}
|
|
525
|
-
]
|
|
526
|
-
})
|
|
527
|
-
}, {
|
|
528
|
-
allowHttp: !isProductionEnv(),
|
|
529
|
-
allowPrivate: !isProductionEnv(),
|
|
530
|
-
timeoutMs: 5000,
|
|
531
|
-
maxRedirects: 0
|
|
532
|
-
});
|
|
533
|
-
}
|
|
534
254
|
|
|
535
255
|
// ../../src/core/tracing/traceContext.ts
|
|
536
256
|
init_asyncContextStore();
|
|
@@ -540,52 +260,64 @@ function runWithTraceContext(context, callback) {
|
|
|
540
260
|
}
|
|
541
261
|
|
|
542
262
|
// ../../src/core/tracing/tracingMiddleware.ts
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
263
|
+
var headerGetter = {
|
|
264
|
+
keys: (headers) => [...headers.keys()],
|
|
265
|
+
get: (headers, key) => headers.get(key) ?? undefined
|
|
266
|
+
};
|
|
267
|
+
var methods = new Set([
|
|
268
|
+
"GET",
|
|
269
|
+
"POST",
|
|
270
|
+
"PUT",
|
|
271
|
+
"PATCH",
|
|
272
|
+
"DELETE",
|
|
273
|
+
"HEAD",
|
|
274
|
+
"OPTIONS",
|
|
275
|
+
"CONNECT",
|
|
276
|
+
"TRACE"
|
|
277
|
+
]);
|
|
278
|
+
function createTracingMiddleware(runtime) {
|
|
555
279
|
return async (request, next) => {
|
|
556
|
-
const
|
|
557
|
-
const
|
|
558
|
-
const
|
|
559
|
-
const
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
})
|
|
586
|
-
|
|
280
|
+
const telemetry = runtime ?? getTracingRuntime();
|
|
281
|
+
const method = methods.has(request.method) ? request.method : "_OTHER";
|
|
282
|
+
const route = currentRequestMeta().routeTemplate ?? "__unmatched__";
|
|
283
|
+
const parent = telemetry.propagator.extract(ROOT_CONTEXT, request.headers, headerGetter);
|
|
284
|
+
const span = telemetry.tracer.startSpan(`${method} ${route}`, {
|
|
285
|
+
kind: SpanKind.SERVER,
|
|
286
|
+
attributes: { "http.request.method": method, "http.route": route }
|
|
287
|
+
}, parent);
|
|
288
|
+
const identity = span.spanContext();
|
|
289
|
+
const started = performance.now();
|
|
290
|
+
return await telemetry.run(trace.setSpan(parent, span), () => runWithTraceContext({ traceId: identity.traceId, spanId: identity.spanId }, async () => {
|
|
291
|
+
try {
|
|
292
|
+
const response = await next();
|
|
293
|
+
span.setAttribute("http.response.status_code", response.status);
|
|
294
|
+
if (response.status >= 500)
|
|
295
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
296
|
+
const headers = new Headers(response.headers);
|
|
297
|
+
headers.delete("tracestate");
|
|
298
|
+
headers.set("x-trace-id", identity.traceId);
|
|
299
|
+
headers.set("x-span-id", identity.spanId);
|
|
300
|
+
telemetry.propagator.inject(trace.setSpan(parent, span), headers, {
|
|
301
|
+
set: (carrier, key, value) => carrier.set(key, value)
|
|
302
|
+
});
|
|
303
|
+
headers.set("server-timing", `app;dur=${(performance.now() - started).toFixed(2)}`);
|
|
304
|
+
return new Response(response.body, {
|
|
305
|
+
status: response.status,
|
|
306
|
+
statusText: response.statusText,
|
|
307
|
+
headers
|
|
308
|
+
});
|
|
309
|
+
} catch (error) {
|
|
310
|
+
span.setStatus({ code: SpanStatusCode.ERROR });
|
|
311
|
+
throw error;
|
|
312
|
+
} finally {
|
|
313
|
+
span.end();
|
|
314
|
+
}
|
|
315
|
+
}));
|
|
587
316
|
};
|
|
588
317
|
}
|
|
589
318
|
export {
|
|
590
|
-
|
|
319
|
+
acquireTracingRuntime,
|
|
320
|
+
createTracingMiddleware,
|
|
321
|
+
createTracingRuntime,
|
|
322
|
+
getTracingRuntime
|
|
591
323
|
};
|