api-observe 1.1.0 → 1.1.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/lib/adapters/express.js +2 -1
- package/lib/dashboard/index.js +36 -11
- package/lib/failure-store.js +5 -1
- package/lib/interceptor.js +49 -13
- package/lib/plugin.js +1 -1
- package/lib/router.js +2 -2
- package/package.json +1 -1
package/lib/adapters/express.js
CHANGED
|
@@ -30,6 +30,7 @@ const { parse: parseUrl } = require('url');
|
|
|
30
30
|
const { FailureStore } = require('../failure-store');
|
|
31
31
|
const { attachInterceptor, attachToAll, defaultSanitize } = require('../interceptor');
|
|
32
32
|
const { createRouter } = require('../router');
|
|
33
|
+
const { requestContext } = require('../request-context');
|
|
33
34
|
|
|
34
35
|
/**
|
|
35
36
|
* Create an Express middleware that captures errors and serves the observe dashboard.
|
|
@@ -54,7 +55,7 @@ function expressMiddleware(opts = {}) {
|
|
|
54
55
|
|
|
55
56
|
// Only handle /observe routes
|
|
56
57
|
if (!pathname.startsWith('/observe')) {
|
|
57
|
-
|
|
58
|
+
requestContext.run({ captured: false, startedAt: Date.now(), inboundRequest: req }, next);
|
|
58
59
|
return;
|
|
59
60
|
}
|
|
60
61
|
|
package/lib/dashboard/index.js
CHANGED
|
@@ -12,7 +12,7 @@ function getDashboardHtml() {
|
|
|
12
12
|
<head>
|
|
13
13
|
<meta charset="UTF-8">
|
|
14
14
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
15
|
-
<title>
|
|
15
|
+
<title>observe | API Failure Tracker</title>
|
|
16
16
|
<style>
|
|
17
17
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
18
18
|
|
|
@@ -306,7 +306,7 @@ function getDashboardHtml() {
|
|
|
306
306
|
</head>
|
|
307
307
|
<body>
|
|
308
308
|
<header>
|
|
309
|
-
<h1>
|
|
309
|
+
<h1>observe</h1>
|
|
310
310
|
<div class="stats">
|
|
311
311
|
<span class="count" id="totalCount">0</span> failures tracked
|
|
312
312
|
</div>
|
|
@@ -329,6 +329,7 @@ function getDashboardHtml() {
|
|
|
329
329
|
</select>
|
|
330
330
|
<input type="number" id="filterStatus" placeholder="Status code..." min="100" max="599" />
|
|
331
331
|
<input type="text" id="filterUrl" placeholder="URL contains..." />
|
|
332
|
+
<input type="text" id="filterCorrelationId" placeholder="Correlation ID..." />
|
|
332
333
|
<button class="refresh-btn" onclick="loadFailures()">Refresh</button>
|
|
333
334
|
<button class="danger" onclick="clearAll()">Clear All</button>
|
|
334
335
|
</div>
|
|
@@ -373,12 +374,14 @@ function getDashboardHtml() {
|
|
|
373
374
|
const method = document.getElementById('filterMethod').value;
|
|
374
375
|
const statusCode = document.getElementById('filterStatus').value;
|
|
375
376
|
const url = document.getElementById('filterUrl').value;
|
|
377
|
+
const correlationId = document.getElementById('filterCorrelationId').value.trim();
|
|
376
378
|
|
|
377
379
|
if (type) params.set('type', type);
|
|
378
380
|
if (service) params.set('service', service);
|
|
379
381
|
if (method) params.set('method', method);
|
|
380
382
|
if (statusCode) params.set('statusCode', statusCode);
|
|
381
383
|
if (url) params.set('url', url);
|
|
384
|
+
if (correlationId) params.set('correlationId', correlationId);
|
|
382
385
|
params.set('limit', PAGE_SIZE);
|
|
383
386
|
params.set('offset', currentOffset);
|
|
384
387
|
|
|
@@ -499,24 +502,46 @@ function getDashboardHtml() {
|
|
|
499
502
|
: ''
|
|
500
503
|
) +
|
|
501
504
|
|
|
502
|
-
'<div class="detail-section"><h3>Request Headers</h3>' +
|
|
505
|
+
'<div class="detail-section"><h3>Inbound Request Headers</h3>' +
|
|
503
506
|
'<pre class="json-block">' + escHtml(formatJson(f.request?.headers)) + '</pre></div>' +
|
|
504
507
|
|
|
505
508
|
(f.request?.query
|
|
506
|
-
? '<div class="detail-section"><h3>
|
|
509
|
+
? '<div class="detail-section"><h3>Inbound Query Params</h3>' +
|
|
507
510
|
'<pre class="json-block">' + escHtml(formatJson(f.request?.query)) + '</pre></div>'
|
|
508
511
|
: ''
|
|
509
512
|
) +
|
|
510
513
|
|
|
511
|
-
'<div class="detail-section"><h3>Request Body</h3>' +
|
|
514
|
+
'<div class="detail-section"><h3>Inbound Request Body</h3>' +
|
|
512
515
|
'<pre class="json-block">' + escHtml(formatJson(f.request?.body)) + '</pre></div>' +
|
|
513
516
|
|
|
514
|
-
(f.
|
|
515
|
-
? '<div class="detail-section"><h3>
|
|
516
|
-
'<
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
517
|
+
(f.upstreamCall
|
|
518
|
+
? '<div class="detail-section"><h3>Upstream Call \u2014 ' + escHtml(f.upstreamCall.service) + '</h3>' +
|
|
519
|
+
'<dl class="detail-grid">' +
|
|
520
|
+
'<dt>Method</dt><dd><span class="method-badge method-' + escHtml(f.upstreamCall.method) + '">' + escHtml(f.upstreamCall.method) + '</span></dd>' +
|
|
521
|
+
'<dt>URL</dt><dd>' + escHtml(f.upstreamCall.url || '') + '</dd>' +
|
|
522
|
+
'<dt>Status Code</dt><dd>' + (f.upstreamCall.statusCode || 'N/A') + '</dd>' +
|
|
523
|
+
'<dt>Duration</dt><dd>' + (f.upstreamCall.durationMs != null ? f.upstreamCall.durationMs + 'ms' : 'N/A') + '</dd>' +
|
|
524
|
+
'<dt>Error</dt><dd>' + escHtml(f.upstreamCall.errorMessage || '') + '</dd>' +
|
|
525
|
+
(f.upstreamCall.errorCode ? '<dt>Error Code</dt><dd>' + escHtml(f.upstreamCall.errorCode) + '</dd>' : '') +
|
|
526
|
+
'</dl></div>' +
|
|
527
|
+
'<div class="detail-section"><h3>Upstream Request Headers</h3>' +
|
|
528
|
+
'<pre class="json-block">' + escHtml(formatJson(f.upstreamCall.request?.headers)) + '</pre></div>' +
|
|
529
|
+
'<div class="detail-section"><h3>Upstream Request Body</h3>' +
|
|
530
|
+
'<pre class="json-block">' + escHtml(formatJson(f.upstreamCall.request?.body)) + '</pre></div>' +
|
|
531
|
+
(f.upstreamCall.response
|
|
532
|
+
? '<div class="detail-section"><h3>Upstream Response Headers</h3>' +
|
|
533
|
+
'<pre class="json-block">' + escHtml(formatJson(f.upstreamCall.response?.headers)) + '</pre></div>' +
|
|
534
|
+
'<div class="detail-section"><h3>Upstream Response Body</h3>' +
|
|
535
|
+
'<pre class="json-block">' + escHtml(formatJson(f.upstreamCall.response?.body)) + '</pre></div>'
|
|
536
|
+
: '<div class="detail-section"><h3>Upstream Response</h3><p>No response received (timeout / network error)</p></div>'
|
|
537
|
+
)
|
|
538
|
+
: (f.response
|
|
539
|
+
? '<div class="detail-section"><h3>Response Headers</h3>' +
|
|
540
|
+
'<pre class="json-block">' + escHtml(formatJson(f.response?.headers)) + '</pre></div>' +
|
|
541
|
+
'<div class="detail-section"><h3>Response Body</h3>' +
|
|
542
|
+
'<pre class="json-block">' + escHtml(formatJson(f.response?.body)) + '</pre></div>'
|
|
543
|
+
: '<div class="detail-section"><h3>Response</h3><p>No response received (timeout / network error)</p></div>'
|
|
544
|
+
)
|
|
520
545
|
);
|
|
521
546
|
|
|
522
547
|
document.getElementById('modalOverlay').classList.add('active');
|
package/lib/failure-store.js
CHANGED
|
@@ -46,7 +46,7 @@ class FailureStore {
|
|
|
46
46
|
|
|
47
47
|
/**
|
|
48
48
|
* Query failures with optional filters.
|
|
49
|
-
* @param {{ service?: string, statusCode?: number, from?: string, to?: string, method?: string, url?: string, limit?: number, offset?: number }} [filters]
|
|
49
|
+
* @param {{ service?: string, statusCode?: number, from?: string, to?: string, method?: string, url?: string, correlationId?: string, limit?: number, offset?: number }} [filters]
|
|
50
50
|
* @returns {{ data: object[], total: number }}
|
|
51
51
|
*/
|
|
52
52
|
query(filters = {}) {
|
|
@@ -81,6 +81,10 @@ class FailureStore {
|
|
|
81
81
|
const url = filters.url.toLowerCase();
|
|
82
82
|
results = results.filter(e => e.url?.toLowerCase().includes(url));
|
|
83
83
|
}
|
|
84
|
+
if (filters.correlationId) {
|
|
85
|
+
const cid = String(filters.correlationId).toLowerCase();
|
|
86
|
+
results = results.filter(e => e.correlationId?.toLowerCase().includes(cid));
|
|
87
|
+
}
|
|
84
88
|
if (filters.from) {
|
|
85
89
|
const from = new Date(filters.from).getTime();
|
|
86
90
|
results = results.filter(e => new Date(e.timestamp).getTime() >= from);
|
package/lib/interceptor.js
CHANGED
|
@@ -63,39 +63,75 @@ function attachInterceptor(client, store, opts = {}) {
|
|
|
63
63
|
const response = error.response;
|
|
64
64
|
const durationMs = Date.now() - (config.metadata?.startMs ?? Date.now());
|
|
65
65
|
|
|
66
|
-
const
|
|
67
|
-
|
|
66
|
+
const ctx = requestContext.getStore();
|
|
67
|
+
const inboundReq = ctx?.inboundRequest ?? null;
|
|
68
|
+
|
|
69
|
+
// ── Downstream call details ────────────────────────────────────────────
|
|
70
|
+
const upstreamCall = {
|
|
68
71
|
service: client.serviceName,
|
|
69
72
|
method: config.method?.toUpperCase() ?? 'UNKNOWN',
|
|
70
73
|
url: buildFullUrl(config),
|
|
71
|
-
correlationId: config.correlationId ?? config.headers?.['x-correlation-id'] ?? null,
|
|
72
74
|
durationMs,
|
|
75
|
+
statusCode: response?.status ?? null,
|
|
73
76
|
errorMessage: error.message,
|
|
74
77
|
errorCode: error.code ?? null,
|
|
75
|
-
|
|
76
|
-
// Request details
|
|
77
78
|
request: {
|
|
78
79
|
headers: sanitize(config.headers),
|
|
79
80
|
params: config.params ?? null,
|
|
80
81
|
body: sanitize(config.data),
|
|
81
82
|
},
|
|
82
|
-
|
|
83
|
-
// Response details (null if no response, e.g. timeout/network error)
|
|
84
|
-
statusCode: response?.status ?? null,
|
|
85
83
|
response: response
|
|
86
|
-
? {
|
|
87
|
-
headers: response.headers ?? null,
|
|
88
|
-
body: sanitize(response.data),
|
|
89
|
-
}
|
|
84
|
+
? { headers: response.headers ?? null, body: sanitize(response.data) }
|
|
90
85
|
: null,
|
|
91
86
|
};
|
|
92
87
|
|
|
88
|
+
let record;
|
|
89
|
+
if (inboundReq) {
|
|
90
|
+
// Top-level = the inbound request that triggered this failure.
|
|
91
|
+
// The downstream call details are nested under upstreamCall.
|
|
92
|
+
record = {
|
|
93
|
+
type: 'upstream',
|
|
94
|
+
service: inboundReq.routeOptions?.url ?? inboundReq.originalUrl ?? inboundReq.url ?? 'unknown',
|
|
95
|
+
method: inboundReq.method ?? 'UNKNOWN',
|
|
96
|
+
url: inboundReq.originalUrl ?? inboundReq.url ?? 'unknown',
|
|
97
|
+
correlationId: inboundReq.correlationId ?? inboundReq.headers?.['x-correlation-id'] ?? config.correlationId ?? config.headers?.['x-correlation-id'] ?? null,
|
|
98
|
+
durationMs,
|
|
99
|
+
errorMessage: error.message,
|
|
100
|
+
errorCode: error.code ?? null,
|
|
101
|
+
statusCode: response?.status ?? null,
|
|
102
|
+
request: {
|
|
103
|
+
headers: sanitize(inboundReq.headers),
|
|
104
|
+
params: inboundReq.params ?? null,
|
|
105
|
+
query: inboundReq.query ?? null,
|
|
106
|
+
body: sanitize(inboundReq.body),
|
|
107
|
+
},
|
|
108
|
+
response: null,
|
|
109
|
+
upstreamCall,
|
|
110
|
+
};
|
|
111
|
+
} else {
|
|
112
|
+
// No inbound context (standalone / direct interceptor usage) —
|
|
113
|
+
// keep the downstream call as the top-level record.
|
|
114
|
+
record = {
|
|
115
|
+
type: 'upstream',
|
|
116
|
+
service: client.serviceName,
|
|
117
|
+
method: upstreamCall.method,
|
|
118
|
+
url: upstreamCall.url,
|
|
119
|
+
correlationId: config.correlationId ?? config.headers?.['x-correlation-id'] ?? null,
|
|
120
|
+
durationMs,
|
|
121
|
+
errorMessage: error.message,
|
|
122
|
+
errorCode: error.code ?? null,
|
|
123
|
+
statusCode: response?.status ?? null,
|
|
124
|
+
request: upstreamCall.request,
|
|
125
|
+
response: upstreamCall.response,
|
|
126
|
+
upstreamCall: null,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
|
|
93
130
|
store.add(record);
|
|
94
131
|
|
|
95
132
|
// Mark the in-flight Fastify request (if any) so the response-level
|
|
96
133
|
// hook in plugin.js does not record a duplicate when the controller
|
|
97
134
|
// catches this error and replies with its own 4xx/5xx.
|
|
98
|
-
const ctx = requestContext.getStore();
|
|
99
135
|
if (ctx) ctx.captured = true;
|
|
100
136
|
|
|
101
137
|
// Re-throw the original error so BaseHttpClient's handler still works
|
package/lib/plugin.js
CHANGED
|
@@ -88,7 +88,7 @@ async function observerPlugin(fastify, opts) {
|
|
|
88
88
|
// interceptor records a capture deep in the call stack, it can reach back
|
|
89
89
|
// and mark this request as already captured.
|
|
90
90
|
fastify.addHook('onRequest', (request, reply, done) => {
|
|
91
|
-
const ctx = { captured: false, startedAt: Date.now() };
|
|
91
|
+
const ctx = { captured: false, startedAt: Date.now(), inboundRequest: request };
|
|
92
92
|
request[CTX_KEY] = ctx;
|
|
93
93
|
requestContext.enterWith(ctx);
|
|
94
94
|
done();
|
package/lib/router.js
CHANGED
|
@@ -42,8 +42,8 @@ function createRouter(store) {
|
|
|
42
42
|
|
|
43
43
|
// ── List failures ──────────────────────────────────────────────────────
|
|
44
44
|
if (method === 'GET' && path === '/observe/api/failures') {
|
|
45
|
-
const { service, statusCode, method: m, url, from, to, limit, offset, type } = query || {};
|
|
46
|
-
const result = store.query({ service, statusCode, method: m, url, from, to, limit, offset, type });
|
|
45
|
+
const { service, statusCode, method: m, url, from, to, limit, offset, type, correlationId } = query || {};
|
|
46
|
+
const result = store.query({ service, statusCode, method: m, url, from, to, limit, offset, type, correlationId });
|
|
47
47
|
return { status: 200, headers: { 'content-type': 'application/json' }, body: result };
|
|
48
48
|
}
|
|
49
49
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "api-observe",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.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": [
|