@ciq-dev/neoiq-foundation-node 1.0.1-beta.2 → 1.0.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bootstrap.d.mts +1 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +31 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bootstrap.mjs +30 -0
- package/dist/bootstrap.mjs.map +1 -0
- package/dist/index.d.mts +77 -60
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +77 -60
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +154 -300
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +116 -244
- package/dist/index.mjs.map +1 -1
- package/dist/tracing-Cv-Y3fZx.mjs +196 -0
- package/dist/tracing-Cv-Y3fZx.mjs.map +1 -0
- package/dist/tracing-DM5OFo7l.js +316 -0
- package/dist/tracing-DM5OFo7l.js.map +1 -0
- package/package.json +13 -6
package/dist/index.mjs
CHANGED
|
@@ -1,13 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
4
|
-
import { SpanStatusCode, SpanStatusCode as SpanStatusCode$1, context, context as context$1, metrics, propagation, propagation as propagation$1, trace, trace as trace$1 } from "@opentelemetry/api";
|
|
5
|
-
import pino from "pino";
|
|
6
|
-
import { NodeSDK } from "@opentelemetry/sdk-node";
|
|
7
|
-
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
8
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
|
|
1
|
+
import { AutoInstrumentationConfigSchema, FeaturesConfigSchema, FoundationConfigSchema, LoggingConfigSchema, OtelConfigSchema, RedactionConfigSchema, RequestLoggingConfigSchema, ShutdownConfigSchema, SpanStatusCode, context, getActiveSpan, getDefaultOtelEndpoint, getTraceContext, getTracer, isTracingEnabled, parseConfig, propagation, setupTracing, shutdownTracing, trace } from "./tracing-Cv-Y3fZx.mjs";
|
|
9
2
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
10
3
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
4
|
+
import { SpanStatusCode as SpanStatusCode$1, context as context$1, metrics, propagation as propagation$1, trace as trace$1 } from "@opentelemetry/api";
|
|
5
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
6
|
+
import pino from "pino";
|
|
11
7
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
|
|
12
8
|
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
13
9
|
import fp from "fastify-plugin";
|
|
@@ -15,90 +11,9 @@ import { randomUUID } from "crypto";
|
|
|
15
11
|
import axios from "axios";
|
|
16
12
|
import axiosRetry from "axios-retry";
|
|
17
13
|
import CircuitBreaker from "opossum";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
18
15
|
import { Readable } from "node:stream";
|
|
19
16
|
|
|
20
|
-
//#region rolldown:runtime
|
|
21
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
22
|
-
|
|
23
|
-
//#endregion
|
|
24
|
-
//#region src/config.ts
|
|
25
|
-
const AutoInstrumentationConfigSchema = z.object({
|
|
26
|
-
http: z.boolean().default(true),
|
|
27
|
-
fastify: z.boolean().default(true),
|
|
28
|
-
express: z.boolean().default(true),
|
|
29
|
-
mongodb: z.boolean().default(true),
|
|
30
|
-
pg: z.boolean().default(true),
|
|
31
|
-
mysql: z.boolean().default(true),
|
|
32
|
-
redis: z.boolean().default(true),
|
|
33
|
-
ioredis: z.boolean().default(true),
|
|
34
|
-
grpc: z.boolean().default(true),
|
|
35
|
-
fs: z.boolean().default(false),
|
|
36
|
-
dns: z.boolean().default(false)
|
|
37
|
-
}).partial();
|
|
38
|
-
const FeaturesConfigSchema = z.object({
|
|
39
|
-
tracing: z.boolean().default(true),
|
|
40
|
-
metrics: z.boolean().default(true),
|
|
41
|
-
logging: z.boolean().default(true),
|
|
42
|
-
autoInstrumentation: AutoInstrumentationConfigSchema.default({})
|
|
43
|
-
}).partial();
|
|
44
|
-
const DEFAULT_OTEL_ENDPOINT = "http://otel-stack-deployment-collector.observability.svc.cluster.local:4317";
|
|
45
|
-
const OtelConfigSchema = z.object({
|
|
46
|
-
endpoint: z.string().default(DEFAULT_OTEL_ENDPOINT),
|
|
47
|
-
metricsIntervalMs: z.number().min(1e3).default(5e3),
|
|
48
|
-
traceSampleRate: z.number().min(0).max(1).default(1)
|
|
49
|
-
}).partial();
|
|
50
|
-
const LoggingConfigSchema = z.object({
|
|
51
|
-
level: z.enum([
|
|
52
|
-
"debug",
|
|
53
|
-
"info",
|
|
54
|
-
"warn",
|
|
55
|
-
"error"
|
|
56
|
-
]).default("info"),
|
|
57
|
-
prettyPrint: z.boolean().optional()
|
|
58
|
-
}).partial();
|
|
59
|
-
const RequestLoggingConfigSchema = z.object({
|
|
60
|
-
logHeaders: z.boolean().default(true),
|
|
61
|
-
logBody: z.boolean().default(false),
|
|
62
|
-
logResponseBody: z.boolean().default(false),
|
|
63
|
-
maxBodySize: z.number().default(10 * 1024),
|
|
64
|
-
redactHeaders: z.array(z.string()).optional()
|
|
65
|
-
}).partial();
|
|
66
|
-
const RedactionConfigSchema = z.object({ additionalPaths: z.array(z.string()).default([]) }).partial();
|
|
67
|
-
const ShutdownConfigSchema = z.object({
|
|
68
|
-
flushOnCrash: z.boolean().default(false),
|
|
69
|
-
flushTimeoutMs: z.number().min(100).default(5e3)
|
|
70
|
-
}).partial();
|
|
71
|
-
const FoundationConfigSchema = z.object({
|
|
72
|
-
serviceName: z.string().min(1, "serviceName is required"),
|
|
73
|
-
serviceVersion: z.string().default(process.env.SERVICE_VERSION || "1.0.0"),
|
|
74
|
-
environment: z.enum([
|
|
75
|
-
"development",
|
|
76
|
-
"staging",
|
|
77
|
-
"qa",
|
|
78
|
-
"production"
|
|
79
|
-
]).default(process.env.NODE_ENV || "development"),
|
|
80
|
-
features: FeaturesConfigSchema.default({}),
|
|
81
|
-
otel: OtelConfigSchema.default({}),
|
|
82
|
-
logging: LoggingConfigSchema.default({}),
|
|
83
|
-
requestLogging: RequestLoggingConfigSchema.default({}),
|
|
84
|
-
redaction: RedactionConfigSchema.default({}),
|
|
85
|
-
shutdown: ShutdownConfigSchema.default({})
|
|
86
|
-
});
|
|
87
|
-
/** Parse and validate configuration */
|
|
88
|
-
function parseConfig(input) {
|
|
89
|
-
const result = FoundationConfigSchema.safeParse(input);
|
|
90
|
-
if (!result.success) {
|
|
91
|
-
const errors = result.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
|
|
92
|
-
throw new Error(`Invalid foundation configuration:\n${errors}`);
|
|
93
|
-
}
|
|
94
|
-
return result.data;
|
|
95
|
-
}
|
|
96
|
-
/** Get default OTEL endpoint */
|
|
97
|
-
function getDefaultOtelEndpoint() {
|
|
98
|
-
return process.env.OTEL_EXPORTER_OTLP_ENDPOINT || DEFAULT_OTEL_ENDPOINT;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
//#endregion
|
|
102
17
|
//#region src/features/context.ts
|
|
103
18
|
const BAGGAGE_CORRELATION_KEY = "correlation.id";
|
|
104
19
|
function setBaggageCorrelationId(correlationId) {
|
|
@@ -385,12 +300,13 @@ function wrapPinoLogger(pinoLogger) {
|
|
|
385
300
|
};
|
|
386
301
|
}
|
|
387
302
|
/** Fallback logger when Pino is not available */
|
|
388
|
-
function createFallbackLogger(serviceName = "unknown") {
|
|
303
|
+
function createFallbackLogger(serviceName = "unknown", baseBindings = {}) {
|
|
389
304
|
const log = (level, obj, msg) => {
|
|
390
305
|
const logObj = {
|
|
391
306
|
timestamp: new Date().toISOString(),
|
|
392
307
|
level,
|
|
393
308
|
service: serviceName,
|
|
309
|
+
...baseBindings,
|
|
394
310
|
...obj,
|
|
395
311
|
msg
|
|
396
312
|
};
|
|
@@ -404,7 +320,10 @@ function createFallbackLogger(serviceName = "unknown") {
|
|
|
404
320
|
info: (obj, msg) => log("info", obj, msg),
|
|
405
321
|
warn: (obj, msg) => log("warn", obj, msg),
|
|
406
322
|
error: (obj, msg) => log("error", obj, msg),
|
|
407
|
-
child: () =>
|
|
323
|
+
child: (bindings) => createFallbackLogger(serviceName, {
|
|
324
|
+
...baseBindings,
|
|
325
|
+
...bindings
|
|
326
|
+
}),
|
|
408
327
|
pino: null
|
|
409
328
|
};
|
|
410
329
|
return fallback;
|
|
@@ -417,98 +336,6 @@ function getGlobalLogger() {
|
|
|
417
336
|
return globalLogger || createFallbackLogger();
|
|
418
337
|
}
|
|
419
338
|
|
|
420
|
-
//#endregion
|
|
421
|
-
//#region src/features/tracing.ts
|
|
422
|
-
let sdk = null;
|
|
423
|
-
let isInitialized$1 = false;
|
|
424
|
-
const MONITORED_MODULES = [
|
|
425
|
-
"pg",
|
|
426
|
-
"mongodb",
|
|
427
|
-
"ioredis",
|
|
428
|
-
"mysql2",
|
|
429
|
-
"express",
|
|
430
|
-
"@grpc/grpc-js"
|
|
431
|
-
];
|
|
432
|
-
function warnPreloadedModules() {
|
|
433
|
-
for (const mod of MONITORED_MODULES) try {
|
|
434
|
-
if (__require.resolve(mod) in (__require.cache || {})) console.warn(`[neoiq-foundation] "${mod}" was imported before tracing init. Auto-instrumentation may not work for this module. Use node -r @ciq-dev/neoiq-foundation-node/bootstrap or call createFoundation() first.`);
|
|
435
|
-
} catch {}
|
|
436
|
-
}
|
|
437
|
-
/** Initialize OpenTelemetry tracing */
|
|
438
|
-
function setupTracing(options) {
|
|
439
|
-
if (isInitialized$1) {
|
|
440
|
-
console.warn("[neoiq-foundation] Tracing already initialized");
|
|
441
|
-
return;
|
|
442
|
-
}
|
|
443
|
-
warnPreloadedModules();
|
|
444
|
-
const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), autoInstrumentation = {} } = options;
|
|
445
|
-
const resource = resourceFromAttributes({
|
|
446
|
-
[ATTR_SERVICE_NAME]: serviceName,
|
|
447
|
-
[ATTR_SERVICE_VERSION]: serviceVersion,
|
|
448
|
-
"deployment.environment": environment
|
|
449
|
-
});
|
|
450
|
-
const traceExporter = new OTLPTraceExporter({ url: endpoint });
|
|
451
|
-
const instrumentationConfig = buildInstrumentationConfig(autoInstrumentation);
|
|
452
|
-
sdk = new NodeSDK({
|
|
453
|
-
resource,
|
|
454
|
-
traceExporter,
|
|
455
|
-
instrumentations: [getNodeAutoInstrumentations(instrumentationConfig)]
|
|
456
|
-
});
|
|
457
|
-
sdk.start();
|
|
458
|
-
isInitialized$1 = true;
|
|
459
|
-
}
|
|
460
|
-
function buildInstrumentationConfig(config) {
|
|
461
|
-
const mapping = {
|
|
462
|
-
http: "@opentelemetry/instrumentation-http",
|
|
463
|
-
fastify: "@opentelemetry/instrumentation-fastify",
|
|
464
|
-
express: "@opentelemetry/instrumentation-express",
|
|
465
|
-
mongodb: "@opentelemetry/instrumentation-mongodb",
|
|
466
|
-
pg: "@opentelemetry/instrumentation-pg",
|
|
467
|
-
mysql: "@opentelemetry/instrumentation-mysql",
|
|
468
|
-
redis: "@opentelemetry/instrumentation-redis",
|
|
469
|
-
ioredis: "@opentelemetry/instrumentation-ioredis",
|
|
470
|
-
grpc: "@opentelemetry/instrumentation-grpc",
|
|
471
|
-
fs: "@opentelemetry/instrumentation-fs",
|
|
472
|
-
dns: "@opentelemetry/instrumentation-dns"
|
|
473
|
-
};
|
|
474
|
-
const result = {};
|
|
475
|
-
for (const [key, instrumentationName] of Object.entries(mapping)) {
|
|
476
|
-
const userSetting = config[key];
|
|
477
|
-
const defaultValue = key !== "fs" && key !== "dns";
|
|
478
|
-
result[instrumentationName] = { enabled: userSetting ?? defaultValue };
|
|
479
|
-
}
|
|
480
|
-
return result;
|
|
481
|
-
}
|
|
482
|
-
/** Shutdown tracing gracefully */
|
|
483
|
-
async function shutdownTracing() {
|
|
484
|
-
if (!sdk) return;
|
|
485
|
-
try {
|
|
486
|
-
await sdk.shutdown();
|
|
487
|
-
isInitialized$1 = false;
|
|
488
|
-
sdk = null;
|
|
489
|
-
} catch (error) {
|
|
490
|
-
console.error("[neoiq-foundation] Error shutting down tracing:", error);
|
|
491
|
-
}
|
|
492
|
-
}
|
|
493
|
-
function getTracer(name) {
|
|
494
|
-
return trace.getTracer(name);
|
|
495
|
-
}
|
|
496
|
-
function getActiveSpan() {
|
|
497
|
-
return trace.getActiveSpan();
|
|
498
|
-
}
|
|
499
|
-
function getTraceContext() {
|
|
500
|
-
const span = trace.getActiveSpan();
|
|
501
|
-
if (!span) return {};
|
|
502
|
-
const ctx = span.spanContext();
|
|
503
|
-
return {
|
|
504
|
-
traceId: ctx.traceId,
|
|
505
|
-
spanId: ctx.spanId
|
|
506
|
-
};
|
|
507
|
-
}
|
|
508
|
-
function isTracingEnabled() {
|
|
509
|
-
return isInitialized$1;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
339
|
//#endregion
|
|
513
340
|
//#region src/features/metrics.ts
|
|
514
341
|
let meterProvider = null;
|
|
@@ -546,6 +373,7 @@ async function shutdownMetrics() {
|
|
|
546
373
|
meterProvider = null;
|
|
547
374
|
} catch (error) {
|
|
548
375
|
console.error("[neoiq-foundation] Error shutting down metrics:", error);
|
|
376
|
+
throw error;
|
|
549
377
|
}
|
|
550
378
|
}
|
|
551
379
|
function getMeter(name, version = "1.0.0") {
|
|
@@ -823,31 +651,34 @@ function createHttpClient(options) {
|
|
|
823
651
|
}
|
|
824
652
|
return Promise.reject(error);
|
|
825
653
|
});
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
654
|
+
if (retry.enabled !== false) {
|
|
655
|
+
const retryConfig = {
|
|
656
|
+
retries: retry.retries ?? 3,
|
|
657
|
+
retryDelay: (retryCount) => (retry.retryDelay ?? 1e3) * Math.pow(2, retryCount - 1),
|
|
658
|
+
retryCondition: (error) => {
|
|
659
|
+
const retryStatusCodes = retry.retryStatusCodes ?? [
|
|
660
|
+
408,
|
|
661
|
+
429,
|
|
662
|
+
500,
|
|
663
|
+
502,
|
|
664
|
+
503,
|
|
665
|
+
504
|
|
666
|
+
];
|
|
667
|
+
return !error.response || retryStatusCodes.includes(error.response?.status || 0);
|
|
668
|
+
},
|
|
669
|
+
onRetry: (retryCount, error, requestConfig) => {
|
|
670
|
+
logger.warn({
|
|
671
|
+
retryCount,
|
|
672
|
+
url: `${requestConfig.baseURL || ""}${requestConfig.url}`,
|
|
673
|
+
error: error.message
|
|
674
|
+
}, "Retrying request");
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
axiosRetry(client, retryConfig);
|
|
678
|
+
}
|
|
679
|
+
if (cbOptions.enabled === true) {
|
|
680
|
+
const originalRequest = client.request.bind(client);
|
|
681
|
+
const breaker = new CircuitBreaker(async (config) => originalRequest(config), {
|
|
851
682
|
timeout,
|
|
852
683
|
resetTimeout: cbOptions.resetTimeout ?? 3e4,
|
|
853
684
|
errorThresholdPercentage: cbOptions.errorThresholdPercentage ?? 50,
|
|
@@ -856,7 +687,6 @@ function createHttpClient(options) {
|
|
|
856
687
|
breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
|
|
857
688
|
breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
|
|
858
689
|
breaker.on("close", () => logger.info({ targetService: serviceName }, "Circuit breaker CLOSED"));
|
|
859
|
-
const originalRequest = client.request.bind(client);
|
|
860
690
|
client.request = (config) => breaker.fire(config);
|
|
861
691
|
client.get = (url, config) => breaker.fire({
|
|
862
692
|
...config,
|
|
@@ -894,10 +724,15 @@ function createHttpClient(options) {
|
|
|
894
724
|
//#endregion
|
|
895
725
|
//#region src/foundation.ts
|
|
896
726
|
const deprecationWarnings = new Set();
|
|
897
|
-
function warnDeprecation(oldPath, newPath) {
|
|
727
|
+
function warnDeprecation(oldPath, newPath, logger) {
|
|
898
728
|
if (deprecationWarnings.has(oldPath)) return;
|
|
899
729
|
deprecationWarnings.add(oldPath);
|
|
900
|
-
|
|
730
|
+
const msg = `foundation.${oldPath}() is deprecated. Use foundation.${newPath}() instead. This alias will be removed in the next major version.`;
|
|
731
|
+
if (logger) logger.warn({
|
|
732
|
+
deprecated: oldPath,
|
|
733
|
+
replacement: newPath
|
|
734
|
+
}, msg);
|
|
735
|
+
else console.warn(`[neoiq-foundation] DEPRECATED: ${msg}`);
|
|
901
736
|
}
|
|
902
737
|
/** Create a fully configured observability foundation */
|
|
903
738
|
function createFoundation(input) {
|
|
@@ -1058,9 +893,11 @@ function createFoundation(input) {
|
|
|
1058
893
|
const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
|
|
1059
894
|
const loggingUp = !loggingError;
|
|
1060
895
|
const allUp = tracingUp && metricsUp && loggingUp;
|
|
1061
|
-
const
|
|
896
|
+
const allDown = (!tracingUp || !features.tracing) && (!metricsUp || !features.metrics) && !loggingUp;
|
|
897
|
+
let status = "healthy";
|
|
898
|
+
if (!allUp) status = allDown ? "unhealthy" : "degraded";
|
|
1062
899
|
return {
|
|
1063
|
-
status
|
|
900
|
+
status,
|
|
1064
901
|
timestamp: new Date().toISOString(),
|
|
1065
902
|
service: serviceName,
|
|
1066
903
|
version: serviceVersion,
|
|
@@ -1134,43 +971,43 @@ function createFoundation(input) {
|
|
|
1134
971
|
tracer: tracerInstance,
|
|
1135
972
|
meter: meterInstance,
|
|
1136
973
|
getTracer: (name) => {
|
|
1137
|
-
warnDeprecation("getTracer", "observability.getTracer");
|
|
974
|
+
warnDeprecation("getTracer", "observability.getTracer", logger);
|
|
1138
975
|
return observability.getTracer(name);
|
|
1139
976
|
},
|
|
1140
977
|
getMeter: (name, version) => {
|
|
1141
|
-
warnDeprecation("getMeter", "observability.getMeter");
|
|
978
|
+
warnDeprecation("getMeter", "observability.getMeter", logger);
|
|
1142
979
|
return observability.getMeter(name, version);
|
|
1143
980
|
},
|
|
1144
981
|
getTraceContext: () => {
|
|
1145
|
-
warnDeprecation("getTraceContext", "observability.getTraceContext");
|
|
982
|
+
warnDeprecation("getTraceContext", "observability.getTraceContext", logger);
|
|
1146
983
|
return observability.getTraceContext();
|
|
1147
984
|
},
|
|
1148
985
|
getActiveSpan: () => {
|
|
1149
|
-
warnDeprecation("getActiveSpan", "observability.getActiveSpan");
|
|
986
|
+
warnDeprecation("getActiveSpan", "observability.getActiveSpan", logger);
|
|
1150
987
|
return observability.getActiveSpan();
|
|
1151
988
|
},
|
|
1152
989
|
createHttpClient: (options) => {
|
|
1153
|
-
warnDeprecation("createHttpClient", "http.createClient");
|
|
990
|
+
warnDeprecation("createHttpClient", "http.createClient", logger);
|
|
1154
991
|
return httpModule.createClient(options);
|
|
1155
992
|
},
|
|
1156
993
|
shutdown: () => {
|
|
1157
|
-
warnDeprecation("shutdown", "lifecycle.shutdown");
|
|
994
|
+
warnDeprecation("shutdown", "lifecycle.shutdown", logger);
|
|
1158
995
|
return lifecycle.shutdown();
|
|
1159
996
|
},
|
|
1160
997
|
isReady: () => {
|
|
1161
|
-
warnDeprecation("isReady", "lifecycle.isReady");
|
|
998
|
+
warnDeprecation("isReady", "lifecycle.isReady", logger);
|
|
1162
999
|
return lifecycle.isReady();
|
|
1163
1000
|
},
|
|
1164
1001
|
health: () => {
|
|
1165
|
-
warnDeprecation("health", "lifecycle.health");
|
|
1002
|
+
warnDeprecation("health", "lifecycle.health", logger);
|
|
1166
1003
|
return lifecycle.health();
|
|
1167
1004
|
},
|
|
1168
1005
|
trace: (name, fn) => {
|
|
1169
|
-
warnDeprecation("trace", "observability.trace");
|
|
1006
|
+
warnDeprecation("trace", "observability.trace", logger);
|
|
1170
1007
|
return observability.trace(name, fn);
|
|
1171
1008
|
},
|
|
1172
1009
|
safeRun: (fn, fallback) => {
|
|
1173
|
-
warnDeprecation("safeRun", "lifecycle.safeRun");
|
|
1010
|
+
warnDeprecation("safeRun", "lifecycle.safeRun", logger);
|
|
1174
1011
|
return lifecycle.safeRun(fn, fallback);
|
|
1175
1012
|
}
|
|
1176
1013
|
};
|
|
@@ -1181,6 +1018,9 @@ const setupObservability = createFoundation;
|
|
|
1181
1018
|
|
|
1182
1019
|
//#endregion
|
|
1183
1020
|
//#region src/integrations/object-store/aws-s3.ts
|
|
1021
|
+
function isAwsError(err) {
|
|
1022
|
+
return typeof err === "object" && err !== null;
|
|
1023
|
+
}
|
|
1184
1024
|
async function loadAwsS3() {
|
|
1185
1025
|
try {
|
|
1186
1026
|
const clientMod = await import("@aws-sdk/client-s3");
|
|
@@ -1195,13 +1035,10 @@ async function loadAwsS3() {
|
|
|
1195
1035
|
getSignedUrl: presignerMod.getSignedUrl
|
|
1196
1036
|
};
|
|
1197
1037
|
} catch (err) {
|
|
1198
|
-
const e = err;
|
|
1038
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1199
1039
|
throw new Error(`AWS SDK not available. Install optional peer deps: @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner. Original error: ${e.message}`);
|
|
1200
1040
|
}
|
|
1201
1041
|
}
|
|
1202
|
-
function normalizeBody(body) {
|
|
1203
|
-
return body;
|
|
1204
|
-
}
|
|
1205
1042
|
/**
|
|
1206
1043
|
* AwsS3ObjectStore - wraps AWS S3 behind the provider-agnostic `ObjectStore` interface.
|
|
1207
1044
|
*
|
|
@@ -1230,7 +1067,7 @@ var AwsS3ObjectStore = class {
|
|
|
1230
1067
|
const res = await s3.send(new aws.PutObjectCommand({
|
|
1231
1068
|
Bucket: ref.bucket,
|
|
1232
1069
|
Key: ref.key,
|
|
1233
|
-
Body:
|
|
1070
|
+
Body: body,
|
|
1234
1071
|
ContentType: options.contentType,
|
|
1235
1072
|
CacheControl: options.cacheControl,
|
|
1236
1073
|
Metadata: options.metadata
|
|
@@ -1280,10 +1117,11 @@ var AwsS3ObjectStore = class {
|
|
|
1280
1117
|
lastModified: res.LastModified
|
|
1281
1118
|
};
|
|
1282
1119
|
} catch (err) {
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1120
|
+
if (isAwsError(err)) {
|
|
1121
|
+
const name = String(err.name ?? "");
|
|
1122
|
+
const httpStatus = err.$metadata?.httpStatusCode;
|
|
1123
|
+
if (httpStatus === 404 || name.includes("NotFound") || name.includes("NoSuchKey")) return { exists: false };
|
|
1124
|
+
}
|
|
1287
1125
|
throw err;
|
|
1288
1126
|
}
|
|
1289
1127
|
}
|
|
@@ -1304,8 +1142,9 @@ var AwsS3ObjectStore = class {
|
|
|
1304
1142
|
ContinuationToken: options.continuationToken,
|
|
1305
1143
|
MaxKeys: options.maxKeys
|
|
1306
1144
|
}));
|
|
1145
|
+
const contents = res?.Contents ?? [];
|
|
1307
1146
|
return {
|
|
1308
|
-
objects: (
|
|
1147
|
+
objects: contents.filter((o) => typeof o.Key === "string").map((o) => ({
|
|
1309
1148
|
key: o.Key,
|
|
1310
1149
|
size: o.Size,
|
|
1311
1150
|
etag: o.ETag,
|
|
@@ -1336,19 +1175,31 @@ var AwsS3ObjectStore = class {
|
|
|
1336
1175
|
|
|
1337
1176
|
//#endregion
|
|
1338
1177
|
//#region src/integrations/object-store/in-memory.ts
|
|
1178
|
+
const DEFAULT_MAX_OBJECTS = 1e4;
|
|
1179
|
+
const STREAM_TIMEOUT_MS = 3e4;
|
|
1339
1180
|
function toBuffer(body) {
|
|
1340
1181
|
if (typeof body === "string") return Buffer.from(body);
|
|
1341
1182
|
if (Buffer.isBuffer(body)) return body;
|
|
1342
1183
|
if (body instanceof Uint8Array) return Buffer.from(body);
|
|
1343
1184
|
return new Promise((resolve, reject) => {
|
|
1344
1185
|
const chunks = [];
|
|
1186
|
+
const timer = setTimeout(() => {
|
|
1187
|
+
body.destroy(new Error("InMemoryObjectStore: stream read timed out"));
|
|
1188
|
+
reject(new Error("InMemoryObjectStore: stream read timed out"));
|
|
1189
|
+
}, STREAM_TIMEOUT_MS);
|
|
1345
1190
|
body.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
1346
|
-
body.on("end", () =>
|
|
1347
|
-
|
|
1191
|
+
body.on("end", () => {
|
|
1192
|
+
clearTimeout(timer);
|
|
1193
|
+
resolve(Buffer.concat(chunks));
|
|
1194
|
+
});
|
|
1195
|
+
body.on("error", (err) => {
|
|
1196
|
+
clearTimeout(timer);
|
|
1197
|
+
reject(err);
|
|
1198
|
+
});
|
|
1348
1199
|
});
|
|
1349
1200
|
}
|
|
1350
|
-
function
|
|
1351
|
-
return `
|
|
1201
|
+
function computeEtag(buf) {
|
|
1202
|
+
return `"${createHash("md5").update(buf).digest("hex")}"`;
|
|
1352
1203
|
}
|
|
1353
1204
|
/**
|
|
1354
1205
|
* InMemoryObjectStore - useful for local dev, unit tests, and as a safe default.
|
|
@@ -1357,6 +1208,11 @@ function simpleEtag(buf) {
|
|
|
1357
1208
|
var InMemoryObjectStore = class {
|
|
1358
1209
|
provider = "in-memory";
|
|
1359
1210
|
buckets = new Map();
|
|
1211
|
+
maxObjects;
|
|
1212
|
+
objectCount = 0;
|
|
1213
|
+
constructor(options = {}) {
|
|
1214
|
+
this.maxObjects = options.maxObjects ?? DEFAULT_MAX_OBJECTS;
|
|
1215
|
+
}
|
|
1360
1216
|
bucketMap(bucket) {
|
|
1361
1217
|
let m = this.buckets.get(bucket);
|
|
1362
1218
|
if (!m) {
|
|
@@ -1366,16 +1222,20 @@ var InMemoryObjectStore = class {
|
|
|
1366
1222
|
return m;
|
|
1367
1223
|
}
|
|
1368
1224
|
async putObject(ref, body, options = {}) {
|
|
1225
|
+
const map = this.bucketMap(ref.bucket);
|
|
1226
|
+
const isNew = !map.has(ref.key);
|
|
1227
|
+
if (isNew && this.objectCount >= this.maxObjects) throw new Error(`InMemoryObjectStore: max object limit reached (${this.maxObjects})`);
|
|
1369
1228
|
const buf = await toBuffer(body);
|
|
1370
1229
|
const obj = {
|
|
1371
1230
|
body: buf,
|
|
1372
1231
|
contentType: options.contentType,
|
|
1373
1232
|
cacheControl: options.cacheControl,
|
|
1374
1233
|
metadata: options.metadata,
|
|
1375
|
-
etag:
|
|
1234
|
+
etag: computeEtag(buf),
|
|
1376
1235
|
lastModified: new Date()
|
|
1377
1236
|
};
|
|
1378
|
-
|
|
1237
|
+
map.set(ref.key, obj);
|
|
1238
|
+
if (isNew) this.objectCount++;
|
|
1379
1239
|
return { etag: obj.etag };
|
|
1380
1240
|
}
|
|
1381
1241
|
async getObject(ref) {
|
|
@@ -1407,18 +1267,30 @@ var InMemoryObjectStore = class {
|
|
|
1407
1267
|
};
|
|
1408
1268
|
}
|
|
1409
1269
|
async deleteObject(ref) {
|
|
1410
|
-
this.bucketMap(ref.bucket)
|
|
1270
|
+
const map = this.bucketMap(ref.bucket);
|
|
1271
|
+
if (map.delete(ref.key)) this.objectCount--;
|
|
1411
1272
|
}
|
|
1412
1273
|
async listObjects(options) {
|
|
1413
|
-
const { bucket, prefix = "" } = options;
|
|
1274
|
+
const { bucket, prefix = "", continuationToken, maxKeys = 1e3 } = options;
|
|
1414
1275
|
const map = this.bucketMap(bucket);
|
|
1415
|
-
const
|
|
1276
|
+
const all = [...map.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, obj]) => ({
|
|
1416
1277
|
key,
|
|
1417
1278
|
size: obj.body.length,
|
|
1418
1279
|
etag: obj.etag,
|
|
1419
1280
|
lastModified: obj.lastModified
|
|
1420
1281
|
})).sort((a, b) => a.key.localeCompare(b.key));
|
|
1421
|
-
|
|
1282
|
+
let startIndex = 0;
|
|
1283
|
+
if (continuationToken) {
|
|
1284
|
+
const idx = all.findIndex((o) => o.key === continuationToken);
|
|
1285
|
+
if (idx === -1) throw new Error(`InMemoryObjectStore: invalid continuationToken "${continuationToken}"`);
|
|
1286
|
+
startIndex = idx + 1;
|
|
1287
|
+
}
|
|
1288
|
+
const page = all.slice(startIndex, startIndex + maxKeys);
|
|
1289
|
+
const hasMore = startIndex + maxKeys < all.length;
|
|
1290
|
+
return {
|
|
1291
|
+
objects: page,
|
|
1292
|
+
nextContinuationToken: hasMore ? page[page.length - 1]?.key : void 0
|
|
1293
|
+
};
|
|
1422
1294
|
}
|
|
1423
1295
|
async presignGetObject(_ref, _options) {
|
|
1424
1296
|
throw new Error("InMemoryObjectStore does not support presigned URLs");
|