@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 +293 -0
- package/dist/http-client.d.ts +80 -0
- package/dist/http-client.d.ts.map +1 -0
- package/dist/http-client.js +188 -0
- package/dist/http-client.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/observability.d.ts +132 -0
- package/dist/observability.d.ts.map +1 -0
- package/dist/observability.js +246 -0
- package/dist/observability.js.map +1 -0
- package/dist/plugin.d.ts +40 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +176 -0
- package/dist/plugin.js.map +1 -0
- package/package.json +47 -0
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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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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":"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"}
|
package/dist/plugin.d.ts
ADDED
|
@@ -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":"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"}
|
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
|
+
}
|