Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Added
- **`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`.
- **`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.
- **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".

Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
- **Honest numbers.** You get total, self and synchronous time for each call. Detailed mode flags calls that blocked the event loop.
- **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.
- **Gets data out.** Chrome/Perfetto traces, OpenTelemetry spans, JSON lines, or your own `onComplete` hook.
- **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.
- **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.
- **Built for teams.** It feeds Prometheus, catches regressions in CI (`traceperf compare --fail`), shows up in Chrome DevTools, and produces HTML flame-chart reports.
- **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.

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

Expand Down
98 changes: 98 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"jest": "^29.7.0",
"koa": "^3.2.1",
"mongodb": "^7.7.0",
"mysql": "^2.18.1",
"mysql2": "^3.24.6",
"pg": "^8.23.1",
"prettier": "^3.5.3",
Expand Down
27 changes: 27 additions & 0 deletions src/__tests__/drivers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
instrumentBetterSqlite3,
instrumentPrisma,
prismaExtension,
instrumentMysql,
instrumentMysql2,
instrumentPg,
instrumentRedis,
Expand Down Expand Up @@ -191,6 +192,32 @@ describe('instrumentMysql2 (real mysql2 client)', () => {
});
});

describe('instrumentMysql (real mysql client)', () => {
test('connection and pool failures are tracked as failed queries', async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const mysql = require('mysql');
const { tp, roots } = setup();
const uninstall = instrumentMysql(tp, mysql);
expect(instrumentMysql(tp, mysql)).toBeInstanceOf(Function); // idempotent
try {
const conn = mysql.createConnection({ host: '127.0.0.1', port: 1, user: 'x', connectTimeout: 2000 });
conn.on('error', () => undefined);
const pool = mysql.createPool({ host: '127.0.0.1', port: 1, user: 'x', connectTimeout: 2000 });
await tp.track(async () => {
await new Promise<void>(resolve => conn.query('SELECT * FROM users WHERE id = ?', [1], () => resolve()));
await new Promise<void>(resolve => pool.query('INSERT INTO orders (a) VALUES (1)', () => resolve()));
}, { label: 'job' });
pool.end(() => undefined);
} finally {
uninstall();
}
const job = roots.find(r => r.label === 'job')!;
expect(labels(job)[0]).toBe('mysql SELECT users');
expect(labels(job)).toContain('mysql INSERT orders');
job.children.forEach(c => expect(c.error).toBeDefined());
});
});

const MYSQL = process.env.TRACEPERF_MYSQL_URL;
(MYSQL ? describe : describe.skip)('instrumentMysql2 (real MySQL)', () => {
test('query/execute through connections, pools and the promise API; failures; nesting', async () => {
Expand Down
7 changes: 4 additions & 3 deletions src/auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@
import type { OptimizedPerformanceMonitor } from './core/optimized-performance-monitor';
import { loadNodeBuiltin } from './core/env';
import { HttpServerInstrumentOptions, instrumentFetch, instrumentHttp, instrumentHttpServer, Uninstall } from './instrument';
import { instrumentBetterSqlite3, instrumentMongo, instrumentMysql2, instrumentPg, instrumentPrisma, instrumentRedis } from './drivers';
import { instrumentBetterSqlite3, instrumentMongo, instrumentMysql, instrumentMysql2, instrumentPg, instrumentPrisma, instrumentRedis } from './drivers';

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

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

export interface AutoInstrumentOptions {
/** Targets to skip, e.g. ['http'] */
Expand Down Expand Up @@ -94,6 +94,7 @@ export function autoInstrument(tp: OptimizedPerformanceMonitor, options: AutoIns
const load = (name: string) => appRequire(name);
if (deps.names.has('pg')) add('pg', () => instrumentPg(tp, load('pg')));
if (deps.names.has('mysql2')) add('mysql2', () => instrumentMysql2(tp, load('mysql2')));
if (deps.names.has('mysql')) add('mysql', () => instrumentMysql(tp, load('mysql')));
if (deps.names.has('better-sqlite3')) add('better-sqlite3', () => instrumentBetterSqlite3(tp, load('better-sqlite3')));
if (deps.names.has('@prisma/client')) add('prisma', () => {
// Prisma 4/5 need the module; 6+ use the tracing hook (and Prisma 7's module may not load on its own)
Expand Down
37 changes: 37 additions & 0 deletions src/drivers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,43 @@ export function instrumentMysql2(tp: OptimizedPerformanceMonitor, mysql2: any, o
return () => restores.forEach(r => r());
}

/**
* mysql (the classic driver, e.g. under TypeORM or Knex): every query,
* "mysql SELECT users", for connections and pools (pools use connection.query).
*
* instrumentMysql(tp, require('mysql'));
*/
export function instrumentMysql(tp: OptimizedPerformanceMonitor, mysql: any, options: DriverOptions = {}): Uninstall {
const restores: Uninstall[] = [];
let proto: any;
try {
// The Connection class is not exported; an unconnected connection shows its prototype
proto = mysql && typeof mysql.createConnection === 'function' && Object.getPrototypeOf(mysql.createConnection({}));
} catch {
proto = undefined;
}
const POOLED = Symbol.for('traceperf.mysql.pooled');
patchMethod(proto, 'query', original => function (this: unknown, ...args: unknown[]) {
// pool.query hands its own Query object to a connection: tracked by the pool patch
const first = args[0] as any;
if (first && typeof first === 'object' && first[POOLED]) return original.apply(this, args);
return trackCall(tp, `mysql ${sqlLabel(first)}`, options, this, original, args);
}, restores);
let poolProto: any;
try {
poolProto = typeof mysql.createPool === 'function' && Object.getPrototypeOf(mysql.createPool({}));
} catch {
poolProto = undefined;
}
// Pools: includes waiting for a free connection, and failures to get one
patchMethod(poolProto, 'query', original => function (this: unknown, ...args: unknown[]) {
const result: any = trackCall(tp, `mysql ${sqlLabel(args[0])}`, options, this, original, args);
if (result && typeof result === 'object') result[POOLED] = true;
return result;
}, restores);
return () => restores.forEach(r => r());
}

const MONGO_COLLECTION_METHODS = [
'insertOne', 'insertMany', 'findOne', 'updateOne', 'updateMany', 'replaceOne', 'deleteOne', 'deleteMany',
'countDocuments', 'estimatedDocumentCount', 'distinct', 'findOneAndUpdate', 'findOneAndDelete',
Expand Down
2 changes: 1 addition & 1 deletion src/register.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
* TRACEPERF_SILENT=1 no per-call console trees
* TRACEPERF_SUMMARY=0 no stats + explanations on exit (default on)
* TRACEPERF_DISABLE=http,fetch skip targets (server, http, fetch, pg, mysql2, mongodb, ioredis,
* prisma, better-sqlite3)
* prisma, better-sqlite3, mysql)
* TRACEPERF_IGNORE=^/health regular expression of request paths not to track
* TRACEPERF_QUIET=1 no "traceperf: tracking ..." line at startup
* TRACEPERF_PROFILE_BLOCKING=1 sample the CPU (V8 profiler) to name the
Expand Down
Loading