flowtrace-omega 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/dashboard/index.html +2045 -0
- package/dashboard/public/docs.html +656 -0
- package/dashboard/public/favicon.png +0 -0
- package/dashboard/public/hero-bg.png +0 -0
- package/dashboard/public/how.html +337 -0
- package/dashboard/public/index.html +637 -0
- package/dashboard/server.js +152 -0
- package/index.js +115 -0
- package/package.json +56 -0
- package/src/context.js +74 -0
- package/src/exporter.js +112 -0
- package/src/integrations/graphql.js +101 -0
- package/src/integrations/http_client.js +107 -0
- package/src/integrations/index.js +26 -0
- package/src/integrations/mysql2.js +67 -0
- package/src/integrations/pg.js +79 -0
- package/src/otlp.js +120 -0
- package/src/wrapper.js +123 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const https = require("https");
|
|
3
|
+
const { getCurrentTraceId, pushSpan } = require("../context");
|
|
4
|
+
|
|
5
|
+
let patched = false;
|
|
6
|
+
|
|
7
|
+
function patchHttpClient() {
|
|
8
|
+
if (patched) return;
|
|
9
|
+
patched = true;
|
|
10
|
+
|
|
11
|
+
[http, https].forEach((module) => {
|
|
12
|
+
const originalRequest = module.request;
|
|
13
|
+
|
|
14
|
+
module.request = function (...args) {
|
|
15
|
+
const traceId = getCurrentTraceId();
|
|
16
|
+
|
|
17
|
+
// If we are not inside a traced request, just pass through
|
|
18
|
+
if (!traceId) {
|
|
19
|
+
return originalRequest.apply(this, args);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 1. Extract and mutate options
|
|
23
|
+
let url = "";
|
|
24
|
+
let options = {};
|
|
25
|
+
let callback = null;
|
|
26
|
+
|
|
27
|
+
if (typeof args[0] === "string" || args[0] instanceof URL) {
|
|
28
|
+
url = args[0].toString();
|
|
29
|
+
if (typeof args[1] === "object") {
|
|
30
|
+
options = args[1];
|
|
31
|
+
if (typeof args[2] === "function") callback = args[2];
|
|
32
|
+
} else if (typeof args[1] === "function") {
|
|
33
|
+
callback = args[1];
|
|
34
|
+
}
|
|
35
|
+
} else if (typeof args[0] === "object") {
|
|
36
|
+
options = args[0];
|
|
37
|
+
url = `${options.protocol||'http:'}//${options.hostname||options.host||'localhost'}${options.path||'/'}`;
|
|
38
|
+
if (typeof args[1] === "function") callback = args[1];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Create proper headers if none exist
|
|
42
|
+
if (!options.headers) options.headers = {};
|
|
43
|
+
|
|
44
|
+
// W3C Trace Context injection
|
|
45
|
+
// 00 - version
|
|
46
|
+
// traceId - the 32 char hex from context (needs to be 32 char exactly, so we strip hyphens if UUID)
|
|
47
|
+
const sanitizedTraceId = traceId.replace(/-/g, "").padEnd(32, "0");
|
|
48
|
+
|
|
49
|
+
const spanIdHex = Math.random().toString(16).slice(2, 18).padEnd(16, "0"); // 16 char hex
|
|
50
|
+
|
|
51
|
+
options.headers["traceparent"] = `00-${sanitizedTraceId}-${spanIdHex}-01`;
|
|
52
|
+
|
|
53
|
+
const startTime = performance.now();
|
|
54
|
+
const internalSpanId = `span_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
55
|
+
const span = {
|
|
56
|
+
spanId: internalSpanId,
|
|
57
|
+
traceId,
|
|
58
|
+
label: url.length > 50 ? url.slice(0, 50) + "..." : url,
|
|
59
|
+
callerLabel: "Node.js",
|
|
60
|
+
method: options.method || "GET",
|
|
61
|
+
args: ["HTTP Outgoing"],
|
|
62
|
+
startTime: Date.now(),
|
|
63
|
+
duration: null,
|
|
64
|
+
error: null,
|
|
65
|
+
type: "http",
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
// We have to put the args back together
|
|
69
|
+
const newArgs = [];
|
|
70
|
+
if (typeof args[0] === "string" || args[0] instanceof URL) {
|
|
71
|
+
newArgs.push(args[0]);
|
|
72
|
+
newArgs.push(options);
|
|
73
|
+
} else {
|
|
74
|
+
newArgs.push(options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Add a wrapper to the callback if it existed
|
|
78
|
+
if (callback) {
|
|
79
|
+
newArgs.push(function (res) {
|
|
80
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
81
|
+
pushSpan(span);
|
|
82
|
+
return callback(res);
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const req = originalRequest.apply(this, newArgs);
|
|
87
|
+
|
|
88
|
+
// If no callback, we listen to the response event
|
|
89
|
+
if (!callback) {
|
|
90
|
+
req.on('response', (res) => {
|
|
91
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
92
|
+
pushSpan(span);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
req.on('error', (err) => {
|
|
97
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
98
|
+
span.error = err.message;
|
|
99
|
+
pushSpan(span);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
return req;
|
|
103
|
+
};
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { patchHttpClient };
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const Module = require("module");
|
|
2
|
+
const { patchPg } = require("./pg");
|
|
3
|
+
const { patchMysql2 } = require("./mysql2");
|
|
4
|
+
const { patchGraphQL } = require("./graphql");
|
|
5
|
+
|
|
6
|
+
function initIntegrations() {
|
|
7
|
+
const originalLoad = Module._load;
|
|
8
|
+
|
|
9
|
+
Module._load = function (request, parent, isMain) {
|
|
10
|
+
const exports = originalLoad.apply(this, arguments);
|
|
11
|
+
|
|
12
|
+
if (request === "pg") {
|
|
13
|
+
return patchPg(exports);
|
|
14
|
+
}
|
|
15
|
+
if (request === "mysql2") {
|
|
16
|
+
return patchMysql2(exports);
|
|
17
|
+
}
|
|
18
|
+
if (request === "graphql") {
|
|
19
|
+
return patchGraphQL(exports);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return exports;
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = { initIntegrations };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
const { getCurrentTraceId, pushSpan } = require("../context");
|
|
2
|
+
|
|
3
|
+
let patched = false;
|
|
4
|
+
|
|
5
|
+
function patchMysql2(mysql) {
|
|
6
|
+
if (!mysql || !mysql.Connection || !mysql.Connection.prototype || !mysql.Connection.prototype.query) return mysql;
|
|
7
|
+
if (patched) return mysql;
|
|
8
|
+
patched = true;
|
|
9
|
+
|
|
10
|
+
const originalQuery = mysql.Connection.prototype.query;
|
|
11
|
+
|
|
12
|
+
mysql.Connection.prototype.query = function (...args) {
|
|
13
|
+
const traceId = getCurrentTraceId();
|
|
14
|
+
if (!traceId) return originalQuery.apply(this, args);
|
|
15
|
+
|
|
16
|
+
const startTime = performance.now();
|
|
17
|
+
let queryText = "?";
|
|
18
|
+
|
|
19
|
+
if (typeof args[0] === "string") {
|
|
20
|
+
queryText = args[0];
|
|
21
|
+
} else if (args[0] && typeof args[0] === "object" && args[0].sql) {
|
|
22
|
+
queryText = args[0].sql;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const spanId = `span_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
26
|
+
const span = {
|
|
27
|
+
spanId,
|
|
28
|
+
traceId,
|
|
29
|
+
label: "MySQL",
|
|
30
|
+
callerLabel: "Node.js",
|
|
31
|
+
method: "QUERY",
|
|
32
|
+
args: [queryText.length > 60 ? queryText.slice(0, 60) + "..." : queryText],
|
|
33
|
+
startTime: Date.now(),
|
|
34
|
+
duration: null,
|
|
35
|
+
error: null,
|
|
36
|
+
type: "db",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const lastArg = args[args.length - 1];
|
|
40
|
+
if (typeof lastArg === "function") {
|
|
41
|
+
args[args.length - 1] = function (...cbArgs) {
|
|
42
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
43
|
+
const err = cbArgs[0];
|
|
44
|
+
if (err) span.error = err.message;
|
|
45
|
+
pushSpan(span);
|
|
46
|
+
return lastArg.apply(this, cbArgs);
|
|
47
|
+
};
|
|
48
|
+
} else {
|
|
49
|
+
// Sometimes no callback is provided in query, though rare.
|
|
50
|
+
const originalCb = args.find(a => typeof a === "function");
|
|
51
|
+
if (!originalCb) {
|
|
52
|
+
// Create dummy callback to catch the end
|
|
53
|
+
args.push(function(err, res) {
|
|
54
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
55
|
+
if(err) span.error = err.message;
|
|
56
|
+
pushSpan(span);
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return originalQuery.apply(this, args);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
return mysql;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = { patchMysql2 };
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { getCurrentTraceId, pushSpan } = require("../context");
|
|
2
|
+
|
|
3
|
+
let patched = false;
|
|
4
|
+
|
|
5
|
+
function patchPg(pg) {
|
|
6
|
+
if (!pg || !pg.Client || !pg.Client.prototype || !pg.Client.prototype.query) return pg;
|
|
7
|
+
if (patched) return pg;
|
|
8
|
+
patched = true;
|
|
9
|
+
|
|
10
|
+
const originalQuery = pg.Client.prototype.query;
|
|
11
|
+
|
|
12
|
+
pg.Client.prototype.query = function (...args) {
|
|
13
|
+
const traceId = getCurrentTraceId();
|
|
14
|
+
if (!traceId) return originalQuery.apply(this, args);
|
|
15
|
+
|
|
16
|
+
const startTime = performance.now();
|
|
17
|
+
let queryText = "?";
|
|
18
|
+
|
|
19
|
+
// pg.query can take a string, an object {text, values}, or a CustomQuery
|
|
20
|
+
if (typeof args[0] === "string") {
|
|
21
|
+
queryText = args[0];
|
|
22
|
+
} else if (args[0] && typeof args[0] === "object" && args[0].text) {
|
|
23
|
+
queryText = args[0].text;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const spanId = `span_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
27
|
+
const span = {
|
|
28
|
+
spanId,
|
|
29
|
+
traceId,
|
|
30
|
+
label: "Database",
|
|
31
|
+
callerLabel: "Node.js",
|
|
32
|
+
method: "QUERY",
|
|
33
|
+
args: [queryText.length > 60 ? queryText.slice(0, 60) + "..." : queryText],
|
|
34
|
+
startTime: Date.now(),
|
|
35
|
+
duration: null,
|
|
36
|
+
error: null,
|
|
37
|
+
type: "db",
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// Callback syntax detection
|
|
41
|
+
const lastArg = args[args.length - 1];
|
|
42
|
+
if (typeof lastArg === "function") {
|
|
43
|
+
args[args.length - 1] = function (...cbArgs) {
|
|
44
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
45
|
+
const err = cbArgs[0];
|
|
46
|
+
if (err) span.error = err.message;
|
|
47
|
+
pushSpan(span);
|
|
48
|
+
return lastArg.apply(this, cbArgs);
|
|
49
|
+
};
|
|
50
|
+
return originalQuery.apply(this, args);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Promise syntax
|
|
54
|
+
const result = originalQuery.apply(this, args);
|
|
55
|
+
if (result && typeof result.then === "function") {
|
|
56
|
+
return result
|
|
57
|
+
.then((res) => {
|
|
58
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
59
|
+
pushSpan(span);
|
|
60
|
+
return res;
|
|
61
|
+
})
|
|
62
|
+
.catch((err) => {
|
|
63
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
64
|
+
span.error = err.message;
|
|
65
|
+
pushSpan(span);
|
|
66
|
+
throw err;
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// fallback sync (edge case)
|
|
71
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
72
|
+
pushSpan(span);
|
|
73
|
+
return result;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
return pg;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { patchPg };
|
package/src/otlp.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const http = require("http");
|
|
2
|
+
const https = require("https");
|
|
3
|
+
|
|
4
|
+
const OTLP_SERVICE_NAME = process.env.OTEL_SERVICE_NAME || "flowtrace-node";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Convert FlowTrace internal trace format to standard OpenTelemetry JSON format
|
|
8
|
+
* and POST it to an active collector.
|
|
9
|
+
*
|
|
10
|
+
* @param {Object} trace - The completed FlowTrace trace
|
|
11
|
+
* @param {string} endpoint - The OTLP endpoint (e.g. http://localhost:4318/v1/traces)
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
function exportToOtlp(trace, endpoint) {
|
|
15
|
+
if (!endpoint) return;
|
|
16
|
+
|
|
17
|
+
// UUIDs must be hex without hyphens for OTLP
|
|
18
|
+
const traceId = trace.traceId.replace(/-/g, "").padEnd(32, "0");
|
|
19
|
+
|
|
20
|
+
const otlpSpans = trace.spans.map((span, idx) => {
|
|
21
|
+
// Span ID must be 16 hex chars
|
|
22
|
+
let spanId = span.spanId ? span.spanId.replace(/[^0-9a-f]/g, "") : "";
|
|
23
|
+
if (spanId.length < 16) {
|
|
24
|
+
spanId = Math.random().toString(16).slice(2, 18).padEnd(16, "0");
|
|
25
|
+
} else {
|
|
26
|
+
spanId = spanId.slice(0, 16);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return {
|
|
30
|
+
traceId,
|
|
31
|
+
spanId,
|
|
32
|
+
parentSpanId: idx === 0 ? undefined : otlpSpans[idx - 1]?.spanId, // Rough tree
|
|
33
|
+
name: span.method ? `${span.label}#${span.method}` : span.label || "Span",
|
|
34
|
+
kind: 1, // SPAN_KIND_INTERNAL
|
|
35
|
+
startTimeUnixNano: String((span.startTime || Date.now()) * 1000000),
|
|
36
|
+
endTimeUnixNano: String(((span.startTime || Date.now()) + (span.duration || 0)) * 1000000),
|
|
37
|
+
attributes: [
|
|
38
|
+
{ key: "flowtrace.caller", value: { stringValue: span.callerLabel || "Unknown" } },
|
|
39
|
+
{ key: "flowtrace.type", value: { stringValue: span.type || "unknown" } },
|
|
40
|
+
],
|
|
41
|
+
status: span.error ? { code: 2, message: span.error } : { code: 1 }, // 2=ERROR, 1=OK
|
|
42
|
+
};
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
// Also create a "Server" entry span for the HTTP handler itself
|
|
46
|
+
const serverSpanId = Math.random().toString(16).slice(2, 18).padEnd(16, "0");
|
|
47
|
+
const serverSpan = {
|
|
48
|
+
traceId,
|
|
49
|
+
spanId: serverSpanId,
|
|
50
|
+
name: `${trace.method} ${trace.url}`,
|
|
51
|
+
kind: 2, // SPAN_KIND_SERVER
|
|
52
|
+
startTimeUnixNano: String(trace.startTime * 1000000),
|
|
53
|
+
endTimeUnixNano: String(trace.endTime * 1000000),
|
|
54
|
+
attributes: [
|
|
55
|
+
{ key: "http.method", value: { stringValue: trace.method } },
|
|
56
|
+
{ key: "http.url", value: { stringValue: trace.url } },
|
|
57
|
+
{ key: "http.status_code", value: { intValue: trace.statusCode } }
|
|
58
|
+
],
|
|
59
|
+
status: trace.statusCode >= 400 ? { code: 2 } : { code: 1 }
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
// Link all child spans to the server span
|
|
63
|
+
otlpSpans.forEach(s => {
|
|
64
|
+
if (!s.parentSpanId) s.parentSpanId = serverSpanId;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
otlpSpans.unshift(serverSpan);
|
|
68
|
+
|
|
69
|
+
const payload = {
|
|
70
|
+
resourceSpans: [
|
|
71
|
+
{
|
|
72
|
+
resource: {
|
|
73
|
+
attributes: [
|
|
74
|
+
{
|
|
75
|
+
key: "service.name",
|
|
76
|
+
value: { stringValue: OTLP_SERVICE_NAME },
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
scopeSpans: [
|
|
81
|
+
{
|
|
82
|
+
scope: { name: "flowtrace-agent", version: "1.0.0" },
|
|
83
|
+
spans: otlpSpans,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
},
|
|
87
|
+
],
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
const payloadString = JSON.stringify(payload);
|
|
91
|
+
const isHttps = endpoint.startsWith("https://");
|
|
92
|
+
const parsedUrl = new URL(endpoint);
|
|
93
|
+
|
|
94
|
+
const options = {
|
|
95
|
+
hostname: parsedUrl.hostname,
|
|
96
|
+
port: parsedUrl.port || (isHttps ? 443 : 80),
|
|
97
|
+
path: parsedUrl.pathname,
|
|
98
|
+
method: "POST",
|
|
99
|
+
headers: {
|
|
100
|
+
"Content-Type": "application/json",
|
|
101
|
+
"Content-Length": Buffer.byteLength(payloadString),
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
const engine = isHttps ? https : http;
|
|
106
|
+
const req = engine.request(options, (res) => {
|
|
107
|
+
res.on("data", () => {});
|
|
108
|
+
res.on("end", () => {
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
req.on("error", (e) => {
|
|
113
|
+
console.warn(`[FlowTrace] Failed to export OpenTelemetry trace: ${e.message}`);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
req.write(payloadString);
|
|
117
|
+
req.end();
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
module.exports = { exportToOtlp };
|
package/src/wrapper.js
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wrapper.js — The Interceptor
|
|
3
|
+
*
|
|
4
|
+
* The magic that spies on the user's code.
|
|
5
|
+
* Wraps any class instance or plain object in a JavaScript Proxy.
|
|
6
|
+
* On every method call, it records a Span: who called what, when, and how long it took.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { getCurrentTraceId, pushSpan } = require("./context");
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate a simple unique span ID.
|
|
13
|
+
*/
|
|
14
|
+
function generateSpanId() {
|
|
15
|
+
return `span_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Wrap a class instance or plain object so every method call is traced.
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* const tracedService = trace(new UserService(), "UserService");
|
|
23
|
+
*
|
|
24
|
+
* @param {Object} target - The object/instance to wrap
|
|
25
|
+
* @param {string} label - Human-readable name shown in diagrams (e.g. "UserService")
|
|
26
|
+
* @param {string} [callerLabel] - Optional: the label of the caller for diagram edges
|
|
27
|
+
* @returns {Proxy} - A transparent proxy that records spans on every method call
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
function trace(target, label, callerLabel = "Client") {
|
|
31
|
+
return new Proxy(target, {
|
|
32
|
+
get(obj, prop, receiver) {
|
|
33
|
+
const original = Reflect.get(obj, prop, receiver);
|
|
34
|
+
|
|
35
|
+
// Only intercept actual functions; pass through everything else
|
|
36
|
+
if (typeof original !== "function") {
|
|
37
|
+
return original;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Return a wrapped version of the function
|
|
41
|
+
return function (...args) {
|
|
42
|
+
const traceId = getCurrentTraceId();
|
|
43
|
+
|
|
44
|
+
// If we're not inside a trace context, just call the original
|
|
45
|
+
if (!traceId) {
|
|
46
|
+
return original.apply(obj, args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const spanId = generateSpanId();
|
|
50
|
+
const startTime = performance.now();
|
|
51
|
+
const methodName = String(prop);
|
|
52
|
+
|
|
53
|
+
// Build the span skeleton before we know the result
|
|
54
|
+
const span = {
|
|
55
|
+
spanId,
|
|
56
|
+
traceId,
|
|
57
|
+
label, // The component being called (e.g. "UserService")
|
|
58
|
+
callerLabel, // The component doing the calling (e.g. "UserController")
|
|
59
|
+
method: methodName,
|
|
60
|
+
args: serializeArgs(args),
|
|
61
|
+
startTime: Date.now(),
|
|
62
|
+
duration: null,
|
|
63
|
+
error: null,
|
|
64
|
+
type: "function",
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
let result;
|
|
68
|
+
try {
|
|
69
|
+
result = original.apply(obj, args);
|
|
70
|
+
|
|
71
|
+
// Handle async functions — wait for the promise to settle
|
|
72
|
+
if (result instanceof Promise) {
|
|
73
|
+
return result
|
|
74
|
+
.then((resolved) => {
|
|
75
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
76
|
+
pushSpan(span);
|
|
77
|
+
return resolved;
|
|
78
|
+
})
|
|
79
|
+
.catch((err) => {
|
|
80
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
81
|
+
span.error = err.message;
|
|
82
|
+
pushSpan(span);
|
|
83
|
+
throw err;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Synchronous function — record immediately
|
|
88
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
89
|
+
pushSpan(span);
|
|
90
|
+
return result;
|
|
91
|
+
|
|
92
|
+
} catch (err) {
|
|
93
|
+
span.duration = +(performance.now() - startTime).toFixed(3);
|
|
94
|
+
span.error = err.message;
|
|
95
|
+
pushSpan(span);
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Safely serialize function arguments for the span log.
|
|
105
|
+
* Avoids circular references and keeps things readable.
|
|
106
|
+
*/
|
|
107
|
+
function serializeArgs(args) {
|
|
108
|
+
return args.map((arg) => {
|
|
109
|
+
if (arg === null || arg === undefined) return String(arg);
|
|
110
|
+
if (typeof arg === "function") return "[Function]";
|
|
111
|
+
if (typeof arg === "object") {
|
|
112
|
+
try {
|
|
113
|
+
const str = JSON.stringify(arg);
|
|
114
|
+
return str.length > 200 ? str.slice(0, 200) + "…" : str;
|
|
115
|
+
} catch {
|
|
116
|
+
return "[Object]";
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return String(arg);
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = { trace };
|