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,152 @@
1
+ /**
2
+ * dashboard/server.js — The Dashboard Server
3
+ *
4
+ * A lightweight Express server that runs in the background.
5
+ * Serves the visual dashboard at localhost:9999
6
+ * and exposes the /traces JSON API endpoint.
7
+ */
8
+
9
+ const express = require("express");
10
+ const path = require("path");
11
+ const { getAllTraces } = require("../src/exporter");
12
+
13
+ const app = express();
14
+ app.use(express.json());
15
+
16
+ // ── Static files (public folder with landing pages) ────
17
+ app.use(express.static(path.join(__dirname, "public")));
18
+
19
+ // ── Landing page routes ────────────────────────────────
20
+ app.get("/", (req, res) => {
21
+ res.sendFile(path.join(__dirname, "public", "index.html"));
22
+ });
23
+
24
+ app.get("/how", (req, res) => {
25
+ res.sendFile(path.join(__dirname, "public", "how.html"));
26
+ });
27
+
28
+ app.get("/docs", (req, res) => {
29
+ res.sendFile(path.join(__dirname, "public", "docs.html"));
30
+ });
31
+
32
+ // ── Dashboard UI (actual tracing visualization) ────────
33
+ app.get("/app", (req, res) => {
34
+ res.sendFile(path.join(__dirname, "index.html"));
35
+ });
36
+
37
+ // ── Traces API ─────────────────────────────────────────
38
+ // GET /traces — returns all recorded traces as JSON
39
+ app.get("/traces", (req, res) => {
40
+ res.json(getAllTraces());
41
+ });
42
+
43
+ // DELETE /traces — clears the in-memory store (UI refresh)
44
+ app.delete("/traces", (req, res) => {
45
+ const store = getAllTraces();
46
+ store.splice(0, store.length); // mutate the shared array
47
+ res.json({ cleared: true });
48
+ });
49
+
50
+ // POST /java-traces — receive and stitch Java agent traces
51
+ app.post("/java-traces", (req, res) => {
52
+ const javaTraces = Array.isArray(req.body) ? req.body : [req.body];
53
+ const store = getAllTraces();
54
+
55
+ javaTraces.forEach(jt => {
56
+ const existing = store.find(t => t.traceId === jt.traceId);
57
+ if (existing) {
58
+ // Context Propagation: Stitch Java spans into the Node.js trace!
59
+ existing.spans = existing.spans.concat(jt.spans || []);
60
+ existing.totalDuration = Math.max(existing.totalDuration || 0, jt.totalDuration || 0);
61
+ } else {
62
+ store.push(jt);
63
+ }
64
+ });
65
+
66
+ res.json({ success: true, stitched: javaTraces.length });
67
+ });
68
+
69
+ // POST /api/proxy — The API Tester (Postman clone) Backend Proxy
70
+ // Allows the frontend to bypass CORS and execute requests natively
71
+ app.post("/api/proxy", async (req, res) => {
72
+ const { url, method = "GET", headers = {}, body } = req.body;
73
+
74
+ if (!url) return res.status(400).json({ error: "URL is required" });
75
+
76
+ // Node 18+ has native fetch
77
+ try {
78
+ const startTime = Date.now();
79
+
80
+ const fetchOptions = {
81
+ method,
82
+ headers,
83
+ };
84
+
85
+ if (body && ["POST", "PUT", "PATCH"].includes(method.toUpperCase())) {
86
+ fetchOptions.body = typeof body === "string" ? body : JSON.stringify(body);
87
+ }
88
+
89
+ const response = await fetch(url, fetchOptions);
90
+ const latency = Date.now() - startTime;
91
+
92
+ // Capture Headers
93
+ const resHeaders = {};
94
+ response.headers.forEach((val, key) => {
95
+ resHeaders[key] = val;
96
+ });
97
+
98
+ // Attempt parsing JSON
99
+ let data = await response.text();
100
+ try {
101
+ data = JSON.parse(data);
102
+ } catch(e) { }
103
+
104
+ res.json({
105
+ status: response.status,
106
+ statusText: response.statusText,
107
+ latency,
108
+ headers: resHeaders,
109
+ data,
110
+ });
111
+
112
+ } catch (err) {
113
+ res.status(500).json({
114
+ error: "Proxy Request Failed",
115
+ message: err.message
116
+ });
117
+ }
118
+ });
119
+
120
+ let serverInstance = null;
121
+
122
+ /**
123
+ * Start the dashboard server.
124
+ * Safe to call multiple times — only starts once.
125
+ *
126
+ * @param {number} [port=9999]
127
+ */
128
+ function startDashboard(port = 9999) {
129
+ if (serverInstance) return; // already running
130
+
131
+ serverInstance = app.listen(port, "0.0.0.0", () => {
132
+ console.log(`\n ⚡ FlowTrace dashboard running at http://localhost:${port}\n`);
133
+ });
134
+
135
+ serverInstance.on("error", (err) => {
136
+ if (err.code === "EADDRINUSE") {
137
+ console.warn(`[FlowTrace] Port ${port} in use — dashboard not started. Try a different port.`);
138
+ }
139
+ });
140
+
141
+ return serverInstance;
142
+ }
143
+
144
+ // ── Standalone Mode (Render/Direct Execution) ────────────
145
+ if (require.main === module) {
146
+ const port = process.env.PORT || 9999;
147
+ startDashboard(port);
148
+ }
149
+
150
+ // ── Module Exports (Vercel/Library Execution) ────────────
151
+ module.exports = app;
152
+ module.exports.startDashboard = startDashboard;
package/index.js ADDED
@@ -0,0 +1,115 @@
1
+ /**
2
+ * index.js — The Initialization Module
3
+ *
4
+ * The single entry point for FlowTrace.
5
+ * The developer adds ONE line to their project:
6
+ *
7
+ * const FlowTrace = require('flowtrace');
8
+ * FlowTrace.start();
9
+ *
10
+ * That's it. Everything else happens automatically.
11
+ */
12
+
13
+ const http = require("http");
14
+ const crypto = require("crypto");
15
+ const { runWithTraceId, getStore } = require("./src/context");
16
+ const { finalizeTrace } = require("./src/exporter");
17
+ const { trace } = require("./src/wrapper");
18
+ const { startDashboard } = require("./dashboard/server");
19
+ const { initIntegrations } = require("./src/integrations/index");
20
+
21
+ let initialized = false;
22
+
23
+ /**
24
+ * Start FlowTrace.
25
+ *
26
+ * @param {Object} [options]
27
+ * @param {number} [options.port=9999] - Dashboard port
28
+ * @param {string} [options.outputPath] - Custom path for trace-output.json
29
+ * @param {boolean} [options.silent=false] - Suppress console output
30
+ */
31
+ function start(options = {}) {
32
+ if (initialized) return;
33
+ initialized = true;
34
+
35
+ const { port = 9999, outputPath, silent = false, otlpEndpoint } = options;
36
+
37
+ // 1. Hook into require() to inject Database/GraphQL integrations dynamically
38
+ initIntegrations();
39
+
40
+ // 1b. Intercept outgoing HTTP requests
41
+ const { patchHttpClient } = require("./src/integrations/http_client");
42
+ patchHttpClient();
43
+
44
+ // 2. Start the visual dashboard server
45
+ startDashboard(port);
46
+
47
+ // 3. Monkey-patch Node's core http module so every incoming
48
+ // request automatically gets a traceId injected into its
49
+ // async context — zero config for the developer.
50
+ patchHttpModule({ outputPath, silent, otlpEndpoint });
51
+
52
+ if (!silent) {
53
+ console.log(" ✓ FlowTrace initialized — http module patched\n");
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Intercept http.createServer to wrap every incoming request
59
+ * in a new AsyncLocalStorage trace context.
60
+ */
61
+ function patchHttpModule({ outputPath, silent, otlpEndpoint }) {
62
+ const originalCreateServer = http.createServer.bind(http);
63
+
64
+ http.createServer = function (options, requestListener) {
65
+ // Handle both http.createServer(listener) and http.createServer(opts, listener)
66
+ let listener = typeof options === "function" ? options : requestListener;
67
+ const serverOptions = typeof options === "object" ? options : {};
68
+
69
+ const wrappedListener = function (req, res) {
70
+ let traceId = crypto.randomUUID();
71
+ const tp = req.headers["traceparent"];
72
+ if (tp && typeof tp === "string") {
73
+ const parts = tp.split("-");
74
+ // W3C format: 00-{trace-id}-{span-id}-{flags}
75
+ if (parts.length === 4) traceId = parts[1];
76
+ }
77
+
78
+ // Run the entire request handler inside a new trace context
79
+ runWithTraceId(traceId, () => {
80
+ const startTime = Date.now();
81
+
82
+ // Intercept res.end so we can finalize the trace when
83
+ // the response is sent — capturing the status code too
84
+ const originalEnd = res.end.bind(res);
85
+ res.end = function (...args) {
86
+ const store = getStore();
87
+ if (store) {
88
+ finalizeTrace({
89
+ traceId,
90
+ startTime,
91
+ method: req.method,
92
+ url: req.url,
93
+ statusCode: res.statusCode,
94
+ spans: store.spans,
95
+ outputPath,
96
+ otlpEndpoint,
97
+ });
98
+ }
99
+ return originalEnd(...args);
100
+ };
101
+
102
+ if (listener) listener(req, res);
103
+ });
104
+ };
105
+
106
+ return Object.keys(serverOptions).length > 0
107
+ ? originalCreateServer(serverOptions, wrappedListener)
108
+ : originalCreateServer(wrappedListener);
109
+ };
110
+ }
111
+
112
+ module.exports = {
113
+ start,
114
+ trace,
115
+ };
package/package.json ADDED
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "flowtrace-omega",
3
+ "version": "1.0.0",
4
+ "description": "Zero-config dynamic tracing for Node.js. Add one line, get live sequence diagrams of every HTTP request.",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "flowtrace": "./bin/cli.js"
8
+ },
9
+ "files": [
10
+ "index.js",
11
+ "src/",
12
+ "dashboard/",
13
+ "adapters/",
14
+ "bin/",
15
+ "README.md"
16
+ ],
17
+ "scripts": {
18
+ "start": "node dashboard/server.js",
19
+ "test": "node test/smoke.js"
20
+ },
21
+ "keywords": [
22
+ "tracing",
23
+ "observability",
24
+ "sequence-diagram",
25
+ "debugging",
26
+ "apm",
27
+ "async-local-storage",
28
+ "proxy",
29
+ "express",
30
+ "nestjs",
31
+ "distributed-tracing",
32
+ "developer-tools"
33
+ ],
34
+ "author": "kavysharma",
35
+ "license": "MIT",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/skavy359/FlowTrace.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/skavy359/FlowTrace/issues"
42
+ },
43
+ "homepage": "https://github.com/skavy359/FlowTrace#readme",
44
+ "engines": {
45
+ "node": ">=18.0.0"
46
+ },
47
+ "dependencies": {
48
+ "express": "^4.18.2"
49
+ },
50
+ "peerDependencies": {
51
+ "express": ">=4.0.0"
52
+ },
53
+ "peerDependenciesMeta": {
54
+ "express": { "optional": true }
55
+ }
56
+ }
package/src/context.js ADDED
@@ -0,0 +1,74 @@
1
+ /**
2
+ * context.js — The Context Manager
3
+ *
4
+ * Solves the async event loop problem using AsyncLocalStorage.
5
+ * Every async operation that flows from a single HTTP request
6
+ * will share the same traceId — automatically, with zero user config.
7
+ */
8
+
9
+ const { AsyncLocalStorage } = require("async_hooks");
10
+
11
+ // Singleton instance shared across the entire library
12
+ const asyncLocalStorage = new AsyncLocalStorage();
13
+
14
+ /**
15
+ * Run a function within a new trace context.
16
+ * Everything called inside `fn` (sync or async) will share this traceId.
17
+ *
18
+ * @param {string} traceId - Unique ID for this request trace
19
+ * @param {Function} fn - The function to run inside the context
20
+ */
21
+
22
+ function runWithTraceId(traceId, fn) {
23
+ const store = {
24
+ traceId,
25
+ spans: [], // All spans collected for this trace
26
+ startTime: Date.now(),
27
+ };
28
+ asyncLocalStorage.run(store, fn);
29
+ }
30
+
31
+ /**
32
+ * Get the full store object for the current async context.
33
+ * Returns null if called outside a trace context.
34
+ */
35
+ function getStore() {
36
+ return asyncLocalStorage.getStore() || null;
37
+ }
38
+
39
+ /**
40
+ * Get just the traceId for the current async context.
41
+ */
42
+ function getCurrentTraceId() {
43
+ const store = getStore();
44
+ return store ? store.traceId : null;
45
+ }
46
+
47
+ /**
48
+ * Push a span into the current trace's span collection.
49
+ * Safe to call from anywhere — silently no-ops if no active context.
50
+ *
51
+ * @param {Object} span - The span object to record
52
+ */
53
+ function pushSpan(span) {
54
+ const store = getStore();
55
+ if (store) {
56
+ store.spans.push(span);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Get all spans for the current trace context.
62
+ */
63
+ function getCurrentSpans() {
64
+ const store = getStore();
65
+ return store ? store.spans : [];
66
+ }
67
+
68
+ module.exports = {
69
+ runWithTraceId,
70
+ getStore,
71
+ getCurrentTraceId,
72
+ pushSpan,
73
+ getCurrentSpans,
74
+ };
@@ -0,0 +1,112 @@
1
+ /**
2
+ * exporter.js — The Data Exporter
3
+ *
4
+ * Handles persisting completed traces to disk as structured JSON.
5
+ * Also maintains an in-memory store of recent traces for the dashboard.
6
+ */
7
+
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+
11
+ // In-memory ring buffer — keeps the last N traces for the live dashboard
12
+ const MAX_TRACES = 50;
13
+ const traceStore = [];
14
+
15
+ /**
16
+ * A completed "Trace" is the top-level object that groups all spans
17
+ * from a single HTTP request lifecycle.
18
+ *
19
+ * @typedef {Object} Trace
20
+ * @property {string} traceId
21
+ * @property {number} startTime - Unix ms timestamp
22
+ * @property {number} totalDuration - ms from first to last span
23
+ * @property {string} method - HTTP method (GET, POST, …)
24
+ * @property {string} url - Request URL
25
+ * @property {number} statusCode - HTTP response status
26
+ * @property {Object[]} spans - All recorded spans
27
+ */
28
+
29
+ const { exportToOtlp } = require("./otlp");
30
+
31
+ /**
32
+ * Finalize a trace and push it to the store + disk.
33
+ *
34
+ * @param {Object} params
35
+ * @param {string} params.traceId
36
+ * @param {number} params.startTime
37
+ * @param {string} params.method
38
+ * @param {string} params.url
39
+ * @param {number} params.statusCode
40
+ * @param {Object[]} params.spans
41
+ * @param {string} [params.outputPath] - Where to write trace-output.json
42
+ * @param {string} [params.otlpEndpoint] - Optional OpenTelemetry endpoint (e.g. Jaeger)
43
+ */
44
+
45
+ function finalizeTrace({ traceId, startTime, method, url, statusCode, spans, outputPath, otlpEndpoint }) {
46
+ const endTime = Date.now();
47
+
48
+ const trace = {
49
+ traceId,
50
+ startTime,
51
+ endTime,
52
+ totalDuration: endTime - startTime,
53
+ method,
54
+ url,
55
+ statusCode,
56
+ spanCount: spans.length,
57
+ spans,
58
+ };
59
+
60
+ // Push to in-memory ring buffer
61
+ traceStore.unshift(trace);
62
+ if (traceStore.length > MAX_TRACES) {
63
+ traceStore.pop();
64
+ }
65
+
66
+ // Write to disk (append mode — one JSON object per line = newline-delimited JSON)
67
+ const filePath = outputPath || path.join(process.cwd(), "trace-output.json");
68
+ persistTrace(trace, filePath);
69
+
70
+ // If the user configured an OpenTelemetry Collector, forward it!
71
+ if (otlpEndpoint) {
72
+ exportToOtlp(trace, otlpEndpoint);
73
+ }
74
+
75
+ return trace;
76
+ }
77
+
78
+ /**
79
+ * Write the entire current traceStore to the JSON file.
80
+ * We overwrite each time to keep the file clean and parseable.
81
+ */
82
+ function persistTrace(trace, filePath) {
83
+ try {
84
+ let existing = [];
85
+ if (fs.existsSync(filePath)) {
86
+ const raw = fs.readFileSync(filePath, "utf-8").trim();
87
+ if (raw) existing = JSON.parse(raw);
88
+ }
89
+ existing.unshift(trace);
90
+ if (existing.length > MAX_TRACES) existing = existing.slice(0, MAX_TRACES);
91
+ fs.writeFileSync(filePath, JSON.stringify(existing, null, 2), "utf-8");
92
+ } catch (err) {
93
+ // Non-fatal — dashboard still works via in-memory store
94
+ console.warn(`[FlowTrace] Could not write trace-output.json: ${err.message}`);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Get all in-memory traces (for the dashboard API endpoint).
100
+ */
101
+ function getAllTraces() {
102
+ return traceStore;
103
+ }
104
+
105
+ /**
106
+ * Get a single trace by ID.
107
+ */
108
+ function getTrace(traceId) {
109
+ return traceStore.find((t) => t.traceId === traceId) || null;
110
+ }
111
+
112
+ module.exports = { finalizeTrace, getAllTraces, getTrace };
@@ -0,0 +1,101 @@
1
+ const { getCurrentTraceId, pushSpan } = require("../context");
2
+
3
+ let patched = false;
4
+
5
+ function patchGraphQL(graphql) {
6
+ if (!graphql || !graphql.execute) return graphql;
7
+ if (patched) return graphql;
8
+ patched = true;
9
+
10
+ const originalExecute = graphql.execute;
11
+
12
+ graphql.execute = function (...args) {
13
+ const traceId = getCurrentTraceId();
14
+ if (!traceId) return originalExecute.apply(this, args);
15
+
16
+ const startTime = performance.now();
17
+ let opName = "GraphQL Query";
18
+ let queryBody = "";
19
+
20
+ // GraphQL execute can receive an object or multiple arguments
21
+ let doc = null;
22
+ if (args.length === 1 && typeof args[0] === "object") {
23
+ doc = args[0].document;
24
+ if (args[0].operationName) opName = args[0].operationName;
25
+ } else if (args.length >= 2) {
26
+ doc = args[1];
27
+ if (args.length >= 6 && args[5]) opName = args[5];
28
+ }
29
+
30
+ if (doc && doc.definitions) {
31
+ const def = doc.definitions.find(d => d.kind === 'OperationDefinition');
32
+ if (def && def.name && def.name.value) {
33
+ opName = def.name.value;
34
+ }
35
+
36
+ // Attempt to extract the first level of fields to show what is being resolved
37
+ if (def && def.selectionSet && def.selectionSet.selections) {
38
+ const fields = def.selectionSet.selections
39
+ .filter(sel => sel.kind === 'Field')
40
+ .map(sel => sel.name.value);
41
+ if (fields.length > 0) {
42
+ queryBody = fields.join(", ");
43
+ }
44
+ }
45
+ }
46
+
47
+ const spanId = `span_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
48
+ const span = {
49
+ spanId,
50
+ traceId,
51
+ label: "GraphQL",
52
+ callerLabel: "Node.js",
53
+ method: "RESOLVE",
54
+ args: [opName + (queryBody ? ` { ${queryBody} }` : "")],
55
+ startTime: Date.now(),
56
+ duration: null,
57
+ error: null,
58
+ type: "graphql",
59
+ };
60
+
61
+ try {
62
+ const result = originalExecute.apply(this, args);
63
+
64
+ if (result && typeof result.then === "function") {
65
+ return result
66
+ .then((res) => {
67
+ span.duration = +(performance.now() - startTime).toFixed(3);
68
+ if (res.errors && res.errors.length > 0) {
69
+ span.error = res.errors[0].message;
70
+ }
71
+ pushSpan(span);
72
+ return res;
73
+ })
74
+ .catch((err) => {
75
+ span.duration = +(performance.now() - startTime).toFixed(3);
76
+ span.error = err.message;
77
+ pushSpan(span);
78
+ throw err;
79
+ });
80
+ }
81
+
82
+ // Sync Execution
83
+ span.duration = +(performance.now() - startTime).toFixed(3);
84
+ if (result.errors && result.errors.length > 0) {
85
+ span.error = result.errors[0].message;
86
+ }
87
+ pushSpan(span);
88
+ return result;
89
+
90
+ } catch (err) {
91
+ span.duration = +(performance.now() - startTime).toFixed(3);
92
+ span.error = err.message;
93
+ pushSpan(span);
94
+ throw err;
95
+ }
96
+ };
97
+
98
+ return graphql;
99
+ }
100
+
101
+ module.exports = { patchGraphQL };