Skip to content

Commit 12f4c90

Browse files
Merge pull request #16 from thelastbackspace/feat/mysql
Track the classic mysql driver
2 parents 881d45b + 76558be commit 12f4c90

8 files changed

Lines changed: 171 additions & 6 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- **`mysql` (the classic driver)**, used by TypeORM, Knex and many older apps: `instrumentMysql(tp, require('mysql'))`, auto-detected. Connections and pools (`pool.query` includes waiting for a free connection, and failures to get one), labelled like mysql2: `mysql SELECT users`.
1112
- **`traceperf run` measures in two phases.** After the warm-up it sends `--probe` requests per path (default 10) one at a time, then the concurrent load. Per-request findings (a dominant call, sequential awaits, N+1) come from the one-at-a-time phase: under load every await also waits for the busy event loop, so a 1 ms query on a 60-row table measured 12 ms and was reported as "check this query's plan". Blocking, queueing, errors and CPU cost still come from the load.
1213
- **Failed responses are called out first:** "2117 of 2117 responses failed (most: 2117× 401 Unauthorized), so the timings below describe error responses", with a hint (`-H` for 401/403, `--path` for 404). An expired token used to give "no issues found".
1314

‎README.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
- **Honest numbers.** You get total, self and synchronous time for each call. Detailed mode flags calls that blocked the event loop.
1414
- **Production-ready.** Stats are bounded per label (p50/p95/p99), sampling is built in, and `enabled: false` makes tracking a pass-through costing ~40 ns.
1515
- **Gets data out.** Chrome/Perfetto traces, OpenTelemetry spans, JSON lines, or your own `onComplete` hook.
16-
- **Zero-effort coverage.** One line tracks every `fetch`, HTTP request or query, for pg, mysql2, MongoDB/Mongoose, ioredis, Prisma, better-sqlite3 (and Drizzle on any of these drivers) and the AWS SDK.
16+
- **Zero-effort coverage.** One line tracks every `fetch`, HTTP request or query, for pg, mysql2, mysql, MongoDB/Mongoose, ioredis, Prisma, better-sqlite3 (and Drizzle on any of these drivers) and the AWS SDK.
1717
- **Built for teams.** It feeds Prometheus, catches regressions in CI (`traceperf compare --fail`), shows up in Chrome DevTools, and produces HTML flame-chart reports.
1818
- **Fits your stack.** Express, Fastify, Hono, Koa, NestJS, Next.js route handlers, decorators, CommonJS, ESM, TypeScript, browsers, Node.js 14+, Bun, Deno, Cloudflare Workers and Vercel Edge. There are no runtime dependencies.
1919

@@ -125,7 +125,7 @@ Database clients are only instrumented when your `package.json` lists them. Conf
125125
| `TRACEPERF_THRESHOLD=50` | Only report calls taking at least 50 ms |
126126
| `TRACEPERF_SAMPLE_RATE=0.1` | Track 10% of requests |
127127
| `TRACEPERF_IGNORE=^/health` | Regular expression of request paths not to track |
128-
| `TRACEPERF_DISABLE=http,fetch` | Skip targets: `server`, `http`, `fetch`, `pg`, `mysql2`, `mongodb`, `ioredis`, `prisma`, `better-sqlite3` |
128+
| `TRACEPERF_DISABLE=http,fetch` | Skip targets: `server`, `http`, `fetch`, `pg`, `mysql2`, `mongodb`, `ioredis`, `prisma`, `better-sqlite3`, `mysql` |
129129
| `TRACEPERF_SILENT=1` / `TRACEPERF_SUMMARY=0` / `TRACEPERF_QUIET=1` | No per-request trees / no exit summary / no startup line |
130130
| `TRACEPERF_ENABLED=0` | Load nothing |
131131

‎package-lock.json‎

Lines changed: 98 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎package.json‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@
142142
"jest": "^29.7.0",
143143
"koa": "^3.2.1",
144144
"mongodb": "^7.7.0",
145+
"mysql": "^2.18.1",
145146
"mysql2": "^3.24.6",
146147
"pg": "^8.23.1",
147148
"prettier": "^3.5.3",

