api-observe 1.0.0 → 1.0.2
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 +395 -0
- package/lib/interceptor.js +2 -2
- package/lib/plugin.js +9 -9
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,395 @@
|
|
|
1
|
+
# api-observe
|
|
2
|
+
|
|
3
|
+
Framework-agnostic API failure tracker for Node.js. Captures failed HTTP requests with full request/response details and serves a built-in dashboard to browse them.
|
|
4
|
+
|
|
5
|
+
Works with **Fastify**, **Express**, **NestJS**, **Koa**, or **plain Node.js HTTP**.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- Captures upstream API failures (via Axios interceptors)
|
|
10
|
+
- Captures controller/route-level errors (via framework hooks)
|
|
11
|
+
- Built-in HTML dashboard at `/observe`
|
|
12
|
+
- REST API for querying failures programmatically
|
|
13
|
+
- In-memory ring buffer with configurable size and TTL
|
|
14
|
+
- Automatic redaction of sensitive headers (authorization, tokens, passwords)
|
|
15
|
+
- Zero external dependencies
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
npm install api-observe
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
### Fastify
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
const Fastify = require('fastify');
|
|
29
|
+
const apiObserve = require('api-observe');
|
|
30
|
+
|
|
31
|
+
const fastify = Fastify();
|
|
32
|
+
|
|
33
|
+
// Register the plugin
|
|
34
|
+
await fastify.register(apiObserve, {
|
|
35
|
+
maxEntries: 500, // max failure records to keep (default: 500)
|
|
36
|
+
ttlMs: 86400000, // TTL in ms before eviction (default: 24h)
|
|
37
|
+
sanitize: (data) => data, // custom sanitizer (optional)
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Attach to your HTTP clients after registration
|
|
41
|
+
fastify.after(() => {
|
|
42
|
+
// Option A: single client
|
|
43
|
+
fastify.observer.attach(myHttpClient);
|
|
44
|
+
|
|
45
|
+
// Option B: auto-attach to all clients on an object
|
|
46
|
+
fastify.observer.attachToAll(fastify.services);
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
await fastify.listen({ port: 3000 });
|
|
50
|
+
// Dashboard: http://localhost:3000/observe
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The Fastify plugin automatically captures:
|
|
54
|
+
- **Upstream errors** - failed HTTP calls from attached clients (via Axios interceptors)
|
|
55
|
+
- **Controller errors** - any error thrown during the request lifecycle (via `onError` hook)
|
|
56
|
+
- **404s** - requests to non-existent routes (via `onResponse` hook)
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
### Express
|
|
61
|
+
|
|
62
|
+
```js
|
|
63
|
+
const express = require('express');
|
|
64
|
+
const { expressMiddleware } = require('api-observe/express');
|
|
65
|
+
|
|
66
|
+
const app = express();
|
|
67
|
+
|
|
68
|
+
// Create the middleware
|
|
69
|
+
const observe = expressMiddleware({
|
|
70
|
+
maxEntries: 500,
|
|
71
|
+
ttlMs: 86400000,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Mount the dashboard and API routes
|
|
75
|
+
app.use(observe);
|
|
76
|
+
|
|
77
|
+
// ... your routes here ...
|
|
78
|
+
|
|
79
|
+
// Mount the error handler AFTER your routes
|
|
80
|
+
app.use(observe.errorHandler);
|
|
81
|
+
|
|
82
|
+
// Attach to your HTTP clients
|
|
83
|
+
observe.attach(myHttpClient);
|
|
84
|
+
observe.attachToAll(myServices);
|
|
85
|
+
|
|
86
|
+
app.listen(3000);
|
|
87
|
+
// Dashboard: http://localhost:3000/observe
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
**`observe`** (main middleware) serves the dashboard and API routes at `/observe`.
|
|
91
|
+
|
|
92
|
+
**`observe.errorHandler`** is an Express error middleware `(err, req, res, next)` that captures controller-level errors. Place it after your routes.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### Plain Node.js HTTP
|
|
97
|
+
|
|
98
|
+
Works with any framework that exposes `(req, res)` — including Koa, NestJS, Hapi, etc.
|
|
99
|
+
|
|
100
|
+
```js
|
|
101
|
+
const http = require('http');
|
|
102
|
+
const { createHttpHandler } = require('api-observe/http');
|
|
103
|
+
|
|
104
|
+
const observe = createHttpHandler({
|
|
105
|
+
maxEntries: 500,
|
|
106
|
+
ttlMs: 86400000,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
const server = http.createServer((req, res) => {
|
|
110
|
+
// Let api-observe handle /observe routes
|
|
111
|
+
if (observe.handle(req, res)) return;
|
|
112
|
+
|
|
113
|
+
// Your app logic...
|
|
114
|
+
res.end('Hello');
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// Attach to your HTTP clients
|
|
118
|
+
observe.attach(myHttpClient);
|
|
119
|
+
|
|
120
|
+
// Manually capture errors
|
|
121
|
+
try {
|
|
122
|
+
await doSomething();
|
|
123
|
+
} catch (err) {
|
|
124
|
+
observe.captureError(err, {
|
|
125
|
+
method: 'POST',
|
|
126
|
+
url: '/api/users',
|
|
127
|
+
headers: req.headers,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
server.listen(3000);
|
|
132
|
+
// Dashboard: http://localhost:3000/observe
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
---
|
|
136
|
+
|
|
137
|
+
### NestJS
|
|
138
|
+
|
|
139
|
+
NestJS runs on Express (or Fastify) under the hood. Use the matching adapter:
|
|
140
|
+
|
|
141
|
+
**NestJS + Express (default):**
|
|
142
|
+
|
|
143
|
+
```js
|
|
144
|
+
import { NestFactory } from '@nestjs/core';
|
|
145
|
+
import { AppModule } from './app.module';
|
|
146
|
+
import { expressMiddleware } from 'api-observe/express';
|
|
147
|
+
|
|
148
|
+
async function bootstrap() {
|
|
149
|
+
const app = await NestFactory.create(AppModule);
|
|
150
|
+
|
|
151
|
+
const observe = expressMiddleware();
|
|
152
|
+
app.use(observe);
|
|
153
|
+
|
|
154
|
+
// Attach to your HTTP clients
|
|
155
|
+
observe.attach(myHttpClient);
|
|
156
|
+
|
|
157
|
+
await app.listen(3000);
|
|
158
|
+
}
|
|
159
|
+
bootstrap();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
**NestJS + Fastify:**
|
|
163
|
+
|
|
164
|
+
```js
|
|
165
|
+
import { NestFactory } from '@nestjs/core';
|
|
166
|
+
import { FastifyAdapter } from '@nestjs/platform-fastify';
|
|
167
|
+
import { AppModule } from './app.module';
|
|
168
|
+
import apiObserve from 'api-observe';
|
|
169
|
+
|
|
170
|
+
async function bootstrap() {
|
|
171
|
+
const app = await NestFactory.create(AppModule, new FastifyAdapter());
|
|
172
|
+
|
|
173
|
+
const fastify = app.getHttpAdapter().getInstance();
|
|
174
|
+
await fastify.register(apiObserve);
|
|
175
|
+
|
|
176
|
+
await app.listen(3000);
|
|
177
|
+
}
|
|
178
|
+
bootstrap();
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## Dashboard
|
|
182
|
+
|
|
183
|
+
Visit `/observe` in your browser to access the built-in dashboard.
|
|
184
|
+
|
|
185
|
+
The dashboard provides:
|
|
186
|
+
- Real-time failure table with filtering by type, service, method, status code, and URL
|
|
187
|
+
- Summary cards showing total failures, upstream vs controller errors, and affected services
|
|
188
|
+
- Detail modal with full request/response headers, bodies, and stack traces
|
|
189
|
+
- Auto-refresh every 30 seconds
|
|
190
|
+
- Pagination for large result sets
|
|
191
|
+
|
|
192
|
+
## REST API
|
|
193
|
+
|
|
194
|
+
All endpoints are served under `/observe/api/`:
|
|
195
|
+
|
|
196
|
+
| Method | Endpoint | Description |
|
|
197
|
+
|--------|----------|-------------|
|
|
198
|
+
| `GET` | `/observe` | HTML dashboard |
|
|
199
|
+
| `GET` | `/observe/api/failures` | List failures (with filters) |
|
|
200
|
+
| `GET` | `/observe/api/failures/:id` | Single failure detail |
|
|
201
|
+
| `GET` | `/observe/api/summary` | Stats grouped by service/status |
|
|
202
|
+
| `DELETE` | `/observe/api/failures` | Clear all stored failures |
|
|
203
|
+
|
|
204
|
+
### Query Parameters for `GET /observe/api/failures`
|
|
205
|
+
|
|
206
|
+
| Parameter | Type | Description |
|
|
207
|
+
|-----------|------|-------------|
|
|
208
|
+
| `type` | string | Filter by `upstream` or `controller` |
|
|
209
|
+
| `service` | string | Filter by service name (partial match) |
|
|
210
|
+
| `method` | string | Filter by HTTP method (`GET`, `POST`, etc.) |
|
|
211
|
+
| `statusCode` | number | Filter by response status code |
|
|
212
|
+
| `url` | string | Filter by URL (partial match) |
|
|
213
|
+
| `from` | string | Start date (ISO 8601) |
|
|
214
|
+
| `to` | string | End date (ISO 8601) |
|
|
215
|
+
| `limit` | number | Results per page (default: 50) |
|
|
216
|
+
| `offset` | number | Pagination offset (default: 0) |
|
|
217
|
+
|
|
218
|
+
### Example
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
# Get all 5xx failures from the auth service
|
|
222
|
+
curl "http://localhost:3000/observe/api/failures?service=auth&statusCode=500"
|
|
223
|
+
|
|
224
|
+
# Get summary stats
|
|
225
|
+
curl "http://localhost:3000/observe/api/summary"
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
## Configuration Options
|
|
229
|
+
|
|
230
|
+
All adapters accept the same options:
|
|
231
|
+
|
|
232
|
+
| Option | Type | Default | Description |
|
|
233
|
+
|--------|------|---------|-------------|
|
|
234
|
+
| `maxEntries` | number | `500` | Maximum failure records to keep in memory |
|
|
235
|
+
| `ttlMs` | number | `86400000` (24h) | Time-to-live in ms before entries are evicted |
|
|
236
|
+
| `sanitize` | function | built-in | Custom sanitizer for request/response bodies |
|
|
237
|
+
|
|
238
|
+
### Default Sanitization
|
|
239
|
+
|
|
240
|
+
The built-in sanitizer redacts these headers/fields automatically:
|
|
241
|
+
|
|
242
|
+
- `authorization`
|
|
243
|
+
- `x-access-token`
|
|
244
|
+
- `cookie` / `set-cookie`
|
|
245
|
+
- `secret` / `password` / `token`
|
|
246
|
+
- `refresh`
|
|
247
|
+
- `x-api-key`
|
|
248
|
+
|
|
249
|
+
To customize, pass your own `sanitize` function:
|
|
250
|
+
|
|
251
|
+
```js
|
|
252
|
+
apiObserve({
|
|
253
|
+
sanitize: (data) => {
|
|
254
|
+
if (!data || typeof data !== 'object') return data;
|
|
255
|
+
const copy = { ...data };
|
|
256
|
+
delete copy.ssn;
|
|
257
|
+
delete copy.creditCard;
|
|
258
|
+
return copy;
|
|
259
|
+
},
|
|
260
|
+
});
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
## Compatible HTTP Client
|
|
264
|
+
|
|
265
|
+
`api-observe` intercepts failures from any HTTP client object that exposes:
|
|
266
|
+
- `_axios` — an Axios instance
|
|
267
|
+
- `serviceName` — a string identifier for the service
|
|
268
|
+
|
|
269
|
+
Below is a sample `BaseHttpClient` class that works out of the box:
|
|
270
|
+
|
|
271
|
+
```js
|
|
272
|
+
const axios = require('axios');
|
|
273
|
+
|
|
274
|
+
class BaseHttpClient {
|
|
275
|
+
constructor({ baseUrl, serviceName, timeoutMs = 5000 }) {
|
|
276
|
+
this.serviceName = serviceName;
|
|
277
|
+
|
|
278
|
+
this._axios = axios.create({
|
|
279
|
+
baseURL: baseUrl,
|
|
280
|
+
timeout: timeoutMs,
|
|
281
|
+
headers: {
|
|
282
|
+
'Content-Type': 'application/json',
|
|
283
|
+
Accept: 'application/json',
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
async get(path, options = {}) {
|
|
289
|
+
const res = await this._axios.get(path, {
|
|
290
|
+
params: options.params,
|
|
291
|
+
correlationId: options.correlationId,
|
|
292
|
+
});
|
|
293
|
+
return res.data;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
async post(path, data, options = {}) {
|
|
297
|
+
const res = await this._axios.post(path, data, {
|
|
298
|
+
correlationId: options.correlationId,
|
|
299
|
+
});
|
|
300
|
+
return res.data;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async put(path, data, options = {}) {
|
|
304
|
+
const res = await this._axios.put(path, data, {
|
|
305
|
+
correlationId: options.correlationId,
|
|
306
|
+
});
|
|
307
|
+
return res.data;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
async delete(path, options = {}) {
|
|
311
|
+
const res = await this._axios.delete(path, {
|
|
312
|
+
correlationId: options.correlationId,
|
|
313
|
+
});
|
|
314
|
+
return res.data;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
module.exports = { BaseHttpClient };
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### Creating a service client
|
|
322
|
+
|
|
323
|
+
Wrap `BaseHttpClient` in a service class for each upstream API:
|
|
324
|
+
|
|
325
|
+
```js
|
|
326
|
+
const { BaseHttpClient } = require('./base-http.client');
|
|
327
|
+
|
|
328
|
+
class UserService {
|
|
329
|
+
constructor(config) {
|
|
330
|
+
this.client = new BaseHttpClient({
|
|
331
|
+
baseUrl: config.baseUrl,
|
|
332
|
+
serviceName: 'user-service',
|
|
333
|
+
timeoutMs: config.timeoutMs,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
async getUserById(id, opts = {}) {
|
|
338
|
+
return this.client.get(`/v1/users/${id}`, {
|
|
339
|
+
correlationId: opts.correlationId,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
async createUser(data, opts = {}) {
|
|
344
|
+
return this.client.post('/v1/users', data, {
|
|
345
|
+
correlationId: opts.correlationId,
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
module.exports = UserService;
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
### Attaching to api-observe
|
|
354
|
+
|
|
355
|
+
```js
|
|
356
|
+
// Single client
|
|
357
|
+
observe.attach(userService.client);
|
|
358
|
+
|
|
359
|
+
// Or auto-attach all clients on an object — api-observe will find
|
|
360
|
+
// any value (or value.client) that has _axios + serviceName
|
|
361
|
+
const services = {
|
|
362
|
+
userService: new UserService({ baseUrl: 'http://user-svc:4001' }),
|
|
363
|
+
orderService: new OrderService({ baseUrl: 'http://order-svc:4002' }),
|
|
364
|
+
};
|
|
365
|
+
observe.attachToAll(services);
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
`attachToAll` walks the object's keys and attaches to:
|
|
369
|
+
- Direct `BaseHttpClient` instances (has `_axios` + `serviceName`)
|
|
370
|
+
- Service wrappers with a `.client` property that is a `BaseHttpClient`
|
|
371
|
+
|
|
372
|
+
## Exports
|
|
373
|
+
|
|
374
|
+
```js
|
|
375
|
+
// Default: Fastify plugin
|
|
376
|
+
const apiObserve = require('api-observe');
|
|
377
|
+
|
|
378
|
+
// Express adapter
|
|
379
|
+
const { expressMiddleware } = require('api-observe/express');
|
|
380
|
+
|
|
381
|
+
// Plain HTTP adapter
|
|
382
|
+
const { createHttpHandler } = require('api-observe/http');
|
|
383
|
+
|
|
384
|
+
// Core building blocks (for custom integrations)
|
|
385
|
+
const {
|
|
386
|
+
FailureStore, // In-memory failure store
|
|
387
|
+
attachInterceptor, // Attach to a single client
|
|
388
|
+
attachToAll, // Auto-attach to all clients on an object
|
|
389
|
+
createRouter, // Framework-agnostic route handler
|
|
390
|
+
} = require('api-observe');
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
## License
|
|
394
|
+
|
|
395
|
+
MIT
|
package/lib/interceptor.js
CHANGED
|
@@ -33,8 +33,8 @@ function attachInterceptor(client, store, opts = {}) {
|
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
// Mark this client so we don't double-attach
|
|
36
|
-
if (client.
|
|
37
|
-
client.
|
|
36
|
+
if (client.__observerAttached) return;
|
|
37
|
+
client.__observerAttached = true;
|
|
38
38
|
|
|
39
39
|
// ── Eject existing response interceptors ───────────���────────────────────
|
|
40
40
|
// Save them, clear them, add ours first, then re-add the originals.
|
package/lib/plugin.js
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
*
|
|
8
8
|
* Usage in a consuming service:
|
|
9
9
|
*
|
|
10
|
-
* const
|
|
10
|
+
* const observer = require('@pruservices/ps-observe');
|
|
11
11
|
*
|
|
12
|
-
* fastify.register(
|
|
12
|
+
* fastify.register(observer, {
|
|
13
13
|
* // (optional) max failure records to keep in memory (default: 500)
|
|
14
14
|
* maxEntries: 500,
|
|
15
15
|
*
|
|
@@ -26,10 +26,10 @@
|
|
|
26
26
|
* // After registering your service clients, attach the interceptors:
|
|
27
27
|
* fastify.after(() => {
|
|
28
28
|
* // Option A: Attach to individual clients
|
|
29
|
-
* fastify.
|
|
29
|
+
* fastify.observer.attach(myClient);
|
|
30
30
|
*
|
|
31
31
|
* // Option B: Auto-attach to all clients on an object
|
|
32
|
-
* fastify.
|
|
32
|
+
* fastify.observer.attachToAll(fastify.services);
|
|
33
33
|
* });
|
|
34
34
|
*/
|
|
35
35
|
|
|
@@ -37,7 +37,7 @@ const { FailureStore } = require('./failure-store');
|
|
|
37
37
|
const { attachInterceptor, attachToAll } = require('./interceptor');
|
|
38
38
|
const { registerRoutes } = require('./routes');
|
|
39
39
|
|
|
40
|
-
async function
|
|
40
|
+
async function observerPlugin(fastify, opts) {
|
|
41
41
|
const store = new FailureStore({
|
|
42
42
|
maxEntries: opts.maxEntries,
|
|
43
43
|
ttlMs: opts.ttlMs,
|
|
@@ -46,7 +46,7 @@ async function psObservePlugin(fastify, opts) {
|
|
|
46
46
|
const sanitizeOpts = opts.sanitize ? { sanitize: opts.sanitize } : {};
|
|
47
47
|
|
|
48
48
|
// Decorate fastify with the observe API so consumers can attach clients
|
|
49
|
-
fastify.decorate('
|
|
49
|
+
fastify.decorate('observer', {
|
|
50
50
|
/** Attach interceptor to a single BaseHttpClient instance */
|
|
51
51
|
attach(client) {
|
|
52
52
|
attachInterceptor(client, store, sanitizeOpts);
|
|
@@ -151,7 +151,7 @@ async function psObservePlugin(fastify, opts) {
|
|
|
151
151
|
registerRoutes(fastify, store);
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
// Skip encapsulation so
|
|
155
|
-
|
|
154
|
+
// Skip encapsulation so observer decorator is visible to the parent scope
|
|
155
|
+
observerPlugin[Symbol.for('skip-override')] = true;
|
|
156
156
|
|
|
157
|
-
module.exports =
|
|
157
|
+
module.exports = observerPlugin;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-observe",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.2",
|
|
4
4
|
"description": "Framework-agnostic plugin that captures and displays API failure details — works with Fastify, Express, NestJS, Koa, or plain Node.js HTTP",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"files": [
|