trickle-observe 0.2.93 → 0.2.95
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/dist/db-observer.d.ts +23 -0
- package/dist/db-observer.js +292 -0
- package/dist/observe-register.js +25 -0
- package/package.json +1 -1
- package/src/db-observer.ts +283 -0
- package/src/observe-register.ts +23 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database query observer — patches popular database drivers to capture
|
|
3
|
+
* SQL queries, execution time, and result shapes.
|
|
4
|
+
*
|
|
5
|
+
* Currently supports:
|
|
6
|
+
* - pg (node-postgres) — used by Prisma, Knex, Sequelize, TypeORM
|
|
7
|
+
*
|
|
8
|
+
* Captured data is written to .trickle/queries.jsonl as:
|
|
9
|
+
* { query: "SELECT ...", params: [...], durationMs: 2.5, rowCount: 42, columns: [...] }
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Patch pg (node-postgres) to capture queries.
|
|
13
|
+
* Called from observe-register when pg is required.
|
|
14
|
+
*/
|
|
15
|
+
export declare function patchPg(pgModule: any, debug: boolean): void;
|
|
16
|
+
/**
|
|
17
|
+
* Patch mysql2 to capture queries.
|
|
18
|
+
*/
|
|
19
|
+
export declare function patchMysql2(mysqlModule: any, debug: boolean): void;
|
|
20
|
+
/**
|
|
21
|
+
* Patch better-sqlite3 to capture queries.
|
|
22
|
+
*/
|
|
23
|
+
export declare function patchBetterSqlite3(dbConstructor: any, debug: boolean): void;
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Database query observer — patches popular database drivers to capture
|
|
4
|
+
* SQL queries, execution time, and result shapes.
|
|
5
|
+
*
|
|
6
|
+
* Currently supports:
|
|
7
|
+
* - pg (node-postgres) — used by Prisma, Knex, Sequelize, TypeORM
|
|
8
|
+
*
|
|
9
|
+
* Captured data is written to .trickle/queries.jsonl as:
|
|
10
|
+
* { query: "SELECT ...", params: [...], durationMs: 2.5, rowCount: 42, columns: [...] }
|
|
11
|
+
*/
|
|
12
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
13
|
+
if (k2 === undefined) k2 = k;
|
|
14
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
15
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
16
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
17
|
+
}
|
|
18
|
+
Object.defineProperty(o, k2, desc);
|
|
19
|
+
}) : (function(o, m, k, k2) {
|
|
20
|
+
if (k2 === undefined) k2 = k;
|
|
21
|
+
o[k2] = m[k];
|
|
22
|
+
}));
|
|
23
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
24
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
25
|
+
}) : function(o, v) {
|
|
26
|
+
o["default"] = v;
|
|
27
|
+
});
|
|
28
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
29
|
+
var ownKeys = function(o) {
|
|
30
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
31
|
+
var ar = [];
|
|
32
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
33
|
+
return ar;
|
|
34
|
+
};
|
|
35
|
+
return ownKeys(o);
|
|
36
|
+
};
|
|
37
|
+
return function (mod) {
|
|
38
|
+
if (mod && mod.__esModule) return mod;
|
|
39
|
+
var result = {};
|
|
40
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
41
|
+
__setModuleDefault(result, mod);
|
|
42
|
+
return result;
|
|
43
|
+
};
|
|
44
|
+
})();
|
|
45
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
46
|
+
exports.patchPg = patchPg;
|
|
47
|
+
exports.patchMysql2 = patchMysql2;
|
|
48
|
+
exports.patchBetterSqlite3 = patchBetterSqlite3;
|
|
49
|
+
const fs = __importStar(require("fs"));
|
|
50
|
+
const path = __importStar(require("path"));
|
|
51
|
+
let queriesFile = null;
|
|
52
|
+
let debugMode = false;
|
|
53
|
+
const MAX_QUERY_LENGTH = 500;
|
|
54
|
+
const MAX_QUERIES = 100;
|
|
55
|
+
let queryCount = 0;
|
|
56
|
+
function getQueriesFile() {
|
|
57
|
+
if (queriesFile)
|
|
58
|
+
return queriesFile;
|
|
59
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
60
|
+
try {
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
queriesFile = path.join(dir, 'queries.jsonl');
|
|
65
|
+
// Clear previous
|
|
66
|
+
try {
|
|
67
|
+
fs.writeFileSync(queriesFile, '');
|
|
68
|
+
}
|
|
69
|
+
catch { }
|
|
70
|
+
return queriesFile;
|
|
71
|
+
}
|
|
72
|
+
function writeQuery(record) {
|
|
73
|
+
if (queryCount >= MAX_QUERIES)
|
|
74
|
+
return;
|
|
75
|
+
queryCount++;
|
|
76
|
+
try {
|
|
77
|
+
fs.appendFileSync(getQueriesFile(), JSON.stringify(record) + '\n');
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Patch pg (node-postgres) to capture queries.
|
|
83
|
+
* Called from observe-register when pg is required.
|
|
84
|
+
*/
|
|
85
|
+
function patchPg(pgModule, debug) {
|
|
86
|
+
debugMode = debug;
|
|
87
|
+
// Patch Client.prototype.query
|
|
88
|
+
const Client = pgModule.Client;
|
|
89
|
+
if (!Client || !Client.prototype)
|
|
90
|
+
return;
|
|
91
|
+
const originalQuery = Client.prototype.query;
|
|
92
|
+
if (originalQuery.__trickle_patched)
|
|
93
|
+
return;
|
|
94
|
+
Client.prototype.query = function patchedQuery(...args) {
|
|
95
|
+
const startTime = performance.now();
|
|
96
|
+
// Extract query text and params
|
|
97
|
+
let queryText = '';
|
|
98
|
+
let params;
|
|
99
|
+
if (typeof args[0] === 'string') {
|
|
100
|
+
queryText = args[0];
|
|
101
|
+
params = Array.isArray(args[1]) ? args[1] : undefined;
|
|
102
|
+
}
|
|
103
|
+
else if (args[0] && typeof args[0] === 'object' && args[0].text) {
|
|
104
|
+
queryText = args[0].text;
|
|
105
|
+
params = args[0].values;
|
|
106
|
+
}
|
|
107
|
+
const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
|
|
108
|
+
? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
|
|
109
|
+
: queryText;
|
|
110
|
+
// Call original
|
|
111
|
+
const result = originalQuery.apply(this, args);
|
|
112
|
+
// Handle promise-based queries
|
|
113
|
+
if (result && typeof result.then === 'function') {
|
|
114
|
+
return result.then((res) => {
|
|
115
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
116
|
+
const columns = res.fields?.map((f) => f.name) || [];
|
|
117
|
+
writeQuery({
|
|
118
|
+
kind: 'query',
|
|
119
|
+
query: truncatedQuery,
|
|
120
|
+
params: params?.slice(0, 5),
|
|
121
|
+
durationMs,
|
|
122
|
+
rowCount: res.rowCount || 0,
|
|
123
|
+
columns: columns.length > 0 ? columns : undefined,
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
});
|
|
126
|
+
if (debugMode) {
|
|
127
|
+
console.log(`[trickle/db] ${truncatedQuery.substring(0, 60)}... (${durationMs}ms, ${res.rowCount} rows)`);
|
|
128
|
+
}
|
|
129
|
+
return res;
|
|
130
|
+
}, (err) => {
|
|
131
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
132
|
+
writeQuery({
|
|
133
|
+
kind: 'query',
|
|
134
|
+
query: truncatedQuery,
|
|
135
|
+
params: params?.slice(0, 5),
|
|
136
|
+
durationMs,
|
|
137
|
+
rowCount: 0,
|
|
138
|
+
error: err.message?.substring(0, 200),
|
|
139
|
+
timestamp: Date.now(),
|
|
140
|
+
});
|
|
141
|
+
throw err;
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return result;
|
|
145
|
+
};
|
|
146
|
+
Client.prototype.query.__trickle_patched = true;
|
|
147
|
+
// Also patch Pool if available
|
|
148
|
+
if (pgModule.Pool) {
|
|
149
|
+
const Pool = pgModule.Pool;
|
|
150
|
+
const origPoolQuery = Pool.prototype.query;
|
|
151
|
+
if (origPoolQuery && !origPoolQuery.__trickle_patched) {
|
|
152
|
+
Pool.prototype.query = function patchedPoolQuery(...args) {
|
|
153
|
+
const startTime = performance.now();
|
|
154
|
+
let queryText = '';
|
|
155
|
+
let params;
|
|
156
|
+
if (typeof args[0] === 'string') {
|
|
157
|
+
queryText = args[0];
|
|
158
|
+
params = Array.isArray(args[1]) ? args[1] : undefined;
|
|
159
|
+
}
|
|
160
|
+
else if (args[0] && typeof args[0] === 'object' && args[0].text) {
|
|
161
|
+
queryText = args[0].text;
|
|
162
|
+
params = args[0].values;
|
|
163
|
+
}
|
|
164
|
+
const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
|
|
165
|
+
? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
|
|
166
|
+
: queryText;
|
|
167
|
+
const result = origPoolQuery.apply(this, args);
|
|
168
|
+
if (result && typeof result.then === 'function') {
|
|
169
|
+
return result.then((res) => {
|
|
170
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
171
|
+
writeQuery({
|
|
172
|
+
kind: 'query',
|
|
173
|
+
query: truncatedQuery,
|
|
174
|
+
params: params?.slice(0, 5),
|
|
175
|
+
durationMs,
|
|
176
|
+
rowCount: res.rowCount || 0,
|
|
177
|
+
columns: res.fields?.map((f) => f.name),
|
|
178
|
+
timestamp: Date.now(),
|
|
179
|
+
});
|
|
180
|
+
return res;
|
|
181
|
+
}, (err) => {
|
|
182
|
+
writeQuery({
|
|
183
|
+
kind: 'query',
|
|
184
|
+
query: truncatedQuery,
|
|
185
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
186
|
+
rowCount: 0,
|
|
187
|
+
error: err.message?.substring(0, 200),
|
|
188
|
+
timestamp: Date.now(),
|
|
189
|
+
});
|
|
190
|
+
throw err;
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return result;
|
|
194
|
+
};
|
|
195
|
+
Pool.prototype.query.__trickle_patched = true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (debugMode) {
|
|
199
|
+
console.log('[trickle/db] PostgreSQL query tracing enabled');
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Patch mysql2 to capture queries.
|
|
204
|
+
*/
|
|
205
|
+
function patchMysql2(mysqlModule, debug) {
|
|
206
|
+
debugMode = debug;
|
|
207
|
+
// Patch Connection.prototype.query and .execute
|
|
208
|
+
const Connection = mysqlModule.Connection;
|
|
209
|
+
if (!Connection || !Connection.prototype)
|
|
210
|
+
return;
|
|
211
|
+
for (const method of ['query', 'execute']) {
|
|
212
|
+
const original = Connection.prototype[method];
|
|
213
|
+
if (!original || original.__trickle_patched)
|
|
214
|
+
continue;
|
|
215
|
+
Connection.prototype[method] = function patchedMethod(...args) {
|
|
216
|
+
const startTime = performance.now();
|
|
217
|
+
let queryText = typeof args[0] === 'string' ? args[0] : args[0]?.sql || '';
|
|
218
|
+
const truncated = queryText.length > MAX_QUERY_LENGTH ? queryText.substring(0, MAX_QUERY_LENGTH) + '...' : queryText;
|
|
219
|
+
const result = original.apply(this, args);
|
|
220
|
+
if (result && typeof result.then === 'function') {
|
|
221
|
+
return result.then((res) => {
|
|
222
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
223
|
+
const rows = Array.isArray(res) ? res[0] : res;
|
|
224
|
+
writeQuery({
|
|
225
|
+
kind: 'query', query: truncated, durationMs,
|
|
226
|
+
rowCount: Array.isArray(rows) ? rows.length : 0,
|
|
227
|
+
columns: Array.isArray(rows) && rows[0] ? Object.keys(rows[0]) : undefined,
|
|
228
|
+
timestamp: Date.now(),
|
|
229
|
+
});
|
|
230
|
+
return res;
|
|
231
|
+
}, (err) => {
|
|
232
|
+
writeQuery({
|
|
233
|
+
kind: 'query', query: truncated,
|
|
234
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
235
|
+
rowCount: 0, error: err.message?.substring(0, 200), timestamp: Date.now(),
|
|
236
|
+
});
|
|
237
|
+
throw err;
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return result;
|
|
241
|
+
};
|
|
242
|
+
Connection.prototype[method].__trickle_patched = true;
|
|
243
|
+
}
|
|
244
|
+
if (debug)
|
|
245
|
+
console.log('[trickle/db] MySQL query tracing enabled');
|
|
246
|
+
}
|
|
247
|
+
/**
|
|
248
|
+
* Patch better-sqlite3 to capture queries.
|
|
249
|
+
*/
|
|
250
|
+
function patchBetterSqlite3(dbConstructor, debug) {
|
|
251
|
+
debugMode = debug;
|
|
252
|
+
// better-sqlite3 returns a Database constructor — patch its prototype
|
|
253
|
+
const origPrepare = dbConstructor.prototype?.prepare;
|
|
254
|
+
if (!origPrepare || origPrepare.__trickle_patched)
|
|
255
|
+
return;
|
|
256
|
+
dbConstructor.prototype.prepare = function patchedPrepare(sql) {
|
|
257
|
+
const stmt = origPrepare.call(this, sql);
|
|
258
|
+
const truncated = sql.length > MAX_QUERY_LENGTH ? sql.substring(0, MAX_QUERY_LENGTH) + '...' : sql;
|
|
259
|
+
// Patch run, get, all methods on the statement
|
|
260
|
+
for (const method of ['run', 'get', 'all']) {
|
|
261
|
+
const origMethod = stmt[method];
|
|
262
|
+
if (!origMethod)
|
|
263
|
+
continue;
|
|
264
|
+
stmt[method] = function (...args) {
|
|
265
|
+
const startTime = performance.now();
|
|
266
|
+
try {
|
|
267
|
+
const result = origMethod.apply(this, args);
|
|
268
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
269
|
+
const rowCount = method === 'all' ? (Array.isArray(result) ? result.length : 0)
|
|
270
|
+
: method === 'get' ? (result ? 1 : 0)
|
|
271
|
+
: (result?.changes || 0);
|
|
272
|
+
writeQuery({
|
|
273
|
+
kind: 'query', query: truncated, durationMs, rowCount, timestamp: Date.now(),
|
|
274
|
+
});
|
|
275
|
+
return result;
|
|
276
|
+
}
|
|
277
|
+
catch (err) {
|
|
278
|
+
writeQuery({
|
|
279
|
+
kind: 'query', query: truncated,
|
|
280
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
281
|
+
rowCount: 0, error: err.message?.substring(0, 200), timestamp: Date.now(),
|
|
282
|
+
});
|
|
283
|
+
throw err;
|
|
284
|
+
}
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
return stmt;
|
|
288
|
+
};
|
|
289
|
+
dbConstructor.prototype.prepare.__trickle_patched = true;
|
|
290
|
+
if (debug)
|
|
291
|
+
console.log('[trickle/db] SQLite query tracing enabled');
|
|
292
|
+
}
|
package/dist/observe-register.js
CHANGED
|
@@ -1123,6 +1123,31 @@ if (enabled) {
|
|
|
1123
1123
|
}
|
|
1124
1124
|
catch { /* fall through to normal processing */ }
|
|
1125
1125
|
}
|
|
1126
|
+
// ── Database auto-detection: patch database drivers to capture SQL queries ──
|
|
1127
|
+
if (request === 'pg' && !expressPatched.has('pg')) {
|
|
1128
|
+
expressPatched.add('pg');
|
|
1129
|
+
try {
|
|
1130
|
+
const { patchPg } = require(path_1.default.join(__dirname, 'db-observer.js'));
|
|
1131
|
+
patchPg(exports, debug);
|
|
1132
|
+
}
|
|
1133
|
+
catch { /* not critical */ }
|
|
1134
|
+
}
|
|
1135
|
+
if ((request === 'mysql2' || request === 'mysql2/promise') && !expressPatched.has('mysql2')) {
|
|
1136
|
+
expressPatched.add('mysql2');
|
|
1137
|
+
try {
|
|
1138
|
+
const { patchMysql2 } = require(path_1.default.join(__dirname, 'db-observer.js'));
|
|
1139
|
+
patchMysql2(exports, debug);
|
|
1140
|
+
}
|
|
1141
|
+
catch { /* not critical */ }
|
|
1142
|
+
}
|
|
1143
|
+
if (request === 'better-sqlite3' && !expressPatched.has('better-sqlite3')) {
|
|
1144
|
+
expressPatched.add('better-sqlite3');
|
|
1145
|
+
try {
|
|
1146
|
+
const { patchBetterSqlite3 } = require(path_1.default.join(__dirname, 'db-observer.js'));
|
|
1147
|
+
patchBetterSqlite3(exports, debug);
|
|
1148
|
+
}
|
|
1149
|
+
catch { /* not critical */ }
|
|
1150
|
+
}
|
|
1126
1151
|
// Resolve to absolute path for dedup — do this FIRST since bundlers like
|
|
1127
1152
|
// tsx/esbuild may use path aliases (e.g., @config/env) that don't start
|
|
1128
1153
|
// with './' or '/'. We need the resolved path to decide if it's user code.
|
package/package.json
CHANGED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database query observer — patches popular database drivers to capture
|
|
3
|
+
* SQL queries, execution time, and result shapes.
|
|
4
|
+
*
|
|
5
|
+
* Currently supports:
|
|
6
|
+
* - pg (node-postgres) — used by Prisma, Knex, Sequelize, TypeORM
|
|
7
|
+
*
|
|
8
|
+
* Captured data is written to .trickle/queries.jsonl as:
|
|
9
|
+
* { query: "SELECT ...", params: [...], durationMs: 2.5, rowCount: 42, columns: [...] }
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import * as fs from 'fs';
|
|
13
|
+
import * as path from 'path';
|
|
14
|
+
|
|
15
|
+
interface QueryRecord {
|
|
16
|
+
kind: 'query';
|
|
17
|
+
query: string;
|
|
18
|
+
params?: unknown[];
|
|
19
|
+
durationMs: number;
|
|
20
|
+
rowCount: number;
|
|
21
|
+
columns?: string[];
|
|
22
|
+
error?: string;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let queriesFile: string | null = null;
|
|
27
|
+
let debugMode = false;
|
|
28
|
+
const MAX_QUERY_LENGTH = 500;
|
|
29
|
+
const MAX_QUERIES = 100;
|
|
30
|
+
let queryCount = 0;
|
|
31
|
+
|
|
32
|
+
function getQueriesFile(): string {
|
|
33
|
+
if (queriesFile) return queriesFile;
|
|
34
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
35
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
36
|
+
queriesFile = path.join(dir, 'queries.jsonl');
|
|
37
|
+
// Clear previous
|
|
38
|
+
try { fs.writeFileSync(queriesFile, ''); } catch {}
|
|
39
|
+
return queriesFile;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeQuery(record: QueryRecord): void {
|
|
43
|
+
if (queryCount >= MAX_QUERIES) return;
|
|
44
|
+
queryCount++;
|
|
45
|
+
try {
|
|
46
|
+
fs.appendFileSync(getQueriesFile(), JSON.stringify(record) + '\n');
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Patch pg (node-postgres) to capture queries.
|
|
52
|
+
* Called from observe-register when pg is required.
|
|
53
|
+
*/
|
|
54
|
+
export function patchPg(pgModule: any, debug: boolean): void {
|
|
55
|
+
debugMode = debug;
|
|
56
|
+
|
|
57
|
+
// Patch Client.prototype.query
|
|
58
|
+
const Client = pgModule.Client;
|
|
59
|
+
if (!Client || !Client.prototype) return;
|
|
60
|
+
|
|
61
|
+
const originalQuery = Client.prototype.query;
|
|
62
|
+
if ((originalQuery as any).__trickle_patched) return;
|
|
63
|
+
|
|
64
|
+
Client.prototype.query = function patchedQuery(...args: any[]): any {
|
|
65
|
+
const startTime = performance.now();
|
|
66
|
+
|
|
67
|
+
// Extract query text and params
|
|
68
|
+
let queryText = '';
|
|
69
|
+
let params: unknown[] | undefined;
|
|
70
|
+
if (typeof args[0] === 'string') {
|
|
71
|
+
queryText = args[0];
|
|
72
|
+
params = Array.isArray(args[1]) ? args[1] : undefined;
|
|
73
|
+
} else if (args[0] && typeof args[0] === 'object' && args[0].text) {
|
|
74
|
+
queryText = args[0].text;
|
|
75
|
+
params = args[0].values;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
|
|
79
|
+
? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
|
|
80
|
+
: queryText;
|
|
81
|
+
|
|
82
|
+
// Call original
|
|
83
|
+
const result = originalQuery.apply(this, args);
|
|
84
|
+
|
|
85
|
+
// Handle promise-based queries
|
|
86
|
+
if (result && typeof result.then === 'function') {
|
|
87
|
+
return result.then(
|
|
88
|
+
(res: any) => {
|
|
89
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
90
|
+
const columns = res.fields?.map((f: any) => f.name) || [];
|
|
91
|
+
writeQuery({
|
|
92
|
+
kind: 'query',
|
|
93
|
+
query: truncatedQuery,
|
|
94
|
+
params: params?.slice(0, 5),
|
|
95
|
+
durationMs,
|
|
96
|
+
rowCount: res.rowCount || 0,
|
|
97
|
+
columns: columns.length > 0 ? columns : undefined,
|
|
98
|
+
timestamp: Date.now(),
|
|
99
|
+
});
|
|
100
|
+
if (debugMode) {
|
|
101
|
+
console.log(`[trickle/db] ${truncatedQuery.substring(0, 60)}... (${durationMs}ms, ${res.rowCount} rows)`);
|
|
102
|
+
}
|
|
103
|
+
return res;
|
|
104
|
+
},
|
|
105
|
+
(err: any) => {
|
|
106
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
107
|
+
writeQuery({
|
|
108
|
+
kind: 'query',
|
|
109
|
+
query: truncatedQuery,
|
|
110
|
+
params: params?.slice(0, 5),
|
|
111
|
+
durationMs,
|
|
112
|
+
rowCount: 0,
|
|
113
|
+
error: err.message?.substring(0, 200),
|
|
114
|
+
timestamp: Date.now(),
|
|
115
|
+
});
|
|
116
|
+
throw err;
|
|
117
|
+
},
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return result;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
(Client.prototype.query as any).__trickle_patched = true;
|
|
125
|
+
|
|
126
|
+
// Also patch Pool if available
|
|
127
|
+
if (pgModule.Pool) {
|
|
128
|
+
const Pool = pgModule.Pool;
|
|
129
|
+
const origPoolQuery = Pool.prototype.query;
|
|
130
|
+
if (origPoolQuery && !(origPoolQuery as any).__trickle_patched) {
|
|
131
|
+
Pool.prototype.query = function patchedPoolQuery(...args: any[]): any {
|
|
132
|
+
const startTime = performance.now();
|
|
133
|
+
let queryText = '';
|
|
134
|
+
let params: unknown[] | undefined;
|
|
135
|
+
if (typeof args[0] === 'string') {
|
|
136
|
+
queryText = args[0];
|
|
137
|
+
params = Array.isArray(args[1]) ? args[1] : undefined;
|
|
138
|
+
} else if (args[0] && typeof args[0] === 'object' && args[0].text) {
|
|
139
|
+
queryText = args[0].text;
|
|
140
|
+
params = args[0].values;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const truncatedQuery = queryText.length > MAX_QUERY_LENGTH
|
|
144
|
+
? queryText.substring(0, MAX_QUERY_LENGTH) + '...'
|
|
145
|
+
: queryText;
|
|
146
|
+
|
|
147
|
+
const result = origPoolQuery.apply(this, args);
|
|
148
|
+
if (result && typeof result.then === 'function') {
|
|
149
|
+
return result.then(
|
|
150
|
+
(res: any) => {
|
|
151
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
152
|
+
writeQuery({
|
|
153
|
+
kind: 'query',
|
|
154
|
+
query: truncatedQuery,
|
|
155
|
+
params: params?.slice(0, 5),
|
|
156
|
+
durationMs,
|
|
157
|
+
rowCount: res.rowCount || 0,
|
|
158
|
+
columns: res.fields?.map((f: any) => f.name),
|
|
159
|
+
timestamp: Date.now(),
|
|
160
|
+
});
|
|
161
|
+
return res;
|
|
162
|
+
},
|
|
163
|
+
(err: any) => {
|
|
164
|
+
writeQuery({
|
|
165
|
+
kind: 'query',
|
|
166
|
+
query: truncatedQuery,
|
|
167
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
168
|
+
rowCount: 0,
|
|
169
|
+
error: err.message?.substring(0, 200),
|
|
170
|
+
timestamp: Date.now(),
|
|
171
|
+
});
|
|
172
|
+
throw err;
|
|
173
|
+
},
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
return result;
|
|
177
|
+
};
|
|
178
|
+
(Pool.prototype.query as any).__trickle_patched = true;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (debugMode) {
|
|
183
|
+
console.log('[trickle/db] PostgreSQL query tracing enabled');
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Patch mysql2 to capture queries.
|
|
189
|
+
*/
|
|
190
|
+
export function patchMysql2(mysqlModule: any, debug: boolean): void {
|
|
191
|
+
debugMode = debug;
|
|
192
|
+
|
|
193
|
+
// Patch Connection.prototype.query and .execute
|
|
194
|
+
const Connection = mysqlModule.Connection;
|
|
195
|
+
if (!Connection || !Connection.prototype) return;
|
|
196
|
+
|
|
197
|
+
for (const method of ['query', 'execute'] as const) {
|
|
198
|
+
const original = Connection.prototype[method];
|
|
199
|
+
if (!original || (original as any).__trickle_patched) continue;
|
|
200
|
+
|
|
201
|
+
Connection.prototype[method] = function patchedMethod(...args: any[]): any {
|
|
202
|
+
const startTime = performance.now();
|
|
203
|
+
let queryText = typeof args[0] === 'string' ? args[0] : args[0]?.sql || '';
|
|
204
|
+
const truncated = queryText.length > MAX_QUERY_LENGTH ? queryText.substring(0, MAX_QUERY_LENGTH) + '...' : queryText;
|
|
205
|
+
|
|
206
|
+
const result = original.apply(this, args);
|
|
207
|
+
if (result && typeof result.then === 'function') {
|
|
208
|
+
return result.then(
|
|
209
|
+
(res: any) => {
|
|
210
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
211
|
+
const rows = Array.isArray(res) ? res[0] : res;
|
|
212
|
+
writeQuery({
|
|
213
|
+
kind: 'query', query: truncated, durationMs,
|
|
214
|
+
rowCount: Array.isArray(rows) ? rows.length : 0,
|
|
215
|
+
columns: Array.isArray(rows) && rows[0] ? Object.keys(rows[0]) : undefined,
|
|
216
|
+
timestamp: Date.now(),
|
|
217
|
+
});
|
|
218
|
+
return res;
|
|
219
|
+
},
|
|
220
|
+
(err: any) => {
|
|
221
|
+
writeQuery({
|
|
222
|
+
kind: 'query', query: truncated,
|
|
223
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
224
|
+
rowCount: 0, error: err.message?.substring(0, 200), timestamp: Date.now(),
|
|
225
|
+
});
|
|
226
|
+
throw err;
|
|
227
|
+
},
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
231
|
+
};
|
|
232
|
+
(Connection.prototype[method] as any).__trickle_patched = true;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (debug) console.log('[trickle/db] MySQL query tracing enabled');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/**
|
|
239
|
+
* Patch better-sqlite3 to capture queries.
|
|
240
|
+
*/
|
|
241
|
+
export function patchBetterSqlite3(dbConstructor: any, debug: boolean): void {
|
|
242
|
+
debugMode = debug;
|
|
243
|
+
|
|
244
|
+
// better-sqlite3 returns a Database constructor — patch its prototype
|
|
245
|
+
const origPrepare = dbConstructor.prototype?.prepare;
|
|
246
|
+
if (!origPrepare || (origPrepare as any).__trickle_patched) return;
|
|
247
|
+
|
|
248
|
+
dbConstructor.prototype.prepare = function patchedPrepare(sql: string): any {
|
|
249
|
+
const stmt = origPrepare.call(this, sql);
|
|
250
|
+
const truncated = sql.length > MAX_QUERY_LENGTH ? sql.substring(0, MAX_QUERY_LENGTH) + '...' : sql;
|
|
251
|
+
|
|
252
|
+
// Patch run, get, all methods on the statement
|
|
253
|
+
for (const method of ['run', 'get', 'all'] as const) {
|
|
254
|
+
const origMethod = stmt[method];
|
|
255
|
+
if (!origMethod) continue;
|
|
256
|
+
stmt[method] = function (...args: any[]): any {
|
|
257
|
+
const startTime = performance.now();
|
|
258
|
+
try {
|
|
259
|
+
const result = origMethod.apply(this, args);
|
|
260
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
261
|
+
const rowCount = method === 'all' ? (Array.isArray(result) ? result.length : 0)
|
|
262
|
+
: method === 'get' ? (result ? 1 : 0)
|
|
263
|
+
: (result?.changes || 0);
|
|
264
|
+
writeQuery({
|
|
265
|
+
kind: 'query', query: truncated, durationMs, rowCount, timestamp: Date.now(),
|
|
266
|
+
});
|
|
267
|
+
return result;
|
|
268
|
+
} catch (err: any) {
|
|
269
|
+
writeQuery({
|
|
270
|
+
kind: 'query', query: truncated,
|
|
271
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
272
|
+
rowCount: 0, error: err.message?.substring(0, 200), timestamp: Date.now(),
|
|
273
|
+
});
|
|
274
|
+
throw err;
|
|
275
|
+
}
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
return stmt;
|
|
279
|
+
};
|
|
280
|
+
(dbConstructor.prototype.prepare as any).__trickle_patched = true;
|
|
281
|
+
|
|
282
|
+
if (debug) console.log('[trickle/db] SQLite query tracing enabled');
|
|
283
|
+
}
|
package/src/observe-register.ts
CHANGED
|
@@ -1113,6 +1113,29 @@ if (enabled) {
|
|
|
1113
1113
|
} catch { /* fall through to normal processing */ }
|
|
1114
1114
|
}
|
|
1115
1115
|
|
|
1116
|
+
// ── Database auto-detection: patch database drivers to capture SQL queries ──
|
|
1117
|
+
if (request === 'pg' && !expressPatched.has('pg')) {
|
|
1118
|
+
expressPatched.add('pg');
|
|
1119
|
+
try {
|
|
1120
|
+
const { patchPg } = require(path.join(__dirname, 'db-observer.js'));
|
|
1121
|
+
patchPg(exports, debug);
|
|
1122
|
+
} catch { /* not critical */ }
|
|
1123
|
+
}
|
|
1124
|
+
if ((request === 'mysql2' || request === 'mysql2/promise') && !expressPatched.has('mysql2')) {
|
|
1125
|
+
expressPatched.add('mysql2');
|
|
1126
|
+
try {
|
|
1127
|
+
const { patchMysql2 } = require(path.join(__dirname, 'db-observer.js'));
|
|
1128
|
+
patchMysql2(exports, debug);
|
|
1129
|
+
} catch { /* not critical */ }
|
|
1130
|
+
}
|
|
1131
|
+
if (request === 'better-sqlite3' && !expressPatched.has('better-sqlite3')) {
|
|
1132
|
+
expressPatched.add('better-sqlite3');
|
|
1133
|
+
try {
|
|
1134
|
+
const { patchBetterSqlite3 } = require(path.join(__dirname, 'db-observer.js'));
|
|
1135
|
+
patchBetterSqlite3(exports, debug);
|
|
1136
|
+
} catch { /* not critical */ }
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1116
1139
|
// Resolve to absolute path for dedup — do this FIRST since bundlers like
|
|
1117
1140
|
// tsx/esbuild may use path aliases (e.g., @config/env) that don't start
|
|
1118
1141
|
// with './' or '/'. We need the resolved path to decide if it's user code.
|