@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.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,
|
|
@@ -126,11 +353,14 @@ function createLogger(options) {
|
|
|
126
353
|
const traceId = spanContext?.traceId || ctx?.traceId;
|
|
127
354
|
const spanId = spanContext?.spanId || ctx?.spanId;
|
|
128
355
|
const correlationId = ctx?.correlationId;
|
|
129
|
-
|
|
356
|
+
const contextData = ctx?.contextData;
|
|
357
|
+
const result = {
|
|
130
358
|
trace_id: traceId,
|
|
131
359
|
span_id: spanId,
|
|
132
360
|
correlation_id: correlationId
|
|
133
361
|
};
|
|
362
|
+
if (contextData && Object.keys(contextData).length > 0) result.context = contextData;
|
|
363
|
+
return result;
|
|
134
364
|
},
|
|
135
365
|
formatters: { level: (label) => ({ level: label }) },
|
|
136
366
|
transport: prettyPrint ? {
|
|
@@ -191,12 +421,26 @@ function getGlobalLogger() {
|
|
|
191
421
|
//#region src/features/tracing.ts
|
|
192
422
|
let sdk = null;
|
|
193
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
|
+
}
|
|
194
437
|
/** Initialize OpenTelemetry tracing */
|
|
195
438
|
function setupTracing(options) {
|
|
196
439
|
if (isInitialized$1) {
|
|
197
440
|
console.warn("[neoiq-foundation] Tracing already initialized");
|
|
198
441
|
return;
|
|
199
442
|
}
|
|
443
|
+
warnPreloadedModules();
|
|
200
444
|
const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), autoInstrumentation = {} } = options;
|
|
201
445
|
const resource = resourceFromAttributes({
|
|
202
446
|
[ATTR_SERVICE_NAME]: serviceName,
|
|
@@ -353,7 +597,6 @@ function createObservabilityPlugin(options) {
|
|
|
353
597
|
const maxBodySize = requestLogging.maxBodySize ?? 10 * 1024;
|
|
354
598
|
const headersToRedact = requestLogging.redactHeaders ?? DEFAULT_REDACT_HEADERS;
|
|
355
599
|
const runInContext = contextManager ? (ctx, fn) => contextManager.run(ctx, fn) : (_ctx, fn) => fn();
|
|
356
|
-
const tracer = tracingEnabled ? trace$1.getTracer("neoiq-foundation-node") : null;
|
|
357
600
|
let requestCounter;
|
|
358
601
|
let requestDuration;
|
|
359
602
|
let requestErrors;
|
|
@@ -370,22 +613,12 @@ function createObservabilityPlugin(options) {
|
|
|
370
613
|
}
|
|
371
614
|
const correlationId = request.headers["x-request-id"] || randomUUID();
|
|
372
615
|
reply.header("x-request-id", correlationId);
|
|
373
|
-
let span;
|
|
374
616
|
let traceId = "";
|
|
375
617
|
let spanId = "";
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
attributes: {
|
|
381
|
-
"http.method": request.method,
|
|
382
|
-
"http.url": request.url,
|
|
383
|
-
"http.route": request.routeOptions?.url || request.url,
|
|
384
|
-
"http.user_agent": request.headers["user-agent"] || "",
|
|
385
|
-
"http.correlation_id": correlationId
|
|
386
|
-
}
|
|
387
|
-
}, parentContext);
|
|
388
|
-
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();
|
|
389
622
|
traceId = spanContext.traceId;
|
|
390
623
|
spanId = spanContext.spanId;
|
|
391
624
|
}
|
|
@@ -395,7 +628,6 @@ function createObservabilityPlugin(options) {
|
|
|
395
628
|
spanId,
|
|
396
629
|
startTime: Date.now()
|
|
397
630
|
};
|
|
398
|
-
request.__span = span;
|
|
399
631
|
request.__requestContext = requestContext;
|
|
400
632
|
runInContext(requestContext, () => {
|
|
401
633
|
const logData = {
|
|
@@ -421,7 +653,7 @@ function createObservabilityPlugin(options) {
|
|
|
421
653
|
runInContext(ctx, () => {
|
|
422
654
|
logger.debug({
|
|
423
655
|
correlation_id: ctx.correlationId,
|
|
424
|
-
body: truncateBody(request.body, maxBodySize)
|
|
656
|
+
body: sanitizeBody(truncateBody(request.body, maxBodySize))
|
|
425
657
|
}, "Request body");
|
|
426
658
|
done();
|
|
427
659
|
});
|
|
@@ -436,14 +668,13 @@ function createObservabilityPlugin(options) {
|
|
|
436
668
|
logger.debug({
|
|
437
669
|
correlation_id: ctx.correlationId,
|
|
438
670
|
statusCode: reply.statusCode,
|
|
439
|
-
body: truncateBody(payload, maxBodySize)
|
|
671
|
+
body: sanitizeBody(truncateBody(payload, maxBodySize))
|
|
440
672
|
}, "Response body");
|
|
441
673
|
done(null, payload);
|
|
442
674
|
});
|
|
443
675
|
});
|
|
444
676
|
fastify.addHook("onResponse", (request, reply, done) => {
|
|
445
677
|
const ctx = request.__requestContext;
|
|
446
|
-
const span = request.__span;
|
|
447
678
|
if (!ctx) {
|
|
448
679
|
done();
|
|
449
680
|
return;
|
|
@@ -467,18 +698,15 @@ function createObservabilityPlugin(options) {
|
|
|
467
698
|
requestDuration.record(durationMs, labels);
|
|
468
699
|
if (reply.statusCode >= 400) requestErrors.add(1, labels);
|
|
469
700
|
}
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
span.setAttribute("http.response_time_ms", durationMs);
|
|
474
|
-
span.end();
|
|
701
|
+
if (tracingEnabled) {
|
|
702
|
+
const activeSpan = trace$1.getActiveSpan();
|
|
703
|
+
if (activeSpan) activeSpan.setAttribute("http.response_time_ms", durationMs);
|
|
475
704
|
}
|
|
476
705
|
done();
|
|
477
706
|
});
|
|
478
707
|
});
|
|
479
708
|
fastify.addHook("onError", (request, _reply, error, done) => {
|
|
480
709
|
const ctx = request.__requestContext;
|
|
481
|
-
const span = request.__span;
|
|
482
710
|
if (!ctx) {
|
|
483
711
|
done();
|
|
484
712
|
return;
|
|
@@ -490,12 +718,15 @@ function createObservabilityPlugin(options) {
|
|
|
490
718
|
url: request.url,
|
|
491
719
|
error: error.message
|
|
492
720
|
}, "Request failed");
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
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
|
+
}
|
|
499
730
|
}
|
|
500
731
|
done();
|
|
501
732
|
});
|
|
@@ -625,17 +856,54 @@ function createHttpClient(options) {
|
|
|
625
856
|
breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
|
|
626
857
|
breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
|
|
627
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;
|
|
628
890
|
}
|
|
629
891
|
return client;
|
|
630
892
|
}
|
|
631
893
|
|
|
632
894
|
//#endregion
|
|
633
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
|
+
}
|
|
634
902
|
/** Create a fully configured observability foundation */
|
|
635
903
|
function createFoundation(input) {
|
|
636
904
|
const startTime = Date.now();
|
|
637
905
|
const config = parseConfig(input);
|
|
638
|
-
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;
|
|
639
907
|
const features = {
|
|
640
908
|
tracing: featuresConfig.tracing ?? true,
|
|
641
909
|
metrics: featuresConfig.metrics ?? true,
|
|
@@ -651,7 +919,8 @@ function createFoundation(input) {
|
|
|
651
919
|
environment,
|
|
652
920
|
level: loggingConfig.level ?? "info",
|
|
653
921
|
prettyPrint: loggingConfig.prettyPrint ?? environment === "development",
|
|
654
|
-
contextManager
|
|
922
|
+
contextManager,
|
|
923
|
+
additionalRedactPaths: redactionConfig.additionalPaths
|
|
655
924
|
});
|
|
656
925
|
setGlobalLogger(logger);
|
|
657
926
|
} catch (err) {
|
|
@@ -723,17 +992,137 @@ function createFoundation(input) {
|
|
|
723
992
|
environment,
|
|
724
993
|
features
|
|
725
994
|
}, "Foundation initialized");
|
|
726
|
-
|
|
727
|
-
|
|
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 = {
|
|
728
1043
|
logger,
|
|
729
|
-
context: contextManager,
|
|
730
1044
|
tracer: tracerInstance,
|
|
731
1045
|
meter: meterInstance,
|
|
732
|
-
features,
|
|
733
1046
|
getTracer: (name) => getTracer(name || serviceName),
|
|
734
1047
|
getMeter: (name, version) => getMeter(name, version),
|
|
735
1048
|
getTraceContext,
|
|
736
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,
|
|
737
1126
|
fastifyPlugin: createObservabilityPlugin({
|
|
738
1127
|
serviceName,
|
|
739
1128
|
logger,
|
|
@@ -742,91 +1131,47 @@ function createFoundation(input) {
|
|
|
742
1131
|
metricsEnabled: features.metrics && !metricsError,
|
|
743
1132
|
requestLogging: requestLoggingConfig
|
|
744
1133
|
}),
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
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();
|
|
756
1159
|
},
|
|
757
1160
|
isReady: () => {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
return true;
|
|
1161
|
+
warnDeprecation("isReady", "lifecycle.isReady");
|
|
1162
|
+
return lifecycle.isReady();
|
|
761
1163
|
},
|
|
762
1164
|
health: () => {
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const loggingUp = !loggingError;
|
|
766
|
-
const allUp = tracingUp && metricsUp && loggingUp;
|
|
767
|
-
const anyDown = features.tracing && tracingError || features.metrics && metricsError || loggingError;
|
|
768
|
-
return {
|
|
769
|
-
status: allUp ? "healthy" : anyDown ? "degraded" : "unhealthy",
|
|
770
|
-
timestamp: new Date().toISOString(),
|
|
771
|
-
service: serviceName,
|
|
772
|
-
version: serviceVersion,
|
|
773
|
-
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
774
|
-
components: {
|
|
775
|
-
tracing: {
|
|
776
|
-
enabled: features.tracing,
|
|
777
|
-
status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
|
|
778
|
-
message: tracingError
|
|
779
|
-
},
|
|
780
|
-
metrics: {
|
|
781
|
-
enabled: features.metrics,
|
|
782
|
-
status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
|
|
783
|
-
message: metricsError
|
|
784
|
-
},
|
|
785
|
-
logging: {
|
|
786
|
-
enabled: features.logging,
|
|
787
|
-
status: loggingError ? "down" : "up",
|
|
788
|
-
message: loggingError
|
|
789
|
-
}
|
|
790
|
-
}
|
|
791
|
-
};
|
|
1165
|
+
warnDeprecation("health", "lifecycle.health");
|
|
1166
|
+
return lifecycle.health();
|
|
792
1167
|
},
|
|
793
|
-
trace:
|
|
794
|
-
|
|
795
|
-
return
|
|
796
|
-
tracer.startActiveSpan(name, async (span) => {
|
|
797
|
-
try {
|
|
798
|
-
const result = await fn();
|
|
799
|
-
span.setStatus({ code: SpanStatusCode.OK });
|
|
800
|
-
span.end();
|
|
801
|
-
resolve(result);
|
|
802
|
-
} catch (err) {
|
|
803
|
-
const error = err;
|
|
804
|
-
span.setStatus({
|
|
805
|
-
code: SpanStatusCode.ERROR,
|
|
806
|
-
message: error.message
|
|
807
|
-
});
|
|
808
|
-
span.recordException(error);
|
|
809
|
-
span.end();
|
|
810
|
-
logger.error({
|
|
811
|
-
span: name,
|
|
812
|
-
error: error.message
|
|
813
|
-
}, "Span failed");
|
|
814
|
-
reject(error);
|
|
815
|
-
}
|
|
816
|
-
});
|
|
817
|
-
});
|
|
1168
|
+
trace: (name, fn) => {
|
|
1169
|
+
warnDeprecation("trace", "observability.trace");
|
|
1170
|
+
return observability.trace(name, fn);
|
|
818
1171
|
},
|
|
819
|
-
safeRun:
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
} catch (err) {
|
|
823
|
-
const error = err;
|
|
824
|
-
logger.error({
|
|
825
|
-
error: error.message,
|
|
826
|
-
stack: error.stack
|
|
827
|
-
}, "safeRun caught error");
|
|
828
|
-
return fallback;
|
|
829
|
-
}
|
|
1172
|
+
safeRun: (fn, fallback) => {
|
|
1173
|
+
warnDeprecation("safeRun", "lifecycle.safeRun");
|
|
1174
|
+
return lifecycle.safeRun(fn, fallback);
|
|
830
1175
|
}
|
|
831
1176
|
};
|
|
832
1177
|
return foundation;
|
|
@@ -835,5 +1180,254 @@ function createFoundation(input) {
|
|
|
835
1180
|
const setupObservability = createFoundation;
|
|
836
1181
|
|
|
837
1182
|
//#endregion
|
|
838
|
-
|
|
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 };
|
|
839
1433
|
//# sourceMappingURL=index.mjs.map
|