@ciq-dev/neoiq-foundation-node 1.0.1-beta.1 → 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 +719 -123
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +716 -122
- 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,
|
|
@@ -150,11 +372,14 @@ function createLogger(options) {
|
|
|
150
372
|
const traceId = spanContext?.traceId || ctx?.traceId;
|
|
151
373
|
const spanId = spanContext?.spanId || ctx?.spanId;
|
|
152
374
|
const correlationId = ctx?.correlationId;
|
|
153
|
-
|
|
375
|
+
const contextData = ctx?.contextData;
|
|
376
|
+
const result = {
|
|
154
377
|
trace_id: traceId,
|
|
155
378
|
span_id: spanId,
|
|
156
379
|
correlation_id: correlationId
|
|
157
380
|
};
|
|
381
|
+
if (contextData && Object.keys(contextData).length > 0) result.context = contextData;
|
|
382
|
+
return result;
|
|
158
383
|
},
|
|
159
384
|
formatters: { level: (label) => ({ level: label }) },
|
|
160
385
|
transport: prettyPrint ? {
|
|
@@ -215,12 +440,26 @@ function getGlobalLogger() {
|
|
|
215
440
|
//#region src/features/tracing.ts
|
|
216
441
|
let sdk = null;
|
|
217
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
|
+
}
|
|
218
456
|
/** Initialize OpenTelemetry tracing */
|
|
219
457
|
function setupTracing(options) {
|
|
220
458
|
if (isInitialized$1) {
|
|
221
459
|
console.warn("[neoiq-foundation] Tracing already initialized");
|
|
222
460
|
return;
|
|
223
461
|
}
|
|
462
|
+
warnPreloadedModules();
|
|
224
463
|
const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), autoInstrumentation = {} } = options;
|
|
225
464
|
const resource = (0, __opentelemetry_resources.resourceFromAttributes)({
|
|
226
465
|
[__opentelemetry_semantic_conventions.ATTR_SERVICE_NAME]: serviceName,
|
|
@@ -377,7 +616,6 @@ function createObservabilityPlugin(options) {
|
|
|
377
616
|
const maxBodySize = requestLogging.maxBodySize ?? 10 * 1024;
|
|
378
617
|
const headersToRedact = requestLogging.redactHeaders ?? DEFAULT_REDACT_HEADERS;
|
|
379
618
|
const runInContext = contextManager ? (ctx, fn) => contextManager.run(ctx, fn) : (_ctx, fn) => fn();
|
|
380
|
-
const tracer = tracingEnabled ? __opentelemetry_api.trace.getTracer("neoiq-foundation-node") : null;
|
|
381
619
|
let requestCounter;
|
|
382
620
|
let requestDuration;
|
|
383
621
|
let requestErrors;
|
|
@@ -394,22 +632,12 @@ function createObservabilityPlugin(options) {
|
|
|
394
632
|
}
|
|
395
633
|
const correlationId = request.headers["x-request-id"] || (0, crypto.randomUUID)();
|
|
396
634
|
reply.header("x-request-id", correlationId);
|
|
397
|
-
let span;
|
|
398
635
|
let traceId = "";
|
|
399
636
|
let spanId = "";
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
attributes: {
|
|
405
|
-
"http.method": request.method,
|
|
406
|
-
"http.url": request.url,
|
|
407
|
-
"http.route": request.routeOptions?.url || request.url,
|
|
408
|
-
"http.user_agent": request.headers["user-agent"] || "",
|
|
409
|
-
"http.correlation_id": correlationId
|
|
410
|
-
}
|
|
411
|
-
}, parentContext);
|
|
412
|
-
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();
|
|
413
641
|
traceId = spanContext.traceId;
|
|
414
642
|
spanId = spanContext.spanId;
|
|
415
643
|
}
|
|
@@ -419,7 +647,6 @@ function createObservabilityPlugin(options) {
|
|
|
419
647
|
spanId,
|
|
420
648
|
startTime: Date.now()
|
|
421
649
|
};
|
|
422
|
-
request.__span = span;
|
|
423
650
|
request.__requestContext = requestContext;
|
|
424
651
|
runInContext(requestContext, () => {
|
|
425
652
|
const logData = {
|
|
@@ -445,7 +672,7 @@ function createObservabilityPlugin(options) {
|
|
|
445
672
|
runInContext(ctx, () => {
|
|
446
673
|
logger.debug({
|
|
447
674
|
correlation_id: ctx.correlationId,
|
|
448
|
-
body: truncateBody(request.body, maxBodySize)
|
|
675
|
+
body: sanitizeBody(truncateBody(request.body, maxBodySize))
|
|
449
676
|
}, "Request body");
|
|
450
677
|
done();
|
|
451
678
|
});
|
|
@@ -460,14 +687,13 @@ function createObservabilityPlugin(options) {
|
|
|
460
687
|
logger.debug({
|
|
461
688
|
correlation_id: ctx.correlationId,
|
|
462
689
|
statusCode: reply.statusCode,
|
|
463
|
-
body: truncateBody(payload, maxBodySize)
|
|
690
|
+
body: sanitizeBody(truncateBody(payload, maxBodySize))
|
|
464
691
|
}, "Response body");
|
|
465
692
|
done(null, payload);
|
|
466
693
|
});
|
|
467
694
|
});
|
|
468
695
|
fastify.addHook("onResponse", (request, reply, done) => {
|
|
469
696
|
const ctx = request.__requestContext;
|
|
470
|
-
const span = request.__span;
|
|
471
697
|
if (!ctx) {
|
|
472
698
|
done();
|
|
473
699
|
return;
|
|
@@ -491,18 +717,15 @@ function createObservabilityPlugin(options) {
|
|
|
491
717
|
requestDuration.record(durationMs, labels);
|
|
492
718
|
if (reply.statusCode >= 400) requestErrors.add(1, labels);
|
|
493
719
|
}
|
|
494
|
-
if (
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
span.setAttribute("http.response_time_ms", durationMs);
|
|
498
|
-
span.end();
|
|
720
|
+
if (tracingEnabled) {
|
|
721
|
+
const activeSpan = __opentelemetry_api.trace.getActiveSpan();
|
|
722
|
+
if (activeSpan) activeSpan.setAttribute("http.response_time_ms", durationMs);
|
|
499
723
|
}
|
|
500
724
|
done();
|
|
501
725
|
});
|
|
502
726
|
});
|
|
503
727
|
fastify.addHook("onError", (request, _reply, error, done) => {
|
|
504
728
|
const ctx = request.__requestContext;
|
|
505
|
-
const span = request.__span;
|
|
506
729
|
if (!ctx) {
|
|
507
730
|
done();
|
|
508
731
|
return;
|
|
@@ -514,12 +737,15 @@ function createObservabilityPlugin(options) {
|
|
|
514
737
|
url: request.url,
|
|
515
738
|
error: error.message
|
|
516
739
|
}, "Request failed");
|
|
517
|
-
if (
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
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
|
+
}
|
|
523
749
|
}
|
|
524
750
|
done();
|
|
525
751
|
});
|
|
@@ -649,17 +875,54 @@ function createHttpClient(options) {
|
|
|
649
875
|
breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
|
|
650
876
|
breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
|
|
651
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;
|
|
652
909
|
}
|
|
653
910
|
return client;
|
|
654
911
|
}
|
|
655
912
|
|
|
656
913
|
//#endregion
|
|
657
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
|
+
}
|
|
658
921
|
/** Create a fully configured observability foundation */
|
|
659
922
|
function createFoundation(input) {
|
|
660
923
|
const startTime = Date.now();
|
|
661
924
|
const config = parseConfig(input);
|
|
662
|
-
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;
|
|
663
926
|
const features = {
|
|
664
927
|
tracing: featuresConfig.tracing ?? true,
|
|
665
928
|
metrics: featuresConfig.metrics ?? true,
|
|
@@ -675,7 +938,8 @@ function createFoundation(input) {
|
|
|
675
938
|
environment,
|
|
676
939
|
level: loggingConfig.level ?? "info",
|
|
677
940
|
prettyPrint: loggingConfig.prettyPrint ?? environment === "development",
|
|
678
|
-
contextManager
|
|
941
|
+
contextManager,
|
|
942
|
+
additionalRedactPaths: redactionConfig.additionalPaths
|
|
679
943
|
});
|
|
680
944
|
setGlobalLogger(logger);
|
|
681
945
|
} catch (err) {
|
|
@@ -747,17 +1011,137 @@ function createFoundation(input) {
|
|
|
747
1011
|
environment,
|
|
748
1012
|
features
|
|
749
1013
|
}, "Foundation initialized");
|
|
750
|
-
|
|
751
|
-
|
|
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 = {
|
|
752
1062
|
logger,
|
|
753
|
-
context: contextManager,
|
|
754
1063
|
tracer: tracerInstance,
|
|
755
1064
|
meter: meterInstance,
|
|
756
|
-
features,
|
|
757
1065
|
getTracer: (name) => getTracer(name || serviceName),
|
|
758
1066
|
getMeter: (name, version) => getMeter(name, version),
|
|
759
1067
|
getTraceContext,
|
|
760
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,
|
|
761
1145
|
fastifyPlugin: createObservabilityPlugin({
|
|
762
1146
|
serviceName,
|
|
763
1147
|
logger,
|
|
@@ -766,91 +1150,47 @@ function createFoundation(input) {
|
|
|
766
1150
|
metricsEnabled: features.metrics && !metricsError,
|
|
767
1151
|
requestLogging: requestLoggingConfig
|
|
768
1152
|
}),
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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();
|
|
780
1178
|
},
|
|
781
1179
|
isReady: () => {
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
return true;
|
|
1180
|
+
warnDeprecation("isReady", "lifecycle.isReady");
|
|
1181
|
+
return lifecycle.isReady();
|
|
785
1182
|
},
|
|
786
1183
|
health: () => {
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
const loggingUp = !loggingError;
|
|
790
|
-
const allUp = tracingUp && metricsUp && loggingUp;
|
|
791
|
-
const anyDown = features.tracing && tracingError || features.metrics && metricsError || loggingError;
|
|
792
|
-
return {
|
|
793
|
-
status: allUp ? "healthy" : anyDown ? "degraded" : "unhealthy",
|
|
794
|
-
timestamp: new Date().toISOString(),
|
|
795
|
-
service: serviceName,
|
|
796
|
-
version: serviceVersion,
|
|
797
|
-
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
798
|
-
components: {
|
|
799
|
-
tracing: {
|
|
800
|
-
enabled: features.tracing,
|
|
801
|
-
status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
|
|
802
|
-
message: tracingError
|
|
803
|
-
},
|
|
804
|
-
metrics: {
|
|
805
|
-
enabled: features.metrics,
|
|
806
|
-
status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
|
|
807
|
-
message: metricsError
|
|
808
|
-
},
|
|
809
|
-
logging: {
|
|
810
|
-
enabled: features.logging,
|
|
811
|
-
status: loggingError ? "down" : "up",
|
|
812
|
-
message: loggingError
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
};
|
|
1184
|
+
warnDeprecation("health", "lifecycle.health");
|
|
1185
|
+
return lifecycle.health();
|
|
816
1186
|
},
|
|
817
|
-
trace:
|
|
818
|
-
|
|
819
|
-
return
|
|
820
|
-
tracer.startActiveSpan(name, async (span) => {
|
|
821
|
-
try {
|
|
822
|
-
const result = await fn();
|
|
823
|
-
span.setStatus({ code: __opentelemetry_api.SpanStatusCode.OK });
|
|
824
|
-
span.end();
|
|
825
|
-
resolve(result);
|
|
826
|
-
} catch (err) {
|
|
827
|
-
const error = err;
|
|
828
|
-
span.setStatus({
|
|
829
|
-
code: __opentelemetry_api.SpanStatusCode.ERROR,
|
|
830
|
-
message: error.message
|
|
831
|
-
});
|
|
832
|
-
span.recordException(error);
|
|
833
|
-
span.end();
|
|
834
|
-
logger.error({
|
|
835
|
-
span: name,
|
|
836
|
-
error: error.message
|
|
837
|
-
}, "Span failed");
|
|
838
|
-
reject(error);
|
|
839
|
-
}
|
|
840
|
-
});
|
|
841
|
-
});
|
|
1187
|
+
trace: (name, fn) => {
|
|
1188
|
+
warnDeprecation("trace", "observability.trace");
|
|
1189
|
+
return observability.trace(name, fn);
|
|
842
1190
|
},
|
|
843
|
-
safeRun:
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
} catch (err) {
|
|
847
|
-
const error = err;
|
|
848
|
-
logger.error({
|
|
849
|
-
error: error.message,
|
|
850
|
-
stack: error.stack
|
|
851
|
-
}, "safeRun caught error");
|
|
852
|
-
return fallback;
|
|
853
|
-
}
|
|
1191
|
+
safeRun: (fn, fallback) => {
|
|
1192
|
+
warnDeprecation("safeRun", "lifecycle.safeRun");
|
|
1193
|
+
return lifecycle.safeRun(fn, fallback);
|
|
854
1194
|
}
|
|
855
1195
|
};
|
|
856
1196
|
return foundation;
|
|
@@ -858,14 +1198,269 @@ function createFoundation(input) {
|
|
|
858
1198
|
/** Alias for createFoundation */
|
|
859
1199
|
const setupObservability = createFoundation;
|
|
860
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
|
+
|
|
861
1450
|
//#endregion
|
|
862
1451
|
exports.AutoInstrumentationConfigSchema = AutoInstrumentationConfigSchema
|
|
1452
|
+
exports.AwsS3ObjectStore = AwsS3ObjectStore
|
|
863
1453
|
exports.FeaturesConfigSchema = FeaturesConfigSchema
|
|
864
1454
|
exports.FoundationConfigSchema = FoundationConfigSchema
|
|
1455
|
+
exports.InMemoryObjectStore = InMemoryObjectStore
|
|
865
1456
|
exports.LoggingConfigSchema = LoggingConfigSchema
|
|
866
1457
|
exports.OtelConfigSchema = OtelConfigSchema
|
|
1458
|
+
exports.REDACT_PATHS = REDACT_PATHS
|
|
1459
|
+
exports.RedactionConfigSchema = RedactionConfigSchema
|
|
867
1460
|
exports.RequestLoggingConfigSchema = RequestLoggingConfigSchema
|
|
1461
|
+
exports.ShutdownConfigSchema = ShutdownConfigSchema
|
|
868
1462
|
exports.SpanStatusCode = __opentelemetry_api.SpanStatusCode
|
|
1463
|
+
exports.buildPinoRedactConfig = buildPinoRedactConfig
|
|
869
1464
|
exports.context = __opentelemetry_api.context
|
|
870
1465
|
exports.createContextManager = createContextManager
|
|
871
1466
|
exports.createFallbackLogger = createFallbackLogger
|
|
@@ -884,6 +1479,7 @@ exports.isTracingEnabled = isTracingEnabled
|
|
|
884
1479
|
exports.metrics = __opentelemetry_api.metrics
|
|
885
1480
|
exports.parseConfig = parseConfig
|
|
886
1481
|
exports.propagation = __opentelemetry_api.propagation
|
|
1482
|
+
exports.sanitizeBody = sanitizeBody
|
|
887
1483
|
exports.setGlobalLogger = setGlobalLogger
|
|
888
1484
|
exports.setupMetrics = setupMetrics
|
|
889
1485
|
exports.setupObservability = setupObservability
|