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 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
@@ -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.__psObserveAttached) return;
37
- client.__psObserveAttached = true;
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 psObserve = require('@pruservices/ps-observe');
10
+ * const observer = require('@pruservices/ps-observe');
11
11
  *
12
- * fastify.register(psObserve, {
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.psObserve.attach(myClient);
29
+ * fastify.observer.attach(myClient);
30
30
  *
31
31
  * // Option B: Auto-attach to all clients on an object
32
- * fastify.psObserve.attachToAll(fastify.services);
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 psObservePlugin(fastify, opts) {
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('psObserve', {
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 psObserve decorator is visible to the parent scope
155
- psObservePlugin[Symbol.for('skip-override')] = true;
154
+ // Skip encapsulation so observer decorator is visible to the parent scope
155
+ observerPlugin[Symbol.for('skip-override')] = true;
156
156
 
157
- module.exports = psObservePlugin;
157
+ module.exports = observerPlugin;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "api-observe",
3
- "version": "1.0.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": [