perfstack 0.2.0

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/index.js ADDED
@@ -0,0 +1,457 @@
1
+ // src/histogram.ts
2
+ var Histogram = class {
3
+ samples = [];
4
+ max;
5
+ constructor(max = 1e3) {
6
+ this.max = max;
7
+ }
8
+ record(value) {
9
+ this.samples.push(value);
10
+ if (this.samples.length > this.max) {
11
+ this.samples.shift();
12
+ }
13
+ }
14
+ summary() {
15
+ const count = this.samples.length;
16
+ if (count === 0) {
17
+ return { count: 0, min: 0, max: 0, avg: 0, p50: 0, p90: 0, p95: 0, p99: 0 };
18
+ }
19
+ const sorted = [...this.samples].sort((a, b) => a - b);
20
+ const sum = sorted.reduce((s, v) => s + v, 0);
21
+ return {
22
+ count,
23
+ min: sorted[0],
24
+ max: sorted[sorted.length - 1],
25
+ avg: round(sum / count),
26
+ p50: percentile(sorted, 0.5),
27
+ p90: percentile(sorted, 0.9),
28
+ p95: percentile(sorted, 0.95),
29
+ p99: percentile(sorted, 0.99)
30
+ };
31
+ }
32
+ reset() {
33
+ this.samples = [];
34
+ }
35
+ };
36
+ function percentile(sortedAscending, p) {
37
+ if (sortedAscending.length === 0) return 0;
38
+ const idx = Math.min(sortedAscending.length - 1, Math.floor(p * sortedAscending.length));
39
+ return round(sortedAscending[idx]);
40
+ }
41
+ function round(n) {
42
+ return Math.round(n * 100) / 100;
43
+ }
44
+
45
+ // src/tracer.ts
46
+ import { AsyncLocalStorage } from "async_hooks";
47
+ import { randomBytes } from "crypto";
48
+ var Tracer = class {
49
+ storage = new AsyncLocalStorage();
50
+ spans = [];
51
+ max;
52
+ constructor(options = {}) {
53
+ this.max = options.maxSpans ?? 500;
54
+ }
55
+ start(name, kind = "custom", metadata = {}) {
56
+ const startedAt = Date.now();
57
+ const id = `s_${randomBytes(6).toString("hex")}`;
58
+ const parent = this.storage.getStore();
59
+ const traceId = parent?.traceId ?? `t_${randomBytes(8).toString("hex")}`;
60
+ const parentId = parent?.spanId;
61
+ return {
62
+ end: (info = {}) => {
63
+ const status = info.error ? "error" : info.status ?? "ok";
64
+ const errorMsg = info.error instanceof Error ? info.error.message : info.error ? String(info.error) : void 0;
65
+ const span = {
66
+ id,
67
+ parentId,
68
+ traceId,
69
+ name,
70
+ kind,
71
+ startedAt,
72
+ durationMs: Date.now() - startedAt,
73
+ metadata: { ...metadata, ...info.extra ?? {} },
74
+ status,
75
+ error: errorMsg
76
+ };
77
+ this.push(span);
78
+ return span;
79
+ }
80
+ };
81
+ }
82
+ async withSpan(name, fn, options = {}) {
83
+ const startedAt = Date.now();
84
+ const parent = this.storage.getStore();
85
+ const traceId = parent?.traceId ?? `t_${randomBytes(8).toString("hex")}`;
86
+ const spanId = `s_${randomBytes(6).toString("hex")}`;
87
+ return this.storage.run({ traceId, spanId }, async () => {
88
+ try {
89
+ const result = await fn();
90
+ this.push({
91
+ id: spanId,
92
+ parentId: parent?.spanId,
93
+ traceId,
94
+ name,
95
+ kind: options.kind ?? "custom",
96
+ startedAt,
97
+ durationMs: Date.now() - startedAt,
98
+ metadata: options.metadata ?? {},
99
+ status: "ok"
100
+ });
101
+ return result;
102
+ } catch (err) {
103
+ this.push({
104
+ id: spanId,
105
+ parentId: parent?.spanId,
106
+ traceId,
107
+ name,
108
+ kind: options.kind ?? "custom",
109
+ startedAt,
110
+ durationMs: Date.now() - startedAt,
111
+ metadata: options.metadata ?? {},
112
+ status: "error",
113
+ error: err instanceof Error ? err.message : String(err)
114
+ });
115
+ throw err;
116
+ }
117
+ });
118
+ }
119
+ runInContext(traceId, fn) {
120
+ const spanId = `s_${randomBytes(6).toString("hex")}`;
121
+ return this.storage.run({ traceId, spanId }, fn);
122
+ }
123
+ currentTraceId() {
124
+ return this.storage.getStore()?.traceId;
125
+ }
126
+ recent(limit = 100) {
127
+ return this.spans.slice(-limit);
128
+ }
129
+ byTrace(traceId) {
130
+ return this.spans.filter((s) => s.traceId === traceId);
131
+ }
132
+ reset() {
133
+ this.spans = [];
134
+ }
135
+ push(span) {
136
+ this.spans.push(span);
137
+ if (this.spans.length > this.max) this.spans.shift();
138
+ }
139
+ };
140
+
141
+ // src/profiler.ts
142
+ var Profiler = class {
143
+ startedAt;
144
+ httpHistogram = new Histogram();
145
+ queryHistogram = new Histogram();
146
+ routes = /* @__PURE__ */ new Map();
147
+ slowestQueries = [];
148
+ memorySamples = [];
149
+ peakRss = 0;
150
+ peakHeapUsed = 0;
151
+ totalRequests = 0;
152
+ totalErrors = 0;
153
+ totalQueries = 0;
154
+ memoryInterval;
155
+ tracer;
156
+ opts;
157
+ constructor(options = {}) {
158
+ this.startedAt = options.startedAt ?? Date.now();
159
+ this.opts = {
160
+ slowRequestThreshold: options.slowRequestThreshold ?? 500,
161
+ slowQueryThreshold: options.slowQueryThreshold ?? 200,
162
+ memorySampleIntervalMs: options.memorySampleIntervalMs ?? 5e3,
163
+ memorySamplesMax: options.memorySamplesMax ?? 120,
164
+ routeBucketsMax: options.routeBucketsMax ?? 200,
165
+ enableMemorySampling: options.enableMemorySampling ?? false
166
+ };
167
+ this.tracer = new Tracer();
168
+ if (this.opts.enableMemorySampling) this.startMemorySampling();
169
+ }
170
+ recordHttp(record) {
171
+ this.totalRequests += 1;
172
+ if (record.statusCode >= 500) this.totalErrors += 1;
173
+ this.httpHistogram.record(record.durationMs);
174
+ const key = `${record.method} ${record.path}`;
175
+ let bucket = this.routes.get(key);
176
+ if (!bucket) {
177
+ if (this.routes.size >= this.opts.routeBucketsMax) {
178
+ const oldest = this.routes.keys().next().value;
179
+ if (oldest !== void 0) this.routes.delete(oldest);
180
+ }
181
+ bucket = { method: record.method, path: record.path, histogram: new Histogram(), errors: 0 };
182
+ this.routes.set(key, bucket);
183
+ }
184
+ bucket.histogram.record(record.durationMs);
185
+ if (record.statusCode >= 500) bucket.errors += 1;
186
+ }
187
+ recordQuery(record) {
188
+ this.totalQueries += 1;
189
+ this.queryHistogram.record(record.durationMs);
190
+ if (record.durationMs >= this.opts.slowQueryThreshold) {
191
+ this.slowestQueries.push(record);
192
+ this.slowestQueries.sort((a, b) => b.durationMs - a.durationMs);
193
+ if (this.slowestQueries.length > 50) this.slowestQueries.length = 50;
194
+ }
195
+ }
196
+ takeMemorySample() {
197
+ const m = process.memoryUsage();
198
+ const snap = {
199
+ takenAt: Date.now(),
200
+ rss: m.rss,
201
+ heapTotal: m.heapTotal,
202
+ heapUsed: m.heapUsed,
203
+ external: m.external,
204
+ arrayBuffers: m.arrayBuffers
205
+ };
206
+ this.memorySamples.push(snap);
207
+ if (this.memorySamples.length > this.opts.memorySamplesMax) this.memorySamples.shift();
208
+ if (snap.rss > this.peakRss) this.peakRss = snap.rss;
209
+ if (snap.heapUsed > this.peakHeapUsed) this.peakHeapUsed = snap.heapUsed;
210
+ return snap;
211
+ }
212
+ startMemorySampling() {
213
+ if (this.memoryInterval) return;
214
+ this.takeMemorySample();
215
+ const interval = setInterval(() => this.takeMemorySample(), this.opts.memorySampleIntervalMs);
216
+ interval.unref?.();
217
+ this.memoryInterval = interval;
218
+ }
219
+ stopMemorySampling() {
220
+ if (this.memoryInterval) {
221
+ clearInterval(this.memoryInterval);
222
+ this.memoryInterval = void 0;
223
+ }
224
+ }
225
+ report() {
226
+ if (this.memorySamples.length === 0) this.takeMemorySample();
227
+ const routes = Array.from(this.routes.values()).map((bucket) => ({
228
+ method: bucket.method,
229
+ path: bucket.path,
230
+ errorCount: bucket.errors,
231
+ ...bucket.histogram.summary()
232
+ }));
233
+ const slowestRoutes = [...routes].sort((a, b) => b.p95 - a.p95).slice(0, 10);
234
+ return {
235
+ uptimeMs: Date.now() - this.startedAt,
236
+ startedAt: this.startedAt,
237
+ takenAt: Date.now(),
238
+ totalRequests: this.totalRequests,
239
+ totalErrors: this.totalErrors,
240
+ totalQueries: this.totalQueries,
241
+ http: this.httpHistogram.summary(),
242
+ routes,
243
+ slowestRoutes,
244
+ queries: this.queryHistogram.summary(),
245
+ slowestQueries: [...this.slowestQueries],
246
+ memory: {
247
+ current: this.memorySamples.at(-1) ?? null,
248
+ samples: [...this.memorySamples],
249
+ peakRss: this.peakRss,
250
+ peakHeapUsed: this.peakHeapUsed
251
+ }
252
+ };
253
+ }
254
+ reset() {
255
+ this.httpHistogram.reset();
256
+ this.queryHistogram.reset();
257
+ this.routes.clear();
258
+ this.slowestQueries = [];
259
+ this.memorySamples = [];
260
+ this.peakRss = 0;
261
+ this.peakHeapUsed = 0;
262
+ this.totalRequests = 0;
263
+ this.totalErrors = 0;
264
+ this.totalQueries = 0;
265
+ this.tracer.reset();
266
+ }
267
+ isSlowRequest(durationMs) {
268
+ return durationMs >= this.opts.slowRequestThreshold;
269
+ }
270
+ };
271
+
272
+ // src/express.ts
273
+ import { randomBytes as randomBytes2 } from "crypto";
274
+ function expressMiddleware(profiler, options = {}) {
275
+ const { ignorePaths = [], traceHeader = "x-trace-id", exposeTraceHeader = true } = options;
276
+ return function perfstackMiddleware(req, res, next) {
277
+ const path = req.originalUrl ?? req.url ?? "/";
278
+ if (ignorePaths.some((p) => path === p || path.startsWith(`${p}/`))) {
279
+ next();
280
+ return;
281
+ }
282
+ const traceHeaderVal = req.headers[traceHeader.toLowerCase()];
283
+ const traceId = typeof traceHeaderVal === "string" && traceHeaderVal || Array.isArray(traceHeaderVal) && traceHeaderVal[0] || `req_${randomBytes2(8).toString("hex")}`;
284
+ if (exposeTraceHeader) res.setHeader(traceHeader, traceId);
285
+ const startedAt = Date.now();
286
+ profiler.tracer.runInContext(traceId, () => {
287
+ res.on("finish", () => {
288
+ profiler.recordHttp({
289
+ method: req.method ?? "GET",
290
+ path: req.route?.path ?? simplifyPath(path),
291
+ statusCode: res.statusCode,
292
+ durationMs: Date.now() - startedAt,
293
+ startedAt,
294
+ traceId
295
+ });
296
+ });
297
+ next();
298
+ });
299
+ };
300
+ }
301
+ function dashboardMiddleware(profiler) {
302
+ return function perfstackDashboard(req, res) {
303
+ const path = req.originalUrl ?? req.url ?? "/";
304
+ if (path.endsWith(".json")) {
305
+ res.setHeader("content-type", "application/json");
306
+ res.statusCode = 200;
307
+ res.end(JSON.stringify(profiler.report(), null, 2));
308
+ return;
309
+ }
310
+ res.setHeader("content-type", "text/html; charset=utf-8");
311
+ res.statusCode = 200;
312
+ res.end(renderDashboard(profiler));
313
+ };
314
+ }
315
+ function simplifyPath(p) {
316
+ return p.split("?")[0].replace(/\/[0-9a-f]{24}\b/gi, "/:id").replace(/\/\d+\b/g, "/:id");
317
+ }
318
+ function renderDashboard(profiler) {
319
+ const report = profiler.report();
320
+ return `<!doctype html>
321
+ <html><head><meta charset="utf-8" />
322
+ <title>perfstack</title>
323
+ <style>
324
+ body{font:14px/1.4 ui-monospace,Menlo,monospace;background:#0b1020;color:#e6e6e6;margin:0;padding:24px}
325
+ h1,h2{font-weight:600;margin:0 0 12px}h2{margin-top:24px}
326
+ table{border-collapse:collapse;width:100%;margin-top:8px}
327
+ th,td{padding:6px 10px;text-align:left;border-bottom:1px solid #1d2440}
328
+ th{color:#8aa6ff;font-weight:600}
329
+ .bad{color:#ff8a8a}.good{color:#8aff9c}
330
+ .card{background:#121838;border-radius:8px;padding:16px;margin-bottom:16px}
331
+ .grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}
332
+ .k{color:#8a93b3}.v{font-size:20px;font-weight:600}
333
+ </style></head>
334
+ <body>
335
+ <h1>perfstack</h1>
336
+ <div class="grid">
337
+ <div class="card"><div class="k">uptime</div><div class="v">${Math.round(report.uptimeMs / 1e3)}s</div></div>
338
+ <div class="card"><div class="k">requests</div><div class="v">${report.totalRequests}</div></div>
339
+ <div class="card"><div class="k">errors</div><div class="v ${report.totalErrors ? "bad" : "good"}">${report.totalErrors}</div></div>
340
+ <div class="card"><div class="k">queries</div><div class="v">${report.totalQueries}</div></div>
341
+ </div>
342
+
343
+ <h2>HTTP latency (ms)</h2>
344
+ <table><tr><th>count</th><th>min</th><th>avg</th><th>p50</th><th>p90</th><th>p95</th><th>p99</th><th>max</th></tr>
345
+ <tr><td>${report.http.count}</td><td>${report.http.min}</td><td>${report.http.avg}</td><td>${report.http.p50}</td><td>${report.http.p90}</td><td>${report.http.p95}</td><td>${report.http.p99}</td><td>${report.http.max}</td></tr>
346
+ </table>
347
+
348
+ <h2>Slowest routes (p95)</h2>
349
+ <table><tr><th>route</th><th>count</th><th>p95</th><th>p99</th><th>max</th><th>errors</th></tr>
350
+ ${report.slowestRoutes.map((r) => `<tr><td>${escapeHtml(r.method)} ${escapeHtml(r.path)}</td><td>${r.count}</td><td>${r.p95}</td><td>${r.p99}</td><td>${r.max}</td><td>${r.errorCount}</td></tr>`).join("\n")}
351
+ </table>
352
+
353
+ <h2>Slowest queries</h2>
354
+ <table><tr><th>collection</th><th>op</th><th>duration</th></tr>
355
+ ${report.slowestQueries.map((q) => `<tr><td>${escapeHtml(q.collection)}</td><td>${escapeHtml(q.op)}</td><td>${q.durationMs}ms</td></tr>`).join("\n")}
356
+ </table>
357
+
358
+ <h2>Memory</h2>
359
+ <table><tr><th>rss</th><th>heap used</th><th>heap total</th><th>peak rss</th><th>peak heap</th></tr>
360
+ <tr>
361
+ <td>${fmtBytes(report.memory.current?.rss ?? 0)}</td>
362
+ <td>${fmtBytes(report.memory.current?.heapUsed ?? 0)}</td>
363
+ <td>${fmtBytes(report.memory.current?.heapTotal ?? 0)}</td>
364
+ <td>${fmtBytes(report.memory.peakRss)}</td>
365
+ <td>${fmtBytes(report.memory.peakHeapUsed)}</td>
366
+ </tr>
367
+ </table>
368
+
369
+ </body></html>`;
370
+ }
371
+ function fmtBytes(n) {
372
+ if (n < 1024) return `${n} B`;
373
+ if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
374
+ if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;
375
+ return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;
376
+ }
377
+ function escapeHtml(s) {
378
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
379
+ }
380
+
381
+ // src/mongoose.ts
382
+ var HOOKS = [
383
+ "find",
384
+ "findOne",
385
+ "findOneAndUpdate",
386
+ "findOneAndDelete",
387
+ "count",
388
+ "countDocuments",
389
+ "updateOne",
390
+ "updateMany",
391
+ "deleteOne",
392
+ "deleteMany",
393
+ "distinct"
394
+ ];
395
+ function mongoosePlugin(profiler) {
396
+ return function perfstackMongoose(schema) {
397
+ for (const hook of HOOKS) {
398
+ schema.pre(hook, function() {
399
+ this._perfstackStart = Date.now();
400
+ this._perfstackOp = hook;
401
+ this._perfstackCollection = this.model?.collection?.name ?? this.model?.modelName ?? "unknown";
402
+ });
403
+ schema.post(hook, function() {
404
+ const startedAt = this._perfstackStart ?? Date.now();
405
+ profiler.recordQuery({
406
+ collection: this._perfstackCollection ?? "unknown",
407
+ op: this._perfstackOp ?? hook,
408
+ durationMs: Date.now() - startedAt,
409
+ startedAt,
410
+ traceId: profiler.tracer.currentTraceId()
411
+ });
412
+ });
413
+ }
414
+ schema.pre("aggregate", function() {
415
+ this._perfstackStart = Date.now();
416
+ const model = typeof this.model === "function" ? this.model() : void 0;
417
+ this._perfstackCollection = model?.collection?.name ?? model?.modelName ?? "unknown";
418
+ });
419
+ schema.post("aggregate", function() {
420
+ const startedAt = this._perfstackStart ?? Date.now();
421
+ profiler.recordQuery({
422
+ collection: this._perfstackCollection ?? "unknown",
423
+ op: "aggregate",
424
+ durationMs: Date.now() - startedAt,
425
+ startedAt,
426
+ traceId: profiler.tracer.currentTraceId()
427
+ });
428
+ });
429
+ };
430
+ }
431
+
432
+ // src/index.ts
433
+ function init(app, options = {}) {
434
+ const profiler = new Profiler(options);
435
+ const middleware = expressMiddleware(profiler);
436
+ const dashboard = dashboardMiddleware(profiler);
437
+ const plugin = mongoosePlugin(profiler);
438
+ if (app) {
439
+ app.use(middleware);
440
+ if (typeof app.get === "function") {
441
+ app.get(options.dashboardPath ?? "/__perf", dashboard);
442
+ app.get((options.dashboardPath ?? "/__perf") + ".json", dashboard);
443
+ }
444
+ }
445
+ return { profiler, middleware, dashboard, mongoosePlugin: plugin };
446
+ }
447
+ export {
448
+ Histogram,
449
+ Profiler,
450
+ Tracer,
451
+ dashboardMiddleware,
452
+ expressMiddleware,
453
+ init,
454
+ mongoosePlugin,
455
+ percentile
456
+ };
457
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/histogram.ts","../src/tracer.ts","../src/profiler.ts","../src/express.ts","../src/mongoose.ts","../src/index.ts"],"sourcesContent":["import type { HistogramSummary } from \"./types.js\";\n\nexport class Histogram {\n private samples: number[] = [];\n private max: number;\n\n constructor(max = 1000) {\n this.max = max;\n }\n\n record(value: number): void {\n this.samples.push(value);\n if (this.samples.length > this.max) {\n this.samples.shift();\n }\n }\n\n summary(): HistogramSummary {\n const count = this.samples.length;\n if (count === 0) {\n return { count: 0, min: 0, max: 0, avg: 0, p50: 0, p90: 0, p95: 0, p99: 0 };\n }\n const sorted = [...this.samples].sort((a, b) => a - b);\n const sum = sorted.reduce((s, v) => s + v, 0);\n return {\n count,\n min: sorted[0]!,\n max: sorted[sorted.length - 1]!,\n avg: round(sum / count),\n p50: percentile(sorted, 0.5),\n p90: percentile(sorted, 0.9),\n p95: percentile(sorted, 0.95),\n p99: percentile(sorted, 0.99),\n };\n }\n\n reset(): void {\n this.samples = [];\n }\n}\n\nexport function percentile(sortedAscending: number[], p: number): number {\n if (sortedAscending.length === 0) return 0;\n const idx = Math.min(sortedAscending.length - 1, Math.floor(p * sortedAscending.length));\n return round(sortedAscending[idx]!);\n}\n\nfunction round(n: number): number {\n return Math.round(n * 100) / 100;\n}\n","import { AsyncLocalStorage } from \"node:async_hooks\";\nimport { randomBytes } from \"node:crypto\";\nimport type { Span } from \"./types.js\";\n\ninterface TraceContext {\n traceId: string;\n spanId: string;\n}\n\nexport class Tracer {\n private storage = new AsyncLocalStorage<TraceContext>();\n private spans: Span[] = [];\n private max: number;\n\n constructor(options: { maxSpans?: number } = {}) {\n this.max = options.maxSpans ?? 500;\n }\n\n start(name: string, kind: Span[\"kind\"] = \"custom\", metadata: Record<string, unknown> = {}): {\n end: (info?: { error?: unknown; status?: Span[\"status\"]; extra?: Record<string, unknown> }) => Span;\n } {\n const startedAt = Date.now();\n const id = `s_${randomBytes(6).toString(\"hex\")}`;\n const parent = this.storage.getStore();\n const traceId = parent?.traceId ?? `t_${randomBytes(8).toString(\"hex\")}`;\n const parentId = parent?.spanId;\n\n return {\n end: (info = {}) => {\n const status: Span[\"status\"] = info.error ? \"error\" : info.status ?? \"ok\";\n const errorMsg = info.error instanceof Error ? info.error.message : info.error ? String(info.error) : undefined;\n const span: Span = {\n id,\n parentId,\n traceId,\n name,\n kind,\n startedAt,\n durationMs: Date.now() - startedAt,\n metadata: { ...metadata, ...(info.extra ?? {}) },\n status,\n error: errorMsg,\n };\n this.push(span);\n return span;\n },\n };\n }\n\n async withSpan<T>(\n name: string,\n fn: () => Promise<T>,\n options: { kind?: Span[\"kind\"]; metadata?: Record<string, unknown> } = {},\n ): Promise<T> {\n const startedAt = Date.now();\n const parent = this.storage.getStore();\n const traceId = parent?.traceId ?? `t_${randomBytes(8).toString(\"hex\")}`;\n const spanId = `s_${randomBytes(6).toString(\"hex\")}`;\n return this.storage.run({ traceId, spanId }, async () => {\n try {\n const result = await fn();\n this.push({\n id: spanId,\n parentId: parent?.spanId,\n traceId,\n name,\n kind: options.kind ?? \"custom\",\n startedAt,\n durationMs: Date.now() - startedAt,\n metadata: options.metadata ?? {},\n status: \"ok\",\n });\n return result;\n } catch (err) {\n this.push({\n id: spanId,\n parentId: parent?.spanId,\n traceId,\n name,\n kind: options.kind ?? \"custom\",\n startedAt,\n durationMs: Date.now() - startedAt,\n metadata: options.metadata ?? {},\n status: \"error\",\n error: err instanceof Error ? err.message : String(err),\n });\n throw err;\n }\n });\n }\n\n runInContext<T>(traceId: string, fn: () => T): T {\n const spanId = `s_${randomBytes(6).toString(\"hex\")}`;\n return this.storage.run({ traceId, spanId }, fn);\n }\n\n currentTraceId(): string | undefined {\n return this.storage.getStore()?.traceId;\n }\n\n recent(limit = 100): Span[] {\n return this.spans.slice(-limit);\n }\n\n byTrace(traceId: string): Span[] {\n return this.spans.filter((s) => s.traceId === traceId);\n }\n\n reset(): void {\n this.spans = [];\n }\n\n private push(span: Span): void {\n this.spans.push(span);\n if (this.spans.length > this.max) this.spans.shift();\n }\n}\n","import { Histogram } from \"./histogram.js\";\nimport { Tracer } from \"./tracer.js\";\nimport type {\n HttpRecord,\n MemorySnapshot,\n PerfReport,\n PerfstackOptions,\n QueryRecord,\n RouteSummary,\n} from \"./types.js\";\n\ninterface RouteBucket {\n method: string;\n path: string;\n histogram: Histogram;\n errors: number;\n}\n\nexport class Profiler {\n private startedAt: number;\n private httpHistogram = new Histogram();\n private queryHistogram = new Histogram();\n private routes = new Map<string, RouteBucket>();\n private slowestQueries: QueryRecord[] = [];\n private memorySamples: MemorySnapshot[] = [];\n private peakRss = 0;\n private peakHeapUsed = 0;\n private totalRequests = 0;\n private totalErrors = 0;\n private totalQueries = 0;\n private memoryInterval?: ReturnType<typeof setInterval>;\n readonly tracer: Tracer;\n private opts: Required<Pick<\n PerfstackOptions,\n | \"slowRequestThreshold\"\n | \"slowQueryThreshold\"\n | \"memorySampleIntervalMs\"\n | \"memorySamplesMax\"\n | \"routeBucketsMax\"\n | \"enableMemorySampling\"\n >>;\n\n constructor(options: PerfstackOptions = {}) {\n this.startedAt = options.startedAt ?? Date.now();\n this.opts = {\n slowRequestThreshold: options.slowRequestThreshold ?? 500,\n slowQueryThreshold: options.slowQueryThreshold ?? 200,\n memorySampleIntervalMs: options.memorySampleIntervalMs ?? 5_000,\n memorySamplesMax: options.memorySamplesMax ?? 120,\n routeBucketsMax: options.routeBucketsMax ?? 200,\n enableMemorySampling: options.enableMemorySampling ?? false,\n };\n this.tracer = new Tracer();\n if (this.opts.enableMemorySampling) this.startMemorySampling();\n }\n\n recordHttp(record: HttpRecord): void {\n this.totalRequests += 1;\n if (record.statusCode >= 500) this.totalErrors += 1;\n this.httpHistogram.record(record.durationMs);\n const key = `${record.method} ${record.path}`;\n let bucket = this.routes.get(key);\n if (!bucket) {\n if (this.routes.size >= this.opts.routeBucketsMax) {\n const oldest = this.routes.keys().next().value;\n if (oldest !== undefined) this.routes.delete(oldest);\n }\n bucket = { method: record.method, path: record.path, histogram: new Histogram(), errors: 0 };\n this.routes.set(key, bucket);\n }\n bucket.histogram.record(record.durationMs);\n if (record.statusCode >= 500) bucket.errors += 1;\n }\n\n recordQuery(record: QueryRecord): void {\n this.totalQueries += 1;\n this.queryHistogram.record(record.durationMs);\n if (record.durationMs >= this.opts.slowQueryThreshold) {\n this.slowestQueries.push(record);\n this.slowestQueries.sort((a, b) => b.durationMs - a.durationMs);\n if (this.slowestQueries.length > 50) this.slowestQueries.length = 50;\n }\n }\n\n takeMemorySample(): MemorySnapshot {\n const m = process.memoryUsage();\n const snap: MemorySnapshot = {\n takenAt: Date.now(),\n rss: m.rss,\n heapTotal: m.heapTotal,\n heapUsed: m.heapUsed,\n external: m.external,\n arrayBuffers: m.arrayBuffers,\n };\n this.memorySamples.push(snap);\n if (this.memorySamples.length > this.opts.memorySamplesMax) this.memorySamples.shift();\n if (snap.rss > this.peakRss) this.peakRss = snap.rss;\n if (snap.heapUsed > this.peakHeapUsed) this.peakHeapUsed = snap.heapUsed;\n return snap;\n }\n\n startMemorySampling(): void {\n if (this.memoryInterval) return;\n this.takeMemorySample();\n const interval = setInterval(() => this.takeMemorySample(), this.opts.memorySampleIntervalMs);\n (interval as { unref?: () => void }).unref?.();\n this.memoryInterval = interval;\n }\n\n stopMemorySampling(): void {\n if (this.memoryInterval) {\n clearInterval(this.memoryInterval);\n this.memoryInterval = undefined;\n }\n }\n\n report(): PerfReport {\n if (this.memorySamples.length === 0) this.takeMemorySample();\n const routes: RouteSummary[] = Array.from(this.routes.values()).map((bucket) => ({\n method: bucket.method,\n path: bucket.path,\n errorCount: bucket.errors,\n ...bucket.histogram.summary(),\n }));\n const slowestRoutes = [...routes].sort((a, b) => b.p95 - a.p95).slice(0, 10);\n return {\n uptimeMs: Date.now() - this.startedAt,\n startedAt: this.startedAt,\n takenAt: Date.now(),\n totalRequests: this.totalRequests,\n totalErrors: this.totalErrors,\n totalQueries: this.totalQueries,\n http: this.httpHistogram.summary(),\n routes,\n slowestRoutes,\n queries: this.queryHistogram.summary(),\n slowestQueries: [...this.slowestQueries],\n memory: {\n current: this.memorySamples.at(-1) ?? null,\n samples: [...this.memorySamples],\n peakRss: this.peakRss,\n peakHeapUsed: this.peakHeapUsed,\n },\n };\n }\n\n reset(): void {\n this.httpHistogram.reset();\n this.queryHistogram.reset();\n this.routes.clear();\n this.slowestQueries = [];\n this.memorySamples = [];\n this.peakRss = 0;\n this.peakHeapUsed = 0;\n this.totalRequests = 0;\n this.totalErrors = 0;\n this.totalQueries = 0;\n this.tracer.reset();\n }\n\n isSlowRequest(durationMs: number): boolean {\n return durationMs >= this.opts.slowRequestThreshold;\n }\n}\n","import { randomBytes } from \"node:crypto\";\nimport type { Profiler } from \"./profiler.js\";\n\ntype ReqLike = {\n method?: string;\n originalUrl?: string;\n url?: string;\n route?: { path?: string };\n headers: Record<string, string | string[] | undefined>;\n};\ntype ResLike = {\n statusCode: number;\n on(event: \"finish\", cb: () => void): void;\n setHeader(name: string, value: string): void;\n};\ntype NextLike = () => void;\n\nexport interface ExpressMiddlewareOptions {\n ignorePaths?: string[];\n traceHeader?: string;\n exposeTraceHeader?: boolean;\n}\n\nexport function expressMiddleware(\n profiler: Profiler,\n options: ExpressMiddlewareOptions = {},\n): (req: ReqLike, res: ResLike, next: NextLike) => void {\n const { ignorePaths = [], traceHeader = \"x-trace-id\", exposeTraceHeader = true } = options;\n return function perfstackMiddleware(req, res, next) {\n const path = req.originalUrl ?? req.url ?? \"/\";\n if (ignorePaths.some((p) => path === p || path.startsWith(`${p}/`))) {\n next();\n return;\n }\n\n const traceHeaderVal = req.headers[traceHeader.toLowerCase()];\n const traceId =\n (typeof traceHeaderVal === \"string\" && traceHeaderVal) ||\n (Array.isArray(traceHeaderVal) && traceHeaderVal[0]) ||\n `req_${randomBytes(8).toString(\"hex\")}`;\n\n if (exposeTraceHeader) res.setHeader(traceHeader, traceId);\n\n const startedAt = Date.now();\n\n profiler.tracer.runInContext(traceId, () => {\n res.on(\"finish\", () => {\n profiler.recordHttp({\n method: req.method ?? \"GET\",\n path: req.route?.path ?? simplifyPath(path),\n statusCode: res.statusCode,\n durationMs: Date.now() - startedAt,\n startedAt,\n traceId,\n });\n });\n next();\n });\n };\n}\n\nexport function dashboardMiddleware(\n profiler: Profiler,\n): (req: ReqLike, res: ResLikeWithSend, next: NextLike) => void {\n return function perfstackDashboard(req, res) {\n const path = req.originalUrl ?? req.url ?? \"/\";\n if (path.endsWith(\".json\")) {\n res.setHeader(\"content-type\", \"application/json\");\n res.statusCode = 200;\n res.end(JSON.stringify(profiler.report(), null, 2));\n return;\n }\n res.setHeader(\"content-type\", \"text/html; charset=utf-8\");\n res.statusCode = 200;\n res.end(renderDashboard(profiler));\n };\n}\n\ntype ResLikeWithSend = ResLike & {\n statusCode: number;\n end(payload?: string): void;\n};\n\nfunction simplifyPath(p: string): string {\n return p\n .split(\"?\")[0]!\n .replace(/\\/[0-9a-f]{24}\\b/gi, \"/:id\")\n .replace(/\\/\\d+\\b/g, \"/:id\");\n}\n\nfunction renderDashboard(profiler: Profiler): string {\n const report = profiler.report();\n return `<!doctype html>\n<html><head><meta charset=\"utf-8\" />\n<title>perfstack</title>\n<style>\nbody{font:14px/1.4 ui-monospace,Menlo,monospace;background:#0b1020;color:#e6e6e6;margin:0;padding:24px}\nh1,h2{font-weight:600;margin:0 0 12px}h2{margin-top:24px}\ntable{border-collapse:collapse;width:100%;margin-top:8px}\nth,td{padding:6px 10px;text-align:left;border-bottom:1px solid #1d2440}\nth{color:#8aa6ff;font-weight:600}\n.bad{color:#ff8a8a}.good{color:#8aff9c}\n.card{background:#121838;border-radius:8px;padding:16px;margin-bottom:16px}\n.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:16px}\n.k{color:#8a93b3}.v{font-size:20px;font-weight:600}\n</style></head>\n<body>\n<h1>perfstack</h1>\n<div class=\"grid\">\n <div class=\"card\"><div class=\"k\">uptime</div><div class=\"v\">${Math.round(report.uptimeMs / 1000)}s</div></div>\n <div class=\"card\"><div class=\"k\">requests</div><div class=\"v\">${report.totalRequests}</div></div>\n <div class=\"card\"><div class=\"k\">errors</div><div class=\"v ${report.totalErrors ? \"bad\" : \"good\"}\">${report.totalErrors}</div></div>\n <div class=\"card\"><div class=\"k\">queries</div><div class=\"v\">${report.totalQueries}</div></div>\n</div>\n\n<h2>HTTP latency (ms)</h2>\n<table><tr><th>count</th><th>min</th><th>avg</th><th>p50</th><th>p90</th><th>p95</th><th>p99</th><th>max</th></tr>\n<tr><td>${report.http.count}</td><td>${report.http.min}</td><td>${report.http.avg}</td><td>${report.http.p50}</td><td>${report.http.p90}</td><td>${report.http.p95}</td><td>${report.http.p99}</td><td>${report.http.max}</td></tr>\n</table>\n\n<h2>Slowest routes (p95)</h2>\n<table><tr><th>route</th><th>count</th><th>p95</th><th>p99</th><th>max</th><th>errors</th></tr>\n${report.slowestRoutes\n .map((r) => `<tr><td>${escapeHtml(r.method)} ${escapeHtml(r.path)}</td><td>${r.count}</td><td>${r.p95}</td><td>${r.p99}</td><td>${r.max}</td><td>${r.errorCount}</td></tr>`)\n .join(\"\\n\")}\n</table>\n\n<h2>Slowest queries</h2>\n<table><tr><th>collection</th><th>op</th><th>duration</th></tr>\n${report.slowestQueries\n .map((q) => `<tr><td>${escapeHtml(q.collection)}</td><td>${escapeHtml(q.op)}</td><td>${q.durationMs}ms</td></tr>`)\n .join(\"\\n\")}\n</table>\n\n<h2>Memory</h2>\n<table><tr><th>rss</th><th>heap used</th><th>heap total</th><th>peak rss</th><th>peak heap</th></tr>\n<tr>\n <td>${fmtBytes(report.memory.current?.rss ?? 0)}</td>\n <td>${fmtBytes(report.memory.current?.heapUsed ?? 0)}</td>\n <td>${fmtBytes(report.memory.current?.heapTotal ?? 0)}</td>\n <td>${fmtBytes(report.memory.peakRss)}</td>\n <td>${fmtBytes(report.memory.peakHeapUsed)}</td>\n</tr>\n</table>\n\n</body></html>`;\n}\n\nfunction fmtBytes(n: number): string {\n if (n < 1024) return `${n} B`;\n if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;\n if (n < 1024 * 1024 * 1024) return `${(n / 1024 / 1024).toFixed(1)} MB`;\n return `${(n / 1024 / 1024 / 1024).toFixed(2)} GB`;\n}\n\nfunction escapeHtml(s: string): string {\n return s\n .replace(/&/g, \"&amp;\")\n .replace(/</g, \"&lt;\")\n .replace(/>/g, \"&gt;\")\n .replace(/\"/g, \"&quot;\");\n}\n","import type { Profiler } from \"./profiler.js\";\n\ntype SchemaLike = {\n pre(hook: string, fn: (this: any) => void): unknown;\n post(hook: string, fn: (this: any, result: unknown) => void): unknown;\n};\n\nconst HOOKS = [\n \"find\",\n \"findOne\",\n \"findOneAndUpdate\",\n \"findOneAndDelete\",\n \"count\",\n \"countDocuments\",\n \"updateOne\",\n \"updateMany\",\n \"deleteOne\",\n \"deleteMany\",\n \"distinct\",\n];\n\nexport function mongoosePlugin(profiler: Profiler) {\n return function perfstackMongoose(schema: SchemaLike) {\n for (const hook of HOOKS) {\n schema.pre(hook, function (this: any) {\n this._perfstackStart = Date.now();\n this._perfstackOp = hook;\n this._perfstackCollection = this.model?.collection?.name ?? this.model?.modelName ?? \"unknown\";\n });\n schema.post(hook, function (this: any) {\n const startedAt = this._perfstackStart ?? Date.now();\n profiler.recordQuery({\n collection: this._perfstackCollection ?? \"unknown\",\n op: this._perfstackOp ?? hook,\n durationMs: Date.now() - startedAt,\n startedAt,\n traceId: profiler.tracer.currentTraceId(),\n });\n });\n }\n schema.pre(\"aggregate\", function (this: any) {\n this._perfstackStart = Date.now();\n const model = typeof this.model === \"function\" ? this.model() : undefined;\n this._perfstackCollection = model?.collection?.name ?? model?.modelName ?? \"unknown\";\n });\n schema.post(\"aggregate\", function (this: any) {\n const startedAt = this._perfstackStart ?? Date.now();\n profiler.recordQuery({\n collection: this._perfstackCollection ?? \"unknown\",\n op: \"aggregate\",\n durationMs: Date.now() - startedAt,\n startedAt,\n traceId: profiler.tracer.currentTraceId(),\n });\n });\n };\n}\n","export { Profiler } from \"./profiler.js\";\nexport { Histogram, percentile } from \"./histogram.js\";\nexport { Tracer } from \"./tracer.js\";\nexport { expressMiddleware, dashboardMiddleware } from \"./express.js\";\nexport { mongoosePlugin } from \"./mongoose.js\";\nexport type {\n HttpRecord,\n QueryRecord,\n MemorySnapshot,\n PerfReport,\n PerfstackOptions,\n RouteSummary,\n HistogramSummary,\n Span,\n} from \"./types.js\";\n\nimport { Profiler } from \"./profiler.js\";\nimport { dashboardMiddleware, expressMiddleware } from \"./express.js\";\nimport { mongoosePlugin } from \"./mongoose.js\";\n\ntype AnyApp = {\n use: (...args: unknown[]) => unknown;\n get?: (path: string, handler: (...args: unknown[]) => unknown) => unknown;\n};\n\nexport interface InitResult {\n profiler: Profiler;\n middleware: ReturnType<typeof expressMiddleware>;\n dashboard: ReturnType<typeof dashboardMiddleware>;\n mongoosePlugin: ReturnType<typeof mongoosePlugin>;\n}\n\nexport function init(\n app?: AnyApp,\n options: ConstructorParameters<typeof Profiler>[0] & { dashboardPath?: string } = {},\n): InitResult {\n const profiler = new Profiler(options);\n const middleware = expressMiddleware(profiler);\n const dashboard = dashboardMiddleware(profiler);\n const plugin = mongoosePlugin(profiler);\n if (app) {\n app.use(middleware);\n if (typeof app.get === \"function\") {\n app.get(options.dashboardPath ?? \"/__perf\", dashboard as unknown as (...a: unknown[]) => unknown);\n app.get((options.dashboardPath ?? \"/__perf\") + \".json\", dashboard as unknown as (...a: unknown[]) => unknown);\n }\n }\n return { profiler, middleware, dashboard, mongoosePlugin: plugin };\n}\n"],"mappings":"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","names":["randomBytes"]}
package/package.json ADDED
@@ -0,0 +1,80 @@
1
+ {
2
+ "name": "perfstack",
3
+ "version": "0.2.0",
4
+ "description": "Fullstack performance profiler: HTTP latency, slow MongoDB queries, memory snapshots, span tracing, and a built-in dashboard endpoint.",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.cjs",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js",
14
+ "require": "./dist/index.cjs"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist",
19
+ "README.md",
20
+ "LICENSE",
21
+ "CHANGELOG.md"
22
+ ],
23
+ "scripts": {
24
+ "build": "tsup",
25
+ "test": "vitest run",
26
+ "typecheck": "tsc --noEmit",
27
+ "prepublishOnly": "npm run build"
28
+ },
29
+ "keywords": [
30
+ "dashboard",
31
+ "express",
32
+ "memory",
33
+ "mern-packages",
34
+ "merndev",
35
+ "mongodb",
36
+ "nodejs",
37
+ "npm-pm",
38
+ "observability",
39
+ "performance",
40
+ "perfstack",
41
+ "profiling",
42
+ "tracing",
43
+ "typescript"
44
+ ],
45
+ "dependencies": {},
46
+ "devDependencies": {
47
+ "@types/express": "^4.17.21",
48
+ "@types/node": "^20.11.0",
49
+ "@types/supertest": "^6.0.2",
50
+ "express": "^4.19.2",
51
+ "supertest": "^7.0.0",
52
+ "tsup": "^8.0.0",
53
+ "typescript": "^5.4.0",
54
+ "vitest": "^1.4.0"
55
+ },
56
+ "peerDependencies": {
57
+ "express": "^4.0.0 || ^5.0.0"
58
+ },
59
+ "peerDependenciesMeta": {
60
+ "express": {
61
+ "optional": true
62
+ }
63
+ },
64
+ "engines": {
65
+ "node": ">=18"
66
+ },
67
+ "author": "Aftab Ahmad Khan (https://github.com/aftab-ahmad-khan-dev)",
68
+ "repository": {
69
+ "type": "git",
70
+ "url": "git+https://github.com/NPM-Packages-Modules/mern.git",
71
+ "directory": "perfstack"
72
+ },
73
+ "bugs": {
74
+ "url": "https://github.com/NPM-Packages-Modules/mern/issues"
75
+ },
76
+ "homepage": "https://github.com/NPM-Packages-Modules/mern/tree/main/perfstack",
77
+ "publishConfig": {
78
+ "access": "public"
79
+ }
80
+ }