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 @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added
- **Fastify route labels:** with `traceperf/auto` or `traceperf run`, Fastify (3–5) requests are labelled by route, `GET /api/articles/:slug`, instead of one label per URL. `traceperf/auto` wraps the `fastify()` factory to add an `onRequest` hook (`wrapFastifyFactory` for doing it by hand). Any framework can do the same by setting the `ROUTE` symbol on the raw request.
- **Koa route labels:** with `@koa/router` or `koa-router`, Koa requests are labelled by the matched route (`GET /api/users/:id`), auto-detected; `labelKoaRoutes(require('koa'))` by hand.

## [1.7.0] - 2026-11-06

Expand Down
36 changes: 36 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 @@ -116,6 +116,7 @@
"devDependencies": {
"@arethetypeswrong/cli": "^0.18.5",
"@aws-sdk/client-s3": "^3.1153.0",
"@koa/router": "^12.0.2",
"@nestjs/common": "^12.2.0",
"@nestjs/core": "^12.2.0",
"@nestjs/platform-express": "^12.2.0",
Expand Down
29 changes: 29 additions & 0 deletions src/__tests__/instrument.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
instrumentHttp,
instrumentHttpServer,
instrumentMethods,
labelKoaRoutes,
prismaExtension,
wrapFastifyFactory,
} from '../index';
Expand Down Expand Up @@ -183,3 +184,31 @@ describe('Fastify route labels (wrapFastifyFactory)', () => {
expect(fastify.fastify).toBe(fastify);
});
});

describe('Koa route labels (labelKoaRoutes)', () => {
test('requests are labelled by the route @koa/router matched, prefixes included', async () => {
// eslint-disable-next-line @typescript-eslint/no-require-imports
const Koa = require('koa');
// eslint-disable-next-line @typescript-eslint/no-require-imports
const Router = require('@koa/router');
const { tp, roots } = setup();
const uninstall = instrumentHttpServer(tp);
const unlabel = labelKoaRoutes(Koa);
expect(labelKoaRoutes(Koa)()).toBeUndefined(); // idempotent: second call is a no-op
const app = new Koa();
const api = new Router({ prefix: '/api' });
api.get('/users/:id', (ctx: any) => { ctx.body = 'ok'; });
app.use(api.routes());
const server = app.listen(0, '127.0.0.1');
try {
await new Promise(r => server.once('listening', r));
const port = (server.address() as AddressInfo).port;
for (const p of ['/api/users/1', '/api/users/2', '/other']) await get(`http://127.0.0.1:${port}${p}`);
} finally {
await new Promise(r => server.close(r));
unlabel();
uninstall();
}
expect(roots.map(r => r.label)).toEqual(['GET /api/users/:id', 'GET /api/users/:id', 'GET /other']);
});
});
10 changes: 9 additions & 1 deletion src/auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
*/
import type { OptimizedPerformanceMonitor } from './core/optimized-performance-monitor';
import { loadNodeBuiltin } from './core/env';
import { HttpServerInstrumentOptions, instrumentFetch, instrumentHttp, instrumentHttpServer, Uninstall, wrapFastifyFactory } from './instrument';
import { HttpServerInstrumentOptions, instrumentFetch, instrumentHttp, instrumentHttpServer, labelKoaRoutes, Uninstall, wrapFastifyFactory } from './instrument';
import { instrumentBetterSqlite3, instrumentMongo, instrumentMysql, instrumentMysql2, instrumentPg, instrumentPrisma, instrumentRedis } from './drivers';

export type AutoTarget = 'server' | 'http' | 'fetch' | 'pg' | 'mysql2' | 'mongodb' | 'ioredis' | 'prisma' | 'better-sqlite3' | 'mysql';
Expand Down Expand Up @@ -92,6 +92,14 @@ export function autoInstrument(tp: OptimizedPerformanceMonitor, options: AutoIns
if (deps) {
const appRequire = moduleApi.createRequire(path.join(deps.dir, 'package.json'));
const load = (name: string) => appRequire(name);
// Koa: label requests by the route @koa/router / koa-router matched
if (deps.names.has('koa') && !disabled.has('server')) {
try {
restores.push(labelKoaRoutes(appRequire('koa')));
} catch {
// labels stay URL-based
}
}
// Fastify: label requests by route. The app gets the wrapped factory
// (require and import both read the module cache, filled here first).
if (deps.names.has('fastify') && !disabled.has('server')) {
Expand Down
33 changes: 32 additions & 1 deletion src/instrument.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,35 @@ export interface HttpServerInstrumentOptions extends InstrumentOptions {
*/
export const ROUTE = Symbol.for('traceperf.route');

const KOA_CONTEXT = Symbol.for('traceperf.koa.context');

/**
* Koa with @koa/router or koa-router: label requests by their route
* ("GET /users/:id"). Links each request to its Koa context, where the
* router records the matched route.
*
* labelKoaRoutes(require('koa'));
*/
export function labelKoaRoutes(Koa: any): Uninstall {
const proto = Koa && (Koa.default || Koa).prototype;
const original = proto && proto.createContext;
if (typeof original !== 'function' || original[PATCHED]) return () => undefined;
const patched = function (this: unknown, req: any, res: unknown) {
const ctx = original.call(this, req, res);
try {
if (req && typeof req === 'object') req[KOA_CONTEXT] = ctx;
} catch {
// never break the app
}
return ctx;
};
(patched as any)[PATCHED] = original;
proto.createContext = patched;
return () => {
if (proto.createContext === patched) proto.createContext = original;
};
}

/**
* Fastify (3, 4, 5): label requests by their route ("GET /api/articles/:slug").
* Wraps the fastify() factory so every instance gets an onRequest hook; returns
Expand Down Expand Up @@ -304,7 +333,9 @@ export function instrumentHttpServer(tp: OptimizedPerformanceMonitor, options: H
if (ended) return;
ended = true;
// A framework set the matched route (traceperf/auto does it for Fastify)
const marked = !labelFor ? req[ROUTE] : undefined;
// ... or the Koa context, where @koa/router puts it
const koa = req[KOA_CONTEXT];
const marked = labelFor ? undefined : req[ROUTE] || (koa && typeof koa._matchedRoute === 'string' ? koa._matchedRoute : undefined);
if (typeof marked === 'string' && marked) span.setLabel(`${req.method || 'GET'} ${marked}`);
// Express (4, 5, and Nest on it) knows the route: "/api/articles/:slug", not the slug itself
const route = !labelFor && !marked && req.route && typeof req.route.path === 'string' ? req.route.path : undefined;
Expand Down
Loading