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