flow-debugger 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/PORTFOLIO_README_SECTION.md +177 -0
- package/README.md +251 -0
- package/dashboard/app.js +339 -0
- package/dashboard/index.html +168 -0
- package/dashboard/style.css +846 -0
- package/dist/cjs/core/Analytics.js +174 -0
- package/dist/cjs/core/Analytics.js.map +1 -0
- package/dist/cjs/core/Classifier.js +66 -0
- package/dist/cjs/core/Classifier.js.map +1 -0
- package/dist/cjs/core/HealthMonitor.js +79 -0
- package/dist/cjs/core/HealthMonitor.js.map +1 -0
- package/dist/cjs/core/RootCause.js +89 -0
- package/dist/cjs/core/RootCause.js.map +1 -0
- package/dist/cjs/core/Sampler.js +34 -0
- package/dist/cjs/core/Sampler.js.map +1 -0
- package/dist/cjs/core/Timeline.js +90 -0
- package/dist/cjs/core/Timeline.js.map +1 -0
- package/dist/cjs/core/TraceEngine.js +222 -0
- package/dist/cjs/core/TraceEngine.js.map +1 -0
- package/dist/cjs/core/types.js +21 -0
- package/dist/cjs/core/types.js.map +1 -0
- package/dist/cjs/index.js +46 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/integrations/axios.js +136 -0
- package/dist/cjs/integrations/axios.js.map +1 -0
- package/dist/cjs/integrations/fetch.js +153 -0
- package/dist/cjs/integrations/fetch.js.map +1 -0
- package/dist/cjs/integrations/mongo.js +111 -0
- package/dist/cjs/integrations/mongo.js.map +1 -0
- package/dist/cjs/integrations/mysql.js +212 -0
- package/dist/cjs/integrations/mysql.js.map +1 -0
- package/dist/cjs/integrations/postgres.js +182 -0
- package/dist/cjs/integrations/postgres.js.map +1 -0
- package/dist/cjs/integrations/redis.js +105 -0
- package/dist/cjs/integrations/redis.js.map +1 -0
- package/dist/cjs/middleware/express.js +255 -0
- package/dist/cjs/middleware/express.js.map +1 -0
- package/dist/esm/core/Analytics.js +170 -0
- package/dist/esm/core/Analytics.js.map +1 -0
- package/dist/esm/core/Classifier.js +61 -0
- package/dist/esm/core/Classifier.js.map +1 -0
- package/dist/esm/core/HealthMonitor.js +75 -0
- package/dist/esm/core/HealthMonitor.js.map +1 -0
- package/dist/esm/core/RootCause.js +86 -0
- package/dist/esm/core/RootCause.js.map +1 -0
- package/dist/esm/core/Sampler.js +30 -0
- package/dist/esm/core/Sampler.js.map +1 -0
- package/dist/esm/core/Timeline.js +86 -0
- package/dist/esm/core/Timeline.js.map +1 -0
- package/dist/esm/core/TraceEngine.js +217 -0
- package/dist/esm/core/TraceEngine.js.map +1 -0
- package/dist/esm/core/types.js +18 -0
- package/dist/esm/core/types.js.map +1 -0
- package/dist/esm/index.js +22 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/integrations/axios.js +133 -0
- package/dist/esm/integrations/axios.js.map +1 -0
- package/dist/esm/integrations/fetch.js +149 -0
- package/dist/esm/integrations/fetch.js.map +1 -0
- package/dist/esm/integrations/mongo.js +107 -0
- package/dist/esm/integrations/mongo.js.map +1 -0
- package/dist/esm/integrations/mysql.js +209 -0
- package/dist/esm/integrations/mysql.js.map +1 -0
- package/dist/esm/integrations/postgres.js +179 -0
- package/dist/esm/integrations/postgres.js.map +1 -0
- package/dist/esm/integrations/redis.js +102 -0
- package/dist/esm/integrations/redis.js.map +1 -0
- package/dist/esm/middleware/express.js +219 -0
- package/dist/esm/middleware/express.js.map +1 -0
- package/dist/types/core/Analytics.d.ts +35 -0
- package/dist/types/core/Analytics.d.ts.map +1 -0
- package/dist/types/core/Classifier.d.ts +21 -0
- package/dist/types/core/Classifier.d.ts.map +1 -0
- package/dist/types/core/HealthMonitor.d.ts +14 -0
- package/dist/types/core/HealthMonitor.d.ts.map +1 -0
- package/dist/types/core/RootCause.d.ts +12 -0
- package/dist/types/core/RootCause.d.ts.map +1 -0
- package/dist/types/core/Sampler.d.ts +13 -0
- package/dist/types/core/Sampler.d.ts.map +1 -0
- package/dist/types/core/Timeline.d.ts +22 -0
- package/dist/types/core/Timeline.d.ts.map +1 -0
- package/dist/types/core/TraceEngine.d.ts +47 -0
- package/dist/types/core/TraceEngine.d.ts.map +1 -0
- package/dist/types/core/types.d.ts +118 -0
- package/dist/types/core/types.d.ts.map +1 -0
- package/dist/types/index.d.ts +18 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/integrations/axios.d.ts +22 -0
- package/dist/types/integrations/axios.d.ts.map +1 -0
- package/dist/types/integrations/fetch.d.ts +25 -0
- package/dist/types/integrations/fetch.d.ts.map +1 -0
- package/dist/types/integrations/mongo.d.ts +26 -0
- package/dist/types/integrations/mongo.d.ts.map +1 -0
- package/dist/types/integrations/mysql.d.ts +20 -0
- package/dist/types/integrations/mysql.d.ts.map +1 -0
- package/dist/types/integrations/postgres.d.ts +20 -0
- package/dist/types/integrations/postgres.d.ts.map +1 -0
- package/dist/types/integrations/redis.d.ts +20 -0
- package/dist/types/integrations/redis.d.ts.map +1 -0
- package/dist/types/middleware/express.d.ts +39 -0
- package/dist/types/middleware/express.d.ts.map +1 -0
- package/example/server.ts +234 -0
- package/jest.config.js +8 -0
- package/package.json +110 -0
- package/portfolio-repo/APIRESPONSE DASH.png +0 -0
- package/portfolio-repo/PAYLOAD.png +0 -0
- package/portfolio-repo/README.md +182 -0
- package/src/core/Analytics.ts +209 -0
- package/src/core/Classifier.ts +82 -0
- package/src/core/HealthMonitor.ts +92 -0
- package/src/core/RootCause.ts +105 -0
- package/src/core/Sampler.ts +35 -0
- package/src/core/Timeline.ts +108 -0
- package/src/core/TraceEngine.ts +266 -0
- package/src/core/types.ts +170 -0
- package/src/index.ts +42 -0
- package/src/integrations/axios.ts +164 -0
- package/src/integrations/fetch.ts +172 -0
- package/src/integrations/mongo.ts +130 -0
- package/src/integrations/mysql.ts +239 -0
- package/src/integrations/postgres.ts +217 -0
- package/src/integrations/redis.ts +122 -0
- package/src/middleware/express.ts +264 -0
- package/tests/Analytics.test.ts +136 -0
- package/tests/Classifier.test.ts +57 -0
- package/tests/RootCause.test.ts +69 -0
- package/tests/TraceEngine.test.ts +110 -0
- package/tsconfig.cjs.json +9 -0
- package/tsconfig.esm.json +9 -0
- package/tsconfig.json +31 -0
- package/tsconfig.types.json +8 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — Fetch API Auto-Instrument
|
|
3
|
+
// Wraps global fetch to trace all HTTP requests
|
|
4
|
+
// ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { RequestTracer } from '../core/TraceEngine';
|
|
7
|
+
import { TraceStep, StepStatus, DebuggerConfig, DEFAULT_CONFIG, ServiceTag } from '../core/types';
|
|
8
|
+
import { classifyQuery } from '../core/Classifier';
|
|
9
|
+
|
|
10
|
+
interface FetchTracerOptions {
|
|
11
|
+
config?: Partial<DebuggerConfig>;
|
|
12
|
+
getTracer?: () => RequestTracer | null;
|
|
13
|
+
/** Map URL patterns to service tags (e.g. 'stripe.com' -> 'stripe') */
|
|
14
|
+
serviceMap?: Record<string, ServiceTag>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const DEFAULT_SERVICE_MAP: Record<string, ServiceTag> = {
|
|
18
|
+
'stripe.com': 'stripe',
|
|
19
|
+
'api.stripe.com': 'stripe',
|
|
20
|
+
'razorpay.com': 'razorpay',
|
|
21
|
+
'api.razorpay.com': 'razorpay',
|
|
22
|
+
'sendgrid.com': 'sendgrid',
|
|
23
|
+
'api.sendgrid.com': 'sendgrid',
|
|
24
|
+
'twilio.com': 'twilio',
|
|
25
|
+
'api.twilio.com': 'twilio',
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
let originalFetch: typeof fetch;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Auto-instrument global fetch to trace all HTTP requests.
|
|
32
|
+
*
|
|
33
|
+
* Usage:
|
|
34
|
+
* fetchTracer({ getTracer: () => currentTracer, serviceMap: { 'myapi.com': 'external' } })
|
|
35
|
+
*
|
|
36
|
+
* Output:
|
|
37
|
+
* Fetch POST https://api.stripe.com/v1/charges → 234ms ✔ [stripe]
|
|
38
|
+
* Fetch GET https://api.razorpay.com/orders → timeout ❌ [razorpay]
|
|
39
|
+
*/
|
|
40
|
+
export function fetchTracer(options?: FetchTracerOptions): void {
|
|
41
|
+
try {
|
|
42
|
+
if (typeof globalThis.fetch === 'undefined') return;
|
|
43
|
+
if ((globalThis.fetch as any).__flowDebuggerPatched) return;
|
|
44
|
+
|
|
45
|
+
const config = { ...DEFAULT_CONFIG, ...options?.config };
|
|
46
|
+
const serviceMap = { ...DEFAULT_SERVICE_MAP, ...options?.serviceMap };
|
|
47
|
+
|
|
48
|
+
originalFetch = globalThis.fetch;
|
|
49
|
+
|
|
50
|
+
globalThis.fetch = async function tracedFetch(input: string | Request | URL, init?: RequestInit): Promise<Response> {
|
|
51
|
+
const tracer = options?.getTracer?.();
|
|
52
|
+
if (!tracer) {
|
|
53
|
+
return originalFetch(input, init);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const url = typeof input === 'string' ? input : input instanceof URL ? input.href : input.url;
|
|
57
|
+
const method = init?.method || 'GET';
|
|
58
|
+
const service = getServiceFromUrl(url, serviceMap);
|
|
59
|
+
const stepName = `Fetch ${method} ${getDomain(url)}`;
|
|
60
|
+
const startTime = performance.now();
|
|
61
|
+
|
|
62
|
+
let status: StepStatus = 'success';
|
|
63
|
+
let error: string | undefined;
|
|
64
|
+
let stackTrace: string | undefined;
|
|
65
|
+
let response: Response;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
response = await originalFetch(input, init);
|
|
69
|
+
|
|
70
|
+
// HTTP errors (4xx, 5xx) are not thrown by fetch, but we should mark them
|
|
71
|
+
if (!response.ok) {
|
|
72
|
+
status = 'error';
|
|
73
|
+
error = `HTTP ${response.status} ${response.statusText}`;
|
|
74
|
+
}
|
|
75
|
+
} catch (err: unknown) {
|
|
76
|
+
status = 'error';
|
|
77
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
78
|
+
error = e.message;
|
|
79
|
+
stackTrace = e.stack;
|
|
80
|
+
|
|
81
|
+
// Detect network errors as timeouts
|
|
82
|
+
if (error.includes('fetch failed') || error.includes('network') || error.includes('ECONNREFUSED')) {
|
|
83
|
+
status = 'timeout';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const endTime = performance.now();
|
|
87
|
+
const duration = endTime - startTime;
|
|
88
|
+
const classification = classifyQuery(duration, status, config);
|
|
89
|
+
|
|
90
|
+
tracer.addStep({
|
|
91
|
+
name: stepName,
|
|
92
|
+
service,
|
|
93
|
+
status,
|
|
94
|
+
classification,
|
|
95
|
+
startTime,
|
|
96
|
+
endTime,
|
|
97
|
+
duration,
|
|
98
|
+
error,
|
|
99
|
+
stackTrace,
|
|
100
|
+
metadata: { url, method, service },
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
throw err;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const endTime = performance.now();
|
|
107
|
+
const duration = endTime - startTime;
|
|
108
|
+
const classification = classifyQuery(duration, status, config);
|
|
109
|
+
|
|
110
|
+
tracer.addStep({
|
|
111
|
+
name: stepName,
|
|
112
|
+
service,
|
|
113
|
+
status,
|
|
114
|
+
classification,
|
|
115
|
+
startTime,
|
|
116
|
+
endTime,
|
|
117
|
+
duration,
|
|
118
|
+
error,
|
|
119
|
+
metadata: { url, method, statusCode: response.status },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
return response;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
(globalThis.fetch as any).__flowDebuggerPatched = true;
|
|
126
|
+
} catch (_) {
|
|
127
|
+
// Production-safe: never crash
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Remove fetch tracer (for testing/cleanup)
|
|
133
|
+
*/
|
|
134
|
+
export function removeFetchTracer(): void {
|
|
135
|
+
try {
|
|
136
|
+
if (originalFetch) {
|
|
137
|
+
globalThis.fetch = originalFetch;
|
|
138
|
+
delete (globalThis.fetch as any).__flowDebuggerPatched;
|
|
139
|
+
}
|
|
140
|
+
} catch (_) { }
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Extract service tag from URL */
|
|
144
|
+
function getServiceFromUrl(url: string, serviceMap: Record<string, ServiceTag>): ServiceTag {
|
|
145
|
+
try {
|
|
146
|
+
const urlObj = new URL(url);
|
|
147
|
+
const hostname = urlObj.hostname;
|
|
148
|
+
|
|
149
|
+
// Check exact match
|
|
150
|
+
if (serviceMap[hostname]) return serviceMap[hostname];
|
|
151
|
+
|
|
152
|
+
// Check partial match
|
|
153
|
+
for (const [pattern, service] of Object.entries(serviceMap)) {
|
|
154
|
+
if (hostname.includes(pattern)) return service;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Default to fetch
|
|
158
|
+
return 'fetch';
|
|
159
|
+
} catch {
|
|
160
|
+
return 'fetch';
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/** Extract domain from URL for display */
|
|
165
|
+
function getDomain(url: string): string {
|
|
166
|
+
try {
|
|
167
|
+
const urlObj = new URL(url);
|
|
168
|
+
return urlObj.hostname + urlObj.pathname.substring(0, 30);
|
|
169
|
+
} catch {
|
|
170
|
+
return url.substring(0, 50);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — MongoDB/Mongoose Auto-Instrument
|
|
3
|
+
// Patches Mongoose to automatically trace all DB queries
|
|
4
|
+
// ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { RequestTracer } from '../core/TraceEngine';
|
|
7
|
+
import { TraceStep, StepStatus, DebuggerConfig, DEFAULT_CONFIG } from '../core/types';
|
|
8
|
+
import { classifyQuery } from '../core/Classifier';
|
|
9
|
+
|
|
10
|
+
type AnyMongoose = any;
|
|
11
|
+
|
|
12
|
+
interface MongoTracerOptions {
|
|
13
|
+
/** Config overrides for slow query thresholds */
|
|
14
|
+
config?: Partial<DebuggerConfig>;
|
|
15
|
+
/** Function to get the current active tracer (for auto-mode) */
|
|
16
|
+
getTracer?: () => RequestTracer | null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Auto-instrument Mongoose to trace all queries.
|
|
21
|
+
*
|
|
22
|
+
* Usage:
|
|
23
|
+
* mongoTracer(mongoose, { getTracer: () => currentTracer })
|
|
24
|
+
*
|
|
25
|
+
* Output:
|
|
26
|
+
* Mongo users.findOne → 22ms ✔
|
|
27
|
+
* Mongo payments.insertMany → error ❌
|
|
28
|
+
*/
|
|
29
|
+
export function mongoTracer(mongoose: AnyMongoose, options?: MongoTracerOptions): void {
|
|
30
|
+
try {
|
|
31
|
+
const config = { ...DEFAULT_CONFIG, ...options?.config };
|
|
32
|
+
const queryProto = mongoose.Query?.prototype;
|
|
33
|
+
|
|
34
|
+
if (!queryProto || queryProto.__flowDebuggerPatched) return;
|
|
35
|
+
queryProto.__flowDebuggerPatched = true;
|
|
36
|
+
|
|
37
|
+
const originalExec = queryProto.exec;
|
|
38
|
+
|
|
39
|
+
queryProto.exec = async function (this: any, ...args: any[]) {
|
|
40
|
+
const tracer = options?.getTracer?.();
|
|
41
|
+
if (!tracer) {
|
|
42
|
+
return originalExec.apply(this, args);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const op = this.op || 'query';
|
|
46
|
+
const collection = this.mongooseCollection?.name || this.model?.collection?.name || 'unknown';
|
|
47
|
+
const stepName = `Mongo ${collection}.${op}`;
|
|
48
|
+
const startTime = performance.now();
|
|
49
|
+
|
|
50
|
+
let status: StepStatus = 'success';
|
|
51
|
+
let error: string | undefined;
|
|
52
|
+
let stackTrace: string | undefined;
|
|
53
|
+
let result: any;
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
result = await originalExec.apply(this, args);
|
|
57
|
+
} catch (err: unknown) {
|
|
58
|
+
status = 'error';
|
|
59
|
+
const e = err instanceof Error ? err : new Error(String(err));
|
|
60
|
+
error = e.message;
|
|
61
|
+
stackTrace = e.stack;
|
|
62
|
+
|
|
63
|
+
// Record step before re-throwing
|
|
64
|
+
const endTime = performance.now();
|
|
65
|
+
const duration = endTime - startTime;
|
|
66
|
+
const classification = classifyQuery(duration, status, config);
|
|
67
|
+
|
|
68
|
+
const step: TraceStep = {
|
|
69
|
+
name: stepName,
|
|
70
|
+
service: 'mongo',
|
|
71
|
+
status,
|
|
72
|
+
classification,
|
|
73
|
+
startTime: startTime,
|
|
74
|
+
endTime,
|
|
75
|
+
duration,
|
|
76
|
+
error,
|
|
77
|
+
stackTrace,
|
|
78
|
+
metadata: { operation: op, collection },
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
tracer.addStep(step);
|
|
82
|
+
throw err;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const endTime = performance.now();
|
|
86
|
+
const duration = endTime - startTime;
|
|
87
|
+
const classification = classifyQuery(duration, status, config);
|
|
88
|
+
|
|
89
|
+
const step: TraceStep = {
|
|
90
|
+
name: stepName,
|
|
91
|
+
service: 'mongo',
|
|
92
|
+
status,
|
|
93
|
+
classification,
|
|
94
|
+
startTime: startTime,
|
|
95
|
+
endTime,
|
|
96
|
+
duration,
|
|
97
|
+
error,
|
|
98
|
+
stackTrace,
|
|
99
|
+
metadata: { operation: op, collection },
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
tracer.addStep(step);
|
|
103
|
+
|
|
104
|
+
// Log slow query warning
|
|
105
|
+
if (duration > (config.slowQueryThreshold ?? 300)) {
|
|
106
|
+
try {
|
|
107
|
+
(config.logger || console.log)(
|
|
108
|
+
`\x1b[33m⚠ Slow MongoDB query: ${stepName} (${Math.round(duration)}ms)\x1b[0m`
|
|
109
|
+
);
|
|
110
|
+
} catch (_) { }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
};
|
|
115
|
+
} catch (_) {
|
|
116
|
+
// Production-safe: never crash the app
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Remove the monkey-patch from Mongoose (for testing/cleanup).
|
|
122
|
+
*/
|
|
123
|
+
export function removeMongoTracer(mongoose: AnyMongoose): void {
|
|
124
|
+
try {
|
|
125
|
+
const queryProto = mongoose.Query?.prototype;
|
|
126
|
+
if (queryProto) {
|
|
127
|
+
delete queryProto.__flowDebuggerPatched;
|
|
128
|
+
}
|
|
129
|
+
} catch (_) { }
|
|
130
|
+
}
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// ─────────────────────────────────────────────────────────────
|
|
2
|
+
// flow-debugger — MySQL Auto-Instrument (mysql2)
|
|
3
|
+
// Patches mysql2 pool to trace all queries
|
|
4
|
+
// ─────────────────────────────────────────────────────────────
|
|
5
|
+
|
|
6
|
+
import { RequestTracer } from '../core/TraceEngine';
|
|
7
|
+
import { TraceStep, StepStatus, DebuggerConfig, DEFAULT_CONFIG } from '../core/types';
|
|
8
|
+
import { classifyQuery } from '../core/Classifier';
|
|
9
|
+
|
|
10
|
+
type AnyPool = any;
|
|
11
|
+
|
|
12
|
+
interface MysqlTracerOptions {
|
|
13
|
+
config?: Partial<DebuggerConfig>;
|
|
14
|
+
getTracer?: () => RequestTracer | null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Auto-instrument mysql2 pool to trace all queries.
|
|
19
|
+
*
|
|
20
|
+
* Usage:
|
|
21
|
+
* mysqlTracer(pool, { getTracer: () => currentTracer })
|
|
22
|
+
*
|
|
23
|
+
* Output:
|
|
24
|
+
* MySQL SELECT users → 12ms ✔
|
|
25
|
+
* MySQL INSERT payments → error ❌
|
|
26
|
+
*/
|
|
27
|
+
export function mysqlTracer(pool: AnyPool, options?: MysqlTracerOptions): void {
|
|
28
|
+
try {
|
|
29
|
+
if (pool.__flowDebuggerPatched) return;
|
|
30
|
+
pool.__flowDebuggerPatched = true;
|
|
31
|
+
|
|
32
|
+
const config = { ...DEFAULT_CONFIG, ...options?.config };
|
|
33
|
+
|
|
34
|
+
// Patch pool.query
|
|
35
|
+
const originalQuery = pool.query.bind(pool);
|
|
36
|
+
pool.query = function tracedQuery(...args: any[]) {
|
|
37
|
+
const tracer = options?.getTracer?.();
|
|
38
|
+
if (!tracer) {
|
|
39
|
+
return originalQuery(...args);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const sql = typeof args[0] === 'string' ? args[0] : args[0]?.sql || 'unknown';
|
|
43
|
+
const operation = extractSqlOperation(sql);
|
|
44
|
+
const stepName = `MySQL ${operation}`;
|
|
45
|
+
const startTime = performance.now();
|
|
46
|
+
|
|
47
|
+
// Check if using callback or promise style
|
|
48
|
+
const lastArg = args[args.length - 1];
|
|
49
|
+
if (typeof lastArg === 'function') {
|
|
50
|
+
// Callback style
|
|
51
|
+
const callback = lastArg;
|
|
52
|
+
args[args.length - 1] = function (err: any, results: any, fields: any) {
|
|
53
|
+
const endTime = performance.now();
|
|
54
|
+
const duration = endTime - startTime;
|
|
55
|
+
const status: StepStatus = err ? 'error' : 'success';
|
|
56
|
+
const classification = classifyQuery(duration, status, config);
|
|
57
|
+
|
|
58
|
+
const step: TraceStep = {
|
|
59
|
+
name: stepName,
|
|
60
|
+
service: 'mysql',
|
|
61
|
+
status,
|
|
62
|
+
classification,
|
|
63
|
+
startTime,
|
|
64
|
+
endTime,
|
|
65
|
+
duration,
|
|
66
|
+
error: err?.message,
|
|
67
|
+
stackTrace: err?.stack,
|
|
68
|
+
metadata: { sql: sql.substring(0, 200), operation },
|
|
69
|
+
};
|
|
70
|
+
tracer.addStep(step);
|
|
71
|
+
|
|
72
|
+
if (duration > (config.slowQueryThreshold ?? 300)) {
|
|
73
|
+
try {
|
|
74
|
+
(config.logger || console.log)(
|
|
75
|
+
`\x1b[33m⚠ Slow MySQL query: ${stepName} (${Math.round(duration)}ms)\n ${sql.substring(0, 100)}\x1b[0m`
|
|
76
|
+
);
|
|
77
|
+
} catch (_) { }
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
callback(err, results, fields);
|
|
81
|
+
};
|
|
82
|
+
return originalQuery(...args);
|
|
83
|
+
} else {
|
|
84
|
+
// Promise style
|
|
85
|
+
const promise = originalQuery(...args);
|
|
86
|
+
if (promise && typeof promise.then === 'function') {
|
|
87
|
+
return promise.then(
|
|
88
|
+
(result: any) => {
|
|
89
|
+
const endTime = performance.now();
|
|
90
|
+
const duration = endTime - startTime;
|
|
91
|
+
const classification = classifyQuery(duration, 'success', config);
|
|
92
|
+
|
|
93
|
+
const step: TraceStep = {
|
|
94
|
+
name: stepName,
|
|
95
|
+
service: 'mysql',
|
|
96
|
+
status: 'success',
|
|
97
|
+
classification,
|
|
98
|
+
startTime,
|
|
99
|
+
endTime,
|
|
100
|
+
duration,
|
|
101
|
+
metadata: { sql: sql.substring(0, 200), operation },
|
|
102
|
+
};
|
|
103
|
+
tracer.addStep(step);
|
|
104
|
+
|
|
105
|
+
if (duration > (config.slowQueryThreshold ?? 300)) {
|
|
106
|
+
try {
|
|
107
|
+
(config.logger || console.log)(
|
|
108
|
+
`\x1b[33m⚠ Slow MySQL query: ${stepName} (${Math.round(duration)}ms)\n ${sql.substring(0, 100)}\x1b[0m`
|
|
109
|
+
);
|
|
110
|
+
} catch (_) { }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result;
|
|
114
|
+
},
|
|
115
|
+
(err: any) => {
|
|
116
|
+
const endTime = performance.now();
|
|
117
|
+
const duration = endTime - startTime;
|
|
118
|
+
const classification = classifyQuery(duration, 'error', config);
|
|
119
|
+
|
|
120
|
+
const step: TraceStep = {
|
|
121
|
+
name: stepName,
|
|
122
|
+
service: 'mysql',
|
|
123
|
+
status: 'error',
|
|
124
|
+
classification,
|
|
125
|
+
startTime,
|
|
126
|
+
endTime,
|
|
127
|
+
duration,
|
|
128
|
+
error: err?.message,
|
|
129
|
+
stackTrace: err?.stack,
|
|
130
|
+
metadata: { sql: sql.substring(0, 200), operation },
|
|
131
|
+
};
|
|
132
|
+
tracer.addStep(step);
|
|
133
|
+
throw err;
|
|
134
|
+
},
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
return promise;
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
// Patch pool.execute (mysql2 specific)
|
|
142
|
+
if (pool.execute) {
|
|
143
|
+
const originalExecute = pool.execute.bind(pool);
|
|
144
|
+
pool.execute = function tracedExecute(...args: any[]) {
|
|
145
|
+
const tracer = options?.getTracer?.();
|
|
146
|
+
if (!tracer) {
|
|
147
|
+
return originalExecute(...args);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const sql = typeof args[0] === 'string' ? args[0] : args[0]?.sql || 'unknown';
|
|
151
|
+
const operation = extractSqlOperation(sql);
|
|
152
|
+
const stepName = `MySQL ${operation}`;
|
|
153
|
+
const startTime = performance.now();
|
|
154
|
+
|
|
155
|
+
const lastArg = args[args.length - 1];
|
|
156
|
+
if (typeof lastArg === 'function') {
|
|
157
|
+
const callback = lastArg;
|
|
158
|
+
args[args.length - 1] = function (err: any, results: any, fields: any) {
|
|
159
|
+
const endTime = performance.now();
|
|
160
|
+
const duration = endTime - startTime;
|
|
161
|
+
const status: StepStatus = err ? 'error' : 'success';
|
|
162
|
+
const classification = classifyQuery(duration, status, config);
|
|
163
|
+
|
|
164
|
+
tracer.addStep({
|
|
165
|
+
name: stepName,
|
|
166
|
+
service: 'mysql',
|
|
167
|
+
status,
|
|
168
|
+
classification,
|
|
169
|
+
startTime,
|
|
170
|
+
endTime,
|
|
171
|
+
duration,
|
|
172
|
+
error: err?.message,
|
|
173
|
+
stackTrace: err?.stack,
|
|
174
|
+
metadata: { sql: sql.substring(0, 200), operation },
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
callback(err, results, fields);
|
|
178
|
+
};
|
|
179
|
+
return originalExecute(...args);
|
|
180
|
+
} else {
|
|
181
|
+
const promise = originalExecute(...args);
|
|
182
|
+
if (promise && typeof promise.then === 'function') {
|
|
183
|
+
return promise.then(
|
|
184
|
+
(result: any) => {
|
|
185
|
+
const endTime = performance.now();
|
|
186
|
+
const duration = endTime - startTime;
|
|
187
|
+
tracer.addStep({
|
|
188
|
+
name: stepName,
|
|
189
|
+
service: 'mysql',
|
|
190
|
+
status: 'success',
|
|
191
|
+
classification: classifyQuery(duration, 'success', config),
|
|
192
|
+
startTime,
|
|
193
|
+
endTime,
|
|
194
|
+
duration,
|
|
195
|
+
metadata: { sql: sql.substring(0, 200), operation },
|
|
196
|
+
});
|
|
197
|
+
return result;
|
|
198
|
+
},
|
|
199
|
+
(err: any) => {
|
|
200
|
+
const endTime = performance.now();
|
|
201
|
+
const duration = endTime - startTime;
|
|
202
|
+
tracer.addStep({
|
|
203
|
+
name: stepName,
|
|
204
|
+
service: 'mysql',
|
|
205
|
+
status: 'error',
|
|
206
|
+
classification: classifyQuery(duration, 'error', config),
|
|
207
|
+
startTime,
|
|
208
|
+
endTime,
|
|
209
|
+
duration,
|
|
210
|
+
error: err?.message,
|
|
211
|
+
stackTrace: err?.stack,
|
|
212
|
+
metadata: { sql: sql.substring(0, 200), operation },
|
|
213
|
+
});
|
|
214
|
+
throw err;
|
|
215
|
+
},
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
return promise;
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
} catch (_) {
|
|
223
|
+
// Production-safe: never crash
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** Extract the SQL operation type from a query string */
|
|
228
|
+
function extractSqlOperation(sql: string): string {
|
|
229
|
+
const trimmed = sql.trim().toUpperCase();
|
|
230
|
+
const match = trimmed.match(/^(SELECT|INSERT|UPDATE|DELETE|CREATE|ALTER|DROP|TRUNCATE|REPLACE|CALL)\b/);
|
|
231
|
+
if (match) {
|
|
232
|
+
const op = match[1];
|
|
233
|
+
// Try to extract table name
|
|
234
|
+
const tableMatch = sql.match(/(?:FROM|INTO|UPDATE|TABLE|JOIN)\s+[`"]?(\w+)[`"]?/i);
|
|
235
|
+
const table = tableMatch ? tableMatch[1] : '';
|
|
236
|
+
return table ? `${op} ${table}` : op;
|
|
237
|
+
}
|
|
238
|
+
return sql.substring(0, 30);
|
|
239
|
+
}
|