@ciq-dev/neoiq-foundation-node 1.0.0 → 1.0.1-beta.0

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/dist/index.js CHANGED
@@ -1,30 +1,887 @@
1
1
  "use strict";
2
- /**
3
- * @commerceiq/neoiq-foundation-node
4
- *
5
- * Node.js observability foundation for CommerceIQ services.
6
- * Integrates with Groundcover via OpenTelemetry.
7
- *
8
- * Flow: App → OpenTelemetry SDK → OTEL Collector → Groundcover
9
- */
10
- Object.defineProperty(exports, "__esModule", { value: true });
11
- exports.createHttpClient = exports.observabilityPlugin = exports.SpanStatusCode = exports.als = exports.shutdown = exports.getTraceContext = exports.runWithContext = exports.getRequestContext = exports.getMeter = exports.getTracer = exports.logger = exports.init = void 0;
12
- // Core initialization and logging
13
- var observability_1 = require("./observability");
14
- Object.defineProperty(exports, "init", { enumerable: true, get: function () { return observability_1.init; } });
15
- Object.defineProperty(exports, "logger", { enumerable: true, get: function () { return observability_1.logger; } });
16
- Object.defineProperty(exports, "getTracer", { enumerable: true, get: function () { return observability_1.getTracer; } });
17
- Object.defineProperty(exports, "getMeter", { enumerable: true, get: function () { return observability_1.getMeter; } });
18
- Object.defineProperty(exports, "getRequestContext", { enumerable: true, get: function () { return observability_1.getRequestContext; } });
19
- Object.defineProperty(exports, "runWithContext", { enumerable: true, get: function () { return observability_1.runWithContext; } });
20
- Object.defineProperty(exports, "getTraceContext", { enumerable: true, get: function () { return observability_1.getTraceContext; } });
21
- Object.defineProperty(exports, "shutdown", { enumerable: true, get: function () { return observability_1.shutdown; } });
22
- Object.defineProperty(exports, "als", { enumerable: true, get: function () { return observability_1.als; } });
23
- Object.defineProperty(exports, "SpanStatusCode", { enumerable: true, get: function () { return observability_1.SpanStatusCode; } });
24
- // Fastify plugin
25
- var plugin_1 = require("./plugin");
26
- Object.defineProperty(exports, "observabilityPlugin", { enumerable: true, get: function () { return plugin_1.observabilityPlugin; } });
27
- // HTTP client
28
- var http_client_1 = require("./http-client");
29
- Object.defineProperty(exports, "createHttpClient", { enumerable: true, get: function () { return http_client_1.createHttpClient; } });
2
+ //#region rolldown:runtime
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
11
+ key = keys[i];
12
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
13
+ get: ((k) => from[k]).bind(null, key),
14
+ enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
15
+ });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
20
+ value: mod,
21
+ enumerable: true
22
+ }) : target, mod));
23
+
24
+ //#endregion
25
+ const zod = __toESM(require("zod"));
26
+ const async_hooks = __toESM(require("async_hooks"));
27
+ const pino = __toESM(require("pino"));
28
+ const __opentelemetry_api = __toESM(require("@opentelemetry/api"));
29
+ const __opentelemetry_sdk_node = __toESM(require("@opentelemetry/sdk-node"));
30
+ const __opentelemetry_auto_instrumentations_node = __toESM(require("@opentelemetry/auto-instrumentations-node"));
31
+ const __opentelemetry_exporter_trace_otlp_grpc = __toESM(require("@opentelemetry/exporter-trace-otlp-grpc"));
32
+ const __opentelemetry_resources = __toESM(require("@opentelemetry/resources"));
33
+ const __opentelemetry_semantic_conventions = __toESM(require("@opentelemetry/semantic-conventions"));
34
+ const __opentelemetry_exporter_metrics_otlp_grpc = __toESM(require("@opentelemetry/exporter-metrics-otlp-grpc"));
35
+ const __opentelemetry_sdk_metrics = __toESM(require("@opentelemetry/sdk-metrics"));
36
+ const fastify_plugin = __toESM(require("fastify-plugin"));
37
+ const crypto = __toESM(require("crypto"));
38
+ const axios = __toESM(require("axios"));
39
+ const axios_retry = __toESM(require("axios-retry"));
40
+ const opossum = __toESM(require("opossum"));
41
+
42
+ //#region src/config.ts
43
+ const AutoInstrumentationConfigSchema = zod.z.object({
44
+ http: zod.z.boolean().default(true),
45
+ fastify: zod.z.boolean().default(true),
46
+ express: zod.z.boolean().default(true),
47
+ mongodb: zod.z.boolean().default(true),
48
+ pg: zod.z.boolean().default(true),
49
+ mysql: zod.z.boolean().default(true),
50
+ redis: zod.z.boolean().default(true),
51
+ ioredis: zod.z.boolean().default(true),
52
+ grpc: zod.z.boolean().default(true),
53
+ fs: zod.z.boolean().default(false),
54
+ dns: zod.z.boolean().default(false)
55
+ }).partial();
56
+ const FeaturesConfigSchema = zod.z.object({
57
+ tracing: zod.z.boolean().default(true),
58
+ metrics: zod.z.boolean().default(true),
59
+ logging: zod.z.boolean().default(true),
60
+ autoInstrumentation: AutoInstrumentationConfigSchema.default({})
61
+ }).partial();
62
+ const DEFAULT_OTEL_ENDPOINT = "http://otel-stack-deployment-collector.observability.svc.cluster.local:4317";
63
+ const OtelConfigSchema = zod.z.object({
64
+ endpoint: zod.z.string().default(DEFAULT_OTEL_ENDPOINT),
65
+ metricsIntervalMs: zod.z.number().min(1e3).default(5e3),
66
+ traceSampleRate: zod.z.number().min(0).max(1).default(1)
67
+ }).partial();
68
+ const LoggingConfigSchema = zod.z.object({
69
+ level: zod.z.enum([
70
+ "debug",
71
+ "info",
72
+ "warn",
73
+ "error"
74
+ ]).default("info"),
75
+ prettyPrint: zod.z.boolean().optional()
76
+ }).partial();
77
+ const RequestLoggingConfigSchema = zod.z.object({
78
+ logHeaders: zod.z.boolean().default(true),
79
+ logBody: zod.z.boolean().default(false),
80
+ logResponseBody: zod.z.boolean().default(false),
81
+ maxBodySize: zod.z.number().default(10 * 1024),
82
+ redactHeaders: zod.z.array(zod.z.string()).optional()
83
+ }).partial();
84
+ const FoundationConfigSchema = zod.z.object({
85
+ serviceName: zod.z.string().min(1, "serviceName is required"),
86
+ serviceVersion: zod.z.string().default(process.env.SERVICE_VERSION || "1.0.0"),
87
+ environment: zod.z.enum([
88
+ "development",
89
+ "staging",
90
+ "qa",
91
+ "production"
92
+ ]).default(process.env.NODE_ENV || "development"),
93
+ features: FeaturesConfigSchema.default({}),
94
+ otel: OtelConfigSchema.default({}),
95
+ logging: LoggingConfigSchema.default({}),
96
+ requestLogging: RequestLoggingConfigSchema.default({})
97
+ });
98
+ /** Parse and validate configuration */
99
+ function parseConfig(input) {
100
+ const result = FoundationConfigSchema.safeParse(input);
101
+ if (!result.success) {
102
+ const errors = result.error.errors.map((e) => ` - ${e.path.join(".")}: ${e.message}`).join("\n");
103
+ throw new Error(`Invalid foundation configuration:\n${errors}`);
104
+ }
105
+ return result.data;
106
+ }
107
+ /** Get default OTEL endpoint */
108
+ function getDefaultOtelEndpoint() {
109
+ return process.env.OTEL_EXPORTER_OTLP_ENDPOINT || DEFAULT_OTEL_ENDPOINT;
110
+ }
111
+
112
+ //#endregion
113
+ //#region src/features/context.ts
114
+ /** Create a new context manager instance */
115
+ function createContextManager() {
116
+ const als = new async_hooks.AsyncLocalStorage();
117
+ return {
118
+ getContext: () => als.getStore(),
119
+ run(context$3, fn) {
120
+ return als.run(context$3, fn);
121
+ },
122
+ get(key) {
123
+ return als.getStore()?.[key];
124
+ },
125
+ update(updates) {
126
+ const current = als.getStore();
127
+ if (!current) return void 0;
128
+ Object.assign(current, updates);
129
+ return current;
130
+ }
131
+ };
132
+ }
133
+
134
+ //#endregion
135
+ //#region src/features/logging.ts
136
+ /** Create a structured logger with automatic trace context injection */
137
+ function createLogger(options) {
138
+ const { serviceName, serviceVersion, environment, level, prettyPrint, contextManager } = options;
139
+ const pinoLogger = (0, pino.default)({
140
+ level,
141
+ base: {
142
+ service: serviceName,
143
+ version: serviceVersion,
144
+ env: environment
145
+ },
146
+ mixin: () => {
147
+ const span = __opentelemetry_api.trace.getActiveSpan();
148
+ const spanContext = span?.spanContext();
149
+ const ctx = contextManager?.getContext();
150
+ return {
151
+ traceId: spanContext?.traceId || ctx?.traceId,
152
+ spanId: spanContext?.spanId || ctx?.spanId,
153
+ correlationId: ctx?.correlationId
154
+ };
155
+ },
156
+ formatters: { level: (label) => ({ level: label }) },
157
+ transport: prettyPrint ? {
158
+ target: "pino-pretty",
159
+ options: {
160
+ colorize: true,
161
+ translateTime: "SYS:standard",
162
+ ignore: "pid,hostname"
163
+ }
164
+ } : void 0
165
+ });
166
+ return wrapPinoLogger(pinoLogger);
167
+ }
168
+ function wrapPinoLogger(pinoLogger) {
169
+ return {
170
+ debug: (obj, msg) => pinoLogger.debug(obj, msg),
171
+ info: (obj, msg) => pinoLogger.info(obj, msg),
172
+ warn: (obj, msg) => pinoLogger.warn(obj, msg),
173
+ error: (obj, msg) => pinoLogger.error(obj, msg),
174
+ child: (bindings) => wrapPinoLogger(pinoLogger.child(bindings)),
175
+ pino: pinoLogger
176
+ };
177
+ }
178
+ /** Fallback logger when Pino is not available */
179
+ function createFallbackLogger(serviceName = "unknown") {
180
+ const log = (level, obj, msg) => {
181
+ const logObj = {
182
+ timestamp: new Date().toISOString(),
183
+ level,
184
+ service: serviceName,
185
+ ...obj,
186
+ msg
187
+ };
188
+ console[level === "debug" ? "log" : level](JSON.stringify(logObj));
189
+ };
190
+ const fallback = {
191
+ debug: (obj, msg) => log("debug", obj, msg),
192
+ info: (obj, msg) => log("info", obj, msg),
193
+ warn: (obj, msg) => log("warn", obj, msg),
194
+ error: (obj, msg) => log("error", obj, msg),
195
+ child: () => fallback,
196
+ pino: null
197
+ };
198
+ return fallback;
199
+ }
200
+ let globalLogger = null;
201
+ function setGlobalLogger(logger) {
202
+ globalLogger = logger;
203
+ }
204
+ function getGlobalLogger() {
205
+ return globalLogger || createFallbackLogger();
206
+ }
207
+
208
+ //#endregion
209
+ //#region src/features/tracing.ts
210
+ let sdk = null;
211
+ let isInitialized$1 = false;
212
+ /** Initialize OpenTelemetry tracing */
213
+ function setupTracing(options) {
214
+ if (isInitialized$1) {
215
+ console.warn("[neoiq-foundation] Tracing already initialized");
216
+ return;
217
+ }
218
+ const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), autoInstrumentation = {} } = options;
219
+ const resource = (0, __opentelemetry_resources.resourceFromAttributes)({
220
+ [__opentelemetry_semantic_conventions.ATTR_SERVICE_NAME]: serviceName,
221
+ [__opentelemetry_semantic_conventions.ATTR_SERVICE_VERSION]: serviceVersion,
222
+ "deployment.environment": environment
223
+ });
224
+ const traceExporter = new __opentelemetry_exporter_trace_otlp_grpc.OTLPTraceExporter({ url: endpoint });
225
+ const instrumentationConfig = buildInstrumentationConfig(autoInstrumentation);
226
+ sdk = new __opentelemetry_sdk_node.NodeSDK({
227
+ resource,
228
+ traceExporter,
229
+ instrumentations: [(0, __opentelemetry_auto_instrumentations_node.getNodeAutoInstrumentations)(instrumentationConfig)]
230
+ });
231
+ sdk.start();
232
+ isInitialized$1 = true;
233
+ }
234
+ function buildInstrumentationConfig(config) {
235
+ const mapping = {
236
+ http: "@opentelemetry/instrumentation-http",
237
+ fastify: "@opentelemetry/instrumentation-fastify",
238
+ express: "@opentelemetry/instrumentation-express",
239
+ mongodb: "@opentelemetry/instrumentation-mongodb",
240
+ pg: "@opentelemetry/instrumentation-pg",
241
+ mysql: "@opentelemetry/instrumentation-mysql",
242
+ redis: "@opentelemetry/instrumentation-redis",
243
+ ioredis: "@opentelemetry/instrumentation-ioredis",
244
+ grpc: "@opentelemetry/instrumentation-grpc",
245
+ fs: "@opentelemetry/instrumentation-fs",
246
+ dns: "@opentelemetry/instrumentation-dns"
247
+ };
248
+ const result = {};
249
+ for (const [key, instrumentationName] of Object.entries(mapping)) {
250
+ const userSetting = config[key];
251
+ const defaultValue = key !== "fs" && key !== "dns";
252
+ result[instrumentationName] = { enabled: userSetting ?? defaultValue };
253
+ }
254
+ return result;
255
+ }
256
+ /** Shutdown tracing gracefully */
257
+ async function shutdownTracing() {
258
+ if (!sdk) return;
259
+ try {
260
+ await sdk.shutdown();
261
+ isInitialized$1 = false;
262
+ sdk = null;
263
+ } catch (error) {
264
+ console.error("[neoiq-foundation] Error shutting down tracing:", error);
265
+ }
266
+ }
267
+ function getTracer(name) {
268
+ return __opentelemetry_api.trace.getTracer(name);
269
+ }
270
+ function getActiveSpan() {
271
+ return __opentelemetry_api.trace.getActiveSpan();
272
+ }
273
+ function getTraceContext() {
274
+ const span = __opentelemetry_api.trace.getActiveSpan();
275
+ if (!span) return {};
276
+ const ctx = span.spanContext();
277
+ return {
278
+ traceId: ctx.traceId,
279
+ spanId: ctx.spanId
280
+ };
281
+ }
282
+ function isTracingEnabled() {
283
+ return isInitialized$1;
284
+ }
285
+
286
+ //#endregion
287
+ //#region src/features/metrics.ts
288
+ let meterProvider = null;
289
+ let isInitialized = false;
290
+ /** Initialize OpenTelemetry metrics */
291
+ function setupMetrics(options) {
292
+ if (isInitialized) {
293
+ console.warn("[neoiq-foundation] Metrics already initialized");
294
+ return;
295
+ }
296
+ const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), intervalMs = 5e3 } = options;
297
+ const resource = (0, __opentelemetry_resources.resourceFromAttributes)({
298
+ [__opentelemetry_semantic_conventions.ATTR_SERVICE_NAME]: serviceName,
299
+ [__opentelemetry_semantic_conventions.ATTR_SERVICE_VERSION]: serviceVersion,
300
+ "deployment.environment": environment
301
+ });
302
+ const metricExporter = new __opentelemetry_exporter_metrics_otlp_grpc.OTLPMetricExporter({ url: endpoint });
303
+ const metricReader = new __opentelemetry_sdk_metrics.PeriodicExportingMetricReader({
304
+ exporter: metricExporter,
305
+ exportIntervalMillis: intervalMs
306
+ });
307
+ meterProvider = new __opentelemetry_sdk_metrics.MeterProvider({
308
+ resource,
309
+ readers: [metricReader]
310
+ });
311
+ __opentelemetry_api.metrics.setGlobalMeterProvider(meterProvider);
312
+ isInitialized = true;
313
+ }
314
+ /** Shutdown metrics gracefully */
315
+ async function shutdownMetrics() {
316
+ if (!meterProvider) return;
317
+ try {
318
+ await meterProvider.shutdown();
319
+ isInitialized = false;
320
+ meterProvider = null;
321
+ } catch (error) {
322
+ console.error("[neoiq-foundation] Error shutting down metrics:", error);
323
+ }
324
+ }
325
+ function getMeter(name, version = "1.0.0") {
326
+ return __opentelemetry_api.metrics.getMeter(name, version);
327
+ }
328
+ function isMetricsEnabled() {
329
+ return isInitialized;
330
+ }
331
+
332
+ //#endregion
333
+ //#region src/integrations/fastify-plugin.ts
334
+ const DEFAULT_REDACT_HEADERS = [
335
+ "authorization",
336
+ "cookie",
337
+ "x-api-key",
338
+ "x-auth-token",
339
+ "set-cookie"
340
+ ];
341
+ /** Redact sensitive headers */
342
+ function redactHeaders(headers, redactList) {
343
+ const redacted = {};
344
+ for (const [key, value] of Object.entries(headers)) if (redactList.includes(key.toLowerCase())) redacted[key] = "[REDACTED]";
345
+ else redacted[key] = value;
346
+ return redacted;
347
+ }
348
+ /** Truncate body if too large */
349
+ function truncateBody(body, maxSize) {
350
+ if (body === void 0 || body === null) return void 0;
351
+ const str = typeof body === "string" ? body : JSON.stringify(body);
352
+ if (str.length > maxSize) return `[TRUNCATED - ${str.length} bytes]`;
353
+ return body;
354
+ }
355
+ /** Create a configured Fastify observability plugin */
356
+ function createObservabilityPlugin(options) {
357
+ const plugin = async (fastify, pluginOpts) => {
358
+ const { serviceName, logger = getGlobalLogger(), contextManager, tracingEnabled = true, metricsEnabled = true, excludeRoutes = [
359
+ "/health",
360
+ "/health/",
361
+ "/healthz",
362
+ "/ready",
363
+ "/live"
364
+ ], requestLogging = {} } = {
365
+ ...options,
366
+ ...pluginOpts
367
+ };
368
+ const logHeaders = requestLogging.logHeaders ?? true;
369
+ const logBody = requestLogging.logBody ?? false;
370
+ const logResponseBody = requestLogging.logResponseBody ?? false;
371
+ const maxBodySize = requestLogging.maxBodySize ?? 10 * 1024;
372
+ const headersToRedact = requestLogging.redactHeaders ?? DEFAULT_REDACT_HEADERS;
373
+ const runInContext = contextManager ? (ctx, fn) => contextManager.run(ctx, fn) : (_ctx, fn) => fn();
374
+ const tracer = tracingEnabled ? __opentelemetry_api.trace.getTracer("neoiq-foundation-node") : null;
375
+ let requestCounter;
376
+ let requestDuration;
377
+ let requestErrors;
378
+ if (metricsEnabled) {
379
+ const meter = getMeter(serviceName);
380
+ requestCounter = meter.createCounter("http.server.requests.total");
381
+ requestDuration = meter.createHistogram("http.server.request.duration", { unit: "ms" });
382
+ requestErrors = meter.createCounter("http.server.requests.errors");
383
+ }
384
+ fastify.addHook("onRequest", (request, reply, done) => {
385
+ if (excludeRoutes.some((route) => request.url.startsWith(route))) {
386
+ done();
387
+ return;
388
+ }
389
+ const correlationId = request.headers["x-request-id"] || (0, crypto.randomUUID)();
390
+ reply.header("x-request-id", correlationId);
391
+ let span;
392
+ let traceId = "";
393
+ let spanId = "";
394
+ if (tracer) {
395
+ const parentContext = __opentelemetry_api.propagation.extract(__opentelemetry_api.context.active(), request.headers);
396
+ span = tracer.startSpan(`${request.method} ${request.routeOptions?.url || request.url}`, {
397
+ kind: 1,
398
+ attributes: {
399
+ "http.method": request.method,
400
+ "http.url": request.url,
401
+ "http.route": request.routeOptions?.url || request.url,
402
+ "http.user_agent": request.headers["user-agent"] || "",
403
+ "http.correlation_id": correlationId
404
+ }
405
+ }, parentContext);
406
+ const spanContext = span.spanContext();
407
+ traceId = spanContext.traceId;
408
+ spanId = spanContext.spanId;
409
+ }
410
+ const requestContext = {
411
+ correlationId,
412
+ traceId,
413
+ spanId,
414
+ startTime: Date.now()
415
+ };
416
+ request.__span = span;
417
+ request.__requestContext = requestContext;
418
+ runInContext(requestContext, () => {
419
+ const logData = {
420
+ correlationId,
421
+ traceId: traceId || void 0,
422
+ method: request.method,
423
+ url: request.url,
424
+ ip: request.ip,
425
+ userAgent: request.headers["user-agent"]
426
+ };
427
+ if (logHeaders) logData.headers = redactHeaders(request.headers, headersToRedact);
428
+ logger.info(logData, "Request received");
429
+ done();
430
+ });
431
+ });
432
+ if (logBody) fastify.addHook("preHandler", (request, _reply, done) => {
433
+ const ctx = request.__requestContext;
434
+ if (!ctx || !request.body) {
435
+ done();
436
+ return;
437
+ }
438
+ runInContext(ctx, () => {
439
+ logger.debug({
440
+ correlationId: ctx.correlationId,
441
+ body: truncateBody(request.body, maxBodySize)
442
+ }, "Request body");
443
+ done();
444
+ });
445
+ });
446
+ if (logResponseBody) fastify.addHook("onSend", (request, reply, payload, done) => {
447
+ const ctx = request.__requestContext;
448
+ if (!ctx) {
449
+ done(null, payload);
450
+ return;
451
+ }
452
+ runInContext(ctx, () => {
453
+ logger.debug({
454
+ correlationId: ctx.correlationId,
455
+ statusCode: reply.statusCode,
456
+ body: truncateBody(payload, maxBodySize)
457
+ }, "Response body");
458
+ done(null, payload);
459
+ });
460
+ });
461
+ fastify.addHook("onResponse", (request, reply, done) => {
462
+ const ctx = request.__requestContext;
463
+ const span = request.__span;
464
+ if (!ctx) {
465
+ done();
466
+ return;
467
+ }
468
+ const durationMs = Date.now() - ctx.startTime;
469
+ const route = request.routeOptions?.url || request.url;
470
+ const labels = {
471
+ method: request.method,
472
+ route,
473
+ status_code: String(reply.statusCode)
474
+ };
475
+ runInContext(ctx, () => {
476
+ logger.info({
477
+ correlationId: ctx.correlationId,
478
+ method: request.method,
479
+ statusCode: reply.statusCode,
480
+ durationMs
481
+ }, "Request completed");
482
+ if (metricsEnabled) {
483
+ requestCounter.add(1, labels);
484
+ requestDuration.record(durationMs, labels);
485
+ if (reply.statusCode >= 400) requestErrors.add(1, labels);
486
+ }
487
+ if (span) {
488
+ span.setStatus({ code: reply.statusCode < 400 ? __opentelemetry_api.SpanStatusCode.OK : __opentelemetry_api.SpanStatusCode.ERROR });
489
+ span.setAttribute("http.status_code", reply.statusCode);
490
+ span.setAttribute("http.response_time_ms", durationMs);
491
+ span.end();
492
+ }
493
+ done();
494
+ });
495
+ });
496
+ fastify.addHook("onError", (request, _reply, error, done) => {
497
+ const ctx = request.__requestContext;
498
+ const span = request.__span;
499
+ if (!ctx) {
500
+ done();
501
+ return;
502
+ }
503
+ runInContext(ctx, () => {
504
+ logger.error({
505
+ correlationId: ctx.correlationId,
506
+ method: request.method,
507
+ url: request.url,
508
+ error: error.message
509
+ }, "Request failed");
510
+ if (span) {
511
+ span.setStatus({
512
+ code: __opentelemetry_api.SpanStatusCode.ERROR,
513
+ message: error.message
514
+ });
515
+ span.recordException(error);
516
+ }
517
+ done();
518
+ });
519
+ });
520
+ };
521
+ return (0, fastify_plugin.default)(plugin, {
522
+ name: "neoiq-observability",
523
+ fastify: ">=4.x"
524
+ });
525
+ }
526
+
527
+ //#endregion
528
+ //#region src/integrations/http-client.ts
529
+ /** Create a configured HTTP client with full observability */
530
+ function createHttpClient(options) {
531
+ const { baseURL, serviceName, timeout = 3e4, retry = {}, circuitBreaker: cbOptions = {}, headers = {}, foundation } = options;
532
+ const logger = foundation?.logger || getGlobalLogger();
533
+ const getContext = () => foundation?.context.getContext();
534
+ const metricsEnabled = foundation?.features.metrics ?? true;
535
+ const tracingEnabled = foundation?.features.tracing ?? true;
536
+ const client = axios.default.create({
537
+ baseURL,
538
+ timeout,
539
+ headers: {
540
+ "Content-Type": "application/json",
541
+ ...headers
542
+ }
543
+ });
544
+ let requestCounter = null;
545
+ let requestDuration = null;
546
+ let requestErrors = null;
547
+ if (metricsEnabled) {
548
+ const meter = getMeter(`http-client-${serviceName}`);
549
+ requestCounter = meter.createCounter("http.client.requests.total");
550
+ requestDuration = meter.createHistogram("http.client.request.duration", { unit: "ms" });
551
+ requestErrors = meter.createCounter("http.client.requests.errors");
552
+ }
553
+ client.interceptors.request.use((config) => {
554
+ if (tracingEnabled) {
555
+ const carrier = {};
556
+ __opentelemetry_api.propagation.inject(__opentelemetry_api.context.active(), carrier);
557
+ if (carrier.traceparent) config.headers.set("traceparent", carrier.traceparent);
558
+ if (carrier.tracestate) config.headers.set("tracestate", carrier.tracestate);
559
+ }
560
+ const reqCtx = getContext();
561
+ if (reqCtx?.correlationId) config.headers.set("x-request-id", reqCtx.correlationId);
562
+ config.__startTime = Date.now();
563
+ logger.debug({
564
+ method: config.method?.toUpperCase(),
565
+ url: `${config.baseURL || ""}${config.url}`,
566
+ targetService: serviceName
567
+ }, "Outbound HTTP request");
568
+ return config;
569
+ });
570
+ client.interceptors.response.use((response) => {
571
+ const config = response.config;
572
+ const durationMs = Date.now() - (config.__startTime || Date.now());
573
+ const labels = {
574
+ target_service: serviceName,
575
+ method: config.method?.toUpperCase() || "GET",
576
+ status_code: String(response.status)
577
+ };
578
+ logger.debug({
579
+ method: config.method?.toUpperCase(),
580
+ statusCode: response.status,
581
+ durationMs,
582
+ targetService: serviceName
583
+ }, "Outbound HTTP response");
584
+ if (metricsEnabled) {
585
+ requestCounter?.add(1, labels);
586
+ requestDuration?.record(durationMs, labels);
587
+ }
588
+ return response;
589
+ }, (error) => {
590
+ const config = error.config;
591
+ const durationMs = config ? Date.now() - (config.__startTime || Date.now()) : 0;
592
+ const statusCode = error.response?.status || 0;
593
+ const labels = {
594
+ target_service: serviceName,
595
+ method: config?.method?.toUpperCase() || "GET",
596
+ status_code: String(statusCode)
597
+ };
598
+ logger.error({
599
+ method: config?.method?.toUpperCase(),
600
+ statusCode,
601
+ durationMs,
602
+ error: error.message,
603
+ targetService: serviceName
604
+ }, "Outbound HTTP error");
605
+ if (metricsEnabled) {
606
+ requestCounter?.add(1, labels);
607
+ requestDuration?.record(durationMs, labels);
608
+ requestErrors?.add(1, labels);
609
+ }
610
+ return Promise.reject(error);
611
+ });
612
+ const retryConfig = {
613
+ retries: retry.retries ?? 3,
614
+ retryDelay: (retryCount) => (retry.retryDelay ?? 1e3) * Math.pow(2, retryCount - 1),
615
+ retryCondition: (error) => {
616
+ const retryStatusCodes = retry.retryStatusCodes ?? [
617
+ 408,
618
+ 429,
619
+ 500,
620
+ 502,
621
+ 503,
622
+ 504
623
+ ];
624
+ return !error.response || retryStatusCodes.includes(error.response?.status || 0);
625
+ },
626
+ onRetry: (retryCount, error, requestConfig) => {
627
+ logger.warn({
628
+ retryCount,
629
+ url: `${requestConfig.baseURL || ""}${requestConfig.url}`,
630
+ error: error.message
631
+ }, "Retrying request");
632
+ }
633
+ };
634
+ (0, axios_retry.default)(client, retryConfig);
635
+ if (cbOptions.enabled !== false) {
636
+ const breaker = new opossum.default(async (config) => client.request(config), {
637
+ timeout,
638
+ resetTimeout: cbOptions.resetTimeout ?? 3e4,
639
+ errorThresholdPercentage: cbOptions.errorThresholdPercentage ?? 50,
640
+ volumeThreshold: 10
641
+ });
642
+ breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
643
+ breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
644
+ breaker.on("close", () => logger.info({ targetService: serviceName }, "Circuit breaker CLOSED"));
645
+ }
646
+ return client;
647
+ }
648
+
649
+ //#endregion
650
+ //#region src/foundation.ts
651
+ /** Create a fully configured observability foundation */
652
+ function createFoundation(input) {
653
+ const startTime = Date.now();
654
+ const config = parseConfig(input);
655
+ const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig } = config;
656
+ const features = {
657
+ tracing: featuresConfig.tracing ?? true,
658
+ metrics: featuresConfig.metrics ?? true,
659
+ logging: featuresConfig.logging ?? true
660
+ };
661
+ const contextManager = createContextManager();
662
+ let logger;
663
+ let loggingError;
664
+ try {
665
+ logger = createLogger({
666
+ serviceName,
667
+ serviceVersion,
668
+ environment,
669
+ level: loggingConfig.level ?? "info",
670
+ prettyPrint: loggingConfig.prettyPrint ?? environment === "development",
671
+ contextManager
672
+ });
673
+ setGlobalLogger(logger);
674
+ } catch (err) {
675
+ loggingError = err.message;
676
+ logger = {
677
+ debug: (obj, msg) => console.debug(JSON.stringify({
678
+ ...obj,
679
+ msg
680
+ })),
681
+ info: (obj, msg) => console.info(JSON.stringify({
682
+ ...obj,
683
+ msg
684
+ })),
685
+ warn: (obj, msg) => console.warn(JSON.stringify({
686
+ ...obj,
687
+ msg
688
+ })),
689
+ error: (obj, msg) => console.error(JSON.stringify({
690
+ ...obj,
691
+ msg
692
+ })),
693
+ child: () => logger,
694
+ pino: null
695
+ };
696
+ console.error("[neoiq-foundation] Logger setup failed, using console:", loggingError);
697
+ }
698
+ let tracerInstance = null;
699
+ let tracingError;
700
+ if (features.tracing) try {
701
+ setupTracing({
702
+ serviceName,
703
+ serviceVersion,
704
+ environment,
705
+ endpoint: otel.endpoint,
706
+ sampleRate: otel.traceSampleRate,
707
+ autoInstrumentation: featuresConfig.autoInstrumentation
708
+ });
709
+ tracerInstance = getTracer(serviceName);
710
+ logger.info({
711
+ feature: "tracing",
712
+ endpoint: otel.endpoint
713
+ }, "Tracing enabled");
714
+ } catch (err) {
715
+ tracingError = err.message;
716
+ logger.error({ error: tracingError }, "Tracing setup failed - continuing without tracing");
717
+ }
718
+ let meterInstance = null;
719
+ let metricsError;
720
+ if (features.metrics) try {
721
+ setupMetrics({
722
+ serviceName,
723
+ serviceVersion,
724
+ environment,
725
+ endpoint: otel.endpoint,
726
+ intervalMs: otel.metricsIntervalMs
727
+ });
728
+ meterInstance = getMeter(serviceName);
729
+ logger.info({
730
+ feature: "metrics",
731
+ interval: `${otel.metricsIntervalMs}ms`
732
+ }, "Metrics enabled");
733
+ } catch (err) {
734
+ metricsError = err.message;
735
+ logger.error({ error: metricsError }, "Metrics setup failed - continuing without metrics");
736
+ }
737
+ logger.info({
738
+ serviceName,
739
+ serviceVersion,
740
+ environment,
741
+ features
742
+ }, "Foundation initialized");
743
+ const foundation = {
744
+ config,
745
+ logger,
746
+ context: contextManager,
747
+ tracer: tracerInstance,
748
+ meter: meterInstance,
749
+ features,
750
+ getTracer: (name) => getTracer(name || serviceName),
751
+ getMeter: (name, version) => getMeter(name, version),
752
+ getTraceContext,
753
+ getActiveSpan,
754
+ fastifyPlugin: createObservabilityPlugin({
755
+ serviceName,
756
+ logger,
757
+ contextManager,
758
+ tracingEnabled: features.tracing && !tracingError,
759
+ metricsEnabled: features.metrics && !metricsError,
760
+ requestLogging: requestLoggingConfig
761
+ }),
762
+ createHttpClient: (options) => createHttpClient({
763
+ ...options,
764
+ foundation
765
+ }),
766
+ shutdown: async () => {
767
+ logger.info({}, "Shutting down foundation...");
768
+ const promises = [];
769
+ if (features.tracing && isTracingEnabled()) promises.push(shutdownTracing());
770
+ if (features.metrics && isMetricsEnabled()) promises.push(shutdownMetrics());
771
+ await Promise.all(promises);
772
+ logger.info({}, "Foundation shutdown complete");
773
+ },
774
+ isReady: () => {
775
+ if (features.tracing && !tracingError && !isTracingEnabled()) return false;
776
+ if (features.metrics && !metricsError && !isMetricsEnabled()) return false;
777
+ return true;
778
+ },
779
+ health: () => {
780
+ const tracingUp = !features.tracing || !tracingError && isTracingEnabled();
781
+ const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
782
+ const loggingUp = !loggingError;
783
+ const allUp = tracingUp && metricsUp && loggingUp;
784
+ const anyDown = features.tracing && tracingError || features.metrics && metricsError || loggingError;
785
+ return {
786
+ status: allUp ? "healthy" : anyDown ? "degraded" : "unhealthy",
787
+ timestamp: new Date().toISOString(),
788
+ service: serviceName,
789
+ version: serviceVersion,
790
+ uptime: Math.floor((Date.now() - startTime) / 1e3),
791
+ components: {
792
+ tracing: {
793
+ enabled: features.tracing,
794
+ status: !features.tracing ? "disabled" : tracingError ? "down" : "up",
795
+ message: tracingError
796
+ },
797
+ metrics: {
798
+ enabled: features.metrics,
799
+ status: !features.metrics ? "disabled" : metricsError ? "down" : "up",
800
+ message: metricsError
801
+ },
802
+ logging: {
803
+ enabled: features.logging,
804
+ status: loggingError ? "down" : "up",
805
+ message: loggingError
806
+ }
807
+ }
808
+ };
809
+ },
810
+ trace: async (name, fn) => {
811
+ const tracer = tracerInstance || getTracer(serviceName);
812
+ return new Promise((resolve, reject) => {
813
+ tracer.startActiveSpan(name, async (span) => {
814
+ try {
815
+ const result = await fn();
816
+ span.setStatus({ code: __opentelemetry_api.SpanStatusCode.OK });
817
+ span.end();
818
+ resolve(result);
819
+ } catch (err) {
820
+ const error = err;
821
+ span.setStatus({
822
+ code: __opentelemetry_api.SpanStatusCode.ERROR,
823
+ message: error.message
824
+ });
825
+ span.recordException(error);
826
+ span.end();
827
+ logger.error({
828
+ span: name,
829
+ error: error.message
830
+ }, "Span failed");
831
+ reject(error);
832
+ }
833
+ });
834
+ });
835
+ },
836
+ safeRun: async (fn, fallback) => {
837
+ try {
838
+ return await fn();
839
+ } catch (err) {
840
+ const error = err;
841
+ logger.error({
842
+ error: error.message,
843
+ stack: error.stack
844
+ }, "safeRun caught error");
845
+ return fallback;
846
+ }
847
+ }
848
+ };
849
+ return foundation;
850
+ }
851
+ /** Alias for createFoundation */
852
+ const setupObservability = createFoundation;
853
+
854
+ //#endregion
855
+ exports.AutoInstrumentationConfigSchema = AutoInstrumentationConfigSchema
856
+ exports.FeaturesConfigSchema = FeaturesConfigSchema
857
+ exports.FoundationConfigSchema = FoundationConfigSchema
858
+ exports.LoggingConfigSchema = LoggingConfigSchema
859
+ exports.OtelConfigSchema = OtelConfigSchema
860
+ exports.RequestLoggingConfigSchema = RequestLoggingConfigSchema
861
+ exports.SpanStatusCode = __opentelemetry_api.SpanStatusCode
862
+ exports.context = __opentelemetry_api.context
863
+ exports.createContextManager = createContextManager
864
+ exports.createFallbackLogger = createFallbackLogger
865
+ exports.createFoundation = createFoundation
866
+ exports.createHttpClient = createHttpClient
867
+ exports.createLogger = createLogger
868
+ exports.createObservabilityPlugin = createObservabilityPlugin
869
+ exports.getActiveSpan = getActiveSpan
870
+ exports.getDefaultOtelEndpoint = getDefaultOtelEndpoint
871
+ exports.getGlobalLogger = getGlobalLogger
872
+ exports.getMeter = getMeter
873
+ exports.getTraceContext = getTraceContext
874
+ exports.getTracer = getTracer
875
+ exports.isMetricsEnabled = isMetricsEnabled
876
+ exports.isTracingEnabled = isTracingEnabled
877
+ exports.metrics = __opentelemetry_api.metrics
878
+ exports.parseConfig = parseConfig
879
+ exports.propagation = __opentelemetry_api.propagation
880
+ exports.setGlobalLogger = setGlobalLogger
881
+ exports.setupMetrics = setupMetrics
882
+ exports.setupObservability = setupObservability
883
+ exports.setupTracing = setupTracing
884
+ exports.shutdownMetrics = shutdownMetrics
885
+ exports.shutdownTracing = shutdownTracing
886
+ exports.trace = __opentelemetry_api.trace
30
887
  //# sourceMappingURL=index.js.map