@ciq-dev/neoiq-foundation-node 1.0.0-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/README.md ADDED
@@ -0,0 +1,293 @@
1
+ # @ciq-dev/neoiq-foundation-node
2
+
3
+ Node.js observability foundation for CommerceIQ services. Integrates with **Groundcover** via **OpenTelemetry**.
4
+
5
+ ```
6
+ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
7
+ │ Our Service │────▶│ OTEL Collector │────▶│ Groundcover │
8
+ │ (auth-service) │ │ (in-cluster) │ │ (dashboard) │
9
+ └─────────────────┘ └─────────────────┘ └─────────────────┘
10
+ │ │
11
+ ├── Traces (OTLP) ───────┤
12
+ └── Metrics (OTLP) ──────┘
13
+
14
+
15
+ └── Logs (stdout) ──────▶ Groundcover Agent scrapes container logs
16
+ ```
17
+
18
+ ## Features
19
+
20
+ - **Traces**: Automatic span creation for HTTP requests (incoming & outgoing)
21
+ - **Metrics**: HTTP request counts, durations, errors + custom business metrics
22
+ - **Logs**: Structured JSON logs with automatic trace context (traceId, spanId, correlationId)
23
+ - **Fastify Plugin**: One-line integration for request lifecycle
24
+ - **HTTP Client**: Axios wrapper with retry, circuit breaker, and trace propagation
25
+
26
+ ## Quick Start
27
+
28
+ ### 1. Install
29
+
30
+ ```bash
31
+ npm install @ciq-dev/neoiq-foundation-node
32
+
33
+ # Peer dependencies
34
+ npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/sdk-metrics \
35
+ @opentelemetry/exporter-trace-otlp-grpc @opentelemetry/exporter-metrics-otlp-grpc \
36
+ @opentelemetry/auto-instrumentations-node @opentelemetry/resources @opentelemetry/semantic-conventions \
37
+ pino pino-pretty axios axios-retry opossum fastify-plugin
38
+ ```
39
+
40
+ ### 2. Initialize (First Line of the App)
41
+
42
+ ```typescript
43
+ import { init, logger, observabilityPlugin } from '@ciq-dev/neoiq-foundation-node';
44
+
45
+ // Must be called BEFORE other imports
46
+ init({
47
+ serviceName: 'neoiq-auth-service',
48
+ serviceVersion: '1.0.0',
49
+ });
50
+ ```
51
+
52
+ ### 3. Add Fastify Plugin
53
+
54
+ ```typescript
55
+ import Fastify from 'fastify';
56
+
57
+ const app = Fastify();
58
+ app.register(observabilityPlugin, { serviceName: 'neoiq-auth-service' });
59
+ ```
60
+
61
+ ### 4. Set Environment Variable in Kubernetes
62
+
63
+ ```yaml
64
+ env:
65
+ - name: OTEL_EXPORTER_OTLP_ENDPOINT
66
+ value: 'http://otel-stack-deployment-collector.observability.svc.cluster.local:4317'
67
+ ```
68
+
69
+ That's it! Traces and metrics will flow to Groundcover.
70
+
71
+ ---
72
+
73
+ ## API Reference
74
+
75
+ ### `init(options)`
76
+
77
+ Initialize OpenTelemetry. **Must be called before other imports.**
78
+
79
+ ```typescript
80
+ init({
81
+ serviceName: 'my-service', // Required
82
+ serviceVersion: '1.0.0', // Default: '1.0.0'
83
+ environment: 'production', // Default: 'development'
84
+ otlpEndpoint: 'http://...:4317', // Default: cluster OTEL Collector
85
+ logLevel: 'info', // 'debug' | 'info' | 'warn' | 'error'
86
+ metricsIntervalMs: 5000, // Default: 5000 (5 seconds)
87
+ });
88
+ ```
89
+
90
+ ### `logger`
91
+
92
+ Structured logger with automatic trace context injection.
93
+
94
+ ```typescript
95
+ import { logger } from '@ciq-dev/neoiq-foundation-node';
96
+
97
+ logger.info({ userId: '123' }, 'User logged in');
98
+ logger.error({ error: err.message }, 'Failed to process');
99
+ ```
100
+
101
+ **Log Output:**
102
+ ```json
103
+ {
104
+ "level": "info",
105
+ "time": 1703318400000,
106
+ "service": "neoiq-auth-service",
107
+ "traceId": "abc123...",
108
+ "spanId": "def456...",
109
+ "correlationId": "req-789",
110
+ "userId": "123",
111
+ "msg": "User logged in"
112
+ }
113
+ ```
114
+
115
+ ### `getMeter(name, version?)`
116
+
117
+ Get a meter for custom metrics.
118
+
119
+ ```typescript
120
+ import { getMeter } from '@ciq-dev/neoiq-foundation-node';
121
+
122
+ const meter = getMeter('neoiq-auth-service');
123
+
124
+ // Counter
125
+ const counter = meter.createCounter('logins.total');
126
+ counter.add(1, { provider: 'workos' });
127
+
128
+ // Histogram
129
+ const histogram = meter.createHistogram('token.validation.duration');
130
+ histogram.record(150, { status: 'success' });
131
+ ```
132
+
133
+ ### `observabilityPlugin`
134
+
135
+ Fastify plugin for automatic request handling.
136
+
137
+ ```typescript
138
+ import { observabilityPlugin } from '@ciq-dev/neoiq-foundation-node';
139
+
140
+ app.register(observabilityPlugin, {
141
+ serviceName: 'my-service',
142
+ excludeRoutes: ['/health', '/metrics'],
143
+ });
144
+ ```
145
+
146
+ **Automatically:**
147
+ - Extracts/generates correlation ID (x-request-id header)
148
+ - Creates OpenTelemetry spans for each request
149
+ - Logs request received/completed with full context
150
+ - Records HTTP metrics (http.server.requests.total, http.server.request.duration)
151
+
152
+ ### `createHttpClient(options)`
153
+
154
+ Create an Axios client with full observability.
155
+
156
+ ```typescript
157
+ import { createHttpClient } from '@ciq-dev/neoiq-foundation-node';
158
+
159
+ const userClient = createHttpClient({
160
+ baseURL: 'http://user-service:3000',
161
+ serviceName: 'user-service',
162
+ timeout: 10000,
163
+ retry: { retries: 3 },
164
+ });
165
+
166
+ const response = await userClient.get('/api/users/123');
167
+ ```
168
+
169
+ **Automatically:**
170
+ - Propagates trace context (traceparent header)
171
+ - Propagates correlation ID (x-request-id header)
172
+ - Logs outbound requests/responses
173
+ - Records HTTP client metrics
174
+ - Retries on 5xx errors with exponential backoff
175
+ - Circuit breaker protection
176
+
177
+ ### `shutdown()`
178
+
179
+ Gracefully shutdown OTEL providers. Call on application shutdown.
180
+
181
+ ```typescript
182
+ import { shutdown } from '@ciq-dev/neoiq-foundation-node';
183
+
184
+ process.on('SIGTERM', async () => {
185
+ await app.close();
186
+ await shutdown(); // Flush telemetry
187
+ });
188
+ ```
189
+
190
+ ---
191
+
192
+ ## Complete Example
193
+
194
+ ```typescript
195
+ // src/index.ts
196
+
197
+ // STEP 1: Initialize OTEL (must be first!)
198
+ import { init, logger, getMeter, observabilityPlugin, createHttpClient, shutdown } from '@ciq-dev/neoiq-foundation-node';
199
+
200
+ init({ serviceName: 'neoiq-auth-service' });
201
+
202
+ // STEP 2: Import dependencies
203
+ import Fastify from 'fastify';
204
+
205
+ const app = Fastify();
206
+
207
+ // STEP 3: Register plugin
208
+ app.register(observabilityPlugin, { serviceName: 'neoiq-auth-service' });
209
+
210
+ // STEP 4: Create HTTP clients
211
+ const userClient = createHttpClient({
212
+ baseURL: process.env.USER_SERVICE_URL,
213
+ serviceName: 'user-service',
214
+ });
215
+
216
+ // STEP 5: Custom metrics
217
+ const meter = getMeter('neoiq-auth-service');
218
+ const loginCounter = meter.createCounter('auth.logins.total');
219
+
220
+ // STEP 6: Routes
221
+ app.post('/api/v1/auth/login', async (req, reply) => {
222
+ const { provider } = req.body as any;
223
+
224
+ loginCounter.add(1, { provider });
225
+ logger.info({ provider }, 'Login attempt');
226
+
227
+ return { success: true };
228
+ });
229
+
230
+ // STEP 7: Start with graceful shutdown
231
+ app.listen({ port: 3000 });
232
+
233
+ process.on('SIGTERM', async () => {
234
+ await app.close();
235
+ await shutdown(); // Flush telemetry
236
+ });
237
+ ```
238
+
239
+ ---
240
+
241
+ ## Environment Variables
242
+
243
+ | Variable | Description | Default |
244
+ |----------|-------------|---------|
245
+ | `OTEL_EXPORTER_OTLP_ENDPOINT` | OTEL Collector URL | `http://otel-stack-deployment-collector.observability.svc.cluster.local:4317` |
246
+ | `OTEL_SERVICE_VERSION` | Service version | `1.0.0` |
247
+ | `OTEL_ENVIRONMENT` | Deployment environment | `development` |
248
+ | `LOG_LEVEL` | Log level | `info` |
249
+
250
+ ---
251
+
252
+ ## What Gets Sent to Groundcover
253
+
254
+ ### Traces
255
+ - Every HTTP request (incoming and outgoing)
256
+ - Correlation ID linking requests across services
257
+ - Span attributes: method, url, status_code, duration
258
+
259
+ ### Metrics
260
+ - `http.server.requests.total` - Incoming request count
261
+ - `http.server.request.duration` - Incoming request latency
262
+ - `http.server.requests.errors` - Incoming request errors
263
+ - `http.client.requests.total` - Outgoing request count
264
+ - `http.client.request.duration` - Outgoing request latency
265
+ - Custom business metrics you define
266
+
267
+ ### Logs (via stdout)
268
+ - Structured JSON logs written to stdout (Pino)
269
+ - Include traceId, spanId, correlationId for correlation
270
+ - Groundcover Agent scrapes container logs
271
+ - Automatically correlated with traces in Groundcover dashboard
272
+
273
+ ---
274
+
275
+ ## Development
276
+
277
+ ```bash
278
+ # Install dependencies
279
+ npm install
280
+
281
+ # Build
282
+ npm run build
283
+
284
+ # Type check
285
+ npm run typecheck
286
+ ```
287
+
288
+ ---
289
+
290
+ ## License
291
+
292
+ MIT
293
+
@@ -0,0 +1,80 @@
1
+ /**
2
+ * HTTP Client with observability, retry, and circuit breaker.
3
+ *
4
+ * Features:
5
+ * - OpenTelemetry trace context propagation (traceparent header)
6
+ * - Correlation ID propagation (x-request-id header)
7
+ * - HTTP client metrics (request count, duration, errors)
8
+ * - Automatic retries with exponential backoff
9
+ * - Circuit breaker pattern
10
+ *
11
+ * Usage:
12
+ * import { createHttpClient } from '@ciq-dev/neoiq-foundation-node';
13
+ *
14
+ * const client = createHttpClient({
15
+ * baseURL: 'https://api.example.com',
16
+ * serviceName: 'example-api',
17
+ * });
18
+ *
19
+ * const response = await client.get('/users');
20
+ */
21
+ import { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
22
+ export interface HttpClientOptions {
23
+ /**
24
+ * Base URL for all requests.
25
+ */
26
+ baseURL: string;
27
+ /**
28
+ * Name of the target service (for metrics and logging).
29
+ */
30
+ serviceName: string;
31
+ /**
32
+ * Request timeout in milliseconds (default: 30000).
33
+ */
34
+ timeout?: number;
35
+ /**
36
+ * Retry configuration.
37
+ */
38
+ retry?: {
39
+ /**
40
+ * Maximum number of retries (default: 3).
41
+ */
42
+ retries?: number;
43
+ /**
44
+ * Base delay in milliseconds for exponential backoff (default: 1000).
45
+ */
46
+ retryDelay?: number;
47
+ /**
48
+ * HTTP status codes to retry on (default: [408, 429, 500, 502, 503, 504]).
49
+ */
50
+ retryStatusCodes?: number[];
51
+ };
52
+ /**
53
+ * Circuit breaker configuration.
54
+ */
55
+ circuitBreaker?: {
56
+ /**
57
+ * Whether to enable circuit breaker (default: true).
58
+ */
59
+ enabled?: boolean;
60
+ /**
61
+ * Time in ms before attempting to close the circuit (default: 30000).
62
+ */
63
+ resetTimeout?: number;
64
+ /**
65
+ * Error percentage threshold to open the circuit (default: 50).
66
+ */
67
+ errorThresholdPercentage?: number;
68
+ };
69
+ /**
70
+ * Additional default headers.
71
+ */
72
+ headers?: Record<string, string>;
73
+ }
74
+ /**
75
+ * Create a configured HTTP client with full observability.
76
+ * Metrics are exported to OTEL Collector → Groundcover.
77
+ */
78
+ export declare function createHttpClient(options: HttpClientOptions): AxiosInstance;
79
+ export { AxiosInstance, AxiosRequestConfig, AxiosResponse };
80
+ //# sourceMappingURL=http-client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-client.d.ts","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,OAAc,EACZ,aAAa,EACb,kBAAkB,EAElB,aAAa,EACd,MAAM,OAAO,CAAC;AAUf,MAAM,WAAW,iBAAiB;IAChC;;OAEG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IAEjB;;OAEG;IACH,KAAK,CAAC,EAAE;QACN;;WAEG;QACH,OAAO,CAAC,EAAE,MAAM,CAAC;QAEjB;;WAEG;QACH,UAAU,CAAC,EAAE,MAAM,CAAC;QAEpB;;WAEG;QACH,gBAAgB,CAAC,EAAE,MAAM,EAAE,CAAC;KAC7B,CAAC;IAEF;;OAEG;IACH,cAAc,CAAC,EAAE;QACf;;WAEG;QACH,OAAO,CAAC,EAAE,OAAO,CAAC;QAElB;;WAEG;QACH,YAAY,CAAC,EAAE,MAAM,CAAC;QAEtB;;WAEG;QACH,wBAAwB,CAAC,EAAE,MAAM,CAAC;KACnC,CAAC;IAEF;;OAEG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAClC;AAMD;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,aAAa,CAqM1E;AAGD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,aAAa,EAAE,CAAC"}
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+ /**
3
+ * HTTP Client with observability, retry, and circuit breaker.
4
+ *
5
+ * Features:
6
+ * - OpenTelemetry trace context propagation (traceparent header)
7
+ * - Correlation ID propagation (x-request-id header)
8
+ * - HTTP client metrics (request count, duration, errors)
9
+ * - Automatic retries with exponential backoff
10
+ * - Circuit breaker pattern
11
+ *
12
+ * Usage:
13
+ * import { createHttpClient } from '@ciq-dev/neoiq-foundation-node';
14
+ *
15
+ * const client = createHttpClient({
16
+ * baseURL: 'https://api.example.com',
17
+ * serviceName: 'example-api',
18
+ * });
19
+ *
20
+ * const response = await client.get('/users');
21
+ */
22
+ var __importDefault = (this && this.__importDefault) || function (mod) {
23
+ return (mod && mod.__esModule) ? mod : { "default": mod };
24
+ };
25
+ Object.defineProperty(exports, "__esModule", { value: true });
26
+ exports.createHttpClient = createHttpClient;
27
+ const axios_1 = __importDefault(require("axios"));
28
+ const axios_retry_1 = __importDefault(require("axios-retry"));
29
+ const opossum_1 = __importDefault(require("opossum"));
30
+ const api_1 = require("@opentelemetry/api");
31
+ const observability_1 = require("./observability");
32
+ // -----------------------------------------------------------------------------
33
+ // HTTP Client Factory
34
+ // -----------------------------------------------------------------------------
35
+ /**
36
+ * Create a configured HTTP client with full observability.
37
+ * Metrics are exported to OTEL Collector → Groundcover.
38
+ */
39
+ function createHttpClient(options) {
40
+ const { baseURL, serviceName, timeout = 30000, retry = {}, circuitBreaker: cbOptions = {}, headers = {}, } = options;
41
+ // Create Axios instance
42
+ const client = axios_1.default.create({
43
+ baseURL,
44
+ timeout,
45
+ headers: {
46
+ 'Content-Type': 'application/json',
47
+ ...headers,
48
+ },
49
+ });
50
+ // Setup metrics (per Groundcover guide)
51
+ const meter = (0, observability_1.getMeter)(`http-client-${serviceName}`);
52
+ const requestCounter = meter.createCounter('http.client.requests.total', {
53
+ description: 'Total number of outbound HTTP requests',
54
+ });
55
+ const requestDuration = meter.createHistogram('http.client.request.duration', {
56
+ description: 'Outbound HTTP request duration in milliseconds',
57
+ unit: 'ms',
58
+ });
59
+ const requestErrors = meter.createCounter('http.client.requests.errors', {
60
+ description: 'Total number of outbound HTTP request errors',
61
+ });
62
+ // ---------------------------------------------------------------------------
63
+ // Request Interceptor - Add trace context and correlation ID
64
+ // ---------------------------------------------------------------------------
65
+ client.interceptors.request.use((config) => {
66
+ // 1. Propagate OpenTelemetry trace context (W3C traceparent)
67
+ const carrier = {};
68
+ api_1.propagation.inject(api_1.context.active(), carrier);
69
+ if (carrier.traceparent) {
70
+ config.headers.set('traceparent', carrier.traceparent);
71
+ }
72
+ if (carrier.tracestate) {
73
+ config.headers.set('tracestate', carrier.tracestate);
74
+ }
75
+ // 2. Propagate correlation ID
76
+ const reqCtx = (0, observability_1.getRequestContext)();
77
+ if (reqCtx?.correlationId) {
78
+ config.headers.set('x-request-id', reqCtx.correlationId);
79
+ }
80
+ // 3. Add timing metadata
81
+ config.__startTime = Date.now();
82
+ // 4. Log outbound request
83
+ observability_1.logger.debug({
84
+ method: config.method?.toUpperCase(),
85
+ url: `${config.baseURL || ''}${config.url}`,
86
+ targetService: serviceName,
87
+ correlationId: reqCtx?.correlationId,
88
+ }, 'Outbound HTTP request');
89
+ return config;
90
+ });
91
+ // ---------------------------------------------------------------------------
92
+ // Response Interceptor - Log and record metrics
93
+ // ---------------------------------------------------------------------------
94
+ client.interceptors.response.use((response) => {
95
+ const config = response.config;
96
+ const durationMs = Date.now() - (config.__startTime || Date.now());
97
+ const reqCtx = (0, observability_1.getRequestContext)();
98
+ const labels = {
99
+ target_service: serviceName,
100
+ method: config.method?.toUpperCase() || 'GET',
101
+ status_code: String(response.status),
102
+ };
103
+ // Log success
104
+ observability_1.logger.debug({
105
+ method: config.method?.toUpperCase(),
106
+ url: `${config.baseURL || ''}${config.url}`,
107
+ targetService: serviceName,
108
+ statusCode: response.status,
109
+ durationMs,
110
+ correlationId: reqCtx?.correlationId,
111
+ }, 'Outbound HTTP response');
112
+ // Record metrics
113
+ requestCounter.add(1, labels);
114
+ requestDuration.record(durationMs, labels);
115
+ return response;
116
+ }, (error) => {
117
+ const config = error.config;
118
+ const durationMs = config ? Date.now() - (config.__startTime || Date.now()) : 0;
119
+ const statusCode = error.response?.status || 0;
120
+ const reqCtx = (0, observability_1.getRequestContext)();
121
+ const labels = {
122
+ target_service: serviceName,
123
+ method: config?.method?.toUpperCase() || 'GET',
124
+ status_code: String(statusCode),
125
+ };
126
+ // Log error
127
+ observability_1.logger.error({
128
+ method: config?.method?.toUpperCase(),
129
+ url: config ? `${config.baseURL || ''}${config.url}` : 'unknown',
130
+ targetService: serviceName,
131
+ statusCode,
132
+ durationMs,
133
+ error: error.message,
134
+ correlationId: reqCtx?.correlationId,
135
+ }, 'Outbound HTTP error');
136
+ // Record metrics
137
+ requestCounter.add(1, labels);
138
+ requestDuration.record(durationMs, labels);
139
+ requestErrors.add(1, labels);
140
+ return Promise.reject(error);
141
+ });
142
+ // ---------------------------------------------------------------------------
143
+ // Configure Retry
144
+ // ---------------------------------------------------------------------------
145
+ const retryConfig = {
146
+ retries: retry.retries ?? 3,
147
+ retryDelay: (retryCount) => {
148
+ const baseDelay = retry.retryDelay ?? 1000;
149
+ return baseDelay * Math.pow(2, retryCount - 1); // Exponential backoff
150
+ },
151
+ retryCondition: (error) => {
152
+ const retryStatusCodes = retry.retryStatusCodes ?? [408, 429, 500, 502, 503, 504];
153
+ const status = error.response?.status;
154
+ return !error.response || retryStatusCodes.includes(status || 0);
155
+ },
156
+ onRetry: (retryCount, error, requestConfig) => {
157
+ observability_1.logger.warn({
158
+ retryCount,
159
+ url: `${requestConfig.baseURL || ''}${requestConfig.url}`,
160
+ error: error.message,
161
+ targetService: serviceName,
162
+ }, 'Retrying HTTP request');
163
+ },
164
+ };
165
+ (0, axios_retry_1.default)(client, retryConfig);
166
+ // ---------------------------------------------------------------------------
167
+ // Configure Circuit Breaker (optional wrapper)
168
+ // ---------------------------------------------------------------------------
169
+ if (cbOptions.enabled !== false) {
170
+ const breaker = new opossum_1.default(async (config) => client.request(config), {
171
+ timeout,
172
+ resetTimeout: cbOptions.resetTimeout ?? 30000,
173
+ errorThresholdPercentage: cbOptions.errorThresholdPercentage ?? 50,
174
+ volumeThreshold: 10,
175
+ });
176
+ breaker.on('open', () => {
177
+ observability_1.logger.warn({ targetService: serviceName, baseURL }, 'Circuit breaker OPEN');
178
+ });
179
+ breaker.on('halfOpen', () => {
180
+ observability_1.logger.info({ targetService: serviceName, baseURL }, 'Circuit breaker HALF-OPEN');
181
+ });
182
+ breaker.on('close', () => {
183
+ observability_1.logger.info({ targetService: serviceName, baseURL }, 'Circuit breaker CLOSED');
184
+ });
185
+ }
186
+ return client;
187
+ }
188
+ //# sourceMappingURL=http-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http-client.js","sourceRoot":"","sources":["../src/http-client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * @commerceiq/neoiq-foundation-node
3
+ *
4
+ * Node.js observability foundation for CommerceIQ services.
5
+ * Integrates with Groundcover via OpenTelemetry.
6
+ *
7
+ * Flow: App → OpenTelemetry SDK → OTEL Collector → Groundcover
8
+ */
9
+ export { init, logger, getTracer, getMeter, getRequestContext, runWithContext, getTraceContext, shutdown, als, SpanStatusCode, type InitOptions, type RequestContext, } from './observability';
10
+ export { observabilityPlugin, type PluginOptions } from './plugin';
11
+ export { createHttpClient, type HttpClientOptions } from './http-client';
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EACL,IAAI,EACJ,MAAM,EACN,SAAS,EACT,QAAQ,EACR,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,QAAQ,EACR,GAAG,EACH,cAAc,EACd,KAAK,WAAW,EAChB,KAAK,cAAc,GACpB,MAAM,iBAAiB,CAAC;AAGzB,OAAO,EAAE,mBAAmB,EAAE,KAAK,aAAa,EAAE,MAAM,UAAU,CAAC;AAGnE,OAAO,EAAE,gBAAgB,EAAE,KAAK,iBAAiB,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,30 @@
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; } });
30
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,kCAAkC;AAClC,iDAayB;AAZvB,qGAAA,IAAI,OAAA;AACJ,uGAAA,MAAM,OAAA;AACN,0GAAA,SAAS,OAAA;AACT,yGAAA,QAAQ,OAAA;AACR,kHAAA,iBAAiB,OAAA;AACjB,+GAAA,cAAc,OAAA;AACd,gHAAA,eAAe,OAAA;AACf,yGAAA,QAAQ,OAAA;AACR,oGAAA,GAAG,OAAA;AACH,+GAAA,cAAc,OAAA;AAKhB,iBAAiB;AACjB,mCAAmE;AAA1D,6GAAA,mBAAmB,OAAA;AAE5B,cAAc;AACd,6CAAyE;AAAhE,+GAAA,gBAAgB,OAAA"}
@@ -0,0 +1,132 @@
1
+ /**
2
+ * OpenTelemetry initialization for Node.js services.
3
+ *
4
+ * Follows Groundcover Integration Guide - Option 2 (Via OTEL Collector)
5
+ *
6
+ * Flow: App → OpenTelemetry SDK → OTEL Collector → Groundcover
7
+ *
8
+ * Environment Variables:
9
+ * OTEL_EXPORTER_OTLP_ENDPOINT: Collector URL (default: cluster OTEL Collector)
10
+ * OTEL_SERVICE_NAME: Service name
11
+ * OTEL_SERVICE_VERSION: Service version (default: 1.0.0)
12
+ * OTEL_ENVIRONMENT: Deployment environment (default: development)
13
+ *
14
+ * Usage:
15
+ * import { init, logger, getMeter } from '@ciq-dev/neoiq-foundation-node';
16
+ *
17
+ * init({ serviceName: 'my-service' });
18
+ *
19
+ * logger.info({ action: 'startup' }, 'Service started');
20
+ *
21
+ * const meter = getMeter('my-service');
22
+ * const counter = meter.createCounter('requests_total');
23
+ */
24
+ import { SpanStatusCode } from '@opentelemetry/api';
25
+ import pino from 'pino';
26
+ import { AsyncLocalStorage } from 'async_hooks';
27
+ export interface InitOptions {
28
+ /**
29
+ * Service name (required). Shows up in Groundcover.
30
+ */
31
+ serviceName: string;
32
+ /**
33
+ * Service version (default: 1.0.0).
34
+ */
35
+ serviceVersion?: string;
36
+ /**
37
+ * Deployment environment (default: development).
38
+ */
39
+ environment?: string;
40
+ /**
41
+ * OTEL Collector endpoint.
42
+ * Default: http://otel-stack-deployment-collector.observability.svc.cluster.local:4317
43
+ */
44
+ otlpEndpoint?: string;
45
+ /**
46
+ * Log level (default: info).
47
+ */
48
+ logLevel?: 'debug' | 'info' | 'warn' | 'error';
49
+ /**
50
+ * Metrics export interval in ms (default: 5000 per Groundcover guide).
51
+ */
52
+ metricsIntervalMs?: number;
53
+ }
54
+ export interface RequestContext {
55
+ correlationId?: string;
56
+ traceId?: string;
57
+ spanId?: string;
58
+ }
59
+ export declare const als: AsyncLocalStorage<RequestContext>;
60
+ /**
61
+ * Initialize OpenTelemetry SDK with traces and metrics.
62
+ *
63
+ * Sends telemetry to OTEL Collector which forwards to Groundcover.
64
+ * Call this at the very start of the application, before other imports.
65
+ *
66
+ * @example
67
+ * init({ serviceName: 'canvas-weaver' });
68
+ */
69
+ export declare function init(options: InitOptions): void;
70
+ /**
71
+ * Structured logger with automatic trace context injection.
72
+ * All logs include traceId, spanId, and correlationId when available.
73
+ */
74
+ export declare const logger: {
75
+ info: (obj: object, msg?: string) => void;
76
+ error: (obj: object, msg?: string) => void;
77
+ warn: (obj: object, msg?: string) => void;
78
+ debug: (obj: object, msg?: string) => void;
79
+ child: (bindings: object) => pino.Logger<never, boolean>;
80
+ };
81
+ /**
82
+ * Get a tracer instance for creating spans.
83
+ *
84
+ * @param name - Tracer name (defaults to service name)
85
+ *
86
+ * @example
87
+ * const tracer = getTracer();
88
+ * tracer.startActiveSpan('db.query', (span) => {
89
+ * // ... do work
90
+ * span.end();
91
+ * });
92
+ */
93
+ export declare function getTracer(name?: string): import("@opentelemetry/api").Tracer;
94
+ /**
95
+ * Get a meter instance for creating metrics.
96
+ * Metrics are exported to OTEL Collector → Groundcover.
97
+ *
98
+ * @param name - Meter name (typically service name)
99
+ * @param version - Meter version (default: 1.0.0)
100
+ *
101
+ * @example
102
+ * const meter = getMeter('canvas-weaver');
103
+ * const counter = meter.createCounter('http.requests.total');
104
+ * counter.add(1, { method: 'GET', route: '/api/reports' });
105
+ *
106
+ * const histogram = meter.createHistogram('http.request.duration');
107
+ * histogram.record(150, { method: 'GET', route: '/api/reports' });
108
+ */
109
+ export declare function getMeter(name: string, version?: string): import("@opentelemetry/api").Meter;
110
+ /**
111
+ * Get current request context from AsyncLocalStorage.
112
+ */
113
+ export declare function getRequestContext(): RequestContext | undefined;
114
+ /**
115
+ * Run a function with a specific context (for manual context propagation).
116
+ */
117
+ export declare function runWithContext<T>(ctx: RequestContext, fn: () => T): T;
118
+ /**
119
+ * Get current trace context as a dictionary.
120
+ * Useful for logging or passing to external systems.
121
+ */
122
+ export declare function getTraceContext(): {
123
+ traceId?: string;
124
+ spanId?: string;
125
+ };
126
+ /**
127
+ * Gracefully shutdown OTEL providers.
128
+ * Call this on application shutdown to flush pending telemetry.
129
+ */
130
+ export declare function shutdown(): Promise<void>;
131
+ export { SpanStatusCode };
132
+ //# sourceMappingURL=observability.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.d.ts","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AASH,OAAO,EAAkB,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACpE,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAUhD,MAAM,WAAW,WAAW;IAC1B;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IAExB;;OAEG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;OAGG;IACH,YAAY,CAAC,EAAE,MAAM,CAAC;IAEtB;;OAEG;IACH,QAAQ,CAAC,EAAE,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,CAAC;IAE/C;;OAEG;IACH,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B;AAMD,MAAM,WAAW,cAAc;IAC7B,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,eAAO,MAAM,GAAG,mCAA0C,CAAC;AAgB3D;;;;;;;;GAQG;AACH,wBAAgB,IAAI,CAAC,OAAO,EAAE,WAAW,GAAG,IAAI,CA2F/C;AAMD;;;GAGG;AACH,eAAO,MAAM,MAAM;gBACL,MAAM,QAAQ,MAAM;iBACnB,MAAM,QAAQ,MAAM;gBACrB,MAAM,QAAQ,MAAM;iBACnB,MAAM,QAAQ,MAAM;sBACf,MAAM;CACzB,CAAC;AAMF;;;;;;;;;;;GAWG;AACH,wBAAgB,SAAS,CAAC,IAAI,CAAC,EAAE,MAAM,uCAEtC;AAMD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,MAAgB,sCAE/D;AAMD;;GAEG;AACH,wBAAgB,iBAAiB,IAAI,cAAc,GAAG,SAAS,CAE9D;AAED;;GAEG;AACH,wBAAgB,cAAc,CAAC,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,EAAE,EAAE,MAAM,CAAC,GAAG,CAAC,CAErE;AAED;;;GAGG;AACH,wBAAgB,eAAe,IAAI;IAAE,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,MAAM,CAAC,EAAE,MAAM,CAAA;CAAE,CASvE;AAMD;;;GAGG;AACH,wBAAsB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAU9C;AAGD,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -0,0 +1,246 @@
1
+ "use strict";
2
+ /**
3
+ * OpenTelemetry initialization for Node.js services.
4
+ *
5
+ * Follows Groundcover Integration Guide - Option 2 (Via OTEL Collector)
6
+ *
7
+ * Flow: App → OpenTelemetry SDK → OTEL Collector → Groundcover
8
+ *
9
+ * Environment Variables:
10
+ * OTEL_EXPORTER_OTLP_ENDPOINT: Collector URL (default: cluster OTEL Collector)
11
+ * OTEL_SERVICE_NAME: Service name
12
+ * OTEL_SERVICE_VERSION: Service version (default: 1.0.0)
13
+ * OTEL_ENVIRONMENT: Deployment environment (default: development)
14
+ *
15
+ * Usage:
16
+ * import { init, logger, getMeter } from '@ciq-dev/neoiq-foundation-node';
17
+ *
18
+ * init({ serviceName: 'my-service' });
19
+ *
20
+ * logger.info({ action: 'startup' }, 'Service started');
21
+ *
22
+ * const meter = getMeter('my-service');
23
+ * const counter = meter.createCounter('requests_total');
24
+ */
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.SpanStatusCode = exports.logger = exports.als = void 0;
30
+ exports.init = init;
31
+ exports.getTracer = getTracer;
32
+ exports.getMeter = getMeter;
33
+ exports.getRequestContext = getRequestContext;
34
+ exports.runWithContext = runWithContext;
35
+ exports.getTraceContext = getTraceContext;
36
+ exports.shutdown = shutdown;
37
+ const sdk_node_1 = require("@opentelemetry/sdk-node");
38
+ const auto_instrumentations_node_1 = require("@opentelemetry/auto-instrumentations-node");
39
+ const exporter_trace_otlp_grpc_1 = require("@opentelemetry/exporter-trace-otlp-grpc");
40
+ const exporter_metrics_otlp_grpc_1 = require("@opentelemetry/exporter-metrics-otlp-grpc");
41
+ const sdk_metrics_1 = require("@opentelemetry/sdk-metrics");
42
+ const resources_1 = require("@opentelemetry/resources");
43
+ const semantic_conventions_1 = require("@opentelemetry/semantic-conventions");
44
+ const api_1 = require("@opentelemetry/api");
45
+ Object.defineProperty(exports, "SpanStatusCode", { enumerable: true, get: function () { return api_1.SpanStatusCode; } });
46
+ const pino_1 = __importDefault(require("pino"));
47
+ const async_hooks_1 = require("async_hooks");
48
+ // -----------------------------------------------------------------------------
49
+ // Configuration
50
+ // -----------------------------------------------------------------------------
51
+ // Default OTEL Collector endpoint (per Groundcover guide - Kubernetes internal)
52
+ const DEFAULT_OTEL_COLLECTOR = 'http://otel-stack-deployment-collector.observability.svc.cluster.local:4317';
53
+ exports.als = new async_hooks_1.AsyncLocalStorage();
54
+ // -----------------------------------------------------------------------------
55
+ // Module State
56
+ // -----------------------------------------------------------------------------
57
+ let sdk = null;
58
+ let meterProvider = null;
59
+ let baseLogger;
60
+ let serviceName = 'unknown';
61
+ let initialized = false;
62
+ // -----------------------------------------------------------------------------
63
+ // Initialization
64
+ // -----------------------------------------------------------------------------
65
+ /**
66
+ * Initialize OpenTelemetry SDK with traces and metrics.
67
+ *
68
+ * Sends telemetry to OTEL Collector which forwards to Groundcover.
69
+ * Call this at the very start of the application, before other imports.
70
+ *
71
+ * @example
72
+ * init({ serviceName: 'canvas-weaver' });
73
+ */
74
+ function init(options) {
75
+ if (initialized) {
76
+ console.warn('[neoiq-foundation-node] Already initialized, skipping...');
77
+ return;
78
+ }
79
+ const { serviceName: svcName, serviceVersion = process.env.OTEL_SERVICE_VERSION || '1.0.0', environment = process.env.OTEL_ENVIRONMENT || 'development', otlpEndpoint = process.env.OTEL_EXPORTER_OTLP_ENDPOINT || DEFAULT_OTEL_COLLECTOR, logLevel = process.env.LOG_LEVEL || 'info', metricsIntervalMs = 5000, // 5 seconds per Groundcover guide
80
+ } = options;
81
+ serviceName = svcName;
82
+ // 1. Create Resource (attached to all telemetry)
83
+ const resource = (0, resources_1.resourceFromAttributes)({
84
+ [semantic_conventions_1.ATTR_SERVICE_NAME]: serviceName,
85
+ [semantic_conventions_1.ATTR_SERVICE_VERSION]: serviceVersion,
86
+ 'deployment.environment': environment,
87
+ });
88
+ // 2. Setup Metrics Exporter (per Groundcover guide)
89
+ const metricExporter = new exporter_metrics_otlp_grpc_1.OTLPMetricExporter({ url: otlpEndpoint });
90
+ const metricReader = new sdk_metrics_1.PeriodicExportingMetricReader({
91
+ exporter: metricExporter,
92
+ exportIntervalMillis: metricsIntervalMs,
93
+ });
94
+ meterProvider = new sdk_metrics_1.MeterProvider({
95
+ resource,
96
+ readers: [metricReader],
97
+ });
98
+ api_1.metrics.setGlobalMeterProvider(meterProvider);
99
+ // 3. Setup Trace Exporter
100
+ const traceExporter = new exporter_trace_otlp_grpc_1.OTLPTraceExporter({ url: otlpEndpoint });
101
+ // 4. Initialize NodeSDK with auto-instrumentation
102
+ sdk = new sdk_node_1.NodeSDK({
103
+ resource,
104
+ traceExporter,
105
+ instrumentations: [
106
+ (0, auto_instrumentations_node_1.getNodeAutoInstrumentations)({
107
+ // Disable fs instrumentation (too noisy)
108
+ '@opentelemetry/instrumentation-fs': { enabled: false },
109
+ }),
110
+ ],
111
+ });
112
+ sdk.start();
113
+ // 5. Setup Pino logger with trace context injection
114
+ baseLogger = (0, pino_1.default)({
115
+ level: logLevel,
116
+ base: {
117
+ service: serviceName,
118
+ version: serviceVersion,
119
+ env: environment,
120
+ },
121
+ mixin: () => {
122
+ // Inject trace context into every log (correlates logs with traces in Groundcover)
123
+ const span = api_1.trace.getActiveSpan();
124
+ const ctx = exports.als.getStore();
125
+ const spanContext = span?.spanContext();
126
+ return {
127
+ traceId: spanContext?.traceId || ctx?.traceId,
128
+ spanId: spanContext?.spanId || ctx?.spanId,
129
+ correlationId: ctx?.correlationId,
130
+ };
131
+ },
132
+ formatters: {
133
+ level: (label) => ({ level: label }),
134
+ },
135
+ transport: environment === 'development'
136
+ ? { target: 'pino-pretty', options: { colorize: true } }
137
+ : undefined,
138
+ });
139
+ baseLogger.info({
140
+ endpoint: otlpEndpoint,
141
+ metricsInterval: `${metricsIntervalMs}ms`,
142
+ }, `OpenTelemetry initialized. Sending to OTEL Collector.`);
143
+ initialized = true;
144
+ }
145
+ // -----------------------------------------------------------------------------
146
+ // Logger API
147
+ // -----------------------------------------------------------------------------
148
+ /**
149
+ * Structured logger with automatic trace context injection.
150
+ * All logs include traceId, spanId, and correlationId when available.
151
+ */
152
+ exports.logger = {
153
+ info: (obj, msg) => baseLogger?.info(obj, msg),
154
+ error: (obj, msg) => baseLogger?.error(obj, msg),
155
+ warn: (obj, msg) => baseLogger?.warn(obj, msg),
156
+ debug: (obj, msg) => baseLogger?.debug(obj, msg),
157
+ child: (bindings) => baseLogger?.child(bindings),
158
+ };
159
+ // -----------------------------------------------------------------------------
160
+ // Tracer API
161
+ // -----------------------------------------------------------------------------
162
+ /**
163
+ * Get a tracer instance for creating spans.
164
+ *
165
+ * @param name - Tracer name (defaults to service name)
166
+ *
167
+ * @example
168
+ * const tracer = getTracer();
169
+ * tracer.startActiveSpan('db.query', (span) => {
170
+ * // ... do work
171
+ * span.end();
172
+ * });
173
+ */
174
+ function getTracer(name) {
175
+ return api_1.trace.getTracer(name || serviceName);
176
+ }
177
+ // -----------------------------------------------------------------------------
178
+ // Meter API
179
+ // -----------------------------------------------------------------------------
180
+ /**
181
+ * Get a meter instance for creating metrics.
182
+ * Metrics are exported to OTEL Collector → Groundcover.
183
+ *
184
+ * @param name - Meter name (typically service name)
185
+ * @param version - Meter version (default: 1.0.0)
186
+ *
187
+ * @example
188
+ * const meter = getMeter('canvas-weaver');
189
+ * const counter = meter.createCounter('http.requests.total');
190
+ * counter.add(1, { method: 'GET', route: '/api/reports' });
191
+ *
192
+ * const histogram = meter.createHistogram('http.request.duration');
193
+ * histogram.record(150, { method: 'GET', route: '/api/reports' });
194
+ */
195
+ function getMeter(name, version = '1.0.0') {
196
+ return api_1.metrics.getMeter(name, version);
197
+ }
198
+ // -----------------------------------------------------------------------------
199
+ // Context Helpers
200
+ // -----------------------------------------------------------------------------
201
+ /**
202
+ * Get current request context from AsyncLocalStorage.
203
+ */
204
+ function getRequestContext() {
205
+ return exports.als.getStore();
206
+ }
207
+ /**
208
+ * Run a function with a specific context (for manual context propagation).
209
+ */
210
+ function runWithContext(ctx, fn) {
211
+ return exports.als.run(ctx, fn);
212
+ }
213
+ /**
214
+ * Get current trace context as a dictionary.
215
+ * Useful for logging or passing to external systems.
216
+ */
217
+ function getTraceContext() {
218
+ const span = api_1.trace.getActiveSpan();
219
+ if (!span)
220
+ return {};
221
+ const ctx = span.spanContext();
222
+ return {
223
+ traceId: ctx.traceId,
224
+ spanId: ctx.spanId,
225
+ };
226
+ }
227
+ // -----------------------------------------------------------------------------
228
+ // Shutdown
229
+ // -----------------------------------------------------------------------------
230
+ /**
231
+ * Gracefully shutdown OTEL providers.
232
+ * Call this on application shutdown to flush pending telemetry.
233
+ */
234
+ async function shutdown() {
235
+ if (!initialized)
236
+ return;
237
+ try {
238
+ await meterProvider?.shutdown();
239
+ await sdk?.shutdown();
240
+ console.log('[neoiq-foundation-node] OTEL shutdown complete');
241
+ }
242
+ catch (error) {
243
+ console.error('[neoiq-foundation-node] Error during shutdown:', error);
244
+ }
245
+ }
246
+ //# sourceMappingURL=observability.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"observability.js","sourceRoot":"","sources":["../src/observability.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;;;;;;AAyFH,oBA2FC;AAkCD,8BAEC;AAqBD,4BAEC;AASD,8CAEC;AAKD,wCAEC;AAMD,0CASC;AAUD,4BAUC;AAlSD,sDAAkD;AAClD,0FAAwF;AACxF,sFAA4E;AAC5E,0FAA+E;AAC/E,4DAA0F;AAC1F,wDAAkE;AAClE,8EAA8F;AAC9F,4CAAoE;AA8R3D,+FA9RgB,oBAAc,OA8RhB;AA7RvB,gDAAwB;AACxB,6CAAgD;AAEhD,gFAAgF;AAChF,gBAAgB;AAChB,gFAAgF;AAEhF,gFAAgF;AAChF,MAAM,sBAAsB,GAC1B,6EAA6E,CAAC;AA6CnE,QAAA,GAAG,GAAG,IAAI,+BAAiB,EAAkB,CAAC;AAE3D,gFAAgF;AAChF,eAAe;AACf,gFAAgF;AAEhF,IAAI,GAAG,GAAmB,IAAI,CAAC;AAC/B,IAAI,aAAa,GAAyB,IAAI,CAAC;AAC/C,IAAI,UAAuB,CAAC;AAC5B,IAAI,WAAW,GAAW,SAAS,CAAC;AACpC,IAAI,WAAW,GAAG,KAAK,CAAC;AAExB,gFAAgF;AAChF,iBAAiB;AACjB,gFAAgF;AAEhF;;;;;;;;GAQG;AACH,SAAgB,IAAI,CAAC,OAAoB;IACvC,IAAI,WAAW,EAAE,CAAC;QAChB,OAAO,CAAC,IAAI,CAAC,0DAA0D,CAAC,CAAC;QACzE,OAAO;IACT,CAAC;IAED,MAAM,EACJ,WAAW,EAAE,OAAO,EACpB,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,oBAAoB,IAAI,OAAO,EAC5D,WAAW,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,aAAa,EAC3D,YAAY,GAAG,OAAO,CAAC,GAAG,CAAC,2BAA2B,IAAI,sBAAsB,EAChF,QAAQ,GAAI,OAAO,CAAC,GAAG,CAAC,SAAiD,IAAI,MAAM,EACnF,iBAAiB,GAAG,IAAI,EAAE,kCAAkC;MAC7D,GAAG,OAAO,CAAC;IAEZ,WAAW,GAAG,OAAO,CAAC;IAEtB,iDAAiD;IACjD,MAAM,QAAQ,GAAG,IAAA,kCAAsB,EAAC;QACtC,CAAC,wCAAiB,CAAC,EAAE,WAAW;QAChC,CAAC,2CAAoB,CAAC,EAAE,cAAc;QACtC,wBAAwB,EAAE,WAAW;KACtC,CAAC,CAAC;IAEH,oDAAoD;IACpD,MAAM,cAAc,GAAG,IAAI,+CAAkB,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC,CAAC;IACrE,MAAM,YAAY,GAAG,IAAI,2CAA6B,CAAC;QACrD,QAAQ,EAAE,cAAc;QACxB,oBAAoB,EAAE,iBAAiB;KACxC,CAAC,CAAC;IACH,aAAa,GAAG,IAAI,2BAAa,CAAC;QAChC,QAAQ;QACR,OAAO,EAAE,CAAC,YAAY,CAAC;KACxB,CAAC,CAAC;IACH,aAAO,CAAC,sBAAsB,CAAC,aAAa,CAAC,CAAC;IAE9C,0BAA0B;IAC1B,MAAM,aAAa,GAAG,IAAI,4CAAiB,CAAC,EAAE,GAAG,EAAE,YAAY,EAAE,CAAC,CAAC;IAEnE,kDAAkD;IAClD,GAAG,GAAG,IAAI,kBAAO,CAAC;QAChB,QAAQ;QACR,aAAa;QACb,gBAAgB,EAAE;YAChB,IAAA,wDAA2B,EAAC;gBAC1B,yCAAyC;gBACzC,mCAAmC,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE;aACxD,CAAC;SACH;KACF,CAAC,CAAC;IAEH,GAAG,CAAC,KAAK,EAAE,CAAC;IAEZ,oDAAoD;IACpD,UAAU,GAAG,IAAA,cAAI,EAAC;QAChB,KAAK,EAAE,QAAQ;QACf,IAAI,EAAE;YACJ,OAAO,EAAE,WAAW;YACpB,OAAO,EAAE,cAAc;YACvB,GAAG,EAAE,WAAW;SACjB;QACD,KAAK,EAAE,GAAG,EAAE;YACV,mFAAmF;YACnF,MAAM,IAAI,GAAG,WAAK,CAAC,aAAa,EAAE,CAAC;YACnC,MAAM,GAAG,GAAG,WAAG,CAAC,QAAQ,EAAE,CAAC;YAC3B,MAAM,WAAW,GAAG,IAAI,EAAE,WAAW,EAAE,CAAC;YAExC,OAAO;gBACL,OAAO,EAAE,WAAW,EAAE,OAAO,IAAI,GAAG,EAAE,OAAO;gBAC7C,MAAM,EAAE,WAAW,EAAE,MAAM,IAAI,GAAG,EAAE,MAAM;gBAC1C,aAAa,EAAE,GAAG,EAAE,aAAa;aAClC,CAAC;QACJ,CAAC;QACD,UAAU,EAAE;YACV,KAAK,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC;SACrC;QACD,SAAS,EACP,WAAW,KAAK,aAAa;YAC3B,CAAC,CAAC,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,EAAE,QAAQ,EAAE,IAAI,EAAE,EAAE;YACxD,CAAC,CAAC,SAAS;KAChB,CAAC,CAAC;IAEH,UAAU,CAAC,IAAI,CACb;QACE,QAAQ,EAAE,YAAY;QACtB,eAAe,EAAE,GAAG,iBAAiB,IAAI;KAC1C,EACD,uDAAuD,CACxD,CAAC;IAEF,WAAW,GAAG,IAAI,CAAC;AACrB,CAAC;AAED,gFAAgF;AAChF,aAAa;AACb,gFAAgF;AAEhF;;;GAGG;AACU,QAAA,MAAM,GAAG;IACpB,IAAI,EAAE,CAAC,GAAW,EAAE,GAAY,EAAE,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC;IAC/D,KAAK,EAAE,CAAC,GAAW,EAAE,GAAY,EAAE,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC;IACjE,IAAI,EAAE,CAAC,GAAW,EAAE,GAAY,EAAE,EAAE,CAAC,UAAU,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,CAAC;IAC/D,KAAK,EAAE,CAAC,GAAW,EAAE,GAAY,EAAE,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC,GAAG,EAAE,GAAG,CAAC;IACjE,KAAK,EAAE,CAAC,QAAgB,EAAE,EAAE,CAAC,UAAU,EAAE,KAAK,CAAC,QAAQ,CAAC;CACzD,CAAC;AAEF,gFAAgF;AAChF,aAAa;AACb,gFAAgF;AAEhF;;;;;;;;;;;GAWG;AACH,SAAgB,SAAS,CAAC,IAAa;IACrC,OAAO,WAAK,CAAC,SAAS,CAAC,IAAI,IAAI,WAAW,CAAC,CAAC;AAC9C,CAAC;AAED,gFAAgF;AAChF,YAAY;AACZ,gFAAgF;AAEhF;;;;;;;;;;;;;;GAcG;AACH,SAAgB,QAAQ,CAAC,IAAY,EAAE,UAAkB,OAAO;IAC9D,OAAO,aAAO,CAAC,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC;AACzC,CAAC;AAED,gFAAgF;AAChF,kBAAkB;AAClB,gFAAgF;AAEhF;;GAEG;AACH,SAAgB,iBAAiB;IAC/B,OAAO,WAAG,CAAC,QAAQ,EAAE,CAAC;AACxB,CAAC;AAED;;GAEG;AACH,SAAgB,cAAc,CAAI,GAAmB,EAAE,EAAW;IAChE,OAAO,WAAG,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;AAC1B,CAAC;AAED;;;GAGG;AACH,SAAgB,eAAe;IAC7B,MAAM,IAAI,GAAG,WAAK,CAAC,aAAa,EAAE,CAAC;IACnC,IAAI,CAAC,IAAI;QAAE,OAAO,EAAE,CAAC;IAErB,MAAM,GAAG,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAC/B,OAAO;QACL,OAAO,EAAE,GAAG,CAAC,OAAO;QACpB,MAAM,EAAE,GAAG,CAAC,MAAM;KACnB,CAAC;AACJ,CAAC;AAED,gFAAgF;AAChF,WAAW;AACX,gFAAgF;AAEhF;;;GAGG;AACI,KAAK,UAAU,QAAQ;IAC5B,IAAI,CAAC,WAAW;QAAE,OAAO;IAEzB,IAAI,CAAC;QACH,MAAM,aAAa,EAAE,QAAQ,EAAE,CAAC;QAChC,MAAM,GAAG,EAAE,QAAQ,EAAE,CAAC;QACtB,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;IAChE,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,gDAAgD,EAAE,KAAK,CAAC,CAAC;IACzE,CAAC;AACH,CAAC"}
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Fastify Observability Plugin
3
+ *
4
+ * Automatically handles:
5
+ * - Correlation ID extraction/generation (x-request-id header)
6
+ * - OpenTelemetry trace context propagation
7
+ * - Request/response logging with trace context
8
+ * - HTTP server metrics (request count, duration)
9
+ *
10
+ * Usage:
11
+ * import { observabilityPlugin } from '@ciq-dev/neoiq-foundation-node';
12
+ * app.register(observabilityPlugin, { serviceName: 'my-service' });
13
+ */
14
+ import { FastifyPluginAsync } from 'fastify';
15
+ import { Span } from '@opentelemetry/api';
16
+ interface PluginRequestContext {
17
+ correlationId: string;
18
+ traceId: string;
19
+ spanId: string;
20
+ startTime: number;
21
+ }
22
+ export interface PluginOptions {
23
+ /**
24
+ * Service name for metrics (required).
25
+ */
26
+ serviceName: string;
27
+ /**
28
+ * Routes to exclude from tracing/metrics (e.g., ['/health']).
29
+ */
30
+ excludeRoutes?: string[];
31
+ }
32
+ declare module 'fastify' {
33
+ interface FastifyRequest {
34
+ __span?: Span;
35
+ __requestContext?: PluginRequestContext;
36
+ }
37
+ }
38
+ export declare const observabilityPlugin: FastifyPluginAsync<PluginOptions>;
39
+ export {};
40
+ //# sourceMappingURL=plugin.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AAEH,OAAO,EAAE,kBAAkB,EAAgC,MAAM,SAAS,CAAC;AAG3E,OAAO,EAA+C,IAAI,EAAE,MAAM,oBAAoB,CAAC;AAOvF,UAAU,oBAAoB;IAC5B,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B;;OAEG;IACH,WAAW,EAAE,MAAM,CAAC;IAEpB;;OAEG;IACH,aAAa,CAAC,EAAE,MAAM,EAAE,CAAC;CAC1B;AAGD,OAAO,QAAQ,SAAS,CAAC;IACvB,UAAU,cAAc;QACtB,MAAM,CAAC,EAAE,IAAI,CAAC;QACd,gBAAgB,CAAC,EAAE,oBAAoB,CAAC;KACzC;CACF;AA8LD,eAAO,MAAM,mBAAmB,mCAG9B,CAAC"}
package/dist/plugin.js ADDED
@@ -0,0 +1,176 @@
1
+ "use strict";
2
+ /**
3
+ * Fastify Observability Plugin
4
+ *
5
+ * Automatically handles:
6
+ * - Correlation ID extraction/generation (x-request-id header)
7
+ * - OpenTelemetry trace context propagation
8
+ * - Request/response logging with trace context
9
+ * - HTTP server metrics (request count, duration)
10
+ *
11
+ * Usage:
12
+ * import { observabilityPlugin } from '@ciq-dev/neoiq-foundation-node';
13
+ * app.register(observabilityPlugin, { serviceName: 'my-service' });
14
+ */
15
+ var __importDefault = (this && this.__importDefault) || function (mod) {
16
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.observabilityPlugin = void 0;
20
+ const fastify_plugin_1 = __importDefault(require("fastify-plugin"));
21
+ const crypto_1 = require("crypto");
22
+ const api_1 = require("@opentelemetry/api");
23
+ const observability_1 = require("./observability");
24
+ // -----------------------------------------------------------------------------
25
+ // Plugin Implementation
26
+ // -----------------------------------------------------------------------------
27
+ const plugin = async (fastify, options) => {
28
+ const { serviceName, excludeRoutes = ['/health', '/health/'] } = options;
29
+ const tracer = api_1.trace.getTracer('neoiq-foundation-node');
30
+ // Setup metrics (per Groundcover guide)
31
+ const meter = (0, observability_1.getMeter)(serviceName);
32
+ const requestCounter = meter.createCounter('http.server.requests.total', {
33
+ description: 'Total number of HTTP requests',
34
+ });
35
+ const requestDuration = meter.createHistogram('http.server.request.duration', {
36
+ description: 'HTTP request duration in milliseconds',
37
+ unit: 'ms',
38
+ });
39
+ const requestErrors = meter.createCounter('http.server.requests.errors', {
40
+ description: 'Total number of HTTP request errors',
41
+ });
42
+ // ---------------------------------------------------------------------------
43
+ // ON REQUEST - Extract context, start span
44
+ // ---------------------------------------------------------------------------
45
+ fastify.addHook('onRequest', (request, reply, done) => {
46
+ // Skip health checks
47
+ if (excludeRoutes.some((route) => request.url.startsWith(route))) {
48
+ done();
49
+ return;
50
+ }
51
+ // 1. Extract or generate correlation ID from x-request-id header
52
+ const correlationId = request.headers['x-request-id'] || (0, crypto_1.randomUUID)();
53
+ // 2. Set correlation ID in response header
54
+ reply.header('x-request-id', correlationId);
55
+ // 3. Extract parent trace context from incoming headers (W3C traceparent)
56
+ const parentContext = api_1.propagation.extract(api_1.context.active(), request.headers);
57
+ // 4. Start a new span for this request
58
+ const span = tracer.startSpan(`${request.method} ${request.routeOptions?.url || request.url}`, {
59
+ kind: 1, // SpanKind.SERVER
60
+ attributes: {
61
+ 'http.method': request.method,
62
+ 'http.url': request.url,
63
+ 'http.route': request.routeOptions?.url || request.url,
64
+ 'http.user_agent': request.headers['user-agent'] || '',
65
+ 'http.correlation_id': correlationId,
66
+ },
67
+ }, parentContext);
68
+ // 5. Get trace/span IDs
69
+ const spanContext = span.spanContext();
70
+ const traceId = spanContext.traceId;
71
+ const spanId = spanContext.spanId;
72
+ // 6. Store context
73
+ const requestContext = {
74
+ correlationId,
75
+ traceId,
76
+ spanId,
77
+ startTime: Date.now(),
78
+ };
79
+ request.__span = span;
80
+ request.__requestContext = requestContext;
81
+ // 7. Run in AsyncLocalStorage context
82
+ observability_1.als.run({ correlationId, traceId, spanId }, () => {
83
+ observability_1.logger.info({
84
+ correlationId,
85
+ traceId,
86
+ spanId,
87
+ method: request.method,
88
+ url: request.url,
89
+ route: request.routeOptions?.url,
90
+ userAgent: request.headers['user-agent'],
91
+ }, 'Request received');
92
+ done();
93
+ });
94
+ });
95
+ // ---------------------------------------------------------------------------
96
+ // ON RESPONSE - End span, record metrics
97
+ // ---------------------------------------------------------------------------
98
+ fastify.addHook('onResponse', (request, reply, done) => {
99
+ const ctx = request.__requestContext;
100
+ const span = request.__span;
101
+ if (!ctx) {
102
+ done();
103
+ return;
104
+ }
105
+ const durationMs = Date.now() - ctx.startTime;
106
+ const route = request.routeOptions?.url || request.url;
107
+ const labels = {
108
+ method: request.method,
109
+ route,
110
+ status_code: String(reply.statusCode),
111
+ };
112
+ // Run in context for proper log correlation
113
+ observability_1.als.run({ correlationId: ctx.correlationId, traceId: ctx.traceId, spanId: ctx.spanId }, () => {
114
+ // Log response
115
+ observability_1.logger.info({
116
+ correlationId: ctx.correlationId,
117
+ traceId: ctx.traceId,
118
+ spanId: ctx.spanId,
119
+ method: request.method,
120
+ url: request.url,
121
+ route,
122
+ statusCode: reply.statusCode,
123
+ durationMs,
124
+ }, 'Request completed');
125
+ // Record metrics (per Groundcover guide)
126
+ requestCounter.add(1, labels);
127
+ requestDuration.record(durationMs, labels);
128
+ if (reply.statusCode >= 400) {
129
+ requestErrors.add(1, labels);
130
+ }
131
+ // End span
132
+ if (span) {
133
+ span.setStatus({
134
+ code: reply.statusCode < 400 ? api_1.SpanStatusCode.OK : api_1.SpanStatusCode.ERROR,
135
+ });
136
+ span.setAttribute('http.status_code', reply.statusCode);
137
+ span.setAttribute('http.response_time_ms', durationMs);
138
+ span.end();
139
+ }
140
+ done();
141
+ });
142
+ });
143
+ // ---------------------------------------------------------------------------
144
+ // ON ERROR - Record exception on span
145
+ // ---------------------------------------------------------------------------
146
+ fastify.addHook('onError', (request, reply, error, done) => {
147
+ const ctx = request.__requestContext;
148
+ const span = request.__span;
149
+ if (!ctx) {
150
+ done();
151
+ return;
152
+ }
153
+ observability_1.als.run({ correlationId: ctx.correlationId, traceId: ctx.traceId, spanId: ctx.spanId }, () => {
154
+ observability_1.logger.error({
155
+ correlationId: ctx.correlationId,
156
+ traceId: ctx.traceId,
157
+ spanId: ctx.spanId,
158
+ method: request.method,
159
+ url: request.url,
160
+ error: error.message,
161
+ stack: error.stack,
162
+ }, 'Request failed');
163
+ if (span) {
164
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error.message });
165
+ span.recordException(error);
166
+ }
167
+ done();
168
+ });
169
+ });
170
+ };
171
+ // Export plugin
172
+ exports.observabilityPlugin = (0, fastify_plugin_1.default)(plugin, {
173
+ name: 'neoiq-observability',
174
+ fastify: '4.x',
175
+ });
176
+ //# sourceMappingURL=plugin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.js","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;;;;;AAGH,oEAAgC;AAChC,mCAAoC;AACpC,4CAAuF;AACvF,mDAAwD;AAiCxD,gFAAgF;AAChF,wBAAwB;AACxB,gFAAgF;AAEhF,MAAM,MAAM,GAAsC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,EAAE;IAC3E,MAAM,EAAE,WAAW,EAAE,aAAa,GAAG,CAAC,SAAS,EAAE,UAAU,CAAC,EAAE,GAAG,OAAO,CAAC;IAEzE,MAAM,MAAM,GAAG,WAAK,CAAC,SAAS,CAAC,uBAAuB,CAAC,CAAC;IAExD,wCAAwC;IACxC,MAAM,KAAK,GAAG,IAAA,wBAAQ,EAAC,WAAW,CAAC,CAAC;IACpC,MAAM,cAAc,GAAG,KAAK,CAAC,aAAa,CAAC,4BAA4B,EAAE;QACvE,WAAW,EAAE,+BAA+B;KAC7C,CAAC,CAAC;IACH,MAAM,eAAe,GAAG,KAAK,CAAC,eAAe,CAAC,8BAA8B,EAAE;QAC5E,WAAW,EAAE,uCAAuC;QACpD,IAAI,EAAE,IAAI;KACX,CAAC,CAAC;IACH,MAAM,aAAa,GAAG,KAAK,CAAC,aAAa,CAAC,6BAA6B,EAAE;QACvE,WAAW,EAAE,qCAAqC;KACnD,CAAC,CAAC;IAEH,8EAA8E;IAC9E,2CAA2C;IAC3C,8EAA8E;IAC9E,OAAO,CAAC,OAAO,CAAC,WAAW,EAAE,CAAC,OAAuB,EAAE,KAAmB,EAAE,IAAI,EAAE,EAAE;QAClF,qBAAqB;QACrB,IAAI,aAAa,CAAC,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC;YACjE,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,iEAAiE;QACjE,MAAM,aAAa,GAAI,OAAO,CAAC,OAAO,CAAC,cAAc,CAAY,IAAI,IAAA,mBAAU,GAAE,CAAC;QAElF,2CAA2C;QAC3C,KAAK,CAAC,MAAM,CAAC,cAAc,EAAE,aAAa,CAAC,CAAC;QAE5C,0EAA0E;QAC1E,MAAM,aAAa,GAAG,iBAAW,CAAC,OAAO,CAAC,aAAO,CAAC,MAAM,EAAE,EAAE,OAAO,CAAC,OAAO,CAAC,CAAC;QAE7E,uCAAuC;QACvC,MAAM,IAAI,GAAG,MAAM,CAAC,SAAS,CAC3B,GAAG,OAAO,CAAC,MAAM,IAAI,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,EAAE,EAC/D;YACE,IAAI,EAAE,CAAC,EAAE,kBAAkB;YAC3B,UAAU,EAAE;gBACV,aAAa,EAAE,OAAO,CAAC,MAAM;gBAC7B,UAAU,EAAE,OAAO,CAAC,GAAG;gBACvB,YAAY,EAAE,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG;gBACtD,iBAAiB,EAAE,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC,IAAI,EAAE;gBACtD,qBAAqB,EAAE,aAAa;aACrC;SACF,EACD,aAAa,CACd,CAAC;QAEF,wBAAwB;QACxB,MAAM,WAAW,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;QACvC,MAAM,OAAO,GAAG,WAAW,CAAC,OAAO,CAAC;QACpC,MAAM,MAAM,GAAG,WAAW,CAAC,MAAM,CAAC;QAElC,mBAAmB;QACnB,MAAM,cAAc,GAAyB;YAC3C,aAAa;YACb,OAAO;YACP,MAAM;YACN,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;SACtB,CAAC;QAEF,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC;QACtB,OAAO,CAAC,gBAAgB,GAAG,cAAc,CAAC;QAE1C,sCAAsC;QACtC,mBAAG,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE;YAC/C,sBAAM,CAAC,IAAI,CACT;gBACE,aAAa;gBACb,OAAO;gBACP,MAAM;gBACN,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,KAAK,EAAE,OAAO,CAAC,YAAY,EAAE,GAAG;gBAChC,SAAS,EAAE,OAAO,CAAC,OAAO,CAAC,YAAY,CAAC;aACzC,EACD,kBAAkB,CACnB,CAAC;YAEF,IAAI,EAAE,CAAC;QACT,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,8EAA8E;IAC9E,yCAAyC;IACzC,8EAA8E;IAC9E,OAAO,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,OAAuB,EAAE,KAAmB,EAAE,IAAI,EAAE,EAAE;QACnF,MAAM,GAAG,GAAG,OAAO,CAAC,gBAAgB,CAAC;QACrC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;QAE5B,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,MAAM,UAAU,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,CAAC,SAAS,CAAC;QAC9C,MAAM,KAAK,GAAG,OAAO,CAAC,YAAY,EAAE,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC;QACvD,MAAM,MAAM,GAAG;YACb,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,KAAK;YACL,WAAW,EAAE,MAAM,CAAC,KAAK,CAAC,UAAU,CAAC;SACtC,CAAC;QAEF,4CAA4C;QAC5C,mBAAG,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,GAAG,CAAC,aAAa,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE;YAC3F,eAAe;YACf,sBAAM,CAAC,IAAI,CACT;gBACE,aAAa,EAAE,GAAG,CAAC,aAAa;gBAChC,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,KAAK;gBACL,UAAU,EAAE,KAAK,CAAC,UAAU;gBAC5B,UAAU;aACX,EACD,mBAAmB,CACpB,CAAC;YAEF,yCAAyC;YACzC,cAAc,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;YAC9B,eAAe,CAAC,MAAM,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;YAE3C,IAAI,KAAK,CAAC,UAAU,IAAI,GAAG,EAAE,CAAC;gBAC5B,aAAa,CAAC,GAAG,CAAC,CAAC,EAAE,MAAM,CAAC,CAAC;YAC/B,CAAC;YAED,WAAW;YACX,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC,SAAS,CAAC;oBACb,IAAI,EAAE,KAAK,CAAC,UAAU,GAAG,GAAG,CAAC,CAAC,CAAC,oBAAc,CAAC,EAAE,CAAC,CAAC,CAAC,oBAAc,CAAC,KAAK;iBACxE,CAAC,CAAC;gBACH,IAAI,CAAC,YAAY,CAAC,kBAAkB,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC;gBACxD,IAAI,CAAC,YAAY,CAAC,uBAAuB,EAAE,UAAU,CAAC,CAAC;gBACvD,IAAI,CAAC,GAAG,EAAE,CAAC;YACb,CAAC;YAED,IAAI,EAAE,CAAC;QACT,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;IAEH,8EAA8E;IAC9E,sCAAsC;IACtC,8EAA8E;IAC9E,OAAO,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC,OAAuB,EAAE,KAAmB,EAAE,KAAY,EAAE,IAAI,EAAE,EAAE;QAC9F,MAAM,GAAG,GAAG,OAAO,CAAC,gBAAgB,CAAC;QACrC,MAAM,IAAI,GAAG,OAAO,CAAC,MAAM,CAAC;QAE5B,IAAI,CAAC,GAAG,EAAE,CAAC;YACT,IAAI,EAAE,CAAC;YACP,OAAO;QACT,CAAC;QAED,mBAAG,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,GAAG,CAAC,aAAa,EAAE,OAAO,EAAE,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,CAAC,MAAM,EAAE,EAAE,GAAG,EAAE;YAC3F,sBAAM,CAAC,KAAK,CACV;gBACE,aAAa,EAAE,GAAG,CAAC,aAAa;gBAChC,OAAO,EAAE,GAAG,CAAC,OAAO;gBACpB,MAAM,EAAE,GAAG,CAAC,MAAM;gBAClB,MAAM,EAAE,OAAO,CAAC,MAAM;gBACtB,GAAG,EAAE,OAAO,CAAC,GAAG;gBAChB,KAAK,EAAE,KAAK,CAAC,OAAO;gBACpB,KAAK,EAAE,KAAK,CAAC,KAAK;aACnB,EACD,gBAAgB,CACjB,CAAC;YAEF,IAAI,IAAI,EAAE,CAAC;gBACT,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,oBAAc,CAAC,KAAK,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;gBACvE,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC;YAC9B,CAAC;YAED,IAAI,EAAE,CAAC;QACT,CAAC,CAAC,CAAC;IACL,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF,gBAAgB;AACH,QAAA,mBAAmB,GAAG,IAAA,wBAAE,EAAC,MAAM,EAAE;IAC5C,IAAI,EAAE,qBAAqB;IAC3B,OAAO,EAAE,KAAK;CACf,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@ciq-dev/neoiq-foundation-node",
3
+ "version": "1.0.0-beta.1",
4
+ "description": "Node.js observability foundation for CommerceIQ services. Integrates with Groundcover via OpenTelemetry.",
5
+ "main": "dist/index.js",
6
+ "module": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "keywords": [
12
+ "observability",
13
+ "opentelemetry",
14
+ "tracing",
15
+ "metrics",
16
+ "logging",
17
+ "groundcover",
18
+ "commerceiq",
19
+ "neoiq"
20
+ ],
21
+ "author": "CommerceIQ",
22
+ "license": "MIT",
23
+ "engines": {
24
+ "node": ">=18.0.0"
25
+ },
26
+ "peerDependencies": {
27
+ "@opentelemetry/api": "^1.7.0",
28
+ "@opentelemetry/auto-instrumentations-node": "^0.41.0",
29
+ "@opentelemetry/exporter-metrics-otlp-grpc": "^0.48.0",
30
+ "@opentelemetry/exporter-trace-otlp-grpc": "^0.48.0",
31
+ "@opentelemetry/resources": "^1.21.0",
32
+ "@opentelemetry/sdk-metrics": "^1.21.0",
33
+ "@opentelemetry/sdk-node": "^0.48.0",
34
+ "@opentelemetry/semantic-conventions": "^1.21.0",
35
+ "axios": "^1.6.0",
36
+ "axios-retry": "^4.0.0",
37
+ "fastify": "^4.25.0",
38
+ "fastify-plugin": "^4.5.0",
39
+ "opossum": "^8.1.0",
40
+ "pino": "^8.17.0",
41
+ "pino-pretty": "^10.3.0"
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/commerceiq/neoiq-foundation-node.git"
46
+ }
47
+ }