@himanshu-panchal/nodescope-sdk 1.0.0
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/auto-detect.js +51 -0
- package/context.js +51 -0
- package/index.js +75 -0
- package/middleware.js +68 -0
- package/package.json +42 -0
- package/patches/axios.js +60 -0
- package/patches/fetch.js +0 -0
- package/patches/mongoose.js +57 -0
- package/patches/mysql.js +62 -0
- package/patches/pg.js +73 -0
- package/patches/redis.js +44 -0
- package/tracer.js +160 -0
package/auto-detect.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { patchPg } = require('./patches/pg');
|
|
2
|
+
const { patchMysql } = require('./patches/mysql');
|
|
3
|
+
const { patchMongoose } = require('./patches/mongoose');
|
|
4
|
+
const { patchRedis } = require('./patches/redis');
|
|
5
|
+
const { patchAxios } = require('./patches/axios');
|
|
6
|
+
|
|
7
|
+
function autoDetectAndPatch() {
|
|
8
|
+
const patched = [];
|
|
9
|
+
|
|
10
|
+
// PostgreSQL detect
|
|
11
|
+
try {
|
|
12
|
+
const pg = require('pg');
|
|
13
|
+
patchPg(pg);
|
|
14
|
+
patched.push('postgresql');
|
|
15
|
+
} catch (e) { /* not installed */ }
|
|
16
|
+
|
|
17
|
+
// MySQL detect
|
|
18
|
+
try {
|
|
19
|
+
const mysql = require('mysql2');
|
|
20
|
+
patchMysql(mysql);
|
|
21
|
+
patched.push('mysql');
|
|
22
|
+
} catch (e) {
|
|
23
|
+
try {
|
|
24
|
+
const mysql = require('mysql');
|
|
25
|
+
patchMysql(mysql);
|
|
26
|
+
patched.push('mysql');
|
|
27
|
+
} catch (e) { /* not installed */ }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// MongoDB/Mongoose detect
|
|
31
|
+
try {
|
|
32
|
+
const mongoose = require('mongoose');
|
|
33
|
+
patchMongoose(mongoose);
|
|
34
|
+
patched.push('mongodb');
|
|
35
|
+
} catch (e) { /* not installed */ }
|
|
36
|
+
|
|
37
|
+
// Axios detect
|
|
38
|
+
try {
|
|
39
|
+
const axios = require('axios');
|
|
40
|
+
patchAxios(axios);
|
|
41
|
+
patched.push('axios');
|
|
42
|
+
} catch (e) { /* not installed */ }
|
|
43
|
+
|
|
44
|
+
if (patched.length > 0) {
|
|
45
|
+
console.log(`✅ NodeScope: Auto-detected [${patched.join(', ')}]`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return patched;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { autoDetectAndPatch };
|
package/context.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { AsyncLocalStorage } = require('async_hooks');
|
|
2
|
+
|
|
3
|
+
const storage = new AsyncLocalStorage();
|
|
4
|
+
|
|
5
|
+
class Context {
|
|
6
|
+
static run(traceId, requestId, route, method, callback) {
|
|
7
|
+
const store = {
|
|
8
|
+
traceId,
|
|
9
|
+
requestId,
|
|
10
|
+
route,
|
|
11
|
+
method,
|
|
12
|
+
startTime: Date.now(),
|
|
13
|
+
spans: new Map(),
|
|
14
|
+
currentSpanId: null,
|
|
15
|
+
};
|
|
16
|
+
return storage.run(store, callback);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
static getStore() {
|
|
20
|
+
return storage.getStore();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
static getTraceId() {
|
|
24
|
+
return storage.getStore()?.traceId;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static addSpan(span) {
|
|
28
|
+
const store = storage.getStore();
|
|
29
|
+
if (store) store.spans.set(span.id, span);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
static getSpan(spanId) {
|
|
33
|
+
return storage.getStore()?.spans.get(spanId);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
static getAllSpans() {
|
|
37
|
+
const store = storage.getStore();
|
|
38
|
+
return store ? Array.from(store.spans.values()) : [];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
static setCurrentSpanId(spanId) {
|
|
42
|
+
const store = storage.getStore();
|
|
43
|
+
if (store) store.currentSpanId = spanId;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static getCurrentSpanId() {
|
|
47
|
+
return storage.getStore()?.currentSpanId;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { Context };
|
package/index.js
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const { createMiddleware } = require('./middleware');
|
|
2
|
+
const { getInstance } = require('./tracer');
|
|
3
|
+
const { Context } = require('./context');
|
|
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
|
+
const { v4: uuidv4 } = require('uuid');
|
|
11
|
+
|
|
12
|
+
let autoDetectDone = false;
|
|
13
|
+
|
|
14
|
+
// Main function
|
|
15
|
+
function nodescope(config) {
|
|
16
|
+
// Auto detect libraries (ek baar)
|
|
17
|
+
if (!autoDetectDone) {
|
|
18
|
+
autoDetectDone = true;
|
|
19
|
+
// Tracer pehle init karo
|
|
20
|
+
getInstance(config);
|
|
21
|
+
// Phir auto detect
|
|
22
|
+
setTimeout(() => autoDetectAndPatch(), 100);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return createMiddleware(config);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Manual deep trace wrapper
|
|
29
|
+
function trace(fn) {
|
|
30
|
+
return function (...args) {
|
|
31
|
+
const tracer = getInstance();
|
|
32
|
+
const traceId = Context.getTraceId();
|
|
33
|
+
|
|
34
|
+
if (!tracer || !traceId) {
|
|
35
|
+
return fn.apply(this, args);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const spanId = uuidv4();
|
|
39
|
+
const name = fn.name || 'anonymous';
|
|
40
|
+
|
|
41
|
+
tracer.startSpan(spanId, traceId, name, 'internal');
|
|
42
|
+
|
|
43
|
+
let result;
|
|
44
|
+
try {
|
|
45
|
+
result = fn.apply(this, args);
|
|
46
|
+
} catch (err) {
|
|
47
|
+
tracer.endSpan(spanId, err);
|
|
48
|
+
throw err;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (result?.then) {
|
|
52
|
+
return result
|
|
53
|
+
.then((res) => { tracer.endSpan(spanId); return res; })
|
|
54
|
+
.catch((err) => { tracer.endSpan(spanId, err); throw err; });
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
tracer.endSpan(spanId);
|
|
58
|
+
return result;
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Exports
|
|
63
|
+
module.exports = nodescope;
|
|
64
|
+
|
|
65
|
+
// Named exports for manual use
|
|
66
|
+
module.exports.nodescope = nodescope;
|
|
67
|
+
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;
|
package/middleware.js
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
const { Context } = require('./context');
|
|
3
|
+
const { getInstance } = require('./tracer');
|
|
4
|
+
|
|
5
|
+
function createMiddleware(config) {
|
|
6
|
+
const tracer = getInstance(config);
|
|
7
|
+
|
|
8
|
+
return function nodeScopeMiddleware(req, res, next) {
|
|
9
|
+
// Health check skip karo
|
|
10
|
+
if (req.path === '/health' || req.path === '/ping') {
|
|
11
|
+
return next();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const traceId = uuidv4();
|
|
15
|
+
const requestId = uuidv4();
|
|
16
|
+
|
|
17
|
+
tracer.startTrace(traceId, req.method, req.path);
|
|
18
|
+
|
|
19
|
+
Context.run(traceId, requestId, req.path, req.method, () => {
|
|
20
|
+
req.nodescope = {
|
|
21
|
+
traceId,
|
|
22
|
+
startSpan: (name) => {
|
|
23
|
+
const spanId = uuidv4();
|
|
24
|
+
tracer.startSpan(spanId, traceId, name, 'internal');
|
|
25
|
+
return {
|
|
26
|
+
end: (error) => tracer.endSpan(spanId, error),
|
|
27
|
+
setAttribute: (key, value) => tracer.addAttribute(spanId, key, value),
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const metadata = {
|
|
33
|
+
ip: req.ip || req.connection?.remoteAddress,
|
|
34
|
+
userAgent: req.get('user-agent'),
|
|
35
|
+
query: Object.keys(req.query).length ? req.query : undefined,
|
|
36
|
+
route: req.route?.path,
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
let ended = false;
|
|
40
|
+
|
|
41
|
+
function endTrace() {
|
|
42
|
+
if (!ended) {
|
|
43
|
+
ended = true;
|
|
44
|
+
tracer.endTrace(traceId, res.statusCode, metadata);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const originalJson = res.json.bind(res);
|
|
49
|
+
const originalSend = res.send.bind(res);
|
|
50
|
+
|
|
51
|
+
res.json = function (data) {
|
|
52
|
+
endTrace();
|
|
53
|
+
return originalJson(data);
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
res.send = function (data) {
|
|
57
|
+
endTrace();
|
|
58
|
+
return originalSend(data);
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
res.on('finish', endTrace);
|
|
62
|
+
|
|
63
|
+
next();
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = { createMiddleware };
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@himanshu-panchal/nodescope-sdk",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Universal Node.js observability SDK - works with any project",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"start": "node index.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"observability",
|
|
11
|
+
"tracing",
|
|
12
|
+
"monitoring",
|
|
13
|
+
"nodejs",
|
|
14
|
+
"express",
|
|
15
|
+
"postgresql",
|
|
16
|
+
"mongodb",
|
|
17
|
+
"mysql",
|
|
18
|
+
"redis",
|
|
19
|
+
"apm"
|
|
20
|
+
],
|
|
21
|
+
"author": "Himanshu Panchal",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"socket.io-client": "^4.6.1",
|
|
25
|
+
"uuid": "^9.0.1"
|
|
26
|
+
},
|
|
27
|
+
"repository": {
|
|
28
|
+
"type": "git",
|
|
29
|
+
"url": "https://github.com/YOUR_USERNAME/nodescope-sdk"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"express": ">=4.0.0"
|
|
33
|
+
},
|
|
34
|
+
"peerDependenciesMeta": {
|
|
35
|
+
"express": { "optional": true },
|
|
36
|
+
"pg": { "optional": true },
|
|
37
|
+
"mysql2": { "optional": true },
|
|
38
|
+
"mongoose": { "optional": true },
|
|
39
|
+
"redis": { "optional": true },
|
|
40
|
+
"axios": { "optional": true }
|
|
41
|
+
}
|
|
42
|
+
}
|
package/patches/axios.js
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
const { Context } = require('../context');
|
|
3
|
+
const { getInstance } = require('../tracer');
|
|
4
|
+
|
|
5
|
+
function patchAxios(axios) {
|
|
6
|
+
if (!axios || axios.__nodescope_patched__) return axios;
|
|
7
|
+
|
|
8
|
+
// Request interceptor
|
|
9
|
+
axios.interceptors.request.use((config) => {
|
|
10
|
+
const tracer = getInstance();
|
|
11
|
+
const traceId = Context.getTraceId();
|
|
12
|
+
|
|
13
|
+
if (!tracer || !traceId) return config;
|
|
14
|
+
|
|
15
|
+
const spanId = uuidv4();
|
|
16
|
+
const method = (config.method || 'GET').toUpperCase();
|
|
17
|
+
const url = config.url || '';
|
|
18
|
+
|
|
19
|
+
tracer.startSpan(spanId, traceId, `http.${method} ${url}`, 'client');
|
|
20
|
+
tracer.addAttribute(spanId, 'http.method', method);
|
|
21
|
+
tracer.addAttribute(spanId, 'http.url', url);
|
|
22
|
+
tracer.addAttribute(spanId, 'http.base_url', config.baseURL || '');
|
|
23
|
+
|
|
24
|
+
config._nodescope_spanId = spanId;
|
|
25
|
+
return config;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
// Response interceptor
|
|
29
|
+
axios.interceptors.response.use(
|
|
30
|
+
(response) => {
|
|
31
|
+
const tracer = getInstance();
|
|
32
|
+
const spanId = response.config?._nodescope_spanId;
|
|
33
|
+
|
|
34
|
+
if (tracer && spanId) {
|
|
35
|
+
tracer.addAttribute(spanId, 'http.status_code', response.status);
|
|
36
|
+
tracer.addAttribute(spanId, 'http.response_size', JSON.stringify(response.data || '').length);
|
|
37
|
+
tracer.endSpan(spanId);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return response;
|
|
41
|
+
},
|
|
42
|
+
(error) => {
|
|
43
|
+
const tracer = getInstance();
|
|
44
|
+
const spanId = error.config?._nodescope_spanId;
|
|
45
|
+
|
|
46
|
+
if (tracer && spanId) {
|
|
47
|
+
tracer.addAttribute(spanId, 'http.status_code', error.response?.status || 0);
|
|
48
|
+
tracer.endSpan(spanId, error);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
throw error;
|
|
52
|
+
}
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
axios.__nodescope_patched__ = true;
|
|
56
|
+
console.log('✅ NodeScope: Axios instrumented');
|
|
57
|
+
return axios;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
module.exports = { patchAxios };
|
package/patches/fetch.js
ADDED
|
File without changes
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
const { Context } = require('../context');
|
|
3
|
+
const { getInstance } = require('../tracer');
|
|
4
|
+
|
|
5
|
+
function patchMongoose(mongoose) {
|
|
6
|
+
if (!mongoose || mongoose.__nodescope_patched__) return mongoose;
|
|
7
|
+
|
|
8
|
+
// Mongoose middleware plugin use karo
|
|
9
|
+
mongoose.plugin(function nodeScopePlugin(schema) {
|
|
10
|
+
const ops = [
|
|
11
|
+
'find', 'findOne', 'findOneAndUpdate', 'findOneAndDelete',
|
|
12
|
+
'save', 'updateOne', 'updateMany', 'deleteOne', 'deleteMany',
|
|
13
|
+
'countDocuments', 'aggregate',
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
ops.forEach((op) => {
|
|
17
|
+
schema.pre(op, function () {
|
|
18
|
+
const tracer = getInstance();
|
|
19
|
+
const traceId = Context.getTraceId();
|
|
20
|
+
if (!tracer || !traceId) return;
|
|
21
|
+
|
|
22
|
+
const spanId = uuidv4();
|
|
23
|
+
const modelName = this?.model?.modelName || this?.modelName || 'Model';
|
|
24
|
+
|
|
25
|
+
this._nodescope_spanId = spanId;
|
|
26
|
+
|
|
27
|
+
tracer.startSpan(spanId, traceId, `mongo.${op} ${modelName}`, 'client');
|
|
28
|
+
tracer.addAttribute(spanId, 'db.system', 'mongodb');
|
|
29
|
+
tracer.addAttribute(spanId, 'db.operation', op);
|
|
30
|
+
tracer.addAttribute(spanId, 'db.collection', modelName);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
schema.post(op, function (result) {
|
|
34
|
+
const tracer = getInstance();
|
|
35
|
+
const spanId = this._nodescope_spanId;
|
|
36
|
+
if (!tracer || !spanId) return;
|
|
37
|
+
|
|
38
|
+
const count = Array.isArray(result) ? result.length : result ? 1 : 0;
|
|
39
|
+
tracer.addAttribute(spanId, 'db.rows_returned', count);
|
|
40
|
+
tracer.endSpan(spanId);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
schema.post(op, function (err, result, next) {
|
|
44
|
+
const tracer = getInstance();
|
|
45
|
+
const spanId = this._nodescope_spanId;
|
|
46
|
+
if (tracer && spanId) tracer.endSpan(spanId, err);
|
|
47
|
+
if (next) next(err);
|
|
48
|
+
});
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
mongoose.__nodescope_patched__ = true;
|
|
53
|
+
console.log('✅ NodeScope: MongoDB/Mongoose instrumented');
|
|
54
|
+
return mongoose;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = { patchMongoose };
|
package/patches/mysql.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
const { Context } = require('../context');
|
|
3
|
+
const { getInstance } = require('../tracer');
|
|
4
|
+
|
|
5
|
+
function patchMysql(mysql) {
|
|
6
|
+
if (!mysql || mysql.__nodescope_patched__) return mysql;
|
|
7
|
+
|
|
8
|
+
const originalCreateConnection = mysql.createConnection;
|
|
9
|
+
const originalCreatePool = mysql.createPool;
|
|
10
|
+
|
|
11
|
+
function patchConnection(conn) {
|
|
12
|
+
const originalQuery = conn.query.bind(conn);
|
|
13
|
+
|
|
14
|
+
conn.query = function (sql, params, callback) {
|
|
15
|
+
const tracer = getInstance();
|
|
16
|
+
const traceId = Context.getTraceId();
|
|
17
|
+
|
|
18
|
+
if (!tracer || !traceId) {
|
|
19
|
+
return originalQuery(sql, params, callback);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const spanId = uuidv4();
|
|
23
|
+
const queryText = typeof sql === 'string' ? sql : sql?.sql || '';
|
|
24
|
+
const operation = queryText.trim().split(/\s+/)[0].toUpperCase();
|
|
25
|
+
const tableMatch = queryText.match(/(?:FROM|INTO|UPDATE|TABLE)\s+`?(\w+)`?/i);
|
|
26
|
+
const table = tableMatch ? tableMatch[1] : 'unknown';
|
|
27
|
+
|
|
28
|
+
tracer.startSpan(spanId, traceId, `mysql.${operation} ${table}`, 'client');
|
|
29
|
+
tracer.addAttribute(spanId, 'db.system', 'mysql');
|
|
30
|
+
tracer.addAttribute(spanId, 'db.operation', operation);
|
|
31
|
+
tracer.addAttribute(spanId, 'db.table', table);
|
|
32
|
+
tracer.addAttribute(spanId, 'db.query', queryText.substring(0, 300));
|
|
33
|
+
|
|
34
|
+
const startTime = Date.now();
|
|
35
|
+
|
|
36
|
+
return originalQuery(sql, params, (err, results) => {
|
|
37
|
+
if (err) {
|
|
38
|
+
tracer.endSpan(spanId, err);
|
|
39
|
+
} else {
|
|
40
|
+
tracer.addAttribute(spanId, 'db.rows_returned', Array.isArray(results) ? results.length : 0);
|
|
41
|
+
tracer.endSpan(spanId);
|
|
42
|
+
}
|
|
43
|
+
if (callback) callback(err, results);
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
return conn;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (originalCreateConnection) {
|
|
51
|
+
mysql.createConnection = function (...args) {
|
|
52
|
+
const conn = originalCreateConnection.apply(this, args);
|
|
53
|
+
return patchConnection(conn);
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
mysql.__nodescope_patched__ = true;
|
|
58
|
+
console.log('✅ NodeScope: MySQL instrumented');
|
|
59
|
+
return mysql;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = { patchMysql };
|
package/patches/pg.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
const { Context } = require('../context');
|
|
3
|
+
const { getInstance } = require('../tracer');
|
|
4
|
+
|
|
5
|
+
function patchPg(pg) {
|
|
6
|
+
if (!pg || pg.__nodescope_patched__) return pg;
|
|
7
|
+
|
|
8
|
+
function patch(ClientClass) {
|
|
9
|
+
if (!ClientClass?.prototype?.query) return;
|
|
10
|
+
|
|
11
|
+
const original = ClientClass.prototype.query;
|
|
12
|
+
|
|
13
|
+
ClientClass.prototype.query = function (...args) {
|
|
14
|
+
const tracer = getInstance();
|
|
15
|
+
const traceId = Context.getTraceId();
|
|
16
|
+
|
|
17
|
+
if (!tracer || !traceId) {
|
|
18
|
+
return original.apply(this, args);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const spanId = uuidv4();
|
|
22
|
+
let queryText = '';
|
|
23
|
+
let queryParams = [];
|
|
24
|
+
|
|
25
|
+
if (typeof args[0] === 'string') {
|
|
26
|
+
queryText = args[0];
|
|
27
|
+
queryParams = args[1] || [];
|
|
28
|
+
} else if (args[0]?.text) {
|
|
29
|
+
queryText = args[0].text;
|
|
30
|
+
queryParams = args[0].values || [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Operation aur table detect karo
|
|
34
|
+
const operation = queryText.trim().split(/\s+/)[0].toUpperCase() || 'QUERY';
|
|
35
|
+
const tableMatch = queryText.match(/(?:FROM|INTO|UPDATE|TABLE|JOIN)\s+["']?(\w+)["']?/i);
|
|
36
|
+
const table = tableMatch ? tableMatch[1] : 'unknown';
|
|
37
|
+
|
|
38
|
+
tracer.startSpan(spanId, traceId, `pg.${operation} ${table}`, 'client');
|
|
39
|
+
tracer.addAttribute(spanId, 'db.system', 'postgresql');
|
|
40
|
+
tracer.addAttribute(spanId, 'db.operation', operation);
|
|
41
|
+
tracer.addAttribute(spanId, 'db.table', table);
|
|
42
|
+
tracer.addAttribute(spanId, 'db.query', queryText.substring(0, 300));
|
|
43
|
+
tracer.addAttribute(spanId, 'db.params_count', queryParams.length);
|
|
44
|
+
|
|
45
|
+
const result = original.apply(this, args);
|
|
46
|
+
|
|
47
|
+
if (result?.then) {
|
|
48
|
+
return result
|
|
49
|
+
.then((res) => {
|
|
50
|
+
tracer.addAttribute(spanId, 'db.rows_returned', res?.rows?.length ?? 0);
|
|
51
|
+
tracer.endSpan(spanId);
|
|
52
|
+
return res;
|
|
53
|
+
})
|
|
54
|
+
.catch((err) => {
|
|
55
|
+
tracer.endSpan(spanId, err);
|
|
56
|
+
throw err;
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
tracer.endSpan(spanId);
|
|
61
|
+
return result;
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (pg.Pool) patch(pg.Pool);
|
|
66
|
+
if (pg.Client) patch(pg.Client);
|
|
67
|
+
|
|
68
|
+
pg.__nodescope_patched__ = true;
|
|
69
|
+
console.log('✅ NodeScope: PostgreSQL instrumented');
|
|
70
|
+
return pg;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { patchPg };
|
package/patches/redis.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
const { v4: uuidv4 } = require('uuid');
|
|
2
|
+
const { Context } = require('../context');
|
|
3
|
+
const { getInstance } = require('../tracer');
|
|
4
|
+
|
|
5
|
+
function patchRedis(redisClient) {
|
|
6
|
+
if (!redisClient || redisClient.__nodescope_patched__) return redisClient;
|
|
7
|
+
|
|
8
|
+
// Redis v4 (modern)
|
|
9
|
+
if (redisClient.sendCommand) {
|
|
10
|
+
const original = redisClient.sendCommand.bind(redisClient);
|
|
11
|
+
|
|
12
|
+
redisClient.sendCommand = async function (args) {
|
|
13
|
+
const tracer = getInstance();
|
|
14
|
+
const traceId = Context.getTraceId();
|
|
15
|
+
|
|
16
|
+
if (!tracer || !traceId) return original(args);
|
|
17
|
+
|
|
18
|
+
const spanId = uuidv4();
|
|
19
|
+
const command = Array.isArray(args) ? args[0] : 'CMD';
|
|
20
|
+
const key = Array.isArray(args) && args[1] ? String(args[1]).substring(0, 50) : '';
|
|
21
|
+
|
|
22
|
+
tracer.startSpan(spanId, traceId, `redis.${command} ${key}`.trim(), 'client');
|
|
23
|
+
tracer.addAttribute(spanId, 'db.system', 'redis');
|
|
24
|
+
tracer.addAttribute(spanId, 'db.operation', command);
|
|
25
|
+
if (key) tracer.addAttribute(spanId, 'db.key', key);
|
|
26
|
+
|
|
27
|
+
try {
|
|
28
|
+
const result = await original(args);
|
|
29
|
+
tracer.addAttribute(spanId, 'cache.hit', result !== null && result !== undefined);
|
|
30
|
+
tracer.endSpan(spanId);
|
|
31
|
+
return result;
|
|
32
|
+
} catch (err) {
|
|
33
|
+
tracer.endSpan(spanId, err);
|
|
34
|
+
throw err;
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
redisClient.__nodescope_patched__ = true;
|
|
40
|
+
console.log('✅ NodeScope: Redis instrumented');
|
|
41
|
+
return redisClient;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
module.exports = { patchRedis };
|
package/tracer.js
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
const { io } = require('socket.io-client');
|
|
2
|
+
const { Context } = require('./context');
|
|
3
|
+
|
|
4
|
+
class Tracer {
|
|
5
|
+
constructor(config) {
|
|
6
|
+
this.config = {
|
|
7
|
+
serverUrl: config.serverUrl || 'http://localhost:3001',
|
|
8
|
+
appName: config.appName || 'my-app',
|
|
9
|
+
environment: config.environment || 'development',
|
|
10
|
+
enabled: config.enabled !== false,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
this.socket = null;
|
|
14
|
+
this.connected = false;
|
|
15
|
+
this.buffer = [];
|
|
16
|
+
this.traces = new Map();
|
|
17
|
+
|
|
18
|
+
if (this.config.enabled) {
|
|
19
|
+
this._connect();
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
_connect() {
|
|
24
|
+
try {
|
|
25
|
+
this.socket = io(this.config.serverUrl, {
|
|
26
|
+
auth: {
|
|
27
|
+
appName: this.config.appName,
|
|
28
|
+
environment: this.config.environment,
|
|
29
|
+
},
|
|
30
|
+
reconnection: true,
|
|
31
|
+
reconnectionDelay: 2000,
|
|
32
|
+
timeout: 5000,
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
this.socket.on('connect', () => {
|
|
36
|
+
console.log(`✅ NodeScope connected → ${this.config.serverUrl}`);
|
|
37
|
+
this.connected = true;
|
|
38
|
+
this._flushBuffer();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
this.socket.on('disconnect', () => {
|
|
42
|
+
console.log('⚠️ NodeScope disconnected');
|
|
43
|
+
this.connected = false;
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
this.socket.on('connect_error', (err) => {
|
|
47
|
+
// Silent fail - app ko affect na kare
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
} catch (err) {
|
|
51
|
+
// Silent fail
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
_send(trace) {
|
|
56
|
+
if (this.connected && this.socket) {
|
|
57
|
+
this.socket.emit('trace', trace);
|
|
58
|
+
} else {
|
|
59
|
+
this.buffer.push(trace);
|
|
60
|
+
if (this.buffer.length > 500) this.buffer.shift();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
_flushBuffer() {
|
|
65
|
+
if (this.buffer.length > 0) {
|
|
66
|
+
this.buffer.forEach(trace => this.socket.emit('trace', trace));
|
|
67
|
+
this.buffer = [];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
startTrace(traceId, method, path) {
|
|
72
|
+
this.traces.set(traceId, {
|
|
73
|
+
traceId,
|
|
74
|
+
method,
|
|
75
|
+
path,
|
|
76
|
+
startTime: Date.now(),
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
endTrace(traceId, statusCode, metadata) {
|
|
81
|
+
const trace = this.traces.get(traceId);
|
|
82
|
+
if (!trace) return;
|
|
83
|
+
|
|
84
|
+
const now = Date.now();
|
|
85
|
+
const fullTrace = {
|
|
86
|
+
traceId: trace.traceId,
|
|
87
|
+
method: trace.method,
|
|
88
|
+
path: trace.path,
|
|
89
|
+
statusCode,
|
|
90
|
+
startTime: trace.startTime,
|
|
91
|
+
endTime: now,
|
|
92
|
+
duration: now - trace.startTime,
|
|
93
|
+
spans: Context.getAllSpans(),
|
|
94
|
+
metadata: metadata || {},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
this._send(fullTrace);
|
|
98
|
+
this.traces.delete(traceId);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
startSpan(spanId, traceId, name, kind, parentId) {
|
|
102
|
+
const span = {
|
|
103
|
+
id: spanId,
|
|
104
|
+
traceId,
|
|
105
|
+
parentId: parentId || Context.getCurrentSpanId() || undefined,
|
|
106
|
+
name,
|
|
107
|
+
kind: kind || 'internal',
|
|
108
|
+
timestamp: Date.now(),
|
|
109
|
+
duration: 0,
|
|
110
|
+
attributes: {},
|
|
111
|
+
status: 'ok',
|
|
112
|
+
events: [],
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
Context.addSpan(span);
|
|
116
|
+
Context.setCurrentSpanId(spanId);
|
|
117
|
+
return span;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
endSpan(spanId, error) {
|
|
121
|
+
const span = Context.getSpan(spanId);
|
|
122
|
+
if (!span) return;
|
|
123
|
+
|
|
124
|
+
span.duration = Date.now() - span.timestamp;
|
|
125
|
+
|
|
126
|
+
if (error) {
|
|
127
|
+
span.status = 'error';
|
|
128
|
+
span.error = {
|
|
129
|
+
message: error.message,
|
|
130
|
+
stack: error.stack,
|
|
131
|
+
type: error.name,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (span.parentId) {
|
|
136
|
+
Context.setCurrentSpanId(span.parentId);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
addAttribute(spanId, key, value) {
|
|
141
|
+
const span = Context.getSpan(spanId);
|
|
142
|
+
if (span) span.attributes[key] = value;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Singleton instance
|
|
147
|
+
let instance = null;
|
|
148
|
+
|
|
149
|
+
function getInstance(config) {
|
|
150
|
+
if (!instance && config) {
|
|
151
|
+
instance = new Tracer(config);
|
|
152
|
+
}
|
|
153
|
+
return instance;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function resetInstance() {
|
|
157
|
+
instance = null;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
module.exports = { Tracer, getInstance, resetInstance };
|