@adjedaini/clockwork-node
v0.0.4
Published
Clockwork Node — framework-agnostic debugging, profiling & metrics for Node.js backends
Readme
@adjedaini/clockwork-node
Framework-agnostic debugging, profiling, and metrics for Node.js. One middleware: API + optional UI + request capture. Automatic capture (opt-out): console.*, uncaught errors, and optionally pg/mysql2 queries are correlated to the current request via AsyncLocalStorage.
Install
npm install @adjedaini/clockwork-nodeUse
Express
import express from 'express';
import { startClockwork } from '@adjedaini/clockwork-node';
const clockwork = startClockwork({
path: '/__clockwork',
ui: true,
autoConsole: true,
autoErrors: true,
captureRequestBody: true,
captureResponseBody: true,
});
const app = express();
app.use(express.json());
app.use(clockwork.middleware);
app.get('/api/hello', (req, res) => {
console.log('Processing hello');
res.json({ message: 'Hello' });
});
app.listen(3000);Fastify
import Fastify from 'fastify';
import { startClockwork } from '@adjedaini/clockwork-node';
const clockwork = startClockwork({ path: '/__clockwork', ui: true });
const app = Fastify();
app.addHook('preHandler', (req, reply, done) => {
clockwork.middleware(
{ method: req.method, url: req.url, originalUrl: req.url, path: req.routerPath, headers: req.headers, body: req.body, query: req.query },
{ setHeader: reply.header.bind(reply), get status() { return reply; }, statusCode: reply.statusCode, on: reply.raw.on.bind(reply.raw), end: (b) => reply.raw.end(b), send: reply.send.bind(reply), json: reply.send.bind(reply) },
done
);
});
app.get('/api/hello', (req, reply) => {
const id = req.raw.clockworkId;
if (id) clockwork.core.captureLog(id, 'info', 'Hello');
return reply.send({ message: 'Hello' });
});
app.listen({ port: 3000 });Node.js http
import { createServer } from 'http';
import { startClockwork } from '@adjedaini/clockwork-node';
const clockwork = startClockwork({ path: '/__clockwork', ui: true });
const server = createServer((req, res) => {
const reqLike = {
method: req.method ?? 'GET',
url: req.url,
originalUrl: req.url,
path: req.url?.split('?')[0],
headers: req.headers,
};
const resLike = {
setHeader: res.setHeader.bind(res),
get statusCode() { return res.statusCode; },
set statusCode(v) { res.statusCode = v; },
status(code) { res.statusCode = code; return resLike; },
on: res.on.bind(res),
end: res.end.bind(res),
send: (b) => { res.setHeader('Content-Type', 'application/json'); res.end(typeof b === 'string' ? b : JSON.stringify(b)); },
json: (b) => { res.setHeader('Content-Type', 'application/json'); res.end(JSON.stringify(b)); },
};
clockwork.middleware(reqLike, resLike, () => {
if (req.url?.startsWith('/api/')) {
const id = req.clockworkId;
if (id) clockwork.core.captureLog(id, 'info', 'Request');
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify({ message: 'Hello' }));
} else {
res.statusCode = 404;
res.end('Not found');
}
});
});
server.listen(3000);Open http://localhost:3000/__clockwork/app. UI is at {path}/app, API at {path}.
TypeScript
For typed req.clockworkId (Express / Node), add to your tsconfig.json or a .d.ts file:
{ "compilerOptions": { "types": ["@adjedaini/clockwork-node/globals"] } }Or in a .d.ts file:
/// <reference types="@adjedaini/clockwork-node/globals" />Options
| Option | Default | Description |
|--------|---------|-------------|
| path | '/__clockwork' | Base path for API and UI. UI is served at {path}/app. |
| ui | true | Enable built-in dashboard at {path}/app. |
| uiPath | (package dist) | Override UI directory. |
| plugins | — | Full plugin set. When set, autoConsole/autoErrors/autoDb/dbPlugins/autoLogPlugins are ignored. |
| autoConsole | true | Intercept console.* (used when plugins not set). |
| autoErrors | true | Capture uncaughtException / unhandledRejection (used when plugins not set). |
| autoLogPlugins | false | Intercept pino/winston when installed (used when plugins not set). |
| autoDb | false | Enable default DB plugins pg + mysql2 (used when plugins not set). |
| dbPlugins | — | DB plugins when not using full plugins (e.g. [pgPlugin]). |
| captureRequestBody | true | Capture request body. |
| captureResponseBody | true | Capture response body. |
| ignoreStartsWith | [] | Skip paths that start with these. |
| core.maxRequests | 100 | Ring buffer size. |
| core.storage | (ring buffer) | Custom storage implementing IRequestStorage. |
The instance has registerPlugin(plugin) to plug in any plugin at runtime, and restore() to undo all patches (e.g. for tests or shutdown).
Generic plugin system
All interception is done via plugins implementing ClockworkPlugin: { name: string, install(ctx: IPluginContext) => () => void }. Context provides ctx.core and ctx.getRequestId().
- Context plugin (
contextPlugin) — provides AsyncLocalStorage and request correlation. Must run first when using defaults. - Console plugin (
consolePlugin) — interceptsconsole.*(native log). - Process errors plugin (
processErrorsPlugin) — captures uncaughtException / unhandledRejection. - Log plugins (
pinoPlugin,winstonPlugin) — intercept pino/winston when installed. Enable withautoLogPlugins: trueor add toplugins. - DB plugins (
pgPlugin,mysql2Plugin) — intercept pg/mysql2 queries. Enable withautoDb: trueordbPlugins: [pgPlugin].
Use the full plugin list or the default builder:
import {
startClockwork,
contextPlugin,
consolePlugin,
processErrorsPlugin,
pinoPlugin,
winstonPlugin,
pgPlugin,
mysql2Plugin,
getDefaultPlugins,
} from '@adjedaini/clockwork-node';
// Option 1: defaults (context + console + processErrors; optional DB / log libs)
const clockwork = startClockwork({
autoConsole: true,
autoErrors: true,
autoLogPlugins: true,
autoDb: true,
});
// Option 2: custom plugin set (you must include contextPlugin first for request correlation)
const clockwork = startClockwork({
plugins: [
contextPlugin,
consolePlugin,
processErrorsPlugin,
pinoPlugin,
pgPlugin,
],
});
// Option 3: build defaults then add more
const plugins = getDefaultPlugins({
console: true,
errors: true,
logPlugins: true,
db: true,
dbPlugins: [pgPlugin],
});
const clockwork = startClockwork({ plugins });
// Option 4: plug in at runtime
const clockwork = startClockwork();
clockwork.registerPlugin(pinoPlugin);
clockwork.registerPlugin(pgPlugin);API: GET {path}, GET {path}/:id, GET {path}/latest, GET {path}/metrics.
Build output: The published package has dist/index.js, dist/index.d.ts, and dist/public/ (index.html + assets). Serve dist/public at {path}/app or rely on the built-in middleware.
Dashboard
The built-in UI at {path}/app is aligned with Clockwork (PHP): requests list, request detail with Request, Log, Database, and Timeline tabs. Override the base path with the path option (default /__clockwork).
- Build:
npm run build→ packages → UI → root bundle → copy UI todist/public. - Packages: Plugins live under
packages/:clockwork-plugins(context, console, process-errors, getDefaultPlugins),clockwork-log-interceptor(pino, winston),clockwork-db-interceptor(pg, mysql2). Root depends onclockwork-plugins, which aggregates the others. - Scripts:
build,dev:example,dev:ui,test,clean,version(changesets),release.
License
MIT