‎src/__tests__/drivers.test.ts‎

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import {
1818
instrumentBetterSqlite3,
1919
instrumentPrisma,
2020
prismaExtension,
21+
instrumentMysql,
2122
instrumentMysql2,
2223
instrumentPg,
2324
instrumentRedis,
@@ -191,6 +192,32 @@ describe('instrumentMysql2 (real mysql2 client)', () => {
191192
});
192193
});
193194

195+
describe('instrumentMysql (real mysql client)', () => {
196+
test('connection and pool failures are tracked as failed queries', async () => {
197+
// eslint-disable-next-line @typescript-eslint/no-require-imports
198+
const mysql = require('mysql');
199+
const { tp, roots } = setup();
200+
const uninstall = instrumentMysql(tp, mysql);
201+
expect(instrumentMysql(tp, mysql)).toBeInstanceOf(Function); // idempotent
202+
try {
203+
const conn = mysql.createConnection({ host: '127.0.0.1', port: 1, user: 'x', connectTimeout: 2000 });
204+
conn.on('error', () => undefined);
205+
const pool = mysql.createPool({ host: '127.0.0.1', port: 1, user: 'x', connectTimeout: 2000 });
206+
await tp.track(async () => {
207+
await new Promise<void>(resolve => conn.query('SELECT * FROM users WHERE id = ?', [1], () => resolve()));
208+
await new Promise<void>(resolve => pool.query('INSERT INTO orders (a) VALUES (1)', () => resolve()));
209+
}, { label: 'job' });
210+
pool.end(() => undefined);
211+
} finally {
212+
uninstall();
213+
}
214+
const job = roots.find(r => r.label === 'job')!;
215+
expect(labels(job)[0]).toBe('mysql SELECT users');
216+
expect(labels(job)).toContain('mysql INSERT orders');
217+
job.children.forEach(c => expect(c.error).toBeDefined());
218+
});
219+
});
220+
194221
const MYSQL = process.env.TRACEPERF_MYSQL_URL;
195222
(MYSQL ? describe : describe.skip)('instrumentMysql2 (real MySQL)', () => {
196223
test('query/execute through connections, pools and the promise API; failures; nesting', async () => {

‎src/auto.ts‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@
1212
import type { OptimizedPerformanceMonitor } from './core/optimized-performance-monitor';
1313
import { loadNodeBuiltin } from './core/env';
1414
import { HttpServerInstrumentOptions, instrumentFetch, instrumentHttp, instrumentHttpServer, Uninstall } from './instrument';
15-
import { instrumentBetterSqlite3, instrumentMongo, instrumentMysql2, instrumentPg, instrumentPrisma, instrumentRedis } from './drivers';
15+
import { instrumentBetterSqlite3, instrumentMongo, instrumentMysql, instrumentMysql2, instrumentPg, instrumentPrisma, instrumentRedis } from './drivers';
1616

17-
export type AutoTarget = 'server' | 'http' | 'fetch' | 'pg' | 'mysql2' | 'mongodb' | 'ioredis' | 'prisma' | 'better-sqlite3';
17+
export type AutoTarget = 'server' | 'http' | 'fetch' | 'pg' | 'mysql2' | 'mongodb' | 'ioredis' | 'prisma' | 'better-sqlite3' | 'mysql';
1818

19-
export const AUTO_TARGETS: AutoTarget[] = ['server', 'http', 'fetch', 'pg', 'mysql2', 'mongodb', 'ioredis', 'prisma', 'better-sqlite3'];
19+
export const AUTO_TARGETS: AutoTarget[] = ['server', 'http', 'fetch', 'pg', 'mysql2', 'mongodb', 'ioredis', 'prisma', 'better-sqlite3', 'mysql'];
2020

2121
export interface AutoInstrumentOptions {
2222
/** Targets to skip, e.g. ['http'] */
@@ -94,6 +94,7 @@ export function autoInstrument(tp: OptimizedPerformanceMonitor, options: AutoIns
9494
const load = (name: string) => appRequire(name);
9595
if (deps.names.has('pg')) add('pg', () => instrumentPg(tp, load('pg')));
9696
if (deps.names.has('mysql2')) add('mysql2', () => instrumentMysql2(tp, load('mysql2')));
97+
if (deps.names.has('mysql')) add('mysql', () => instrumentMysql(tp, load('mysql')));
9798
if (deps.names.has('better-sqlite3')) add('better-sqlite3', () => instrumentBetterSqlite3(tp, load('better-sqlite3')));
9899
if (deps.names.has('@prisma/client')) add('prisma', () => {
99100
// Prisma 4/5 need the module; 6+ use the tracing hook (and Prisma 7's module may not load on its own)

‎src/drivers.ts‎

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,43 @@ export function instrumentMysql2(tp: OptimizedPerformanceMonitor, mysql2: any, o
146146
return () => restores.forEach(r => r());
147147
}
148148

149+
/**
150+
* mysql (the classic driver, e.g. under TypeORM or Knex): every query,
151+
* "mysql SELECT users", for connections and pools (pools use connection.query).
152+
*
153+
* instrumentMysql(tp, require('mysql'));
154+
*/
155+
export function instrumentMysql(tp: OptimizedPerformanceMonitor, mysql: any, options: DriverOptions = {}): Uninstall {
156+
const restores: Uninstall[] = [];
157+
let proto: any;
158+
try {
159+
// The Connection class is not exported; an unconnected connection shows its prototype
160+
proto = mysql && typeof mysql.createConnection === 'function' && Object.getPrototypeOf(mysql.createConnection({}));
161+
} catch {
162+
proto = undefined;
163+
}
164+
const POOLED = Symbol.for('traceperf.mysql.pooled');
165+
patchMethod(proto, 'query', original => function (this: unknown, ...args: unknown[]) {
166+
// pool.query hands its own Query object to a connection: tracked by the pool patch
167+
const first = args[0] as any;
168+
if (first && typeof first === 'object' && first[POOLED]) return original.apply(this, args);
169+
return trackCall(tp, `mysql ${sqlLabel(first)}`, options, this, original, args);
170+
}, restores);
171+
let poolProto: any;
172+
try {
173+
poolProto = typeof mysql.createPool === 'function' && Object.getPrototypeOf(mysql.createPool({}));
174+
} catch {
175+
poolProto = undefined;
176+
}
177+
// Pools: includes waiting for a free connection, and failures to get one
178+
patchMethod(poolProto, 'query', original => function (this: unknown, ...args: unknown[]) {
179+
const result: any = trackCall(tp, `mysql ${sqlLabel(args[0])}`, options, this, original, args);
180+
if (result && typeof result === 'object') result[POOLED] = true;
181+
return result;
182+
}, restores);
183+
return () => restores.forEach(r => r());
184+
}
185+
149186
const MONGO_COLLECTION_METHODS = [
150187
'insertOne', 'insertMany', 'findOne', 'updateOne', 'updateMany', 'replaceOne', 'deleteOne', 'deleteMany',
151188
'countDocuments', 'estimatedDocumentCount', 'distinct', 'findOneAndUpdate', 'findOneAndDelete',

‎src/register.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
* TRACEPERF_SILENT=1 no per-call console trees
1414
* TRACEPERF_SUMMARY=0 no stats + explanations on exit (default on)
1515
* TRACEPERF_DISABLE=http,fetch skip targets (server, http, fetch, pg, mysql2, mongodb, ioredis,
16-
* prisma, better-sqlite3)
16+
* prisma, better-sqlite3, mysql)
1717
* TRACEPERF_IGNORE=^/health regular expression of request paths not to track
1818
* TRACEPERF_QUIET=1 no "traceperf: tracking ..." line at startup
1919
* TRACEPERF_PROFILE_BLOCKING=1 sample the CPU (V8 profiler) to name the

0 commit comments

Comments
 (0)