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

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