@ciq-dev/neoiq-foundation-node 1.0.1-beta.0 → 1.0.1-beta.2
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/README.md +40 -1
- package/dist/index.d.mts +271 -10
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +269 -8
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +736 -133
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +733 -132
- package/dist/index.mjs.map +1 -1
- package/package.json +16 -2
package/dist/index.js
CHANGED
|
@@ -24,8 +24,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
|
|
|
24
24
|
//#endregion
|
|
25
25
|
const zod = __toESM(require("zod"));
|
|
26
26
|
const async_hooks = __toESM(require("async_hooks"));
|
|
27
|
-
const pino = __toESM(require("pino"));
|
|
28
27
|
const __opentelemetry_api = __toESM(require("@opentelemetry/api"));
|
|
28
|
+
const pino = __toESM(require("pino"));
|
|
29
29
|
const __opentelemetry_sdk_node = __toESM(require("@opentelemetry/sdk-node"));
|
|
30
30
|
const __opentelemetry_auto_instrumentations_node = __toESM(require("@opentelemetry/auto-instrumentations-node"));
|
|
31
31
|
const __opentelemetry_exporter_trace_otlp_grpc = __toESM(require("@opentelemetry/exporter-trace-otlp-grpc"));
|
|
@@ -38,6 +38,7 @@ const crypto = __toESM(require("crypto"));
|
|
|
38
38
|
const axios = __toESM(require("axios"));
|
|
39
39
|
const axios_retry = __toESM(require("axios-retry"));
|
|
40
40
|
const opossum = __toESM(require("opossum"));
|
|
41
|
+
const node_stream = __toESM(require("node:stream"));
|
|
41
42
|
|
|
42
43
|
//#region src/config.ts
|
|
43
44
|
const AutoInstrumentationConfigSchema = zod.z.object({
|
|
@@ -81,6 +82,11 @@ const RequestLoggingConfigSchema = zod.z.object({
|
|
|
81
82
|
maxBodySize: zod.z.number().default(10 * 1024),
|
|
82
83
|
redactHeaders: zod.z.array(zod.z.string()).optional()
|
|
83
84
|
}).partial();
|
|
85
|
+
const RedactionConfigSchema = zod.z.object({ additionalPaths: zod.z.array(zod.z.string()).default([]) }).partial();
|
|
86
|
+
const ShutdownConfigSchema = zod.z.object({
|
|
87
|
+
flushOnCrash: zod.z.boolean().default(false),
|
|
88
|
+
flushTimeoutMs: zod.z.number().min(100).default(5e3)
|
|
89
|
+
}).partial();
|
|
84
90
|
const FoundationConfigSchema = zod.z.object({
|
|
85
91
|
serviceName: zod.z.string().min(1, "serviceName is required"),
|
|
86
92
|
serviceVersion: zod.z.string().default(process.env.SERVICE_VERSION || "1.0.0"),
|
|
@@ -93,7 +99,9 @@ const FoundationConfigSchema = zod.z.object({
|
|
|
93
99
|
features: FeaturesConfigSchema.default({}),
|
|
94
100
|
otel: OtelConfigSchema.default({}),
|
|
95
101
|
logging: LoggingConfigSchema.default({}),
|
|
96
|
-
requestLogging: RequestLoggingConfigSchema.default({})
|
|
102
|
+
requestLogging: RequestLoggingConfigSchema.default({}),
|
|
103
|
+
redaction: RedactionConfigSchema.default({}),
|
|
104
|
+
shutdown: ShutdownConfigSchema.default({})
|
|
97
105
|
});
|
|
98
106
|
/** Parse and validate configuration */
|
|
99
107
|
function parseConfig(input) {
|
|
@@ -111,33 +119,247 @@ function getDefaultOtelEndpoint() {
|
|
|
111
119
|
|
|
112
120
|
//#endregion
|
|
113
121
|
//#region src/features/context.ts
|
|
122
|
+
const BAGGAGE_CORRELATION_KEY = "correlation.id";
|
|
123
|
+
function setBaggageCorrelationId(correlationId) {
|
|
124
|
+
const currentBaggage = __opentelemetry_api.propagation.getBaggage(__opentelemetry_api.context.active());
|
|
125
|
+
const baggage = (currentBaggage ?? __opentelemetry_api.propagation.createBaggage()).setEntry(BAGGAGE_CORRELATION_KEY, { value: correlationId });
|
|
126
|
+
return __opentelemetry_api.propagation.setBaggage(__opentelemetry_api.context.active(), baggage);
|
|
127
|
+
}
|
|
128
|
+
function getBaggageCorrelationId() {
|
|
129
|
+
const baggage = __opentelemetry_api.propagation.getBaggage(__opentelemetry_api.context.active());
|
|
130
|
+
return baggage?.getEntry(BAGGAGE_CORRELATION_KEY)?.value;
|
|
131
|
+
}
|
|
114
132
|
/** Create a new context manager instance */
|
|
115
133
|
function createContextManager() {
|
|
116
134
|
const als = new async_hooks.AsyncLocalStorage();
|
|
117
135
|
return {
|
|
118
|
-
getContext
|
|
119
|
-
|
|
120
|
-
|
|
136
|
+
getContext() {
|
|
137
|
+
const alsCtx = als.getStore();
|
|
138
|
+
if (!alsCtx) return void 0;
|
|
139
|
+
const baggageCorrelationId = getBaggageCorrelationId();
|
|
140
|
+
if (baggageCorrelationId && baggageCorrelationId !== alsCtx.correlationId) return {
|
|
141
|
+
...alsCtx,
|
|
142
|
+
correlationId: baggageCorrelationId
|
|
143
|
+
};
|
|
144
|
+
return alsCtx;
|
|
145
|
+
},
|
|
146
|
+
run(context$2, fn) {
|
|
147
|
+
if (context$2.correlationId) {
|
|
148
|
+
const otelCtx = setBaggageCorrelationId(context$2.correlationId);
|
|
149
|
+
return __opentelemetry_api.context.with(otelCtx, () => als.run(context$2, fn));
|
|
150
|
+
}
|
|
151
|
+
return als.run(context$2, fn);
|
|
121
152
|
},
|
|
122
153
|
get(key) {
|
|
154
|
+
if (key === "correlationId") return getBaggageCorrelationId() ?? als.getStore()?.correlationId;
|
|
123
155
|
return als.getStore()?.[key];
|
|
124
156
|
},
|
|
125
157
|
update(updates) {
|
|
126
158
|
const current = als.getStore();
|
|
127
159
|
if (!current) return void 0;
|
|
160
|
+
if (updates.correlationId) setBaggageCorrelationId(updates.correlationId);
|
|
128
161
|
Object.assign(current, updates);
|
|
129
162
|
return current;
|
|
163
|
+
},
|
|
164
|
+
setContextValue(key, value) {
|
|
165
|
+
const current = als.getStore();
|
|
166
|
+
if (!current) return;
|
|
167
|
+
if (!current.contextData) current.contextData = {};
|
|
168
|
+
current.contextData[key] = value;
|
|
169
|
+
},
|
|
170
|
+
getContextValue(key) {
|
|
171
|
+
return als.getStore()?.contextData?.[key];
|
|
172
|
+
},
|
|
173
|
+
getContextData() {
|
|
174
|
+
return als.getStore()?.contextData ?? {};
|
|
175
|
+
},
|
|
176
|
+
setContextData(data) {
|
|
177
|
+
const current = als.getStore();
|
|
178
|
+
if (!current) return;
|
|
179
|
+
current.contextData = {
|
|
180
|
+
...current.contextData ?? {},
|
|
181
|
+
...data
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
clearContextData() {
|
|
185
|
+
const current = als.getStore();
|
|
186
|
+
if (!current) return;
|
|
187
|
+
current.contextData = {};
|
|
130
188
|
}
|
|
131
189
|
};
|
|
132
190
|
}
|
|
133
191
|
|
|
192
|
+
//#endregion
|
|
193
|
+
//#region src/features/redaction.ts
|
|
194
|
+
/**
|
|
195
|
+
* Log Redaction & PII Sanitization
|
|
196
|
+
*
|
|
197
|
+
* Two-layer approach:
|
|
198
|
+
* - Layer A: Pino native `redact` (fast-redact) for key-based redaction on every log call.
|
|
199
|
+
* Compiled at init, near-zero runtime cost.
|
|
200
|
+
* - Layer B: Deep-traverse sanitizer for value-pattern detection (JWTs, AWS keys, etc.).
|
|
201
|
+
* Only used for request/response body logging — NOT on every log call.
|
|
202
|
+
*/
|
|
203
|
+
const PLACEHOLDER = "[REDACTED]";
|
|
204
|
+
/**
|
|
205
|
+
* Key names that Pino's fast-redact will censor automatically.
|
|
206
|
+
* Supports wildcards: '*.password' matches nested keys one level deep.
|
|
207
|
+
*/
|
|
208
|
+
const REDACT_PATHS = [
|
|
209
|
+
"password",
|
|
210
|
+
"passwd",
|
|
211
|
+
"pass",
|
|
212
|
+
"pwd",
|
|
213
|
+
"secret",
|
|
214
|
+
"secretKey",
|
|
215
|
+
"secret_key",
|
|
216
|
+
"token",
|
|
217
|
+
"accessToken",
|
|
218
|
+
"access_token",
|
|
219
|
+
"refreshToken",
|
|
220
|
+
"refresh_token",
|
|
221
|
+
"idToken",
|
|
222
|
+
"id_token",
|
|
223
|
+
"apiKey",
|
|
224
|
+
"api_key",
|
|
225
|
+
"apiSecret",
|
|
226
|
+
"api_secret",
|
|
227
|
+
"authorization",
|
|
228
|
+
"auth",
|
|
229
|
+
"credentials",
|
|
230
|
+
"privateKey",
|
|
231
|
+
"private_key",
|
|
232
|
+
"cookie",
|
|
233
|
+
"setCookie",
|
|
234
|
+
"set_cookie",
|
|
235
|
+
"creditCard",
|
|
236
|
+
"credit_card",
|
|
237
|
+
"cardNumber",
|
|
238
|
+
"card_number",
|
|
239
|
+
"ccNumber",
|
|
240
|
+
"cc_number",
|
|
241
|
+
"cvv",
|
|
242
|
+
"cvc",
|
|
243
|
+
"securityCode",
|
|
244
|
+
"security_code",
|
|
245
|
+
"accountNumber",
|
|
246
|
+
"account_number",
|
|
247
|
+
"ssn",
|
|
248
|
+
"socialSecurity",
|
|
249
|
+
"social_security",
|
|
250
|
+
"dateOfBirth",
|
|
251
|
+
"date_of_birth",
|
|
252
|
+
"dob",
|
|
253
|
+
"*.password",
|
|
254
|
+
"*.secret",
|
|
255
|
+
"*.token",
|
|
256
|
+
"*.apiKey",
|
|
257
|
+
"*.api_key",
|
|
258
|
+
"*.authorization",
|
|
259
|
+
"*.cookie",
|
|
260
|
+
"*.credentials",
|
|
261
|
+
"*.creditCard",
|
|
262
|
+
"*.cardNumber",
|
|
263
|
+
"*.cvv",
|
|
264
|
+
"*.ssn",
|
|
265
|
+
"*.privateKey",
|
|
266
|
+
"*.private_key"
|
|
267
|
+
];
|
|
268
|
+
/**
|
|
269
|
+
* Value patterns that indicate a secret regardless of the key name.
|
|
270
|
+
* Used only for body sanitization (Layer B).
|
|
271
|
+
*/
|
|
272
|
+
const VALUE_PATTERNS = [
|
|
273
|
+
{
|
|
274
|
+
label: "jwt",
|
|
275
|
+
pattern: /^eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+$/
|
|
276
|
+
},
|
|
277
|
+
{
|
|
278
|
+
label: "aws_access_key",
|
|
279
|
+
pattern: /(?:^|[^A-Za-z0-9])AKIA[0-9A-Z]{16}(?:$|[^A-Za-z0-9])/
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
label: "stripe_key",
|
|
283
|
+
pattern: /^[sr]k_(live|test)_[A-Za-z0-9]{10,}$/
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
label: "openai_key",
|
|
287
|
+
pattern: /^sk-[A-Za-z0-9_-]{20,}$/
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
label: "github_token",
|
|
291
|
+
pattern: /^gh[ps]_[A-Za-z0-9]{36,}$/
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
label: "pem_private_key",
|
|
295
|
+
pattern: /-----BEGIN\s+(RSA\s+|EC\s+)?PRIVATE\s+KEY-----/
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
label: "connection_string",
|
|
299
|
+
pattern: /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:]+:[^@]+@/
|
|
300
|
+
},
|
|
301
|
+
{
|
|
302
|
+
label: "bearer_token",
|
|
303
|
+
pattern: /^Bearer\s+\S{10,}$/i
|
|
304
|
+
}
|
|
305
|
+
];
|
|
306
|
+
const SENSITIVE_KEYS = new Set(REDACT_PATHS.filter((p) => !p.includes("*")).map((k) => k.toLowerCase()));
|
|
307
|
+
const MAX_DEPTH = 10;
|
|
308
|
+
const MAX_KEYS = 200;
|
|
309
|
+
function mask(value) {
|
|
310
|
+
if (typeof value !== "string" || value.length <= 8) return PLACEHOLDER;
|
|
311
|
+
return `${value.slice(0, 4)}${"*".repeat(Math.min(value.length - 8, 20))}${value.slice(-4)}`;
|
|
312
|
+
}
|
|
313
|
+
function isSensitiveValue(value) {
|
|
314
|
+
return VALUE_PATTERNS.some((p) => p.pattern.test(value));
|
|
315
|
+
}
|
|
316
|
+
function sanitizeValue(value, depth) {
|
|
317
|
+
if (depth > MAX_DEPTH) return value;
|
|
318
|
+
if (typeof value === "string" && isSensitiveValue(value)) return mask(value);
|
|
319
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, depth + 1));
|
|
320
|
+
if (value !== null && typeof value === "object") return sanitizeObject(value, depth + 1);
|
|
321
|
+
return value;
|
|
322
|
+
}
|
|
323
|
+
function sanitizeObject(obj, depth) {
|
|
324
|
+
if (depth > MAX_DEPTH) return obj;
|
|
325
|
+
const keys = Object.keys(obj);
|
|
326
|
+
if (keys.length > MAX_KEYS) return obj;
|
|
327
|
+
const result = {};
|
|
328
|
+
for (const key of keys) {
|
|
329
|
+
const value = obj[key];
|
|
330
|
+
if (SENSITIVE_KEYS.has(key.toLowerCase())) result[key] = typeof value === "string" ? mask(value) : PLACEHOLDER;
|
|
331
|
+
else result[key] = sanitizeValue(value, depth);
|
|
332
|
+
}
|
|
333
|
+
return result;
|
|
334
|
+
}
|
|
335
|
+
/**
|
|
336
|
+
* Deep-traverse sanitizer for request/response bodies.
|
|
337
|
+
* Checks both key names (deny-list) and value patterns (JWT, AWS keys, etc.).
|
|
338
|
+
* NOT intended for every log call — use Pino native `redact` for that.
|
|
339
|
+
*/
|
|
340
|
+
function sanitizeBody(body) {
|
|
341
|
+
if (body === null || body === void 0) return body;
|
|
342
|
+
if (typeof body === "string") return isSensitiveValue(body) ? mask(body) : body;
|
|
343
|
+
if (typeof body !== "object") return body;
|
|
344
|
+
if (Array.isArray(body)) return body.map((item) => sanitizeValue(item, 0));
|
|
345
|
+
return sanitizeObject(body, 0);
|
|
346
|
+
}
|
|
347
|
+
/** Build the Pino `redact` config object for fast-redact integration */
|
|
348
|
+
function buildPinoRedactConfig(additionalPaths = []) {
|
|
349
|
+
return {
|
|
350
|
+
paths: [...REDACT_PATHS, ...additionalPaths],
|
|
351
|
+
censor: PLACEHOLDER
|
|
352
|
+
};
|
|
353
|
+
}
|
|
354
|
+
|
|
134
355
|
//#endregion
|
|
135
356
|
//#region src/features/logging.ts
|
|
136
357
|
/** Create a structured logger with automatic trace context injection */
|
|
137
358
|
function createLogger(options) {
|
|
138
|
-
const { serviceName, serviceVersion, environment, level, prettyPrint, contextManager } = options;
|
|
359
|
+
const { serviceName, serviceVersion, environment, level, prettyPrint, contextManager, additionalRedactPaths = [] } = options;
|
|
139
360
|
const pinoLogger = (0, pino.default)({
|
|
140
361
|
level,
|
|
362
|
+
redact: buildPinoRedactConfig(additionalRedactPaths),
|
|
141
363
|
base: {
|
|
142
364
|
service: serviceName,
|
|
143
365
|
version: serviceVersion,
|
|
@@ -147,11 +369,17 @@ function createLogger(options) {
|
|
|
147
369
|
const span = __opentelemetry_api.trace.getActiveSpan();
|
|
148
370
|
const spanContext = span?.spanContext();
|
|
149
371
|
const ctx = contextManager?.getContext();
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
372
|
+
const traceId = spanContext?.traceId || ctx?.traceId;
|
|
373
|
+
const spanId = spanContext?.spanId || ctx?.spanId;
|
|
374
|
+
const correlationId = ctx?.correlationId;
|
|
375
|
+
const contextData = ctx?.contextData;
|
|
376
|
+
const result = {
|
|
377
|
+
trace_id: traceId,
|
|
378
|
+
span_id: spanId,
|
|
379
|
+
correlation_id: correlationId
|
|
154
380
|
};
|
|
381
|
+
if (contextData && Object.keys(contextData).length > 0) result.context = contextData;
|
|
382
|
+
return result;
|
|
155
383
|
},
|
|
156
384
|
formatters: { level: (label) => ({ level: label }) },
|
|
157
385
|
transport: prettyPrint ? {
|
|
@@ -185,7 +413,10 @@ function createFallbackLogger(serviceName = "unknown") {
|
|
|
185
413
|
...obj,
|
|
186
414
|
msg
|
|
187
415
|
};
|
|
188
|
-
|
|
416
|
+
const payload = JSON.stringify(logObj);
|
|
417
|
+
if (level === "warn") console.warn(payload);
|
|
418
|
+
else if (level === "error") console.error(payload);
|
|
419
|
+
else console.log(payload);
|
|
189
420
|
};
|
|
190
421
|
const fallback = {
|
|
191
422
|
debug: (obj, msg) => log("debug", obj, msg),
|
|
@@ -209,12 +440,26 @@ function getGlobalLogger() {
|
|
|
209
440
|
//#region src/features/tracing.ts
|
|
210
441
|
let sdk = null;
|
|
211
442
|
let isInitialized$1 = false;
|
|
443
|
+
const MONITORED_MODULES = [
|
|
444
|
+
"pg",
|
|
445
|
+
"mongodb",
|
|
446
|
+
"ioredis",
|
|
447
|
+
"mysql2",
|
|
448
|
+
"express",
|
|
449
|
+
"@grpc/grpc-js"
|
|
450
|
+
];
|
|
451
|
+
function warnPreloadedModules() {
|
|
452
|
+
for (const mod of MONITORED_MODULES) try {
|
|
453
|
+
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.`);
|
|
454
|
+
} catch {}
|
|
455
|
+
}
|
|
212
456
|
/** Initialize OpenTelemetry tracing */
|
|
213
457
|
function setupTracing(options) {
|
|
214
458
|
if (isInitialized$1) {
|
|
215
459
|
console.warn("[neoiq-foundation] Tracing already initialized");
|
|
216
460
|
return;
|
|
217
461
|
}
|
|
462
|
+
warnPreloadedModules();
|
|
218
463
|
const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), autoInstrumentation = {} } = options;
|
|
219
464
|
const resource = (0, __opentelemetry_resources.resourceFromAttributes)({
|
|
220
465
|
[__opentelemetry_semantic_conventions.ATTR_SERVICE_NAME]: serviceName,
|
|
@@ -371,7 +616,6 @@ function createObservabilityPlugin(options) {
|
|
|
371
616
|
const maxBodySize = requestLogging.maxBodySize ?? 10 * 1024;
|
|
372
617
|
const headersToRedact = requestLogging.redactHeaders ?? DEFAULT_REDACT_HEADERS;
|
|
373
618
|
const runInContext = contextManager ? (ctx, fn) => contextManager.run(ctx, fn) : (_ctx, fn) => fn();
|
|
374
|
-
const tracer = tracingEnabled ? __opentelemetry_api.trace.getTracer("neoiq-foundation-node") : null;
|
|
375
619
|
let requestCounter;
|
|
376
620
|
let requestDuration;
|
|
377
621
|
let requestErrors;
|
|
@@ -388,22 +632,12 @@ function createObservabilityPlugin(options) {
|
|
|
388
632
|
}
|
|
389
633
|
const correlationId = request.headers["x-request-id"] || (0, crypto.randomUUID)();
|
|
390
634
|
reply.header("x-request-id", correlationId);
|
|
391
|
-
let span;
|
|
392
635
|
let traceId = "";
|
|
393
636
|
let spanId = "";
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
attributes: {
|
|
399
|
-
"http.method": request.method,
|
|
400
|
-
"http.url": request.url,
|
|
401
|
-
"http.route": request.routeOptions?.url || request.url,
|
|
402
|
-
"http.user_agent": request.headers["user-agent"] || "",
|
|
403
|
-
"http.correlation_id": correlationId
|
|
404
|
-
}
|
|
405
|
-
}, parentContext);
|
|
406
|
-
const spanContext = span.spanContext();
|
|
637
|
+
const activeSpan = tracingEnabled ? __opentelemetry_api.trace.getActiveSpan() : void 0;
|
|
638
|
+
if (activeSpan) {
|
|
639
|
+
activeSpan.setAttribute("http.correlation_id", correlationId);
|
|
640
|
+
const spanContext = activeSpan.spanContext();
|
|
407
641
|
traceId = spanContext.traceId;
|
|
408
642
|
spanId = spanContext.spanId;
|
|
409
643
|
}
|
|
@@ -413,12 +647,12 @@ function createObservabilityPlugin(options) {
|
|
|
413
647
|
spanId,
|
|
414
648
|
startTime: Date.now()
|
|
415
649
|
};
|
|
416
|
-
request.__span = span;
|
|
417
650
|
request.__requestContext = requestContext;
|
|
418
651
|
runInContext(requestContext, () => {
|
|
419
652
|
const logData = {
|
|
420
|
-
correlationId,
|
|
421
|
-
|
|
653
|
+
correlation_id: correlationId,
|
|
654
|
+
trace_id: traceId || void 0,
|
|
655
|
+
span_id: spanId || void 0,
|
|
422
656
|
method: request.method,
|
|
423
657
|
url: request.url,
|
|
424
658
|
ip: request.ip,
|
|
@@ -437,8 +671,8 @@ function createObservabilityPlugin(options) {
|
|
|
437
671
|
}
|
|
438
672
|
runInContext(ctx, () => {
|
|
439
673
|
logger.debug({
|
|
440
|
-
|
|
441
|
-
body: truncateBody(request.body, maxBodySize)
|
|
674
|
+
correlation_id: ctx.correlationId,
|
|
675
|
+
body: sanitizeBody(truncateBody(request.body, maxBodySize))
|
|
442
676
|
}, "Request body");
|
|
443
677
|
done();
|
|
444
678
|
});
|
|
@@ -451,16 +685,15 @@ function createObservabilityPlugin(options) {
|
|
|
451
685
|
}
|
|
452
686
|
runInContext(ctx, () => {
|
|
453
687
|
logger.debug({
|
|
454
|
-
|
|
688
|
+
correlation_id: ctx.correlationId,
|
|
455
689
|
statusCode: reply.statusCode,
|
|
456
|
-
body: truncateBody(payload, maxBodySize)
|
|
690
|
+
body: sanitizeBody(truncateBody(payload, maxBodySize))
|
|
457
691
|
}, "Response body");
|
|
458
692
|
done(null, payload);
|
|
459
693
|
});
|
|
460
694
|
});
|
|
461
695
|
fastify.addHook("onResponse", (request, reply, done) => {
|
|
462
696
|
const ctx = request.__requestContext;
|
|
463
|
-
const span = request.__span;
|
|
464
697
|
if (!ctx) {
|
|
465
698
|
done();
|
|
466
699
|
return;
|
|
@@ -474,7 +707,7 @@ function createObservabilityPlugin(options) {
|
|
|
474
707
|
};
|
|
475
708
|
runInContext(ctx, () => {
|
|
476
709
|
logger.info({
|
|
477
|
-
|
|
710
|
+
correlation_id: ctx.correlationId,
|
|
478
711
|
method: request.method,
|
|
479
712
|
statusCode: reply.statusCode,
|
|
480
713
|
durationMs
|
|
@@ -484,35 +717,35 @@ function createObservabilityPlugin(options) {
|
|
|
484
717
|
requestDuration.record(durationMs, labels);
|
|
485
718
|
if (reply.statusCode >= 400) requestErrors.add(1, labels);
|
|
486
719
|
}
|
|
487
|
-
if (
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
span.setAttribute("http.response_time_ms", durationMs);
|
|
491
|
-
span.end();
|
|
720
|
+
if (tracingEnabled) {
|
|
721
|
+
const activeSpan = __opentelemetry_api.trace.getActiveSpan();
|
|
722
|
+
if (activeSpan) activeSpan.setAttribute("http.response_time_ms", durationMs);
|
|
492
723
|
}
|
|
493
724
|
done();
|
|
494
725
|
});
|
|
495
726
|
});
|
|
496
727
|
fastify.addHook("onError", (request, _reply, error, done) => {
|
|
497
728
|
const ctx = request.__requestContext;
|
|
498
|
-
const span = request.__span;
|
|
499
729
|
if (!ctx) {
|
|
500
730
|
done();
|
|
501
731
|
return;
|
|
502
732
|
}
|
|
503
733
|
runInContext(ctx, () => {
|
|
504
734
|
logger.error({
|
|
505
|
-
|
|
735
|
+
correlation_id: ctx.correlationId,
|
|
506
736
|
method: request.method,
|
|
507
737
|
url: request.url,
|
|
508
738
|
error: error.message
|
|
509
739
|
}, "Request failed");
|
|
510
|
-
if (
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
740
|
+
if (tracingEnabled) {
|
|
741
|
+
const activeSpan = __opentelemetry_api.trace.getActiveSpan();
|
|
742
|
+
if (activeSpan) {
|
|
743
|
+
activeSpan.setStatus({
|
|
744
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
|
745
|
+
message: error.message
|
|
746
|
+
});
|
|
747
|
+
activeSpan.recordException(error);
|
|
748
|
+
}
|
|
516
749
|
}
|
|
517
750
|
done();
|
|
518
751
|
});
|
|
@@ -642,17 +875,54 @@ function createHttpClient(options) {
|
|
|
642
875
|
breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
|
|
643
876
|
breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
|
|
644
877
|
breaker.on("close", () => logger.info({ targetService: serviceName }, "Circuit breaker CLOSED"));
|
|
878
|
+
const originalRequest = client.request.bind(client);
|
|
879
|
+
client.request = (config) => breaker.fire(config);
|
|
880
|
+
client.get = (url, config) => breaker.fire({
|
|
881
|
+
...config,
|
|
882
|
+
method: "GET",
|
|
883
|
+
url
|
|
884
|
+
});
|
|
885
|
+
client.post = (url, data, config) => breaker.fire({
|
|
886
|
+
...config,
|
|
887
|
+
method: "POST",
|
|
888
|
+
url,
|
|
889
|
+
data
|
|
890
|
+
});
|
|
891
|
+
client.put = (url, data, config) => breaker.fire({
|
|
892
|
+
...config,
|
|
893
|
+
method: "PUT",
|
|
894
|
+
url,
|
|
895
|
+
data
|
|
896
|
+
});
|
|
897
|
+
client.delete = (url, config) => breaker.fire({
|
|
898
|
+
...config,
|
|
899
|
+
method: "DELETE",
|
|
900
|
+
url
|
|
901
|
+
});
|
|
902
|
+
client.patch = (url, data, config) => breaker.fire({
|
|
903
|
+
...config,
|
|
904
|
+
method: "PATCH",
|
|
905
|
+
url,
|
|
906
|
+
data
|
|
907
|
+
});
|
|
908
|
+
client.__originalRequest = originalRequest;
|
|
645
909
|
}
|
|
646
910
|
return client;
|
|
647
911
|
}
|
|
648
912
|
|
|
649
913
|
//#endregion
|
|
650
914
|
//#region src/foundation.ts
|
|
915
|
+
const deprecationWarnings = new Set();
|
|
916
|
+
function warnDeprecation(oldPath, newPath) {
|
|
917
|
+
if (deprecationWarnings.has(oldPath)) return;
|
|
918
|
+
deprecationWarnings.add(oldPath);
|
|
919
|
+
console.warn(`[neoiq-foundation] DEPRECATED: foundation.${oldPath}() is deprecated. Use foundation.${newPath}() instead. This alias will be removed in the next major version.`);
|
|
920
|
+
}
|
|
651
921
|
/** Create a fully configured observability foundation */
|
|
652
922
|
function createFoundation(input) {
|
|
653
923
|
const startTime = Date.now();
|
|
654
924
|
const config = parseConfig(input);
|
|
655
|
-
const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig } = config;
|
|
925
|
+
const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig, redaction: redactionConfig, shutdown: shutdownConfig } = config;
|
|
656
926
|
const features = {
|
|
657
927
|
tracing: featuresConfig.tracing ?? true,
|
|
658
928
|
metrics: featuresConfig.metrics ?? true,
|
|
@@ -668,7 +938,8 @@ function createFoundation(input) {
|
|
|
668
938
|
environment,
|
|
669
939
|
level: loggingConfig.level ?? "info",
|
|
670
940
|
prettyPrint: loggingConfig.prettyPrint ?? environment === "development",
|
|
671
|
-
contextManager
|
|
941
|
+
contextManager,
|
|
942
|
+
additionalRedactPaths: redactionConfig.additionalPaths
|
|
672
943
|
});
|
|
673
944
|
setGlobalLogger(logger);
|
|
674
945
|
} catch (err) {
|
|
@@ -740,17 +1011,137 @@ function createFoundation(input) {
|
|
|
740
1011
|
environment,
|
|
741
1012
|
features
|
|
742
1013
|
}, "Foundation initialized");
|
|
743
|
-
|
|
744
|
-
|
|
1014
|
+
if (shutdownConfig.flushOnCrash) {
|
|
1015
|
+
const flushTimeoutMs = shutdownConfig.flushTimeoutMs ?? 5e3;
|
|
1016
|
+
const crashFlush = (origin, err) => {
|
|
1017
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
1018
|
+
logger.error({
|
|
1019
|
+
error: error.message,
|
|
1020
|
+
stack: error.stack,
|
|
1021
|
+
origin
|
|
1022
|
+
}, "Process crash detected, flushing telemetry");
|
|
1023
|
+
const flushPromises = [];
|
|
1024
|
+
if (features.tracing && isTracingEnabled()) flushPromises.push(shutdownTracing());
|
|
1025
|
+
if (features.metrics && isMetricsEnabled()) flushPromises.push(shutdownMetrics());
|
|
1026
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, flushTimeoutMs));
|
|
1027
|
+
Promise.race([Promise.allSettled(flushPromises), timeout]).finally(() => {
|
|
1028
|
+
process.exit(1);
|
|
1029
|
+
});
|
|
1030
|
+
};
|
|
1031
|
+
process.on("uncaughtException", (err) => crashFlush("uncaughtException", err));
|
|
1032
|
+
process.on("unhandledRejection", (reason) => crashFlush("unhandledRejection", reason));
|
|
1033
|
+
logger.info({ flushTimeoutMs }, "Crash-flush handlers registered");
|
|
1034
|
+
}
|
|
1035
|
+
const traceInSpan = async (name, fn) => {
|
|
1036
|
+
const tracer = tracerInstance || getTracer(serviceName);
|
|
1037
|
+
return new Promise((resolve, reject) => {
|
|
1038
|
+
tracer.startActiveSpan(name, async (span) => {
|
|
1039
|
+
try {
|
|
1040
|
+
const result = await fn();
|
|
1041
|
+
span.setStatus({ code: __opentelemetry_api.SpanStatusCode.OK });
|
|
1042
|
+
span.end();
|
|
1043
|
+
resolve(result);
|
|
1044
|
+
} catch (err) {
|
|
1045
|
+
const error = err;
|
|
1046
|
+
span.setStatus({
|
|
1047
|
+
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
|
1048
|
+
message: error.message
|
|
1049
|
+
});
|
|
1050
|
+
span.recordException(error);
|
|
1051
|
+
span.end();
|
|
1052
|
+
logger.error({
|
|
1053
|
+
span: name,
|
|
1054
|
+
error: error.message
|
|
1055
|
+
}, "Span failed");
|
|
1056
|
+
reject(error);
|
|
1057
|
+
}
|
|
1058
|
+
});
|
|
1059
|
+
});
|
|
1060
|
+
};
|
|
1061
|
+
const observability = {
|
|
745
1062
|
logger,
|
|
746
|
-
context: contextManager,
|
|
747
1063
|
tracer: tracerInstance,
|
|
748
1064
|
meter: meterInstance,
|
|
749
|
-
features,
|
|
750
1065
|
getTracer: (name) => getTracer(name || serviceName),
|
|
751
1066
|
getMeter: (name, version) => getMeter(name, version),
|
|
752
1067
|
getTraceContext,
|
|
753
1068
|
getActiveSpan,
|
|
1069
|
+
trace: traceInSpan
|
|
1070
|
+
};
|
|
1071
|
+
const httpModule = { createClient: (options) => createHttpClient({
|
|
1072
|
+
...options,
|
|
1073
|
+
foundation
|
|
1074
|
+
}) };
|
|
1075
|
+
const buildHealthStatus = () => {
|
|
1076
|
+
const tracingUp = !features.tracing || !tracingError && isTracingEnabled();
|
|
1077
|
+
const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
|
|
1078
|
+
const loggingUp = !loggingError;
|
|
1079
|
+
const allUp = tracingUp && metricsUp && loggingUp;
|
|
1080
|
+
const anyDown = features.tracing && tracingError || features.metrics && metricsError || loggingError;
|
|
1081
|
+
return {
|
|
1082
|
+
status: allUp ? "healthy" : anyDown ? "degraded" : "unhealthy",
|
|
1083
|
+
timestamp: new Date().toISOString(),
|
|
1084
|
+
service: serviceName,
|
|
1085
|
+
version: serviceVersion,
|
|
1086
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
1087
|
+
components: {
|
|
1088
|
+
tracing: {
|
|
1089
|
+
enabled: features.tracing,
|
|
1090
|
+
status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
|
|
1091
|
+
message: tracingError
|
|
1092
|
+
},
|
|
1093
|
+
metrics: {
|
|
1094
|
+
enabled: features.metrics,
|
|
1095
|
+
status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
|
|
1096
|
+
message: metricsError
|
|
1097
|
+
},
|
|
1098
|
+
logging: {
|
|
1099
|
+
enabled: features.logging,
|
|
1100
|
+
status: loggingError ? "down" : "up",
|
|
1101
|
+
message: loggingError
|
|
1102
|
+
}
|
|
1103
|
+
}
|
|
1104
|
+
};
|
|
1105
|
+
};
|
|
1106
|
+
const shutdownFn = async () => {
|
|
1107
|
+
logger.info({}, "Shutting down foundation...");
|
|
1108
|
+
const promises = [];
|
|
1109
|
+
if (features.tracing && isTracingEnabled()) promises.push(shutdownTracing());
|
|
1110
|
+
if (features.metrics && isMetricsEnabled()) promises.push(shutdownMetrics());
|
|
1111
|
+
await Promise.all(promises);
|
|
1112
|
+
logger.info({}, "Foundation shutdown complete");
|
|
1113
|
+
};
|
|
1114
|
+
const isReadyFn = () => {
|
|
1115
|
+
if (features.tracing && !tracingError && !isTracingEnabled()) return false;
|
|
1116
|
+
if (features.metrics && !metricsError && !isMetricsEnabled()) return false;
|
|
1117
|
+
return true;
|
|
1118
|
+
};
|
|
1119
|
+
const safeRunFn = async (fn, fallback) => {
|
|
1120
|
+
try {
|
|
1121
|
+
return await fn();
|
|
1122
|
+
} catch (err) {
|
|
1123
|
+
const error = err;
|
|
1124
|
+
logger.error({
|
|
1125
|
+
error: error.message,
|
|
1126
|
+
stack: error.stack
|
|
1127
|
+
}, "safeRun caught error");
|
|
1128
|
+
return fallback;
|
|
1129
|
+
}
|
|
1130
|
+
};
|
|
1131
|
+
const lifecycle = {
|
|
1132
|
+
health: buildHealthStatus,
|
|
1133
|
+
isReady: isReadyFn,
|
|
1134
|
+
shutdown: shutdownFn,
|
|
1135
|
+
safeRun: safeRunFn
|
|
1136
|
+
};
|
|
1137
|
+
const foundation = {
|
|
1138
|
+
config,
|
|
1139
|
+
features,
|
|
1140
|
+
observability,
|
|
1141
|
+
http: httpModule,
|
|
1142
|
+
lifecycle,
|
|
1143
|
+
logger,
|
|
1144
|
+
context: contextManager,
|
|
754
1145
|
fastifyPlugin: createObservabilityPlugin({
|
|
755
1146
|
serviceName,
|
|
756
1147
|
logger,
|
|
@@ -759,91 +1150,47 @@ function createFoundation(input) {
|
|
|
759
1150
|
metricsEnabled: features.metrics && !metricsError,
|
|
760
1151
|
requestLogging: requestLoggingConfig
|
|
761
1152
|
}),
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
1153
|
+
tracer: tracerInstance,
|
|
1154
|
+
meter: meterInstance,
|
|
1155
|
+
getTracer: (name) => {
|
|
1156
|
+
warnDeprecation("getTracer", "observability.getTracer");
|
|
1157
|
+
return observability.getTracer(name);
|
|
1158
|
+
},
|
|
1159
|
+
getMeter: (name, version) => {
|
|
1160
|
+
warnDeprecation("getMeter", "observability.getMeter");
|
|
1161
|
+
return observability.getMeter(name, version);
|
|
1162
|
+
},
|
|
1163
|
+
getTraceContext: () => {
|
|
1164
|
+
warnDeprecation("getTraceContext", "observability.getTraceContext");
|
|
1165
|
+
return observability.getTraceContext();
|
|
1166
|
+
},
|
|
1167
|
+
getActiveSpan: () => {
|
|
1168
|
+
warnDeprecation("getActiveSpan", "observability.getActiveSpan");
|
|
1169
|
+
return observability.getActiveSpan();
|
|
1170
|
+
},
|
|
1171
|
+
createHttpClient: (options) => {
|
|
1172
|
+
warnDeprecation("createHttpClient", "http.createClient");
|
|
1173
|
+
return httpModule.createClient(options);
|
|
1174
|
+
},
|
|
1175
|
+
shutdown: () => {
|
|
1176
|
+
warnDeprecation("shutdown", "lifecycle.shutdown");
|
|
1177
|
+
return lifecycle.shutdown();
|
|
773
1178
|
},
|
|
774
1179
|
isReady: () => {
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
return true;
|
|
1180
|
+
warnDeprecation("isReady", "lifecycle.isReady");
|
|
1181
|
+
return lifecycle.isReady();
|
|
778
1182
|
},
|
|
779
1183
|
health: () => {
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
const loggingUp = !loggingError;
|
|
783
|
-
const allUp = tracingUp && metricsUp && loggingUp;
|
|
784
|
-
const anyDown = features.tracing && tracingError || features.metrics && metricsError || loggingError;
|
|
785
|
-
return {
|
|
786
|
-
status: allUp ? "healthy" : anyDown ? "degraded" : "unhealthy",
|
|
787
|
-
timestamp: new Date().toISOString(),
|
|
788
|
-
service: serviceName,
|
|
789
|
-
version: serviceVersion,
|
|
790
|
-
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
791
|
-
components: {
|
|
792
|
-
tracing: {
|
|
793
|
-
enabled: features.tracing,
|
|
794
|
-
status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
|
|
795
|
-
message: tracingError
|
|
796
|
-
},
|
|
797
|
-
metrics: {
|
|
798
|
-
enabled: features.metrics,
|
|
799
|
-
status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
|
|
800
|
-
message: metricsError
|
|
801
|
-
},
|
|
802
|
-
logging: {
|
|
803
|
-
enabled: features.logging,
|
|
804
|
-
status: loggingError ? "down" : "up",
|
|
805
|
-
message: loggingError
|
|
806
|
-
}
|
|
807
|
-
}
|
|
808
|
-
};
|
|
1184
|
+
warnDeprecation("health", "lifecycle.health");
|
|
1185
|
+
return lifecycle.health();
|
|
809
1186
|
},
|
|
810
|
-
trace:
|
|
811
|
-
|
|
812
|
-
return
|
|
813
|
-
tracer.startActiveSpan(name, async (span) => {
|
|
814
|
-
try {
|
|
815
|
-
const result = await fn();
|
|
816
|
-
span.setStatus({ code: __opentelemetry_api.SpanStatusCode.OK });
|
|
817
|
-
span.end();
|
|
818
|
-
resolve(result);
|
|
819
|
-
} catch (err) {
|
|
820
|
-
const error = err;
|
|
821
|
-
span.setStatus({
|
|
822
|
-
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
|
823
|
-
message: error.message
|
|
824
|
-
});
|
|
825
|
-
span.recordException(error);
|
|
826
|
-
span.end();
|
|
827
|
-
logger.error({
|
|
828
|
-
span: name,
|
|
829
|
-
error: error.message
|
|
830
|
-
}, "Span failed");
|
|
831
|
-
reject(error);
|
|
832
|
-
}
|
|
833
|
-
});
|
|
834
|
-
});
|
|
1187
|
+
trace: (name, fn) => {
|
|
1188
|
+
warnDeprecation("trace", "observability.trace");
|
|
1189
|
+
return observability.trace(name, fn);
|
|
835
1190
|
},
|
|
836
|
-
safeRun:
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
} catch (err) {
|
|
840
|
-
const error = err;
|
|
841
|
-
logger.error({
|
|
842
|
-
error: error.message,
|
|
843
|
-
stack: error.stack
|
|
844
|
-
}, "safeRun caught error");
|
|
845
|
-
return fallback;
|
|
846
|
-
}
|
|
1191
|
+
safeRun: (fn, fallback) => {
|
|
1192
|
+
warnDeprecation("safeRun", "lifecycle.safeRun");
|
|
1193
|
+
return lifecycle.safeRun(fn, fallback);
|
|
847
1194
|
}
|
|
848
1195
|
};
|
|
849
1196
|
return foundation;
|
|
@@ -851,14 +1198,269 @@ function createFoundation(input) {
|
|
|
851
1198
|
/** Alias for createFoundation */
|
|
852
1199
|
const setupObservability = createFoundation;
|
|
853
1200
|
|
|
1201
|
+
//#endregion
|
|
1202
|
+
//#region src/integrations/object-store/aws-s3.ts
|
|
1203
|
+
async function loadAwsS3() {
|
|
1204
|
+
try {
|
|
1205
|
+
const clientMod = await import("@aws-sdk/client-s3");
|
|
1206
|
+
const presignerMod = await import("@aws-sdk/s3-request-presigner");
|
|
1207
|
+
return {
|
|
1208
|
+
S3Client: clientMod.S3Client,
|
|
1209
|
+
PutObjectCommand: clientMod.PutObjectCommand,
|
|
1210
|
+
GetObjectCommand: clientMod.GetObjectCommand,
|
|
1211
|
+
HeadObjectCommand: clientMod.HeadObjectCommand,
|
|
1212
|
+
DeleteObjectCommand: clientMod.DeleteObjectCommand,
|
|
1213
|
+
ListObjectsV2Command: clientMod.ListObjectsV2Command,
|
|
1214
|
+
getSignedUrl: presignerMod.getSignedUrl
|
|
1215
|
+
};
|
|
1216
|
+
} catch (err) {
|
|
1217
|
+
const e = err;
|
|
1218
|
+
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}`);
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
function normalizeBody(body) {
|
|
1222
|
+
return body;
|
|
1223
|
+
}
|
|
1224
|
+
/**
|
|
1225
|
+
* AwsS3ObjectStore - wraps AWS S3 behind the provider-agnostic `ObjectStore` interface.
|
|
1226
|
+
*
|
|
1227
|
+
* This implementation uses dynamic imports so `neoiq-foundation-node` can be used without AWS SDK.
|
|
1228
|
+
*/
|
|
1229
|
+
var AwsS3ObjectStore = class {
|
|
1230
|
+
provider = "aws-s3";
|
|
1231
|
+
clientPromise;
|
|
1232
|
+
awsPromise = loadAwsS3();
|
|
1233
|
+
constructor(options = {}) {
|
|
1234
|
+
if (options.client) {
|
|
1235
|
+
this.clientPromise = Promise.resolve(options.client);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
1238
|
+
this.clientPromise = (async () => {
|
|
1239
|
+
const aws = await this.awsPromise;
|
|
1240
|
+
return new aws.S3Client(options.clientOptions ?? {});
|
|
1241
|
+
})();
|
|
1242
|
+
}
|
|
1243
|
+
async client() {
|
|
1244
|
+
return this.clientPromise;
|
|
1245
|
+
}
|
|
1246
|
+
async putObject(ref, body, options = {}) {
|
|
1247
|
+
const aws = await this.awsPromise;
|
|
1248
|
+
const s3 = await this.client();
|
|
1249
|
+
const res = await s3.send(new aws.PutObjectCommand({
|
|
1250
|
+
Bucket: ref.bucket,
|
|
1251
|
+
Key: ref.key,
|
|
1252
|
+
Body: normalizeBody(body),
|
|
1253
|
+
ContentType: options.contentType,
|
|
1254
|
+
CacheControl: options.cacheControl,
|
|
1255
|
+
Metadata: options.metadata
|
|
1256
|
+
}));
|
|
1257
|
+
return {
|
|
1258
|
+
etag: res?.ETag,
|
|
1259
|
+
versionId: res?.VersionId
|
|
1260
|
+
};
|
|
1261
|
+
}
|
|
1262
|
+
async getObject(ref) {
|
|
1263
|
+
const aws = await this.awsPromise;
|
|
1264
|
+
const s3 = await this.client();
|
|
1265
|
+
const res = await s3.send(new aws.GetObjectCommand({
|
|
1266
|
+
Bucket: ref.bucket,
|
|
1267
|
+
Key: ref.key
|
|
1268
|
+
}));
|
|
1269
|
+
if (!res?.Body) {
|
|
1270
|
+
const err = new Error(`S3 GetObject returned empty body: ${ref.bucket}/${ref.key}`);
|
|
1271
|
+
err.code = "EMPTY_BODY";
|
|
1272
|
+
throw err;
|
|
1273
|
+
}
|
|
1274
|
+
return {
|
|
1275
|
+
body: res.Body,
|
|
1276
|
+
contentType: res.ContentType,
|
|
1277
|
+
contentLength: res.ContentLength,
|
|
1278
|
+
etag: res.ETag,
|
|
1279
|
+
versionId: res.VersionId,
|
|
1280
|
+
metadata: res.Metadata,
|
|
1281
|
+
lastModified: res.LastModified
|
|
1282
|
+
};
|
|
1283
|
+
}
|
|
1284
|
+
async headObject(ref) {
|
|
1285
|
+
const aws = await this.awsPromise;
|
|
1286
|
+
const s3 = await this.client();
|
|
1287
|
+
try {
|
|
1288
|
+
const res = await s3.send(new aws.HeadObjectCommand({
|
|
1289
|
+
Bucket: ref.bucket,
|
|
1290
|
+
Key: ref.key
|
|
1291
|
+
}));
|
|
1292
|
+
return {
|
|
1293
|
+
exists: true,
|
|
1294
|
+
contentType: res.ContentType,
|
|
1295
|
+
contentLength: res.ContentLength,
|
|
1296
|
+
etag: res.ETag,
|
|
1297
|
+
versionId: res.VersionId,
|
|
1298
|
+
metadata: res.Metadata,
|
|
1299
|
+
lastModified: res.LastModified
|
|
1300
|
+
};
|
|
1301
|
+
} catch (err) {
|
|
1302
|
+
const e = err;
|
|
1303
|
+
const name = String(e?.name || "");
|
|
1304
|
+
const httpStatus = e?.$metadata?.httpStatusCode;
|
|
1305
|
+
if (httpStatus === 404 || name.includes("NotFound") || name.includes("NoSuchKey")) return { exists: false };
|
|
1306
|
+
throw err;
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
async deleteObject(ref) {
|
|
1310
|
+
const aws = await this.awsPromise;
|
|
1311
|
+
const s3 = await this.client();
|
|
1312
|
+
await s3.send(new aws.DeleteObjectCommand({
|
|
1313
|
+
Bucket: ref.bucket,
|
|
1314
|
+
Key: ref.key
|
|
1315
|
+
}));
|
|
1316
|
+
}
|
|
1317
|
+
async listObjects(options) {
|
|
1318
|
+
const aws = await this.awsPromise;
|
|
1319
|
+
const s3 = await this.client();
|
|
1320
|
+
const res = await s3.send(new aws.ListObjectsV2Command({
|
|
1321
|
+
Bucket: options.bucket,
|
|
1322
|
+
Prefix: options.prefix,
|
|
1323
|
+
ContinuationToken: options.continuationToken,
|
|
1324
|
+
MaxKeys: options.maxKeys
|
|
1325
|
+
}));
|
|
1326
|
+
return {
|
|
1327
|
+
objects: (res?.Contents ?? []).map((o) => ({
|
|
1328
|
+
key: o.Key,
|
|
1329
|
+
size: o.Size,
|
|
1330
|
+
etag: o.ETag,
|
|
1331
|
+
lastModified: o.LastModified
|
|
1332
|
+
})),
|
|
1333
|
+
nextContinuationToken: res?.NextContinuationToken
|
|
1334
|
+
};
|
|
1335
|
+
}
|
|
1336
|
+
async presignGetObject(ref, options) {
|
|
1337
|
+
const aws = await this.awsPromise;
|
|
1338
|
+
const s3 = await this.client();
|
|
1339
|
+
return aws.getSignedUrl(s3, new aws.GetObjectCommand({
|
|
1340
|
+
Bucket: ref.bucket,
|
|
1341
|
+
Key: ref.key,
|
|
1342
|
+
ResponseContentType: options.responseContentType
|
|
1343
|
+
}), { expiresIn: options.expiresInSeconds });
|
|
1344
|
+
}
|
|
1345
|
+
async presignPutObject(ref, options) {
|
|
1346
|
+
const aws = await this.awsPromise;
|
|
1347
|
+
const s3 = await this.client();
|
|
1348
|
+
return aws.getSignedUrl(s3, new aws.PutObjectCommand({
|
|
1349
|
+
Bucket: ref.bucket,
|
|
1350
|
+
Key: ref.key,
|
|
1351
|
+
ContentType: options.contentType
|
|
1352
|
+
}), { expiresIn: options.expiresInSeconds });
|
|
1353
|
+
}
|
|
1354
|
+
};
|
|
1355
|
+
|
|
1356
|
+
//#endregion
|
|
1357
|
+
//#region src/integrations/object-store/in-memory.ts
|
|
1358
|
+
function toBuffer(body) {
|
|
1359
|
+
if (typeof body === "string") return Buffer.from(body);
|
|
1360
|
+
if (Buffer.isBuffer(body)) return body;
|
|
1361
|
+
if (body instanceof Uint8Array) return Buffer.from(body);
|
|
1362
|
+
return new Promise((resolve, reject) => {
|
|
1363
|
+
const chunks = [];
|
|
1364
|
+
body.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
1365
|
+
body.on("end", () => resolve(Buffer.concat(chunks)));
|
|
1366
|
+
body.on("error", reject);
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
function simpleEtag(buf) {
|
|
1370
|
+
return `mem-${buf.length}-${buf.subarray(0, 8).toString("hex")}`;
|
|
1371
|
+
}
|
|
1372
|
+
/**
|
|
1373
|
+
* InMemoryObjectStore - useful for local dev, unit tests, and as a safe default.
|
|
1374
|
+
* NOTE: presign* methods are not meaningful here and will throw.
|
|
1375
|
+
*/
|
|
1376
|
+
var InMemoryObjectStore = class {
|
|
1377
|
+
provider = "in-memory";
|
|
1378
|
+
buckets = new Map();
|
|
1379
|
+
bucketMap(bucket) {
|
|
1380
|
+
let m = this.buckets.get(bucket);
|
|
1381
|
+
if (!m) {
|
|
1382
|
+
m = new Map();
|
|
1383
|
+
this.buckets.set(bucket, m);
|
|
1384
|
+
}
|
|
1385
|
+
return m;
|
|
1386
|
+
}
|
|
1387
|
+
async putObject(ref, body, options = {}) {
|
|
1388
|
+
const buf = await toBuffer(body);
|
|
1389
|
+
const obj = {
|
|
1390
|
+
body: buf,
|
|
1391
|
+
contentType: options.contentType,
|
|
1392
|
+
cacheControl: options.cacheControl,
|
|
1393
|
+
metadata: options.metadata,
|
|
1394
|
+
etag: simpleEtag(buf),
|
|
1395
|
+
lastModified: new Date()
|
|
1396
|
+
};
|
|
1397
|
+
this.bucketMap(ref.bucket).set(ref.key, obj);
|
|
1398
|
+
return { etag: obj.etag };
|
|
1399
|
+
}
|
|
1400
|
+
async getObject(ref) {
|
|
1401
|
+
const obj = this.bucketMap(ref.bucket).get(ref.key);
|
|
1402
|
+
if (!obj) {
|
|
1403
|
+
const err = new Error(`Object not found: ${ref.bucket}/${ref.key}`);
|
|
1404
|
+
err.code = "OBJECT_NOT_FOUND";
|
|
1405
|
+
throw err;
|
|
1406
|
+
}
|
|
1407
|
+
return {
|
|
1408
|
+
body: node_stream.Readable.from(obj.body),
|
|
1409
|
+
contentType: obj.contentType,
|
|
1410
|
+
contentLength: obj.body.length,
|
|
1411
|
+
etag: obj.etag,
|
|
1412
|
+
metadata: obj.metadata,
|
|
1413
|
+
lastModified: obj.lastModified
|
|
1414
|
+
};
|
|
1415
|
+
}
|
|
1416
|
+
async headObject(ref) {
|
|
1417
|
+
const obj = this.bucketMap(ref.bucket).get(ref.key);
|
|
1418
|
+
if (!obj) return { exists: false };
|
|
1419
|
+
return {
|
|
1420
|
+
exists: true,
|
|
1421
|
+
contentType: obj.contentType,
|
|
1422
|
+
contentLength: obj.body.length,
|
|
1423
|
+
etag: obj.etag,
|
|
1424
|
+
metadata: obj.metadata,
|
|
1425
|
+
lastModified: obj.lastModified
|
|
1426
|
+
};
|
|
1427
|
+
}
|
|
1428
|
+
async deleteObject(ref) {
|
|
1429
|
+
this.bucketMap(ref.bucket).delete(ref.key);
|
|
1430
|
+
}
|
|
1431
|
+
async listObjects(options) {
|
|
1432
|
+
const { bucket, prefix = "" } = options;
|
|
1433
|
+
const map = this.bucketMap(bucket);
|
|
1434
|
+
const objects = [...map.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, obj]) => ({
|
|
1435
|
+
key,
|
|
1436
|
+
size: obj.body.length,
|
|
1437
|
+
etag: obj.etag,
|
|
1438
|
+
lastModified: obj.lastModified
|
|
1439
|
+
})).sort((a, b) => a.key.localeCompare(b.key));
|
|
1440
|
+
return { objects };
|
|
1441
|
+
}
|
|
1442
|
+
async presignGetObject(_ref, _options) {
|
|
1443
|
+
throw new Error("InMemoryObjectStore does not support presigned URLs");
|
|
1444
|
+
}
|
|
1445
|
+
async presignPutObject(_ref, _options) {
|
|
1446
|
+
throw new Error("InMemoryObjectStore does not support presigned URLs");
|
|
1447
|
+
}
|
|
1448
|
+
};
|
|
1449
|
+
|
|
854
1450
|
//#endregion
|
|
855
1451
|
exports.AutoInstrumentationConfigSchema = AutoInstrumentationConfigSchema
|
|
1452
|
+
exports.AwsS3ObjectStore = AwsS3ObjectStore
|
|
856
1453
|
exports.FeaturesConfigSchema = FeaturesConfigSchema
|
|
857
1454
|
exports.FoundationConfigSchema = FoundationConfigSchema
|
|
1455
|
+
exports.InMemoryObjectStore = InMemoryObjectStore
|
|
858
1456
|
exports.LoggingConfigSchema = LoggingConfigSchema
|
|
859
1457
|
exports.OtelConfigSchema = OtelConfigSchema
|
|
1458
|
+
exports.REDACT_PATHS = REDACT_PATHS
|
|
1459
|
+
exports.RedactionConfigSchema = RedactionConfigSchema
|
|
860
1460
|
exports.RequestLoggingConfigSchema = RequestLoggingConfigSchema
|
|
1461
|
+
exports.ShutdownConfigSchema = ShutdownConfigSchema
|
|
861
1462
|
exports.SpanStatusCode = __opentelemetry_api.SpanStatusCode
|
|
1463
|
+
exports.buildPinoRedactConfig = buildPinoRedactConfig
|
|
862
1464
|
exports.context = __opentelemetry_api.context
|
|
863
1465
|
exports.createContextManager = createContextManager
|
|
864
1466
|
exports.createFallbackLogger = createFallbackLogger
|
|
@@ -877,6 +1479,7 @@ exports.isTracingEnabled = isTracingEnabled
|
|
|
877
1479
|
exports.metrics = __opentelemetry_api.metrics
|
|
878
1480
|
exports.parseConfig = parseConfig
|
|
879
1481
|
exports.propagation = __opentelemetry_api.propagation
|
|
1482
|
+
exports.sanitizeBody = sanitizeBody
|
|
880
1483
|
exports.setGlobalLogger = setGlobalLogger
|
|
881
1484
|
exports.setupMetrics = setupMetrics
|
|
882
1485
|
exports.setupObservability = setupObservability
|