@himanshu-panchal/nodescope-sdk 1.0.0 → 1.0.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/index.js +54 -26
- package/middleware.js +263 -11
- package/package.json +20 -8
package/index.js
CHANGED
|
@@ -2,30 +2,29 @@ const { createMiddleware } = require('./middleware');
|
|
|
2
2
|
const { getInstance } = require('./tracer');
|
|
3
3
|
const { Context } = require('./context');
|
|
4
4
|
const { autoDetectAndPatch } = require('./auto-detect');
|
|
5
|
-
const { patchPg } = require('./patches/pg');
|
|
6
|
-
const { patchMysql } = require('./patches/mysql');
|
|
7
|
-
const { patchMongoose } = require('./patches/mongoose');
|
|
8
|
-
const { patchRedis } = require('./patches/redis');
|
|
9
|
-
const { patchAxios } = require('./patches/axios');
|
|
10
5
|
const { v4: uuidv4 } = require('uuid');
|
|
11
6
|
|
|
12
7
|
let autoDetectDone = false;
|
|
13
8
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
function nodescope(config = {}) {
|
|
10
|
+
// Enhanced default config
|
|
11
|
+
const enhancedConfig = {
|
|
12
|
+
...config,
|
|
13
|
+
captureHeaders: config.captureHeaders || false,
|
|
14
|
+
captureBody: config.captureBody || false,
|
|
15
|
+
captureResponse: config.captureResponse || false,
|
|
16
|
+
captureQueries: config.captureQueries !== false, // true by default
|
|
17
|
+
};
|
|
18
|
+
|
|
17
19
|
if (!autoDetectDone) {
|
|
18
20
|
autoDetectDone = true;
|
|
19
|
-
|
|
20
|
-
getInstance(config);
|
|
21
|
-
// Phir auto detect
|
|
21
|
+
getInstance(enhancedConfig);
|
|
22
22
|
setTimeout(() => autoDetectAndPatch(), 100);
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
return createMiddleware(
|
|
25
|
+
return createMiddleware(enhancedConfig);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
// Manual deep trace wrapper
|
|
29
28
|
function trace(fn) {
|
|
30
29
|
return function (...args) {
|
|
31
30
|
const tracer = getInstance();
|
|
@@ -40,6 +39,20 @@ function trace(fn) {
|
|
|
40
39
|
|
|
41
40
|
tracer.startSpan(spanId, traceId, name, 'internal');
|
|
42
41
|
|
|
42
|
+
// Capture function arguments (safely)
|
|
43
|
+
if (args.length > 0) {
|
|
44
|
+
const safeArgs = args.map((arg, i) => {
|
|
45
|
+
if (typeof arg === 'object' && arg !== null) {
|
|
46
|
+
// Truncate large objects
|
|
47
|
+
return JSON.stringify(arg).length > 500
|
|
48
|
+
? `[Object: ${Object.keys(arg).length} keys]`
|
|
49
|
+
: arg;
|
|
50
|
+
}
|
|
51
|
+
return arg;
|
|
52
|
+
});
|
|
53
|
+
tracer.addAttribute(spanId, 'function.arguments', safeArgs);
|
|
54
|
+
}
|
|
55
|
+
|
|
43
56
|
let result;
|
|
44
57
|
try {
|
|
45
58
|
result = fn.apply(this, args);
|
|
@@ -50,8 +63,33 @@ function trace(fn) {
|
|
|
50
63
|
|
|
51
64
|
if (result?.then) {
|
|
52
65
|
return result
|
|
53
|
-
.then((res) => {
|
|
54
|
-
|
|
66
|
+
.then((res) => {
|
|
67
|
+
// Capture return value (safely)
|
|
68
|
+
if (res !== undefined) {
|
|
69
|
+
const safeResult = typeof res === 'object' && res !== null
|
|
70
|
+
? JSON.stringify(res).length > 500
|
|
71
|
+
? `[Object: ${Object.keys(res).length} keys]`
|
|
72
|
+
: res
|
|
73
|
+
: res;
|
|
74
|
+
tracer.addAttribute(spanId, 'function.result', safeResult);
|
|
75
|
+
}
|
|
76
|
+
tracer.endSpan(spanId);
|
|
77
|
+
return res;
|
|
78
|
+
})
|
|
79
|
+
.catch((err) => {
|
|
80
|
+
tracer.endSpan(spanId, err);
|
|
81
|
+
throw err;
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Capture synchronous return value
|
|
86
|
+
if (result !== undefined) {
|
|
87
|
+
const safeResult = typeof result === 'object' && result !== null
|
|
88
|
+
? JSON.stringify(result).length > 500
|
|
89
|
+
? `[Object: ${Object.keys(result).length} keys]`
|
|
90
|
+
: result
|
|
91
|
+
: result;
|
|
92
|
+
tracer.addAttribute(spanId, 'function.result', safeResult);
|
|
55
93
|
}
|
|
56
94
|
|
|
57
95
|
tracer.endSpan(spanId);
|
|
@@ -59,17 +97,7 @@ function trace(fn) {
|
|
|
59
97
|
};
|
|
60
98
|
}
|
|
61
99
|
|
|
62
|
-
// Exports
|
|
63
100
|
module.exports = nodescope;
|
|
64
|
-
|
|
65
|
-
// Named exports for manual use
|
|
66
101
|
module.exports.nodescope = nodescope;
|
|
67
102
|
module.exports.trace = trace;
|
|
68
|
-
module.exports.Context = Context;
|
|
69
|
-
|
|
70
|
-
// Manual patches (agar auto-detect kaam na kare)
|
|
71
|
-
module.exports.patchPg = patchPg;
|
|
72
|
-
module.exports.patchMysql = patchMysql;
|
|
73
|
-
module.exports.patchMongoose = patchMongoose;
|
|
74
|
-
module.exports.patchRedis = patchRedis;
|
|
75
|
-
module.exports.patchAxios = patchAxios;
|
|
103
|
+
module.exports.Context = Context;
|
package/middleware.js
CHANGED
|
@@ -2,21 +2,25 @@ const { v4: uuidv4 } = require('uuid');
|
|
|
2
2
|
const { Context } = require('./context');
|
|
3
3
|
const { getInstance } = require('./tracer');
|
|
4
4
|
|
|
5
|
-
function createMiddleware(config) {
|
|
5
|
+
function createMiddleware(config = {}) {
|
|
6
6
|
const tracer = getInstance(config);
|
|
7
7
|
|
|
8
8
|
return function nodeScopeMiddleware(req, res, next) {
|
|
9
|
-
//
|
|
10
|
-
if (req.path === '/health' || req.path === '/ping') {
|
|
9
|
+
// Skip health checks and static assets
|
|
10
|
+
if (req.path === '/health' || req.path === '/ping' || req.path.startsWith('/static')) {
|
|
11
11
|
return next();
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
const traceId = uuidv4();
|
|
15
15
|
const requestId = uuidv4();
|
|
16
16
|
|
|
17
|
+
// Start the main request trace
|
|
17
18
|
tracer.startTrace(traceId, req.method, req.path);
|
|
18
19
|
|
|
20
|
+
// Run within AsyncLocalStorage context
|
|
19
21
|
Context.run(traceId, requestId, req.path, req.method, () => {
|
|
22
|
+
|
|
23
|
+
// Attach NodeScope helper to request object
|
|
20
24
|
req.nodescope = {
|
|
21
25
|
traceId,
|
|
22
26
|
startSpan: (name) => {
|
|
@@ -25,44 +29,292 @@ function createMiddleware(config) {
|
|
|
25
29
|
return {
|
|
26
30
|
end: (error) => tracer.endSpan(spanId, error),
|
|
27
31
|
setAttribute: (key, value) => tracer.addAttribute(spanId, key, value),
|
|
32
|
+
addEvent: (name, attributes) => tracer.addAttribute(spanId, `event.${name}`, attributes || {}),
|
|
28
33
|
};
|
|
29
34
|
},
|
|
30
35
|
};
|
|
31
36
|
|
|
37
|
+
// Capture request metadata
|
|
32
38
|
const metadata = {
|
|
33
|
-
ip: req.ip || req.connection?.remoteAddress,
|
|
39
|
+
ip: req.ip || req.connection?.remoteAddress || req.socket?.remoteAddress,
|
|
34
40
|
userAgent: req.get('user-agent'),
|
|
35
|
-
|
|
36
|
-
|
|
41
|
+
route: req.route?.path || req.path,
|
|
42
|
+
method: req.method,
|
|
43
|
+
protocol: req.protocol,
|
|
44
|
+
hostname: req.hostname,
|
|
45
|
+
originalUrl: req.originalUrl,
|
|
46
|
+
timestamp: new Date().toISOString(),
|
|
37
47
|
};
|
|
38
48
|
|
|
49
|
+
// Capture query parameters
|
|
50
|
+
if (req.query && Object.keys(req.query).length > 0) {
|
|
51
|
+
metadata.query = req.query;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Capture request headers (with masking)
|
|
55
|
+
if (config.captureHeaders) {
|
|
56
|
+
metadata.headers = maskSensitiveHeaders(req.headers, config.mask || []);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Capture request body (with size limit and masking)
|
|
60
|
+
if (config.captureBody && req.body) {
|
|
61
|
+
metadata.body = maskSensitiveData(req.body, config.mask || []);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Capture request size
|
|
65
|
+
const contentLength = req.get('content-length');
|
|
66
|
+
if (contentLength) {
|
|
67
|
+
metadata.requestSize = parseInt(contentLength);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Response tracking variables
|
|
39
71
|
let ended = false;
|
|
72
|
+
let responseBody = null;
|
|
73
|
+
let responseStartTime = Date.now();
|
|
40
74
|
|
|
41
75
|
function endTrace() {
|
|
42
|
-
if (
|
|
43
|
-
|
|
44
|
-
|
|
76
|
+
if (ended) return;
|
|
77
|
+
ended = true;
|
|
78
|
+
|
|
79
|
+
// Calculate response time
|
|
80
|
+
const responseTime = Date.now() - responseStartTime;
|
|
81
|
+
|
|
82
|
+
// Add response metadata
|
|
83
|
+
const responseMetadata = {
|
|
84
|
+
...metadata,
|
|
85
|
+
responseTime,
|
|
86
|
+
statusCode: res.statusCode,
|
|
87
|
+
statusMessage: res.statusMessage,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
// Capture response headers
|
|
91
|
+
if (config.captureHeaders) {
|
|
92
|
+
responseMetadata.responseHeaders = res.getHeaders();
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Capture response body
|
|
96
|
+
if (config.captureResponse && responseBody !== null) {
|
|
97
|
+
responseMetadata.responseBody = maskSensitiveData(responseBody, config.mask || []);
|
|
45
98
|
}
|
|
99
|
+
|
|
100
|
+
// Capture response size
|
|
101
|
+
const responseLength = res.get('content-length');
|
|
102
|
+
if (responseLength) {
|
|
103
|
+
responseMetadata.responseSize = parseInt(responseLength);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// End the trace with all metadata
|
|
107
|
+
tracer.endTrace(traceId, res.statusCode, responseMetadata);
|
|
46
108
|
}
|
|
47
109
|
|
|
110
|
+
// Intercept response methods
|
|
48
111
|
const originalJson = res.json.bind(res);
|
|
49
112
|
const originalSend = res.send.bind(res);
|
|
113
|
+
const originalEnd = res.end.bind(res);
|
|
50
114
|
|
|
115
|
+
// Override res.json()
|
|
51
116
|
res.json = function (data) {
|
|
117
|
+
if (config.captureResponse && data !== undefined) {
|
|
118
|
+
responseBody = data;
|
|
119
|
+
}
|
|
52
120
|
endTrace();
|
|
53
121
|
return originalJson(data);
|
|
54
122
|
};
|
|
55
123
|
|
|
124
|
+
// Override res.send()
|
|
56
125
|
res.send = function (data) {
|
|
126
|
+
if (config.captureResponse && data !== undefined) {
|
|
127
|
+
try {
|
|
128
|
+
// Try to parse as JSON if it's a string
|
|
129
|
+
responseBody = typeof data === 'string' ? JSON.parse(data) : data;
|
|
130
|
+
} catch (e) {
|
|
131
|
+
// If not JSON, store as string (truncated if too long)
|
|
132
|
+
responseBody = typeof data === 'string' && data.length > 1000
|
|
133
|
+
? data.substring(0, 1000) + '...[truncated]'
|
|
134
|
+
: data;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
57
137
|
endTrace();
|
|
58
138
|
return originalSend(data);
|
|
59
139
|
};
|
|
60
140
|
|
|
61
|
-
res.
|
|
141
|
+
// Override res.end()
|
|
142
|
+
res.end = function (chunk, encoding) {
|
|
143
|
+
if (config.captureResponse && chunk && !responseBody) {
|
|
144
|
+
try {
|
|
145
|
+
responseBody = chunk.toString();
|
|
146
|
+
if (responseBody.length > 1000) {
|
|
147
|
+
responseBody = responseBody.substring(0, 1000) + '...[truncated]';
|
|
148
|
+
}
|
|
149
|
+
} catch (e) {
|
|
150
|
+
responseBody = '[Binary data]';
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
endTrace();
|
|
154
|
+
return originalEnd(chunk, encoding);
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// Handle response finish event as fallback
|
|
158
|
+
res.on('finish', () => {
|
|
159
|
+
endTrace();
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
// Handle response close event (for aborted requests)
|
|
163
|
+
res.on('close', () => {
|
|
164
|
+
if (!res.headersSent) {
|
|
165
|
+
endTrace();
|
|
166
|
+
}
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
// Error handling
|
|
170
|
+
res.on('error', (error) => {
|
|
171
|
+
metadata.error = {
|
|
172
|
+
message: error.message,
|
|
173
|
+
stack: error.stack,
|
|
174
|
+
type: error.name,
|
|
175
|
+
};
|
|
176
|
+
endTrace();
|
|
177
|
+
});
|
|
62
178
|
|
|
179
|
+
// Continue to next middleware
|
|
63
180
|
next();
|
|
64
181
|
});
|
|
65
182
|
};
|
|
66
183
|
}
|
|
67
184
|
|
|
68
|
-
|
|
185
|
+
// Helper function to mask sensitive headers
|
|
186
|
+
function maskSensitiveHeaders(headers, maskKeys = []) {
|
|
187
|
+
const sensitiveKeys = [
|
|
188
|
+
'authorization', 'cookie', 'set-cookie', 'x-api-key',
|
|
189
|
+
'x-auth-token', 'auth-token', 'access-token', 'refresh-token',
|
|
190
|
+
'x-csrf-token', 'csrf-token', 'session-id', 'sessionid',
|
|
191
|
+
...maskKeys
|
|
192
|
+
];
|
|
193
|
+
|
|
194
|
+
const masked = {};
|
|
195
|
+
|
|
196
|
+
for (const [key, value] of Object.entries(headers)) {
|
|
197
|
+
const lowerKey = key.toLowerCase();
|
|
198
|
+
const shouldMask = sensitiveKeys.some(sensitive =>
|
|
199
|
+
lowerKey.includes(sensitive.toLowerCase())
|
|
200
|
+
);
|
|
201
|
+
|
|
202
|
+
if (shouldMask) {
|
|
203
|
+
masked[key] = '***MASKED***';
|
|
204
|
+
} else {
|
|
205
|
+
// Truncate very long header values
|
|
206
|
+
masked[key] = typeof value === 'string' && value.length > 200
|
|
207
|
+
? value.substring(0, 200) + '...[truncated]'
|
|
208
|
+
: value;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return masked;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// Helper function to mask sensitive data in objects
|
|
216
|
+
function maskSensitiveData(data, maskKeys = []) {
|
|
217
|
+
if (!data || typeof data !== 'object') {
|
|
218
|
+
return data;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const sensitiveKeys = [
|
|
222
|
+
'password', 'passwd', 'pwd', 'secret', 'token', 'key', 'auth',
|
|
223
|
+
'authorization', 'cookie', 'session', 'csrf', 'ssn', 'social',
|
|
224
|
+
'credit', 'card', 'cvv', 'pin', 'otp', 'email', 'phone', 'mobile',
|
|
225
|
+
...maskKeys
|
|
226
|
+
];
|
|
227
|
+
|
|
228
|
+
function maskObject(obj) {
|
|
229
|
+
if (Array.isArray(obj)) {
|
|
230
|
+
return obj.map(item => maskObject(item));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (obj === null || typeof obj !== 'object') {
|
|
234
|
+
return obj;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const masked = {};
|
|
238
|
+
|
|
239
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
240
|
+
const lowerKey = key.toLowerCase();
|
|
241
|
+
const shouldMask = sensitiveKeys.some(sensitive =>
|
|
242
|
+
lowerKey.includes(sensitive.toLowerCase())
|
|
243
|
+
);
|
|
244
|
+
|
|
245
|
+
if (shouldMask) {
|
|
246
|
+
masked[key] = '***MASKED***';
|
|
247
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
248
|
+
masked[key] = maskObject(value);
|
|
249
|
+
} else if (typeof value === 'string' && value.length > 1000) {
|
|
250
|
+
// Truncate very long strings
|
|
251
|
+
masked[key] = value.substring(0, 1000) + '...[truncated]';
|
|
252
|
+
} else {
|
|
253
|
+
masked[key] = value;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
return masked;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return maskObject(data);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// Helper function to safely stringify with size limit
|
|
264
|
+
function safeStringify(obj, maxSize = 10000) {
|
|
265
|
+
try {
|
|
266
|
+
const str = JSON.stringify(obj, null, 2);
|
|
267
|
+
return str.length > maxSize
|
|
268
|
+
? str.substring(0, maxSize) + '\n...[truncated]'
|
|
269
|
+
: str;
|
|
270
|
+
} catch (error) {
|
|
271
|
+
return '[Circular reference or non-serializable data]';
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Helper function to get request size estimate
|
|
276
|
+
function getRequestSizeEstimate(req) {
|
|
277
|
+
let size = 0;
|
|
278
|
+
|
|
279
|
+
// Headers size
|
|
280
|
+
if (req.headers) {
|
|
281
|
+
size += JSON.stringify(req.headers).length;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// Body size
|
|
285
|
+
if (req.body) {
|
|
286
|
+
size += JSON.stringify(req.body).length;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// Query size
|
|
290
|
+
if (req.query && Object.keys(req.query).length > 0) {
|
|
291
|
+
size += JSON.stringify(req.query).length;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return size;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Helper function to detect content type
|
|
298
|
+
function getResponseContentType(res) {
|
|
299
|
+
const contentType = res.get('content-type');
|
|
300
|
+
if (!contentType) return 'unknown';
|
|
301
|
+
|
|
302
|
+
if (contentType.includes('application/json')) return 'json';
|
|
303
|
+
if (contentType.includes('text/html')) return 'html';
|
|
304
|
+
if (contentType.includes('text/plain')) return 'text';
|
|
305
|
+
if (contentType.includes('application/xml')) return 'xml';
|
|
306
|
+
if (contentType.includes('image/')) return 'image';
|
|
307
|
+
if (contentType.includes('video/')) return 'video';
|
|
308
|
+
if (contentType.includes('audio/')) return 'audio';
|
|
309
|
+
|
|
310
|
+
return 'other';
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
module.exports = {
|
|
314
|
+
createMiddleware,
|
|
315
|
+
maskSensitiveHeaders,
|
|
316
|
+
maskSensitiveData,
|
|
317
|
+
safeStringify,
|
|
318
|
+
getRequestSizeEstimate,
|
|
319
|
+
getResponseContentType
|
|
320
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@himanshu-panchal/nodescope-sdk",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.1",
|
|
4
4
|
"description": "Universal Node.js observability SDK - works with any project",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -32,11 +32,23 @@
|
|
|
32
32
|
"express": ">=4.0.0"
|
|
33
33
|
},
|
|
34
34
|
"peerDependenciesMeta": {
|
|
35
|
-
"express": {
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
"
|
|
39
|
-
|
|
40
|
-
|
|
35
|
+
"express": {
|
|
36
|
+
"optional": true
|
|
37
|
+
},
|
|
38
|
+
"pg": {
|
|
39
|
+
"optional": true
|
|
40
|
+
},
|
|
41
|
+
"mysql2": {
|
|
42
|
+
"optional": true
|
|
43
|
+
},
|
|
44
|
+
"mongoose": {
|
|
45
|
+
"optional": true
|
|
46
|
+
},
|
|
47
|
+
"redis": {
|
|
48
|
+
"optional": true
|
|
49
|
+
},
|
|
50
|
+
"axios": {
|
|
51
|
+
"optional": true
|
|
52
|
+
}
|
|
41
53
|
}
|
|
42
|
-
}
|
|
54
|
+
}
|