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