@ciq-dev/neoiq-foundation-node 1.0.1-beta.1 → 1.0.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +40 -1
- package/dist/bootstrap.d.mts +1 -0
- package/dist/bootstrap.d.ts +1 -0
- package/dist/bootstrap.js +31 -0
- package/dist/bootstrap.js.map +1 -0
- package/dist/bootstrap.mjs +30 -0
- package/dist/bootstrap.mjs.map +1 -0
- package/dist/index.d.mts +332 -54
- package/dist/index.d.mts.map +1 -1
- package/dist/index.d.ts +332 -54
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +806 -356
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +768 -302
- package/dist/index.mjs.map +1 -1
- package/dist/tracing-Cv-Y3fZx.mjs +196 -0
- package/dist/tracing-Cv-Y3fZx.mjs.map +1 -0
- package/dist/tracing-DM5OFo7l.js +316 -0
- package/dist/tracing-DM5OFo7l.js.map +1 -0
- package/package.json +24 -3
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { AsyncLocalStorage } from "async_hooks";
|
|
3
|
-
import pino from "pino";
|
|
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 { NodeSDK } from "@opentelemetry/sdk-node";
|
|
6
|
-
import { getNodeAutoInstrumentations } from "@opentelemetry/auto-instrumentations-node";
|
|
7
|
-
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-grpc";
|
|
1
|
+
import { AutoInstrumentationConfigSchema, FeaturesConfigSchema, FoundationConfigSchema, LoggingConfigSchema, OtelConfigSchema, RedactionConfigSchema, RequestLoggingConfigSchema, ShutdownConfigSchema, SpanStatusCode, context, getActiveSpan, getDefaultOtelEndpoint, getTraceContext, getTracer, isTracingEnabled, parseConfig, propagation, setupTracing, shutdownTracing, trace } from "./tracing-Cv-Y3fZx.mjs";
|
|
8
2
|
import { resourceFromAttributes } from "@opentelemetry/resources";
|
|
9
3
|
import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
|
|
4
|
+
import { SpanStatusCode as SpanStatusCode$1, context as context$1, metrics, propagation as propagation$1, trace as trace$1 } from "@opentelemetry/api";
|
|
5
|
+
import { AsyncLocalStorage } from "async_hooks";
|
|
6
|
+
import pino from "pino";
|
|
10
7
|
import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
|
|
11
8
|
import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
|
|
12
9
|
import fp from "fastify-plugin";
|
|
@@ -14,106 +11,251 @@ import { randomUUID } from "crypto";
|
|
|
14
11
|
import axios from "axios";
|
|
15
12
|
import axiosRetry from "axios-retry";
|
|
16
13
|
import CircuitBreaker from "opossum";
|
|
14
|
+
import { createHash } from "node:crypto";
|
|
15
|
+
import { Readable } from "node:stream";
|
|
17
16
|
|
|
18
|
-
//#region src/
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
pg: z.boolean().default(true),
|
|
25
|
-
mysql: z.boolean().default(true),
|
|
26
|
-
redis: z.boolean().default(true),
|
|
27
|
-
ioredis: z.boolean().default(true),
|
|
28
|
-
grpc: z.boolean().default(true),
|
|
29
|
-
fs: z.boolean().default(false),
|
|
30
|
-
dns: z.boolean().default(false)
|
|
31
|
-
}).partial();
|
|
32
|
-
const FeaturesConfigSchema = z.object({
|
|
33
|
-
tracing: z.boolean().default(true),
|
|
34
|
-
metrics: z.boolean().default(true),
|
|
35
|
-
logging: z.boolean().default(true),
|
|
36
|
-
autoInstrumentation: AutoInstrumentationConfigSchema.default({})
|
|
37
|
-
}).partial();
|
|
38
|
-
const DEFAULT_OTEL_ENDPOINT = "http://otel-stack-deployment-collector.observability.svc.cluster.local:4317";
|
|
39
|
-
const OtelConfigSchema = z.object({
|
|
40
|
-
endpoint: z.string().default(DEFAULT_OTEL_ENDPOINT),
|
|
41
|
-
metricsIntervalMs: z.number().min(1e3).default(5e3),
|
|
42
|
-
traceSampleRate: z.number().min(0).max(1).default(1)
|
|
43
|
-
}).partial();
|
|
44
|
-
const LoggingConfigSchema = z.object({
|
|
45
|
-
level: z.enum([
|
|
46
|
-
"debug",
|
|
47
|
-
"info",
|
|
48
|
-
"warn",
|
|
49
|
-
"error"
|
|
50
|
-
]).default("info"),
|
|
51
|
-
prettyPrint: z.boolean().optional()
|
|
52
|
-
}).partial();
|
|
53
|
-
const RequestLoggingConfigSchema = z.object({
|
|
54
|
-
logHeaders: z.boolean().default(true),
|
|
55
|
-
logBody: z.boolean().default(false),
|
|
56
|
-
logResponseBody: z.boolean().default(false),
|
|
57
|
-
maxBodySize: z.number().default(10 * 1024),
|
|
58
|
-
redactHeaders: z.array(z.string()).optional()
|
|
59
|
-
}).partial();
|
|
60
|
-
const FoundationConfigSchema = z.object({
|
|
61
|
-
serviceName: z.string().min(1, "serviceName is required"),
|
|
62
|
-
serviceVersion: z.string().default(process.env.SERVICE_VERSION || "1.0.0"),
|
|
63
|
-
environment: z.enum([
|
|
64
|
-
"development",
|
|
65
|
-
"staging",
|
|
66
|
-
"qa",
|
|
67
|
-
"production"
|
|
68
|
-
]).default(process.env.NODE_ENV || "development"),
|
|
69
|
-
features: FeaturesConfigSchema.default({}),
|
|
70
|
-
otel: OtelConfigSchema.default({}),
|
|
71
|
-
logging: LoggingConfigSchema.default({}),
|
|
72
|
-
requestLogging: RequestLoggingConfigSchema.default({})
|
|
73
|
-
});
|
|
74
|
-
/** Parse and validate configuration */
|
|
75
|
-
function parseConfig(input) {
|
|
76
|
-
const result = FoundationConfigSchema.safeParse(input);
|
|
77
|
-
if (!result.success) {
|
|
78
|
-
const errors = result.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
|
|
79
|
-
throw new Error(`Invalid foundation configuration:\n${errors}`);
|
|
80
|
-
}
|
|
81
|
-
return result.data;
|
|
17
|
+
//#region src/features/context.ts
|
|
18
|
+
const BAGGAGE_CORRELATION_KEY = "correlation.id";
|
|
19
|
+
function setBaggageCorrelationId(correlationId) {
|
|
20
|
+
const currentBaggage = propagation$1.getBaggage(context$1.active());
|
|
21
|
+
const baggage = (currentBaggage ?? propagation$1.createBaggage()).setEntry(BAGGAGE_CORRELATION_KEY, { value: correlationId });
|
|
22
|
+
return propagation$1.setBaggage(context$1.active(), baggage);
|
|
82
23
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
return
|
|
24
|
+
function getBaggageCorrelationId() {
|
|
25
|
+
const baggage = propagation$1.getBaggage(context$1.active());
|
|
26
|
+
return baggage?.getEntry(BAGGAGE_CORRELATION_KEY)?.value;
|
|
86
27
|
}
|
|
87
|
-
|
|
88
|
-
//#endregion
|
|
89
|
-
//#region src/features/context.ts
|
|
90
28
|
/** Create a new context manager instance */
|
|
91
29
|
function createContextManager() {
|
|
92
30
|
const als = new AsyncLocalStorage();
|
|
93
31
|
return {
|
|
94
|
-
getContext
|
|
32
|
+
getContext() {
|
|
33
|
+
const alsCtx = als.getStore();
|
|
34
|
+
if (!alsCtx) return void 0;
|
|
35
|
+
const baggageCorrelationId = getBaggageCorrelationId();
|
|
36
|
+
if (baggageCorrelationId && baggageCorrelationId !== alsCtx.correlationId) return {
|
|
37
|
+
...alsCtx,
|
|
38
|
+
correlationId: baggageCorrelationId
|
|
39
|
+
};
|
|
40
|
+
return alsCtx;
|
|
41
|
+
},
|
|
95
42
|
run(context$2, fn) {
|
|
43
|
+
if (context$2.correlationId) {
|
|
44
|
+
const otelCtx = setBaggageCorrelationId(context$2.correlationId);
|
|
45
|
+
return context$1.with(otelCtx, () => als.run(context$2, fn));
|
|
46
|
+
}
|
|
96
47
|
return als.run(context$2, fn);
|
|
97
48
|
},
|
|
98
49
|
get(key) {
|
|
50
|
+
if (key === "correlationId") return getBaggageCorrelationId() ?? als.getStore()?.correlationId;
|
|
99
51
|
return als.getStore()?.[key];
|
|
100
52
|
},
|
|
101
53
|
update(updates) {
|
|
102
54
|
const current = als.getStore();
|
|
103
55
|
if (!current) return void 0;
|
|
56
|
+
if (updates.correlationId) setBaggageCorrelationId(updates.correlationId);
|
|
104
57
|
Object.assign(current, updates);
|
|
105
58
|
return current;
|
|
59
|
+
},
|
|
60
|
+
setContextValue(key, value) {
|
|
61
|
+
const current = als.getStore();
|
|
62
|
+
if (!current) return;
|
|
63
|
+
if (!current.contextData) current.contextData = {};
|
|
64
|
+
current.contextData[key] = value;
|
|
65
|
+
},
|
|
66
|
+
getContextValue(key) {
|
|
67
|
+
return als.getStore()?.contextData?.[key];
|
|
68
|
+
},
|
|
69
|
+
getContextData() {
|
|
70
|
+
return als.getStore()?.contextData ?? {};
|
|
71
|
+
},
|
|
72
|
+
setContextData(data) {
|
|
73
|
+
const current = als.getStore();
|
|
74
|
+
if (!current) return;
|
|
75
|
+
current.contextData = {
|
|
76
|
+
...current.contextData ?? {},
|
|
77
|
+
...data
|
|
78
|
+
};
|
|
79
|
+
},
|
|
80
|
+
clearContextData() {
|
|
81
|
+
const current = als.getStore();
|
|
82
|
+
if (!current) return;
|
|
83
|
+
current.contextData = {};
|
|
106
84
|
}
|
|
107
85
|
};
|
|
108
86
|
}
|
|
109
87
|
|
|
88
|
+
//#endregion
|
|
89
|
+
//#region src/features/redaction.ts
|
|
90
|
+
/**
|
|
91
|
+
* Log Redaction & PII Sanitization
|
|
92
|
+
*
|
|
93
|
+
* Two-layer approach:
|
|
94
|
+
* - Layer A: Pino native `redact` (fast-redact) for key-based redaction on every log call.
|
|
95
|
+
* Compiled at init, near-zero runtime cost.
|
|
96
|
+
* - Layer B: Deep-traverse sanitizer for value-pattern detection (JWTs, AWS keys, etc.).
|
|
97
|
+
* Only used for request/response body logging — NOT on every log call.
|
|
98
|
+
*/
|
|
99
|
+
const PLACEHOLDER = "[REDACTED]";
|
|
100
|
+
/**
|
|
101
|
+
* Key names that Pino's fast-redact will censor automatically.
|
|
102
|
+
* Supports wildcards: '*.password' matches nested keys one level deep.
|
|
103
|
+
*/
|
|
104
|
+
const REDACT_PATHS = [
|
|
105
|
+
"password",
|
|
106
|
+
"passwd",
|
|
107
|
+
"pass",
|
|
108
|
+
"pwd",
|
|
109
|
+
"secret",
|
|
110
|
+
"secretKey",
|
|
111
|
+
"secret_key",
|
|
112
|
+
"token",
|
|
113
|
+
"accessToken",
|
|
114
|
+
"access_token",
|
|
115
|
+
"refreshToken",
|
|
116
|
+
"refresh_token",
|
|
117
|
+
"idToken",
|
|
118
|
+
"id_token",
|
|
119
|
+
"apiKey",
|
|
120
|
+
"api_key",
|
|
121
|
+
"apiSecret",
|
|
122
|
+
"api_secret",
|
|
123
|
+
"authorization",
|
|
124
|
+
"auth",
|
|
125
|
+
"credentials",
|
|
126
|
+
"privateKey",
|
|
127
|
+
"private_key",
|
|
128
|
+
"cookie",
|
|
129
|
+
"setCookie",
|
|
130
|
+
"set_cookie",
|
|
131
|
+
"creditCard",
|
|
132
|
+
"credit_card",
|
|
133
|
+
"cardNumber",
|
|
134
|
+
"card_number",
|
|
135
|
+
"ccNumber",
|
|
136
|
+
"cc_number",
|
|
137
|
+
"cvv",
|
|
138
|
+
"cvc",
|
|
139
|
+
"securityCode",
|
|
140
|
+
"security_code",
|
|
141
|
+
"accountNumber",
|
|
142
|
+
"account_number",
|
|
143
|
+
"ssn",
|
|
144
|
+
"socialSecurity",
|
|
145
|
+
"social_security",
|
|
146
|
+
"dateOfBirth",
|
|
147
|
+
"date_of_birth",
|
|
148
|
+
"dob",
|
|
149
|
+
"*.password",
|
|
150
|
+
"*.secret",
|
|
151
|
+
"*.token",
|
|
152
|
+
"*.apiKey",
|
|
153
|
+
"*.api_key",
|
|
154
|
+
"*.authorization",
|
|
155
|
+
"*.cookie",
|
|
156
|
+
"*.credentials",
|
|
157
|
+
"*.creditCard",
|
|
158
|
+
"*.cardNumber",
|
|
159
|
+
"*.cvv",
|
|
160
|
+
"*.ssn",
|
|
161
|
+
"*.privateKey",
|
|
162
|
+
"*.private_key"
|
|
163
|
+
];
|
|
164
|
+
/**
|
|
165
|
+
* Value patterns that indicate a secret regardless of the key name.
|
|
166
|
+
* Used only for body sanitization (Layer B).
|
|
167
|
+
*/
|
|
168
|
+
const VALUE_PATTERNS = [
|
|
169
|
+
{
|
|
170
|
+
label: "jwt",
|
|
171
|
+
pattern: /^eyJ[A-Za-z0-9_-]{10,}\.eyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]+$/
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
label: "aws_access_key",
|
|
175
|
+
pattern: /(?:^|[^A-Za-z0-9])AKIA[0-9A-Z]{16}(?:$|[^A-Za-z0-9])/
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
label: "stripe_key",
|
|
179
|
+
pattern: /^[sr]k_(live|test)_[A-Za-z0-9]{10,}$/
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
label: "openai_key",
|
|
183
|
+
pattern: /^sk-[A-Za-z0-9_-]{20,}$/
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
label: "github_token",
|
|
187
|
+
pattern: /^gh[ps]_[A-Za-z0-9]{36,}$/
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
label: "pem_private_key",
|
|
191
|
+
pattern: /-----BEGIN\s+(RSA\s+|EC\s+)?PRIVATE\s+KEY-----/
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
label: "connection_string",
|
|
195
|
+
pattern: /^[a-zA-Z][a-zA-Z0-9+.-]*:\/\/[^:]+:[^@]+@/
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
label: "bearer_token",
|
|
199
|
+
pattern: /^Bearer\s+\S{10,}$/i
|
|
200
|
+
}
|
|
201
|
+
];
|
|
202
|
+
const SENSITIVE_KEYS = new Set(REDACT_PATHS.filter((p) => !p.includes("*")).map((k) => k.toLowerCase()));
|
|
203
|
+
const MAX_DEPTH = 10;
|
|
204
|
+
const MAX_KEYS = 200;
|
|
205
|
+
function mask(value) {
|
|
206
|
+
if (typeof value !== "string" || value.length <= 8) return PLACEHOLDER;
|
|
207
|
+
return `${value.slice(0, 4)}${"*".repeat(Math.min(value.length - 8, 20))}${value.slice(-4)}`;
|
|
208
|
+
}
|
|
209
|
+
function isSensitiveValue(value) {
|
|
210
|
+
return VALUE_PATTERNS.some((p) => p.pattern.test(value));
|
|
211
|
+
}
|
|
212
|
+
function sanitizeValue(value, depth) {
|
|
213
|
+
if (depth > MAX_DEPTH) return value;
|
|
214
|
+
if (typeof value === "string" && isSensitiveValue(value)) return mask(value);
|
|
215
|
+
if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, depth + 1));
|
|
216
|
+
if (value !== null && typeof value === "object") return sanitizeObject(value, depth + 1);
|
|
217
|
+
return value;
|
|
218
|
+
}
|
|
219
|
+
function sanitizeObject(obj, depth) {
|
|
220
|
+
if (depth > MAX_DEPTH) return obj;
|
|
221
|
+
const keys = Object.keys(obj);
|
|
222
|
+
if (keys.length > MAX_KEYS) return obj;
|
|
223
|
+
const result = {};
|
|
224
|
+
for (const key of keys) {
|
|
225
|
+
const value = obj[key];
|
|
226
|
+
if (SENSITIVE_KEYS.has(key.toLowerCase())) result[key] = typeof value === "string" ? mask(value) : PLACEHOLDER;
|
|
227
|
+
else result[key] = sanitizeValue(value, depth);
|
|
228
|
+
}
|
|
229
|
+
return result;
|
|
230
|
+
}
|
|
231
|
+
/**
|
|
232
|
+
* Deep-traverse sanitizer for request/response bodies.
|
|
233
|
+
* Checks both key names (deny-list) and value patterns (JWT, AWS keys, etc.).
|
|
234
|
+
* NOT intended for every log call — use Pino native `redact` for that.
|
|
235
|
+
*/
|
|
236
|
+
function sanitizeBody(body) {
|
|
237
|
+
if (body === null || body === void 0) return body;
|
|
238
|
+
if (typeof body === "string") return isSensitiveValue(body) ? mask(body) : body;
|
|
239
|
+
if (typeof body !== "object") return body;
|
|
240
|
+
if (Array.isArray(body)) return body.map((item) => sanitizeValue(item, 0));
|
|
241
|
+
return sanitizeObject(body, 0);
|
|
242
|
+
}
|
|
243
|
+
/** Build the Pino `redact` config object for fast-redact integration */
|
|
244
|
+
function buildPinoRedactConfig(additionalPaths = []) {
|
|
245
|
+
return {
|
|
246
|
+
paths: [...REDACT_PATHS, ...additionalPaths],
|
|
247
|
+
censor: PLACEHOLDER
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
110
251
|
//#endregion
|
|
111
252
|
//#region src/features/logging.ts
|
|
112
253
|
/** Create a structured logger with automatic trace context injection */
|
|
113
254
|
function createLogger(options) {
|
|
114
|
-
const { serviceName, serviceVersion, environment, level, prettyPrint, contextManager } = options;
|
|
255
|
+
const { serviceName, serviceVersion, environment, level, prettyPrint, contextManager, additionalRedactPaths = [] } = options;
|
|
115
256
|
const pinoLogger = pino({
|
|
116
257
|
level,
|
|
258
|
+
redact: buildPinoRedactConfig(additionalRedactPaths),
|
|
117
259
|
base: {
|
|
118
260
|
service: serviceName,
|
|
119
261
|
version: serviceVersion,
|
|
@@ -126,11 +268,14 @@ function createLogger(options) {
|
|
|
126
268
|
const traceId = spanContext?.traceId || ctx?.traceId;
|
|
127
269
|
const spanId = spanContext?.spanId || ctx?.spanId;
|
|
128
270
|
const correlationId = ctx?.correlationId;
|
|
129
|
-
|
|
271
|
+
const contextData = ctx?.contextData;
|
|
272
|
+
const result = {
|
|
130
273
|
trace_id: traceId,
|
|
131
274
|
span_id: spanId,
|
|
132
275
|
correlation_id: correlationId
|
|
133
276
|
};
|
|
277
|
+
if (contextData && Object.keys(contextData).length > 0) result.context = contextData;
|
|
278
|
+
return result;
|
|
134
279
|
},
|
|
135
280
|
formatters: { level: (label) => ({ level: label }) },
|
|
136
281
|
transport: prettyPrint ? {
|
|
@@ -155,12 +300,13 @@ function wrapPinoLogger(pinoLogger) {
|
|
|
155
300
|
};
|
|
156
301
|
}
|
|
157
302
|
/** Fallback logger when Pino is not available */
|
|
158
|
-
function createFallbackLogger(serviceName = "unknown") {
|
|
303
|
+
function createFallbackLogger(serviceName = "unknown", baseBindings = {}) {
|
|
159
304
|
const log = (level, obj, msg) => {
|
|
160
305
|
const logObj = {
|
|
161
306
|
timestamp: new Date().toISOString(),
|
|
162
307
|
level,
|
|
163
308
|
service: serviceName,
|
|
309
|
+
...baseBindings,
|
|
164
310
|
...obj,
|
|
165
311
|
msg
|
|
166
312
|
};
|
|
@@ -174,7 +320,10 @@ function createFallbackLogger(serviceName = "unknown") {
|
|
|
174
320
|
info: (obj, msg) => log("info", obj, msg),
|
|
175
321
|
warn: (obj, msg) => log("warn", obj, msg),
|
|
176
322
|
error: (obj, msg) => log("error", obj, msg),
|
|
177
|
-
child: () =>
|
|
323
|
+
child: (bindings) => createFallbackLogger(serviceName, {
|
|
324
|
+
...baseBindings,
|
|
325
|
+
...bindings
|
|
326
|
+
}),
|
|
178
327
|
pino: null
|
|
179
328
|
};
|
|
180
329
|
return fallback;
|
|
@@ -187,84 +336,6 @@ function getGlobalLogger() {
|
|
|
187
336
|
return globalLogger || createFallbackLogger();
|
|
188
337
|
}
|
|
189
338
|
|
|
190
|
-
//#endregion
|
|
191
|
-
//#region src/features/tracing.ts
|
|
192
|
-
let sdk = null;
|
|
193
|
-
let isInitialized$1 = false;
|
|
194
|
-
/** Initialize OpenTelemetry tracing */
|
|
195
|
-
function setupTracing(options) {
|
|
196
|
-
if (isInitialized$1) {
|
|
197
|
-
console.warn("[neoiq-foundation] Tracing already initialized");
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), autoInstrumentation = {} } = options;
|
|
201
|
-
const resource = resourceFromAttributes({
|
|
202
|
-
[ATTR_SERVICE_NAME]: serviceName,
|
|
203
|
-
[ATTR_SERVICE_VERSION]: serviceVersion,
|
|
204
|
-
"deployment.environment": environment
|
|
205
|
-
});
|
|
206
|
-
const traceExporter = new OTLPTraceExporter({ url: endpoint });
|
|
207
|
-
const instrumentationConfig = buildInstrumentationConfig(autoInstrumentation);
|
|
208
|
-
sdk = new NodeSDK({
|
|
209
|
-
resource,
|
|
210
|
-
traceExporter,
|
|
211
|
-
instrumentations: [getNodeAutoInstrumentations(instrumentationConfig)]
|
|
212
|
-
});
|
|
213
|
-
sdk.start();
|
|
214
|
-
isInitialized$1 = true;
|
|
215
|
-
}
|
|
216
|
-
function buildInstrumentationConfig(config) {
|
|
217
|
-
const mapping = {
|
|
218
|
-
http: "@opentelemetry/instrumentation-http",
|
|
219
|
-
fastify: "@opentelemetry/instrumentation-fastify",
|
|
220
|
-
express: "@opentelemetry/instrumentation-express",
|
|
221
|
-
mongodb: "@opentelemetry/instrumentation-mongodb",
|
|
222
|
-
pg: "@opentelemetry/instrumentation-pg",
|
|
223
|
-
mysql: "@opentelemetry/instrumentation-mysql",
|
|
224
|
-
redis: "@opentelemetry/instrumentation-redis",
|
|
225
|
-
ioredis: "@opentelemetry/instrumentation-ioredis",
|
|
226
|
-
grpc: "@opentelemetry/instrumentation-grpc",
|
|
227
|
-
fs: "@opentelemetry/instrumentation-fs",
|
|
228
|
-
dns: "@opentelemetry/instrumentation-dns"
|
|
229
|
-
};
|
|
230
|
-
const result = {};
|
|
231
|
-
for (const [key, instrumentationName] of Object.entries(mapping)) {
|
|
232
|
-
const userSetting = config[key];
|
|
233
|
-
const defaultValue = key !== "fs" && key !== "dns";
|
|
234
|
-
result[instrumentationName] = { enabled: userSetting ?? defaultValue };
|
|
235
|
-
}
|
|
236
|
-
return result;
|
|
237
|
-
}
|
|
238
|
-
/** Shutdown tracing gracefully */
|
|
239
|
-
async function shutdownTracing() {
|
|
240
|
-
if (!sdk) return;
|
|
241
|
-
try {
|
|
242
|
-
await sdk.shutdown();
|
|
243
|
-
isInitialized$1 = false;
|
|
244
|
-
sdk = null;
|
|
245
|
-
} catch (error) {
|
|
246
|
-
console.error("[neoiq-foundation] Error shutting down tracing:", error);
|
|
247
|
-
}
|
|
248
|
-
}
|
|
249
|
-
function getTracer(name) {
|
|
250
|
-
return trace.getTracer(name);
|
|
251
|
-
}
|
|
252
|
-
function getActiveSpan() {
|
|
253
|
-
return trace.getActiveSpan();
|
|
254
|
-
}
|
|
255
|
-
function getTraceContext() {
|
|
256
|
-
const span = trace.getActiveSpan();
|
|
257
|
-
if (!span) return {};
|
|
258
|
-
const ctx = span.spanContext();
|
|
259
|
-
return {
|
|
260
|
-
traceId: ctx.traceId,
|
|
261
|
-
spanId: ctx.spanId
|
|
262
|
-
};
|
|
263
|
-
}
|
|
264
|
-
function isTracingEnabled() {
|
|
265
|
-
return isInitialized$1;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
339
|
//#endregion
|
|
269
340
|
//#region src/features/metrics.ts
|
|
270
341
|
let meterProvider = null;
|
|
@@ -302,6 +373,7 @@ async function shutdownMetrics() {
|
|
|
302
373
|
meterProvider = null;
|
|
303
374
|
} catch (error) {
|
|
304
375
|
console.error("[neoiq-foundation] Error shutting down metrics:", error);
|
|
376
|
+
throw error;
|
|
305
377
|
}
|
|
306
378
|
}
|
|
307
379
|
function getMeter(name, version = "1.0.0") {
|
|
@@ -353,7 +425,6 @@ function createObservabilityPlugin(options) {
|
|
|
353
425
|
const maxBodySize = requestLogging.maxBodySize ?? 10 * 1024;
|
|
354
426
|
const headersToRedact = requestLogging.redactHeaders ?? DEFAULT_REDACT_HEADERS;
|
|
355
427
|
const runInContext = contextManager ? (ctx, fn) => contextManager.run(ctx, fn) : (_ctx, fn) => fn();
|
|
356
|
-
const tracer = tracingEnabled ? trace$1.getTracer("neoiq-foundation-node") : null;
|
|
357
428
|
let requestCounter;
|
|
358
429
|
let requestDuration;
|
|
359
430
|
let requestErrors;
|
|
@@ -370,22 +441,12 @@ function createObservabilityPlugin(options) {
|
|
|
370
441
|
}
|
|
371
442
|
const correlationId = request.headers["x-request-id"] || randomUUID();
|
|
372
443
|
reply.header("x-request-id", correlationId);
|
|
373
|
-
let span;
|
|
374
444
|
let traceId = "";
|
|
375
445
|
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();
|
|
446
|
+
const activeSpan = tracingEnabled ? trace$1.getActiveSpan() : void 0;
|
|
447
|
+
if (activeSpan) {
|
|
448
|
+
activeSpan.setAttribute("http.correlation_id", correlationId);
|
|
449
|
+
const spanContext = activeSpan.spanContext();
|
|
389
450
|
traceId = spanContext.traceId;
|
|
390
451
|
spanId = spanContext.spanId;
|
|
391
452
|
}
|
|
@@ -395,7 +456,6 @@ function createObservabilityPlugin(options) {
|
|
|
395
456
|
spanId,
|
|
396
457
|
startTime: Date.now()
|
|
397
458
|
};
|
|
398
|
-
request.__span = span;
|
|
399
459
|
request.__requestContext = requestContext;
|
|
400
460
|
runInContext(requestContext, () => {
|
|
401
461
|
const logData = {
|
|
@@ -421,7 +481,7 @@ function createObservabilityPlugin(options) {
|
|
|
421
481
|
runInContext(ctx, () => {
|
|
422
482
|
logger.debug({
|
|
423
483
|
correlation_id: ctx.correlationId,
|
|
424
|
-
body: truncateBody(request.body, maxBodySize)
|
|
484
|
+
body: sanitizeBody(truncateBody(request.body, maxBodySize))
|
|
425
485
|
}, "Request body");
|
|
426
486
|
done();
|
|
427
487
|
});
|
|
@@ -436,14 +496,13 @@ function createObservabilityPlugin(options) {
|
|
|
436
496
|
logger.debug({
|
|
437
497
|
correlation_id: ctx.correlationId,
|
|
438
498
|
statusCode: reply.statusCode,
|
|
439
|
-
body: truncateBody(payload, maxBodySize)
|
|
499
|
+
body: sanitizeBody(truncateBody(payload, maxBodySize))
|
|
440
500
|
}, "Response body");
|
|
441
501
|
done(null, payload);
|
|
442
502
|
});
|
|
443
503
|
});
|
|
444
504
|
fastify.addHook("onResponse", (request, reply, done) => {
|
|
445
505
|
const ctx = request.__requestContext;
|
|
446
|
-
const span = request.__span;
|
|
447
506
|
if (!ctx) {
|
|
448
507
|
done();
|
|
449
508
|
return;
|
|
@@ -467,18 +526,15 @@ function createObservabilityPlugin(options) {
|
|
|
467
526
|
requestDuration.record(durationMs, labels);
|
|
468
527
|
if (reply.statusCode >= 400) requestErrors.add(1, labels);
|
|
469
528
|
}
|
|
470
|
-
if (
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
span.setAttribute("http.response_time_ms", durationMs);
|
|
474
|
-
span.end();
|
|
529
|
+
if (tracingEnabled) {
|
|
530
|
+
const activeSpan = trace$1.getActiveSpan();
|
|
531
|
+
if (activeSpan) activeSpan.setAttribute("http.response_time_ms", durationMs);
|
|
475
532
|
}
|
|
476
533
|
done();
|
|
477
534
|
});
|
|
478
535
|
});
|
|
479
536
|
fastify.addHook("onError", (request, _reply, error, done) => {
|
|
480
537
|
const ctx = request.__requestContext;
|
|
481
|
-
const span = request.__span;
|
|
482
538
|
if (!ctx) {
|
|
483
539
|
done();
|
|
484
540
|
return;
|
|
@@ -490,12 +546,15 @@ function createObservabilityPlugin(options) {
|
|
|
490
546
|
url: request.url,
|
|
491
547
|
error: error.message
|
|
492
548
|
}, "Request failed");
|
|
493
|
-
if (
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
549
|
+
if (tracingEnabled) {
|
|
550
|
+
const activeSpan = trace$1.getActiveSpan();
|
|
551
|
+
if (activeSpan) {
|
|
552
|
+
activeSpan.setStatus({
|
|
553
|
+
code: SpanStatusCode$1.ERROR,
|
|
554
|
+
message: error.message
|
|
555
|
+
});
|
|
556
|
+
activeSpan.recordException(error);
|
|
557
|
+
}
|
|
499
558
|
}
|
|
500
559
|
done();
|
|
501
560
|
});
|
|
@@ -592,31 +651,34 @@ function createHttpClient(options) {
|
|
|
592
651
|
}
|
|
593
652
|
return Promise.reject(error);
|
|
594
653
|
});
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
654
|
+
if (retry.enabled !== false) {
|
|
655
|
+
const retryConfig = {
|
|
656
|
+
retries: retry.retries ?? 3,
|
|
657
|
+
retryDelay: (retryCount) => (retry.retryDelay ?? 1e3) * Math.pow(2, retryCount - 1),
|
|
658
|
+
retryCondition: (error) => {
|
|
659
|
+
const retryStatusCodes = retry.retryStatusCodes ?? [
|
|
660
|
+
408,
|
|
661
|
+
429,
|
|
662
|
+
500,
|
|
663
|
+
502,
|
|
664
|
+
503,
|
|
665
|
+
504
|
|
666
|
+
];
|
|
667
|
+
return !error.response || retryStatusCodes.includes(error.response?.status || 0);
|
|
668
|
+
},
|
|
669
|
+
onRetry: (retryCount, error, requestConfig) => {
|
|
670
|
+
logger.warn({
|
|
671
|
+
retryCount,
|
|
672
|
+
url: `${requestConfig.baseURL || ""}${requestConfig.url}`,
|
|
673
|
+
error: error.message
|
|
674
|
+
}, "Retrying request");
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
axiosRetry(client, retryConfig);
|
|
678
|
+
}
|
|
679
|
+
if (cbOptions.enabled === true) {
|
|
680
|
+
const originalRequest = client.request.bind(client);
|
|
681
|
+
const breaker = new CircuitBreaker(async (config) => originalRequest(config), {
|
|
620
682
|
timeout,
|
|
621
683
|
resetTimeout: cbOptions.resetTimeout ?? 3e4,
|
|
622
684
|
errorThresholdPercentage: cbOptions.errorThresholdPercentage ?? 50,
|
|
@@ -625,17 +687,58 @@ function createHttpClient(options) {
|
|
|
625
687
|
breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
|
|
626
688
|
breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
|
|
627
689
|
breaker.on("close", () => logger.info({ targetService: serviceName }, "Circuit breaker CLOSED"));
|
|
690
|
+
client.request = (config) => breaker.fire(config);
|
|
691
|
+
client.get = (url, config) => breaker.fire({
|
|
692
|
+
...config,
|
|
693
|
+
method: "GET",
|
|
694
|
+
url
|
|
695
|
+
});
|
|
696
|
+
client.post = (url, data, config) => breaker.fire({
|
|
697
|
+
...config,
|
|
698
|
+
method: "POST",
|
|
699
|
+
url,
|
|
700
|
+
data
|
|
701
|
+
});
|
|
702
|
+
client.put = (url, data, config) => breaker.fire({
|
|
703
|
+
...config,
|
|
704
|
+
method: "PUT",
|
|
705
|
+
url,
|
|
706
|
+
data
|
|
707
|
+
});
|
|
708
|
+
client.delete = (url, config) => breaker.fire({
|
|
709
|
+
...config,
|
|
710
|
+
method: "DELETE",
|
|
711
|
+
url
|
|
712
|
+
});
|
|
713
|
+
client.patch = (url, data, config) => breaker.fire({
|
|
714
|
+
...config,
|
|
715
|
+
method: "PATCH",
|
|
716
|
+
url,
|
|
717
|
+
data
|
|
718
|
+
});
|
|
719
|
+
client.__originalRequest = originalRequest;
|
|
628
720
|
}
|
|
629
721
|
return client;
|
|
630
722
|
}
|
|
631
723
|
|
|
632
724
|
//#endregion
|
|
633
725
|
//#region src/foundation.ts
|
|
726
|
+
const deprecationWarnings = new Set();
|
|
727
|
+
function warnDeprecation(oldPath, newPath, logger) {
|
|
728
|
+
if (deprecationWarnings.has(oldPath)) return;
|
|
729
|
+
deprecationWarnings.add(oldPath);
|
|
730
|
+
const msg = `foundation.${oldPath}() is deprecated. Use foundation.${newPath}() instead. This alias will be removed in the next major version.`;
|
|
731
|
+
if (logger) logger.warn({
|
|
732
|
+
deprecated: oldPath,
|
|
733
|
+
replacement: newPath
|
|
734
|
+
}, msg);
|
|
735
|
+
else console.warn(`[neoiq-foundation] DEPRECATED: ${msg}`);
|
|
736
|
+
}
|
|
634
737
|
/** Create a fully configured observability foundation */
|
|
635
738
|
function createFoundation(input) {
|
|
636
739
|
const startTime = Date.now();
|
|
637
740
|
const config = parseConfig(input);
|
|
638
|
-
const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig } = config;
|
|
741
|
+
const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig, redaction: redactionConfig, shutdown: shutdownConfig } = config;
|
|
639
742
|
const features = {
|
|
640
743
|
tracing: featuresConfig.tracing ?? true,
|
|
641
744
|
metrics: featuresConfig.metrics ?? true,
|
|
@@ -651,7 +754,8 @@ function createFoundation(input) {
|
|
|
651
754
|
environment,
|
|
652
755
|
level: loggingConfig.level ?? "info",
|
|
653
756
|
prettyPrint: loggingConfig.prettyPrint ?? environment === "development",
|
|
654
|
-
contextManager
|
|
757
|
+
contextManager,
|
|
758
|
+
additionalRedactPaths: redactionConfig.additionalPaths
|
|
655
759
|
});
|
|
656
760
|
setGlobalLogger(logger);
|
|
657
761
|
} catch (err) {
|
|
@@ -723,17 +827,139 @@ function createFoundation(input) {
|
|
|
723
827
|
environment,
|
|
724
828
|
features
|
|
725
829
|
}, "Foundation initialized");
|
|
726
|
-
|
|
727
|
-
|
|
830
|
+
if (shutdownConfig.flushOnCrash) {
|
|
831
|
+
const flushTimeoutMs = shutdownConfig.flushTimeoutMs ?? 5e3;
|
|
832
|
+
const crashFlush = (origin, err) => {
|
|
833
|
+
const error = err instanceof Error ? err : new Error(String(err));
|
|
834
|
+
logger.error({
|
|
835
|
+
error: error.message,
|
|
836
|
+
stack: error.stack,
|
|
837
|
+
origin
|
|
838
|
+
}, "Process crash detected, flushing telemetry");
|
|
839
|
+
const flushPromises = [];
|
|
840
|
+
if (features.tracing && isTracingEnabled()) flushPromises.push(shutdownTracing());
|
|
841
|
+
if (features.metrics && isMetricsEnabled()) flushPromises.push(shutdownMetrics());
|
|
842
|
+
const timeout = new Promise((resolve) => setTimeout(resolve, flushTimeoutMs));
|
|
843
|
+
Promise.race([Promise.allSettled(flushPromises), timeout]).finally(() => {
|
|
844
|
+
process.exit(1);
|
|
845
|
+
});
|
|
846
|
+
};
|
|
847
|
+
process.on("uncaughtException", (err) => crashFlush("uncaughtException", err));
|
|
848
|
+
process.on("unhandledRejection", (reason) => crashFlush("unhandledRejection", reason));
|
|
849
|
+
logger.info({ flushTimeoutMs }, "Crash-flush handlers registered");
|
|
850
|
+
}
|
|
851
|
+
const traceInSpan = async (name, fn) => {
|
|
852
|
+
const tracer = tracerInstance || getTracer(serviceName);
|
|
853
|
+
return new Promise((resolve, reject) => {
|
|
854
|
+
tracer.startActiveSpan(name, async (span) => {
|
|
855
|
+
try {
|
|
856
|
+
const result = await fn();
|
|
857
|
+
span.setStatus({ code: SpanStatusCode.OK });
|
|
858
|
+
span.end();
|
|
859
|
+
resolve(result);
|
|
860
|
+
} catch (err) {
|
|
861
|
+
const error = err;
|
|
862
|
+
span.setStatus({
|
|
863
|
+
code: SpanStatusCode.ERROR,
|
|
864
|
+
message: error.message
|
|
865
|
+
});
|
|
866
|
+
span.recordException(error);
|
|
867
|
+
span.end();
|
|
868
|
+
logger.error({
|
|
869
|
+
span: name,
|
|
870
|
+
error: error.message
|
|
871
|
+
}, "Span failed");
|
|
872
|
+
reject(error);
|
|
873
|
+
}
|
|
874
|
+
});
|
|
875
|
+
});
|
|
876
|
+
};
|
|
877
|
+
const observability = {
|
|
728
878
|
logger,
|
|
729
|
-
context: contextManager,
|
|
730
879
|
tracer: tracerInstance,
|
|
731
880
|
meter: meterInstance,
|
|
732
|
-
features,
|
|
733
881
|
getTracer: (name) => getTracer(name || serviceName),
|
|
734
882
|
getMeter: (name, version) => getMeter(name, version),
|
|
735
883
|
getTraceContext,
|
|
736
884
|
getActiveSpan,
|
|
885
|
+
trace: traceInSpan
|
|
886
|
+
};
|
|
887
|
+
const httpModule = { createClient: (options) => createHttpClient({
|
|
888
|
+
...options,
|
|
889
|
+
foundation
|
|
890
|
+
}) };
|
|
891
|
+
const buildHealthStatus = () => {
|
|
892
|
+
const tracingUp = !features.tracing || !tracingError && isTracingEnabled();
|
|
893
|
+
const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
|
|
894
|
+
const loggingUp = !loggingError;
|
|
895
|
+
const allUp = tracingUp && metricsUp && loggingUp;
|
|
896
|
+
const allDown = (!tracingUp || !features.tracing) && (!metricsUp || !features.metrics) && !loggingUp;
|
|
897
|
+
let status = "healthy";
|
|
898
|
+
if (!allUp) status = allDown ? "unhealthy" : "degraded";
|
|
899
|
+
return {
|
|
900
|
+
status,
|
|
901
|
+
timestamp: new Date().toISOString(),
|
|
902
|
+
service: serviceName,
|
|
903
|
+
version: serviceVersion,
|
|
904
|
+
uptime: Math.floor((Date.now() - startTime) / 1e3),
|
|
905
|
+
components: {
|
|
906
|
+
tracing: {
|
|
907
|
+
enabled: features.tracing,
|
|
908
|
+
status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
|
|
909
|
+
message: tracingError
|
|
910
|
+
},
|
|
911
|
+
metrics: {
|
|
912
|
+
enabled: features.metrics,
|
|
913
|
+
status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
|
|
914
|
+
message: metricsError
|
|
915
|
+
},
|
|
916
|
+
logging: {
|
|
917
|
+
enabled: features.logging,
|
|
918
|
+
status: loggingError ? "down" : "up",
|
|
919
|
+
message: loggingError
|
|
920
|
+
}
|
|
921
|
+
}
|
|
922
|
+
};
|
|
923
|
+
};
|
|
924
|
+
const shutdownFn = async () => {
|
|
925
|
+
logger.info({}, "Shutting down foundation...");
|
|
926
|
+
const promises = [];
|
|
927
|
+
if (features.tracing && isTracingEnabled()) promises.push(shutdownTracing());
|
|
928
|
+
if (features.metrics && isMetricsEnabled()) promises.push(shutdownMetrics());
|
|
929
|
+
await Promise.all(promises);
|
|
930
|
+
logger.info({}, "Foundation shutdown complete");
|
|
931
|
+
};
|
|
932
|
+
const isReadyFn = () => {
|
|
933
|
+
if (features.tracing && !tracingError && !isTracingEnabled()) return false;
|
|
934
|
+
if (features.metrics && !metricsError && !isMetricsEnabled()) return false;
|
|
935
|
+
return true;
|
|
936
|
+
};
|
|
937
|
+
const safeRunFn = async (fn, fallback) => {
|
|
938
|
+
try {
|
|
939
|
+
return await fn();
|
|
940
|
+
} catch (err) {
|
|
941
|
+
const error = err;
|
|
942
|
+
logger.error({
|
|
943
|
+
error: error.message,
|
|
944
|
+
stack: error.stack
|
|
945
|
+
}, "safeRun caught error");
|
|
946
|
+
return fallback;
|
|
947
|
+
}
|
|
948
|
+
};
|
|
949
|
+
const lifecycle = {
|
|
950
|
+
health: buildHealthStatus,
|
|
951
|
+
isReady: isReadyFn,
|
|
952
|
+
shutdown: shutdownFn,
|
|
953
|
+
safeRun: safeRunFn
|
|
954
|
+
};
|
|
955
|
+
const foundation = {
|
|
956
|
+
config,
|
|
957
|
+
features,
|
|
958
|
+
observability,
|
|
959
|
+
http: httpModule,
|
|
960
|
+
lifecycle,
|
|
961
|
+
logger,
|
|
962
|
+
context: contextManager,
|
|
737
963
|
fastifyPlugin: createObservabilityPlugin({
|
|
738
964
|
serviceName,
|
|
739
965
|
logger,
|
|
@@ -742,91 +968,47 @@ function createFoundation(input) {
|
|
|
742
968
|
metricsEnabled: features.metrics && !metricsError,
|
|
743
969
|
requestLogging: requestLoggingConfig
|
|
744
970
|
}),
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
971
|
+
tracer: tracerInstance,
|
|
972
|
+
meter: meterInstance,
|
|
973
|
+
getTracer: (name) => {
|
|
974
|
+
warnDeprecation("getTracer", "observability.getTracer", logger);
|
|
975
|
+
return observability.getTracer(name);
|
|
976
|
+
},
|
|
977
|
+
getMeter: (name, version) => {
|
|
978
|
+
warnDeprecation("getMeter", "observability.getMeter", logger);
|
|
979
|
+
return observability.getMeter(name, version);
|
|
980
|
+
},
|
|
981
|
+
getTraceContext: () => {
|
|
982
|
+
warnDeprecation("getTraceContext", "observability.getTraceContext", logger);
|
|
983
|
+
return observability.getTraceContext();
|
|
984
|
+
},
|
|
985
|
+
getActiveSpan: () => {
|
|
986
|
+
warnDeprecation("getActiveSpan", "observability.getActiveSpan", logger);
|
|
987
|
+
return observability.getActiveSpan();
|
|
988
|
+
},
|
|
989
|
+
createHttpClient: (options) => {
|
|
990
|
+
warnDeprecation("createHttpClient", "http.createClient", logger);
|
|
991
|
+
return httpModule.createClient(options);
|
|
992
|
+
},
|
|
993
|
+
shutdown: () => {
|
|
994
|
+
warnDeprecation("shutdown", "lifecycle.shutdown", logger);
|
|
995
|
+
return lifecycle.shutdown();
|
|
756
996
|
},
|
|
757
997
|
isReady: () => {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
return true;
|
|
998
|
+
warnDeprecation("isReady", "lifecycle.isReady", logger);
|
|
999
|
+
return lifecycle.isReady();
|
|
761
1000
|
},
|
|
762
1001
|
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
|
-
};
|
|
1002
|
+
warnDeprecation("health", "lifecycle.health", logger);
|
|
1003
|
+
return lifecycle.health();
|
|
792
1004
|
},
|
|
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
|
-
});
|
|
1005
|
+
trace: (name, fn) => {
|
|
1006
|
+
warnDeprecation("trace", "observability.trace", logger);
|
|
1007
|
+
return observability.trace(name, fn);
|
|
818
1008
|
},
|
|
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
|
-
}
|
|
1009
|
+
safeRun: (fn, fallback) => {
|
|
1010
|
+
warnDeprecation("safeRun", "lifecycle.safeRun", logger);
|
|
1011
|
+
return lifecycle.safeRun(fn, fallback);
|
|
830
1012
|
}
|
|
831
1013
|
};
|
|
832
1014
|
return foundation;
|
|
@@ -835,5 +1017,289 @@ function createFoundation(input) {
|
|
|
835
1017
|
const setupObservability = createFoundation;
|
|
836
1018
|
|
|
837
1019
|
//#endregion
|
|
838
|
-
|
|
1020
|
+
//#region src/integrations/object-store/aws-s3.ts
|
|
1021
|
+
function isAwsError(err) {
|
|
1022
|
+
return typeof err === "object" && err !== null;
|
|
1023
|
+
}
|
|
1024
|
+
async function loadAwsS3() {
|
|
1025
|
+
try {
|
|
1026
|
+
const clientMod = await import("@aws-sdk/client-s3");
|
|
1027
|
+
const presignerMod = await import("@aws-sdk/s3-request-presigner");
|
|
1028
|
+
return {
|
|
1029
|
+
S3Client: clientMod.S3Client,
|
|
1030
|
+
PutObjectCommand: clientMod.PutObjectCommand,
|
|
1031
|
+
GetObjectCommand: clientMod.GetObjectCommand,
|
|
1032
|
+
HeadObjectCommand: clientMod.HeadObjectCommand,
|
|
1033
|
+
DeleteObjectCommand: clientMod.DeleteObjectCommand,
|
|
1034
|
+
ListObjectsV2Command: clientMod.ListObjectsV2Command,
|
|
1035
|
+
getSignedUrl: presignerMod.getSignedUrl
|
|
1036
|
+
};
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
1039
|
+
throw new Error(`AWS SDK not available. Install optional peer deps: @aws-sdk/client-s3 and @aws-sdk/s3-request-presigner. Original error: ${e.message}`);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* AwsS3ObjectStore - wraps AWS S3 behind the provider-agnostic `ObjectStore` interface.
|
|
1044
|
+
*
|
|
1045
|
+
* This implementation uses dynamic imports so `neoiq-foundation-node` can be used without AWS SDK.
|
|
1046
|
+
*/
|
|
1047
|
+
var AwsS3ObjectStore = class {
|
|
1048
|
+
provider = "aws-s3";
|
|
1049
|
+
clientPromise;
|
|
1050
|
+
awsPromise = loadAwsS3();
|
|
1051
|
+
constructor(options = {}) {
|
|
1052
|
+
if (options.client) {
|
|
1053
|
+
this.clientPromise = Promise.resolve(options.client);
|
|
1054
|
+
return;
|
|
1055
|
+
}
|
|
1056
|
+
this.clientPromise = (async () => {
|
|
1057
|
+
const aws = await this.awsPromise;
|
|
1058
|
+
return new aws.S3Client(options.clientOptions ?? {});
|
|
1059
|
+
})();
|
|
1060
|
+
}
|
|
1061
|
+
async client() {
|
|
1062
|
+
return this.clientPromise;
|
|
1063
|
+
}
|
|
1064
|
+
async putObject(ref, body, options = {}) {
|
|
1065
|
+
const aws = await this.awsPromise;
|
|
1066
|
+
const s3 = await this.client();
|
|
1067
|
+
const res = await s3.send(new aws.PutObjectCommand({
|
|
1068
|
+
Bucket: ref.bucket,
|
|
1069
|
+
Key: ref.key,
|
|
1070
|
+
Body: body,
|
|
1071
|
+
ContentType: options.contentType,
|
|
1072
|
+
CacheControl: options.cacheControl,
|
|
1073
|
+
Metadata: options.metadata
|
|
1074
|
+
}));
|
|
1075
|
+
return {
|
|
1076
|
+
etag: res?.ETag,
|
|
1077
|
+
versionId: res?.VersionId
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
async getObject(ref) {
|
|
1081
|
+
const aws = await this.awsPromise;
|
|
1082
|
+
const s3 = await this.client();
|
|
1083
|
+
const res = await s3.send(new aws.GetObjectCommand({
|
|
1084
|
+
Bucket: ref.bucket,
|
|
1085
|
+
Key: ref.key
|
|
1086
|
+
}));
|
|
1087
|
+
if (!res?.Body) {
|
|
1088
|
+
const err = new Error(`S3 GetObject returned empty body: ${ref.bucket}/${ref.key}`);
|
|
1089
|
+
err.code = "EMPTY_BODY";
|
|
1090
|
+
throw err;
|
|
1091
|
+
}
|
|
1092
|
+
return {
|
|
1093
|
+
body: res.Body,
|
|
1094
|
+
contentType: res.ContentType,
|
|
1095
|
+
contentLength: res.ContentLength,
|
|
1096
|
+
etag: res.ETag,
|
|
1097
|
+
versionId: res.VersionId,
|
|
1098
|
+
metadata: res.Metadata,
|
|
1099
|
+
lastModified: res.LastModified
|
|
1100
|
+
};
|
|
1101
|
+
}
|
|
1102
|
+
async headObject(ref) {
|
|
1103
|
+
const aws = await this.awsPromise;
|
|
1104
|
+
const s3 = await this.client();
|
|
1105
|
+
try {
|
|
1106
|
+
const res = await s3.send(new aws.HeadObjectCommand({
|
|
1107
|
+
Bucket: ref.bucket,
|
|
1108
|
+
Key: ref.key
|
|
1109
|
+
}));
|
|
1110
|
+
return {
|
|
1111
|
+
exists: true,
|
|
1112
|
+
contentType: res.ContentType,
|
|
1113
|
+
contentLength: res.ContentLength,
|
|
1114
|
+
etag: res.ETag,
|
|
1115
|
+
versionId: res.VersionId,
|
|
1116
|
+
metadata: res.Metadata,
|
|
1117
|
+
lastModified: res.LastModified
|
|
1118
|
+
};
|
|
1119
|
+
} catch (err) {
|
|
1120
|
+
if (isAwsError(err)) {
|
|
1121
|
+
const name = String(err.name ?? "");
|
|
1122
|
+
const httpStatus = err.$metadata?.httpStatusCode;
|
|
1123
|
+
if (httpStatus === 404 || name.includes("NotFound") || name.includes("NoSuchKey")) return { exists: false };
|
|
1124
|
+
}
|
|
1125
|
+
throw err;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
async deleteObject(ref) {
|
|
1129
|
+
const aws = await this.awsPromise;
|
|
1130
|
+
const s3 = await this.client();
|
|
1131
|
+
await s3.send(new aws.DeleteObjectCommand({
|
|
1132
|
+
Bucket: ref.bucket,
|
|
1133
|
+
Key: ref.key
|
|
1134
|
+
}));
|
|
1135
|
+
}
|
|
1136
|
+
async listObjects(options) {
|
|
1137
|
+
const aws = await this.awsPromise;
|
|
1138
|
+
const s3 = await this.client();
|
|
1139
|
+
const res = await s3.send(new aws.ListObjectsV2Command({
|
|
1140
|
+
Bucket: options.bucket,
|
|
1141
|
+
Prefix: options.prefix,
|
|
1142
|
+
ContinuationToken: options.continuationToken,
|
|
1143
|
+
MaxKeys: options.maxKeys
|
|
1144
|
+
}));
|
|
1145
|
+
const contents = res?.Contents ?? [];
|
|
1146
|
+
return {
|
|
1147
|
+
objects: contents.filter((o) => typeof o.Key === "string").map((o) => ({
|
|
1148
|
+
key: o.Key,
|
|
1149
|
+
size: o.Size,
|
|
1150
|
+
etag: o.ETag,
|
|
1151
|
+
lastModified: o.LastModified
|
|
1152
|
+
})),
|
|
1153
|
+
nextContinuationToken: res?.NextContinuationToken
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
async presignGetObject(ref, options) {
|
|
1157
|
+
const aws = await this.awsPromise;
|
|
1158
|
+
const s3 = await this.client();
|
|
1159
|
+
return aws.getSignedUrl(s3, new aws.GetObjectCommand({
|
|
1160
|
+
Bucket: ref.bucket,
|
|
1161
|
+
Key: ref.key,
|
|
1162
|
+
ResponseContentType: options.responseContentType
|
|
1163
|
+
}), { expiresIn: options.expiresInSeconds });
|
|
1164
|
+
}
|
|
1165
|
+
async presignPutObject(ref, options) {
|
|
1166
|
+
const aws = await this.awsPromise;
|
|
1167
|
+
const s3 = await this.client();
|
|
1168
|
+
return aws.getSignedUrl(s3, new aws.PutObjectCommand({
|
|
1169
|
+
Bucket: ref.bucket,
|
|
1170
|
+
Key: ref.key,
|
|
1171
|
+
ContentType: options.contentType
|
|
1172
|
+
}), { expiresIn: options.expiresInSeconds });
|
|
1173
|
+
}
|
|
1174
|
+
};
|
|
1175
|
+
|
|
1176
|
+
//#endregion
|
|
1177
|
+
//#region src/integrations/object-store/in-memory.ts
|
|
1178
|
+
const DEFAULT_MAX_OBJECTS = 1e4;
|
|
1179
|
+
const STREAM_TIMEOUT_MS = 3e4;
|
|
1180
|
+
function toBuffer(body) {
|
|
1181
|
+
if (typeof body === "string") return Buffer.from(body);
|
|
1182
|
+
if (Buffer.isBuffer(body)) return body;
|
|
1183
|
+
if (body instanceof Uint8Array) return Buffer.from(body);
|
|
1184
|
+
return new Promise((resolve, reject) => {
|
|
1185
|
+
const chunks = [];
|
|
1186
|
+
const timer = setTimeout(() => {
|
|
1187
|
+
body.destroy(new Error("InMemoryObjectStore: stream read timed out"));
|
|
1188
|
+
reject(new Error("InMemoryObjectStore: stream read timed out"));
|
|
1189
|
+
}, STREAM_TIMEOUT_MS);
|
|
1190
|
+
body.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
1191
|
+
body.on("end", () => {
|
|
1192
|
+
clearTimeout(timer);
|
|
1193
|
+
resolve(Buffer.concat(chunks));
|
|
1194
|
+
});
|
|
1195
|
+
body.on("error", (err) => {
|
|
1196
|
+
clearTimeout(timer);
|
|
1197
|
+
reject(err);
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
function computeEtag(buf) {
|
|
1202
|
+
return `"${createHash("md5").update(buf).digest("hex")}"`;
|
|
1203
|
+
}
|
|
1204
|
+
/**
|
|
1205
|
+
* InMemoryObjectStore - useful for local dev, unit tests, and as a safe default.
|
|
1206
|
+
* NOTE: presign* methods are not meaningful here and will throw.
|
|
1207
|
+
*/
|
|
1208
|
+
var InMemoryObjectStore = class {
|
|
1209
|
+
provider = "in-memory";
|
|
1210
|
+
buckets = new Map();
|
|
1211
|
+
maxObjects;
|
|
1212
|
+
objectCount = 0;
|
|
1213
|
+
constructor(options = {}) {
|
|
1214
|
+
this.maxObjects = options.maxObjects ?? DEFAULT_MAX_OBJECTS;
|
|
1215
|
+
}
|
|
1216
|
+
bucketMap(bucket) {
|
|
1217
|
+
let m = this.buckets.get(bucket);
|
|
1218
|
+
if (!m) {
|
|
1219
|
+
m = new Map();
|
|
1220
|
+
this.buckets.set(bucket, m);
|
|
1221
|
+
}
|
|
1222
|
+
return m;
|
|
1223
|
+
}
|
|
1224
|
+
async putObject(ref, body, options = {}) {
|
|
1225
|
+
const map = this.bucketMap(ref.bucket);
|
|
1226
|
+
const isNew = !map.has(ref.key);
|
|
1227
|
+
if (isNew && this.objectCount >= this.maxObjects) throw new Error(`InMemoryObjectStore: max object limit reached (${this.maxObjects})`);
|
|
1228
|
+
const buf = await toBuffer(body);
|
|
1229
|
+
const obj = {
|
|
1230
|
+
body: buf,
|
|
1231
|
+
contentType: options.contentType,
|
|
1232
|
+
cacheControl: options.cacheControl,
|
|
1233
|
+
metadata: options.metadata,
|
|
1234
|
+
etag: computeEtag(buf),
|
|
1235
|
+
lastModified: new Date()
|
|
1236
|
+
};
|
|
1237
|
+
map.set(ref.key, obj);
|
|
1238
|
+
if (isNew) this.objectCount++;
|
|
1239
|
+
return { etag: obj.etag };
|
|
1240
|
+
}
|
|
1241
|
+
async getObject(ref) {
|
|
1242
|
+
const obj = this.bucketMap(ref.bucket).get(ref.key);
|
|
1243
|
+
if (!obj) {
|
|
1244
|
+
const err = new Error(`Object not found: ${ref.bucket}/${ref.key}`);
|
|
1245
|
+
err.code = "OBJECT_NOT_FOUND";
|
|
1246
|
+
throw err;
|
|
1247
|
+
}
|
|
1248
|
+
return {
|
|
1249
|
+
body: Readable.from(obj.body),
|
|
1250
|
+
contentType: obj.contentType,
|
|
1251
|
+
contentLength: obj.body.length,
|
|
1252
|
+
etag: obj.etag,
|
|
1253
|
+
metadata: obj.metadata,
|
|
1254
|
+
lastModified: obj.lastModified
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
async headObject(ref) {
|
|
1258
|
+
const obj = this.bucketMap(ref.bucket).get(ref.key);
|
|
1259
|
+
if (!obj) return { exists: false };
|
|
1260
|
+
return {
|
|
1261
|
+
exists: true,
|
|
1262
|
+
contentType: obj.contentType,
|
|
1263
|
+
contentLength: obj.body.length,
|
|
1264
|
+
etag: obj.etag,
|
|
1265
|
+
metadata: obj.metadata,
|
|
1266
|
+
lastModified: obj.lastModified
|
|
1267
|
+
};
|
|
1268
|
+
}
|
|
1269
|
+
async deleteObject(ref) {
|
|
1270
|
+
const map = this.bucketMap(ref.bucket);
|
|
1271
|
+
if (map.delete(ref.key)) this.objectCount--;
|
|
1272
|
+
}
|
|
1273
|
+
async listObjects(options) {
|
|
1274
|
+
const { bucket, prefix = "", continuationToken, maxKeys = 1e3 } = options;
|
|
1275
|
+
const map = this.bucketMap(bucket);
|
|
1276
|
+
const all = [...map.entries()].filter(([key]) => key.startsWith(prefix)).map(([key, obj]) => ({
|
|
1277
|
+
key,
|
|
1278
|
+
size: obj.body.length,
|
|
1279
|
+
etag: obj.etag,
|
|
1280
|
+
lastModified: obj.lastModified
|
|
1281
|
+
})).sort((a, b) => a.key.localeCompare(b.key));
|
|
1282
|
+
let startIndex = 0;
|
|
1283
|
+
if (continuationToken) {
|
|
1284
|
+
const idx = all.findIndex((o) => o.key === continuationToken);
|
|
1285
|
+
if (idx === -1) throw new Error(`InMemoryObjectStore: invalid continuationToken "${continuationToken}"`);
|
|
1286
|
+
startIndex = idx + 1;
|
|
1287
|
+
}
|
|
1288
|
+
const page = all.slice(startIndex, startIndex + maxKeys);
|
|
1289
|
+
const hasMore = startIndex + maxKeys < all.length;
|
|
1290
|
+
return {
|
|
1291
|
+
objects: page,
|
|
1292
|
+
nextContinuationToken: hasMore ? page[page.length - 1]?.key : void 0
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
async presignGetObject(_ref, _options) {
|
|
1296
|
+
throw new Error("InMemoryObjectStore does not support presigned URLs");
|
|
1297
|
+
}
|
|
1298
|
+
async presignPutObject(_ref, _options) {
|
|
1299
|
+
throw new Error("InMemoryObjectStore does not support presigned URLs");
|
|
1300
|
+
}
|
|
1301
|
+
};
|
|
1302
|
+
|
|
1303
|
+
//#endregion
|
|
1304
|
+
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
1305
|
//# sourceMappingURL=index.mjs.map
|