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.
@@ -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 };