@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.mjs ADDED
@@ -0,0 +1,839 @@
1
+ import { z } from "zod";
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";
8
+ import { resourceFromAttributes } from "@opentelemetry/resources";
9
+ import { ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION } from "@opentelemetry/semantic-conventions";
10
+ import { OTLPMetricExporter } from "@opentelemetry/exporter-metrics-otlp-grpc";
11
+ import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
12
+ import fp from "fastify-plugin";
13
+ import { randomUUID } from "crypto";
14
+ import axios from "axios";
15
+ import axiosRetry from "axios-retry";
16
+ import CircuitBreaker from "opossum";
17
+
18
+ //#region src/config.ts
19
+ const AutoInstrumentationConfigSchema = z.object({
20
+ http: z.boolean().default(true),
21
+ fastify: z.boolean().default(true),
22
+ express: z.boolean().default(true),
23
+ mongodb: z.boolean().default(true),
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;
82
+ }
83
+ /** Get default OTEL endpoint */
84
+ function getDefaultOtelEndpoint() {
85
+ return process.env.OTEL_EXPORTER_OTLP_ENDPOINT || DEFAULT_OTEL_ENDPOINT;
86
+ }
87
+
88
+ //#endregion
89
+ //#region src/features/context.ts
90
+ /** Create a new context manager instance */
91
+ function createContextManager() {
92
+ const als = new AsyncLocalStorage();
93
+ return {
94
+ getContext: () => als.getStore(),
95
+ run(context$2, fn) {
96
+ return als.run(context$2, fn);
97
+ },
98
+ get(key) {
99
+ return als.getStore()?.[key];
100
+ },
101
+ update(updates) {
102
+ const current = als.getStore();
103
+ if (!current) return void 0;
104
+ Object.assign(current, updates);
105
+ return current;
106
+ }
107
+ };
108
+ }
109
+
110
+ //#endregion
111
+ //#region src/features/logging.ts
112
+ /** Create a structured logger with automatic trace context injection */
113
+ function createLogger(options) {
114
+ const { serviceName, serviceVersion, environment, level, prettyPrint, contextManager } = options;
115
+ const pinoLogger = pino({
116
+ level,
117
+ base: {
118
+ service: serviceName,
119
+ version: serviceVersion,
120
+ env: environment
121
+ },
122
+ mixin: () => {
123
+ const span = trace$1.getActiveSpan();
124
+ const spanContext = span?.spanContext();
125
+ const ctx = contextManager?.getContext();
126
+ const traceId = spanContext?.traceId || ctx?.traceId;
127
+ const spanId = spanContext?.spanId || ctx?.spanId;
128
+ const correlationId = ctx?.correlationId;
129
+ return {
130
+ trace_id: traceId,
131
+ span_id: spanId,
132
+ correlation_id: correlationId
133
+ };
134
+ },
135
+ formatters: { level: (label) => ({ level: label }) },
136
+ transport: prettyPrint ? {
137
+ target: "pino-pretty",
138
+ options: {
139
+ colorize: true,
140
+ translateTime: "SYS:standard",
141
+ ignore: "pid,hostname"
142
+ }
143
+ } : void 0
144
+ });
145
+ return wrapPinoLogger(pinoLogger);
146
+ }
147
+ function wrapPinoLogger(pinoLogger) {
148
+ return {
149
+ debug: (obj, msg) => pinoLogger.debug(obj, msg),
150
+ info: (obj, msg) => pinoLogger.info(obj, msg),
151
+ warn: (obj, msg) => pinoLogger.warn(obj, msg),
152
+ error: (obj, msg) => pinoLogger.error(obj, msg),
153
+ child: (bindings) => wrapPinoLogger(pinoLogger.child(bindings)),
154
+ pino: pinoLogger
155
+ };
156
+ }
157
+ /** Fallback logger when Pino is not available */
158
+ function createFallbackLogger(serviceName = "unknown") {
159
+ const log = (level, obj, msg) => {
160
+ const logObj = {
161
+ timestamp: new Date().toISOString(),
162
+ level,
163
+ service: serviceName,
164
+ ...obj,
165
+ msg
166
+ };
167
+ const payload = JSON.stringify(logObj);
168
+ if (level === "warn") console.warn(payload);
169
+ else if (level === "error") console.error(payload);
170
+ else console.log(payload);
171
+ };
172
+ const fallback = {
173
+ debug: (obj, msg) => log("debug", obj, msg),
174
+ info: (obj, msg) => log("info", obj, msg),
175
+ warn: (obj, msg) => log("warn", obj, msg),
176
+ error: (obj, msg) => log("error", obj, msg),
177
+ child: () => fallback,
178
+ pino: null
179
+ };
180
+ return fallback;
181
+ }
182
+ let globalLogger = null;
183
+ function setGlobalLogger(logger) {
184
+ globalLogger = logger;
185
+ }
186
+ function getGlobalLogger() {
187
+ return globalLogger || createFallbackLogger();
188
+ }
189
+
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
+ //#endregion
269
+ //#region src/features/metrics.ts
270
+ let meterProvider = null;
271
+ let isInitialized = false;
272
+ /** Initialize OpenTelemetry metrics */
273
+ function setupMetrics(options) {
274
+ if (isInitialized) {
275
+ console.warn("[neoiq-foundation] Metrics already initialized");
276
+ return;
277
+ }
278
+ const { serviceName, serviceVersion, environment, endpoint = getDefaultOtelEndpoint(), intervalMs = 5e3 } = options;
279
+ const resource = resourceFromAttributes({
280
+ [ATTR_SERVICE_NAME]: serviceName,
281
+ [ATTR_SERVICE_VERSION]: serviceVersion,
282
+ "deployment.environment": environment
283
+ });
284
+ const metricExporter = new OTLPMetricExporter({ url: endpoint });
285
+ const metricReader = new PeriodicExportingMetricReader({
286
+ exporter: metricExporter,
287
+ exportIntervalMillis: intervalMs
288
+ });
289
+ meterProvider = new MeterProvider({
290
+ resource,
291
+ readers: [metricReader]
292
+ });
293
+ metrics.setGlobalMeterProvider(meterProvider);
294
+ isInitialized = true;
295
+ }
296
+ /** Shutdown metrics gracefully */
297
+ async function shutdownMetrics() {
298
+ if (!meterProvider) return;
299
+ try {
300
+ await meterProvider.shutdown();
301
+ isInitialized = false;
302
+ meterProvider = null;
303
+ } catch (error) {
304
+ console.error("[neoiq-foundation] Error shutting down metrics:", error);
305
+ }
306
+ }
307
+ function getMeter(name, version = "1.0.0") {
308
+ return metrics.getMeter(name, version);
309
+ }
310
+ function isMetricsEnabled() {
311
+ return isInitialized;
312
+ }
313
+
314
+ //#endregion
315
+ //#region src/integrations/fastify-plugin.ts
316
+ const DEFAULT_REDACT_HEADERS = [
317
+ "authorization",
318
+ "cookie",
319
+ "x-api-key",
320
+ "x-auth-token",
321
+ "set-cookie"
322
+ ];
323
+ /** Redact sensitive headers */
324
+ function redactHeaders(headers, redactList) {
325
+ const redacted = {};
326
+ for (const [key, value] of Object.entries(headers)) if (redactList.includes(key.toLowerCase())) redacted[key] = "[REDACTED]";
327
+ else redacted[key] = value;
328
+ return redacted;
329
+ }
330
+ /** Truncate body if too large */
331
+ function truncateBody(body, maxSize) {
332
+ if (body === void 0 || body === null) return void 0;
333
+ const str = typeof body === "string" ? body : JSON.stringify(body);
334
+ if (str.length > maxSize) return `[TRUNCATED - ${str.length} bytes]`;
335
+ return body;
336
+ }
337
+ /** Create a configured Fastify observability plugin */
338
+ function createObservabilityPlugin(options) {
339
+ const plugin = async (fastify, pluginOpts) => {
340
+ const { serviceName, logger = getGlobalLogger(), contextManager, tracingEnabled = true, metricsEnabled = true, excludeRoutes = [
341
+ "/health",
342
+ "/health/",
343
+ "/healthz",
344
+ "/ready",
345
+ "/live"
346
+ ], requestLogging = {} } = {
347
+ ...options,
348
+ ...pluginOpts
349
+ };
350
+ const logHeaders = requestLogging.logHeaders ?? true;
351
+ const logBody = requestLogging.logBody ?? false;
352
+ const logResponseBody = requestLogging.logResponseBody ?? false;
353
+ const maxBodySize = requestLogging.maxBodySize ?? 10 * 1024;
354
+ const headersToRedact = requestLogging.redactHeaders ?? DEFAULT_REDACT_HEADERS;
355
+ const runInContext = contextManager ? (ctx, fn) => contextManager.run(ctx, fn) : (_ctx, fn) => fn();
356
+ const tracer = tracingEnabled ? trace$1.getTracer("neoiq-foundation-node") : null;
357
+ let requestCounter;
358
+ let requestDuration;
359
+ let requestErrors;
360
+ if (metricsEnabled) {
361
+ const meter = getMeter(serviceName);
362
+ requestCounter = meter.createCounter("http.server.requests.total");
363
+ requestDuration = meter.createHistogram("http.server.request.duration", { unit: "ms" });
364
+ requestErrors = meter.createCounter("http.server.requests.errors");
365
+ }
366
+ fastify.addHook("onRequest", (request, reply, done) => {
367
+ if (excludeRoutes.some((route) => request.url.startsWith(route))) {
368
+ done();
369
+ return;
370
+ }
371
+ const correlationId = request.headers["x-request-id"] || randomUUID();
372
+ reply.header("x-request-id", correlationId);
373
+ let span;
374
+ let traceId = "";
375
+ let spanId = "";
376
+ if (tracer) {
377
+ const parentContext = propagation$1.extract(context$1.active(), request.headers);
378
+ span = tracer.startSpan(`${request.method} ${request.routeOptions?.url || request.url}`, {
379
+ kind: 1,
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();
389
+ traceId = spanContext.traceId;
390
+ spanId = spanContext.spanId;
391
+ }
392
+ const requestContext = {
393
+ correlationId,
394
+ traceId,
395
+ spanId,
396
+ startTime: Date.now()
397
+ };
398
+ request.__span = span;
399
+ request.__requestContext = requestContext;
400
+ runInContext(requestContext, () => {
401
+ const logData = {
402
+ correlation_id: correlationId,
403
+ trace_id: traceId || void 0,
404
+ span_id: spanId || void 0,
405
+ method: request.method,
406
+ url: request.url,
407
+ ip: request.ip,
408
+ userAgent: request.headers["user-agent"]
409
+ };
410
+ if (logHeaders) logData.headers = redactHeaders(request.headers, headersToRedact);
411
+ logger.info(logData, "Request received");
412
+ done();
413
+ });
414
+ });
415
+ if (logBody) fastify.addHook("preHandler", (request, _reply, done) => {
416
+ const ctx = request.__requestContext;
417
+ if (!ctx || !request.body) {
418
+ done();
419
+ return;
420
+ }
421
+ runInContext(ctx, () => {
422
+ logger.debug({
423
+ correlation_id: ctx.correlationId,
424
+ body: truncateBody(request.body, maxBodySize)
425
+ }, "Request body");
426
+ done();
427
+ });
428
+ });
429
+ if (logResponseBody) fastify.addHook("onSend", (request, reply, payload, done) => {
430
+ const ctx = request.__requestContext;
431
+ if (!ctx) {
432
+ done(null, payload);
433
+ return;
434
+ }
435
+ runInContext(ctx, () => {
436
+ logger.debug({
437
+ correlation_id: ctx.correlationId,
438
+ statusCode: reply.statusCode,
439
+ body: truncateBody(payload, maxBodySize)
440
+ }, "Response body");
441
+ done(null, payload);
442
+ });
443
+ });
444
+ fastify.addHook("onResponse", (request, reply, done) => {
445
+ const ctx = request.__requestContext;
446
+ const span = request.__span;
447
+ if (!ctx) {
448
+ done();
449
+ return;
450
+ }
451
+ const durationMs = Date.now() - ctx.startTime;
452
+ const route = request.routeOptions?.url || request.url;
453
+ const labels = {
454
+ method: request.method,
455
+ route,
456
+ status_code: String(reply.statusCode)
457
+ };
458
+ runInContext(ctx, () => {
459
+ logger.info({
460
+ correlation_id: ctx.correlationId,
461
+ method: request.method,
462
+ statusCode: reply.statusCode,
463
+ durationMs
464
+ }, "Request completed");
465
+ if (metricsEnabled) {
466
+ requestCounter.add(1, labels);
467
+ requestDuration.record(durationMs, labels);
468
+ if (reply.statusCode >= 400) requestErrors.add(1, labels);
469
+ }
470
+ if (span) {
471
+ span.setStatus({ code: reply.statusCode < 400 ? SpanStatusCode$1.OK : SpanStatusCode$1.ERROR });
472
+ span.setAttribute("http.status_code", reply.statusCode);
473
+ span.setAttribute("http.response_time_ms", durationMs);
474
+ span.end();
475
+ }
476
+ done();
477
+ });
478
+ });
479
+ fastify.addHook("onError", (request, _reply, error, done) => {
480
+ const ctx = request.__requestContext;
481
+ const span = request.__span;
482
+ if (!ctx) {
483
+ done();
484
+ return;
485
+ }
486
+ runInContext(ctx, () => {
487
+ logger.error({
488
+ correlation_id: ctx.correlationId,
489
+ method: request.method,
490
+ url: request.url,
491
+ error: error.message
492
+ }, "Request failed");
493
+ if (span) {
494
+ span.setStatus({
495
+ code: SpanStatusCode$1.ERROR,
496
+ message: error.message
497
+ });
498
+ span.recordException(error);
499
+ }
500
+ done();
501
+ });
502
+ });
503
+ };
504
+ return fp(plugin, {
505
+ name: "neoiq-observability",
506
+ fastify: ">=4.x"
507
+ });
508
+ }
509
+
510
+ //#endregion
511
+ //#region src/integrations/http-client.ts
512
+ /** Create a configured HTTP client with full observability */
513
+ function createHttpClient(options) {
514
+ const { baseURL, serviceName, timeout = 3e4, retry = {}, circuitBreaker: cbOptions = {}, headers = {}, foundation } = options;
515
+ const logger = foundation?.logger || getGlobalLogger();
516
+ const getContext = () => foundation?.context.getContext();
517
+ const metricsEnabled = foundation?.features.metrics ?? true;
518
+ const tracingEnabled = foundation?.features.tracing ?? true;
519
+ const client = axios.create({
520
+ baseURL,
521
+ timeout,
522
+ headers: {
523
+ "Content-Type": "application/json",
524
+ ...headers
525
+ }
526
+ });
527
+ let requestCounter = null;
528
+ let requestDuration = null;
529
+ let requestErrors = null;
530
+ if (metricsEnabled) {
531
+ const meter = getMeter(`http-client-${serviceName}`);
532
+ requestCounter = meter.createCounter("http.client.requests.total");
533
+ requestDuration = meter.createHistogram("http.client.request.duration", { unit: "ms" });
534
+ requestErrors = meter.createCounter("http.client.requests.errors");
535
+ }
536
+ client.interceptors.request.use((config) => {
537
+ if (tracingEnabled) {
538
+ const carrier = {};
539
+ propagation$1.inject(context$1.active(), carrier);
540
+ if (carrier.traceparent) config.headers.set("traceparent", carrier.traceparent);
541
+ if (carrier.tracestate) config.headers.set("tracestate", carrier.tracestate);
542
+ }
543
+ const reqCtx = getContext();
544
+ if (reqCtx?.correlationId) config.headers.set("x-request-id", reqCtx.correlationId);
545
+ config.__startTime = Date.now();
546
+ logger.debug({
547
+ method: config.method?.toUpperCase(),
548
+ url: `${config.baseURL || ""}${config.url}`,
549
+ targetService: serviceName
550
+ }, "Outbound HTTP request");
551
+ return config;
552
+ });
553
+ client.interceptors.response.use((response) => {
554
+ const config = response.config;
555
+ const durationMs = Date.now() - (config.__startTime || Date.now());
556
+ const labels = {
557
+ target_service: serviceName,
558
+ method: config.method?.toUpperCase() || "GET",
559
+ status_code: String(response.status)
560
+ };
561
+ logger.debug({
562
+ method: config.method?.toUpperCase(),
563
+ statusCode: response.status,
564
+ durationMs,
565
+ targetService: serviceName
566
+ }, "Outbound HTTP response");
567
+ if (metricsEnabled) {
568
+ requestCounter?.add(1, labels);
569
+ requestDuration?.record(durationMs, labels);
570
+ }
571
+ return response;
572
+ }, (error) => {
573
+ const config = error.config;
574
+ const durationMs = config ? Date.now() - (config.__startTime || Date.now()) : 0;
575
+ const statusCode = error.response?.status || 0;
576
+ const labels = {
577
+ target_service: serviceName,
578
+ method: config?.method?.toUpperCase() || "GET",
579
+ status_code: String(statusCode)
580
+ };
581
+ logger.error({
582
+ method: config?.method?.toUpperCase(),
583
+ statusCode,
584
+ durationMs,
585
+ error: error.message,
586
+ targetService: serviceName
587
+ }, "Outbound HTTP error");
588
+ if (metricsEnabled) {
589
+ requestCounter?.add(1, labels);
590
+ requestDuration?.record(durationMs, labels);
591
+ requestErrors?.add(1, labels);
592
+ }
593
+ return Promise.reject(error);
594
+ });
595
+ const retryConfig = {
596
+ retries: retry.retries ?? 3,
597
+ retryDelay: (retryCount) => (retry.retryDelay ?? 1e3) * Math.pow(2, retryCount - 1),
598
+ retryCondition: (error) => {
599
+ const retryStatusCodes = retry.retryStatusCodes ?? [
600
+ 408,
601
+ 429,
602
+ 500,
603
+ 502,
604
+ 503,
605
+ 504
606
+ ];
607
+ return !error.response || retryStatusCodes.includes(error.response?.status || 0);
608
+ },
609
+ onRetry: (retryCount, error, requestConfig) => {
610
+ logger.warn({
611
+ retryCount,
612
+ url: `${requestConfig.baseURL || ""}${requestConfig.url}`,
613
+ error: error.message
614
+ }, "Retrying request");
615
+ }
616
+ };
617
+ axiosRetry(client, retryConfig);
618
+ if (cbOptions.enabled !== false) {
619
+ const breaker = new CircuitBreaker(async (config) => client.request(config), {
620
+ timeout,
621
+ resetTimeout: cbOptions.resetTimeout ?? 3e4,
622
+ errorThresholdPercentage: cbOptions.errorThresholdPercentage ?? 50,
623
+ volumeThreshold: 10
624
+ });
625
+ breaker.on("open", () => logger.warn({ targetService: serviceName }, "Circuit breaker OPEN"));
626
+ breaker.on("halfOpen", () => logger.info({ targetService: serviceName }, "Circuit breaker HALF-OPEN"));
627
+ breaker.on("close", () => logger.info({ targetService: serviceName }, "Circuit breaker CLOSED"));
628
+ }
629
+ return client;
630
+ }
631
+
632
+ //#endregion
633
+ //#region src/foundation.ts
634
+ /** Create a fully configured observability foundation */
635
+ function createFoundation(input) {
636
+ const startTime = Date.now();
637
+ const config = parseConfig(input);
638
+ const { serviceName, serviceVersion, environment, features: featuresConfig, otel, logging: loggingConfig, requestLogging: requestLoggingConfig } = config;
639
+ const features = {
640
+ tracing: featuresConfig.tracing ?? true,
641
+ metrics: featuresConfig.metrics ?? true,
642
+ logging: featuresConfig.logging ?? true
643
+ };
644
+ const contextManager = createContextManager();
645
+ let logger;
646
+ let loggingError;
647
+ try {
648
+ logger = createLogger({
649
+ serviceName,
650
+ serviceVersion,
651
+ environment,
652
+ level: loggingConfig.level ?? "info",
653
+ prettyPrint: loggingConfig.prettyPrint ?? environment === "development",
654
+ contextManager
655
+ });
656
+ setGlobalLogger(logger);
657
+ } catch (err) {
658
+ loggingError = err.message;
659
+ logger = {
660
+ debug: (obj, msg) => console.debug(JSON.stringify({
661
+ ...obj,
662
+ msg
663
+ })),
664
+ info: (obj, msg) => console.info(JSON.stringify({
665
+ ...obj,
666
+ msg
667
+ })),
668
+ warn: (obj, msg) => console.warn(JSON.stringify({
669
+ ...obj,
670
+ msg
671
+ })),
672
+ error: (obj, msg) => console.error(JSON.stringify({
673
+ ...obj,
674
+ msg
675
+ })),
676
+ child: () => logger,
677
+ pino: null
678
+ };
679
+ console.error("[neoiq-foundation] Logger setup failed, using console:", loggingError);
680
+ }
681
+ let tracerInstance = null;
682
+ let tracingError;
683
+ if (features.tracing) try {
684
+ setupTracing({
685
+ serviceName,
686
+ serviceVersion,
687
+ environment,
688
+ endpoint: otel.endpoint,
689
+ sampleRate: otel.traceSampleRate,
690
+ autoInstrumentation: featuresConfig.autoInstrumentation
691
+ });
692
+ tracerInstance = getTracer(serviceName);
693
+ logger.info({
694
+ feature: "tracing",
695
+ endpoint: otel.endpoint
696
+ }, "Tracing enabled");
697
+ } catch (err) {
698
+ tracingError = err.message;
699
+ logger.error({ error: tracingError }, "Tracing setup failed - continuing without tracing");
700
+ }
701
+ let meterInstance = null;
702
+ let metricsError;
703
+ if (features.metrics) try {
704
+ setupMetrics({
705
+ serviceName,
706
+ serviceVersion,
707
+ environment,
708
+ endpoint: otel.endpoint,
709
+ intervalMs: otel.metricsIntervalMs
710
+ });
711
+ meterInstance = getMeter(serviceName);
712
+ logger.info({
713
+ feature: "metrics",
714
+ interval: `${otel.metricsIntervalMs}ms`
715
+ }, "Metrics enabled");
716
+ } catch (err) {
717
+ metricsError = err.message;
718
+ logger.error({ error: metricsError }, "Metrics setup failed - continuing without metrics");
719
+ }
720
+ logger.info({
721
+ serviceName,
722
+ serviceVersion,
723
+ environment,
724
+ features
725
+ }, "Foundation initialized");
726
+ const foundation = {
727
+ config,
728
+ logger,
729
+ context: contextManager,
730
+ tracer: tracerInstance,
731
+ meter: meterInstance,
732
+ features,
733
+ getTracer: (name) => getTracer(name || serviceName),
734
+ getMeter: (name, version) => getMeter(name, version),
735
+ getTraceContext,
736
+ getActiveSpan,
737
+ fastifyPlugin: createObservabilityPlugin({
738
+ serviceName,
739
+ logger,
740
+ contextManager,
741
+ tracingEnabled: features.tracing && !tracingError,
742
+ metricsEnabled: features.metrics && !metricsError,
743
+ requestLogging: requestLoggingConfig
744
+ }),
745
+ createHttpClient: (options) => createHttpClient({
746
+ ...options,
747
+ foundation
748
+ }),
749
+ shutdown: async () => {
750
+ logger.info({}, "Shutting down foundation...");
751
+ const promises = [];
752
+ if (features.tracing && isTracingEnabled()) promises.push(shutdownTracing());
753
+ if (features.metrics && isMetricsEnabled()) promises.push(shutdownMetrics());
754
+ await Promise.all(promises);
755
+ logger.info({}, "Foundation shutdown complete");
756
+ },
757
+ isReady: () => {
758
+ if (features.tracing && !tracingError && !isTracingEnabled()) return false;
759
+ if (features.metrics && !metricsError && !isMetricsEnabled()) return false;
760
+ return true;
761
+ },
762
+ health: () => {
763
+ const tracingUp = !features.tracing || !tracingError && isTracingEnabled();
764
+ const metricsUp = !features.metrics || !metricsError && isMetricsEnabled();
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
+ };
792
+ },
793
+ trace: async (name, fn) => {
794
+ const tracer = tracerInstance || getTracer(serviceName);
795
+ return new Promise((resolve, reject) => {
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
+ });
818
+ },
819
+ safeRun: async (fn, fallback) => {
820
+ try {
821
+ return await fn();
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
+ }
830
+ }
831
+ };
832
+ return foundation;
833
+ }
834
+ /** Alias for createFoundation */
835
+ const setupObservability = createFoundation;
836
+
837
+ //#endregion
838
+ export { AutoInstrumentationConfigSchema, FeaturesConfigSchema, FoundationConfigSchema, LoggingConfigSchema, OtelConfigSchema, RequestLoggingConfigSchema, SpanStatusCode, context, createContextManager, createFallbackLogger, createFoundation, createHttpClient, createLogger, createObservabilityPlugin, getActiveSpan, getDefaultOtelEndpoint, getGlobalLogger, getMeter, getTraceContext, getTracer, isMetricsEnabled, isTracingEnabled, metrics, parseConfig, propagation, setGlobalLogger, setupMetrics, setupObservability, setupTracing, shutdownMetrics, shutdownTracing, trace };
839
+ //# sourceMappingURL=index.mjs.map