express-performance-toolkit 1.0.0 → 2.0.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.
Files changed (78) hide show
  1. package/README.md +119 -76
  2. package/dashboard-ui/README.md +73 -0
  3. package/dashboard-ui/eslint.config.js +23 -0
  4. package/dashboard-ui/index.html +13 -0
  5. package/dashboard-ui/package-lock.json +3382 -0
  6. package/dashboard-ui/package.json +32 -0
  7. package/dashboard-ui/src/App.css +184 -0
  8. package/dashboard-ui/src/App.tsx +182 -0
  9. package/dashboard-ui/src/components/BlockedModal.tsx +108 -0
  10. package/dashboard-ui/src/components/CachePanel.tsx +45 -0
  11. package/dashboard-ui/src/components/HealthCharts.tsx +142 -0
  12. package/dashboard-ui/src/components/InsightsPanel.tsx +49 -0
  13. package/dashboard-ui/src/components/KpiGrid.tsx +178 -0
  14. package/dashboard-ui/src/components/LiveLogs.tsx +76 -0
  15. package/dashboard-ui/src/components/Login.tsx +83 -0
  16. package/dashboard-ui/src/components/RoutesTable.tsx +110 -0
  17. package/dashboard-ui/src/hooks/useMetrics.ts +131 -0
  18. package/dashboard-ui/src/index.css +652 -0
  19. package/dashboard-ui/src/main.tsx +10 -0
  20. package/dashboard-ui/src/pages/InsightsPage.tsx +42 -0
  21. package/dashboard-ui/src/pages/LogsPage.tsx +26 -0
  22. package/dashboard-ui/src/pages/OverviewPage.tsx +32 -0
  23. package/dashboard-ui/src/pages/RoutesPage.tsx +26 -0
  24. package/dashboard-ui/src/utils/formatters.ts +27 -0
  25. package/dashboard-ui/tsconfig.app.json +28 -0
  26. package/dashboard-ui/tsconfig.json +7 -0
  27. package/dashboard-ui/tsconfig.node.json +26 -0
  28. package/dashboard-ui/vite.config.ts +12 -0
  29. package/dist/analyzer.d.ts +6 -0
  30. package/dist/analyzer.d.ts.map +1 -0
  31. package/dist/analyzer.js +70 -0
  32. package/dist/analyzer.js.map +1 -0
  33. package/dist/dashboard/dashboardRouter.d.ts +4 -4
  34. package/dist/dashboard/dashboardRouter.d.ts.map +1 -1
  35. package/dist/dashboard/dashboardRouter.js +67 -21
  36. package/dist/dashboard/dashboardRouter.js.map +1 -1
  37. package/dist/dashboard-ui/assets/index-CX-zE-Qy.css +1 -0
  38. package/dist/dashboard-ui/assets/index-Q9TGkd8n.js +41 -0
  39. package/dist/dashboard-ui/index.html +14 -0
  40. package/dist/index.d.ts +11 -10
  41. package/dist/index.d.ts.map +1 -1
  42. package/dist/index.js +35 -11
  43. package/dist/index.js.map +1 -1
  44. package/dist/logger.d.ts +3 -3
  45. package/dist/logger.d.ts.map +1 -1
  46. package/dist/logger.js +167 -9
  47. package/dist/logger.js.map +1 -1
  48. package/dist/queryHelper.d.ts.map +1 -1
  49. package/dist/queryHelper.js +1 -0
  50. package/dist/queryHelper.js.map +1 -1
  51. package/dist/rateLimit.d.ts +5 -0
  52. package/dist/rateLimit.d.ts.map +1 -0
  53. package/dist/rateLimit.js +67 -0
  54. package/dist/rateLimit.js.map +1 -0
  55. package/dist/store.d.ts +9 -2
  56. package/dist/store.d.ts.map +1 -1
  57. package/dist/store.js +147 -25
  58. package/dist/store.js.map +1 -1
  59. package/dist/types.d.ts +93 -0
  60. package/dist/types.d.ts.map +1 -1
  61. package/example/server.ts +68 -37
  62. package/package.json +9 -6
  63. package/src/analyzer.ts +78 -0
  64. package/src/dashboard/dashboardRouter.ts +88 -23
  65. package/src/index.ts +70 -30
  66. package/src/logger.ts +177 -13
  67. package/src/queryHelper.ts +2 -0
  68. package/src/rateLimit.ts +86 -0
  69. package/src/store.ts +136 -27
  70. package/src/types.ts +98 -0
  71. package/tests/analyzer.test.ts +108 -0
  72. package/tests/auth.test.ts +79 -0
  73. package/tests/bandwidth.test.ts +72 -0
  74. package/tests/integration.test.ts +51 -54
  75. package/tests/rateLimit.test.ts +57 -0
  76. package/tests/store.test.ts +37 -18
  77. package/tsconfig.json +1 -0
  78. package/src/dashboard/dashboard.html +0 -756
package/package.json CHANGED
@@ -1,16 +1,17 @@
1
1
  {
2
2
  "name": "express-performance-toolkit",
3
- "version": "1.0.0",
3
+ "version": "2.0.0",
4
4
  "description": "⚡ A powerful Express middleware that automatically optimizes your app with request caching, response compression, slow API detection, and a real-time performance dashboard.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
- "build": "tsc",
8
+ "build:ui": "cd dashboard-ui && npm install && npm run build",
9
+ "build": "tsc && npm run build:ui",
9
10
  "postbuild": "npm run copy-assets",
10
- "copy-assets": "mkdir -p dist/dashboard && cp src/dashboard/dashboard.html dist/dashboard/",
11
+ "copy-assets": "rm -rf dist/dashboard-ui && cp -R dashboard-ui/dist dist/dashboard-ui",
11
12
  "dev": "tsc --watch",
12
13
  "test": "jest --verbose",
13
- "example": "ts-node example/server.ts",
14
+ "example": "npm run build && ts-node example/server.ts",
14
15
  "prepublishOnly": "npm run build"
15
16
  },
16
17
  "keywords": [
@@ -26,11 +27,13 @@
26
27
  "logging",
27
28
  "typescript"
28
29
  ],
29
- "author": "",
30
+ "author": "Mayank Saini",
30
31
  "license": "MIT",
31
32
  "dependencies": {
32
33
  "compression": "^1.7.4",
33
- "on-finished": "^2.4.1"
34
+ "lucide-react": "^0.577.0",
35
+ "on-finished": "^2.4.1",
36
+ "recharts": "^3.8.0"
34
37
  },
35
38
  "peerDependencies": {
36
39
  "express": ">=4.0.0"
@@ -0,0 +1,78 @@
1
+ import { Metrics, Insight } from "./types";
2
+
3
+ /**
4
+ * Heuristic-based analysis engine that transforms raw metrics into actionable advice.
5
+ */
6
+ export function analyzeMetrics(metrics: Metrics): Insight[] {
7
+ const insights: Insight[] = [];
8
+
9
+ // 1. Caching Suggestions
10
+ Object.entries(metrics.routes).forEach(([path, stats]) => {
11
+ // If a route is slow and has low/zero cache hits
12
+ if (stats.avgTime > 500 && stats.count > 5) {
13
+ if (metrics.cacheHitRate < 10) {
14
+ insights.push({
15
+ type: "warning",
16
+ title: "Slow Route: Caching Opportunity",
17
+ message: `Route "${path}" is slow (avg ${stats.avgTime}ms).`,
18
+ action: "Consider enabling the cache middleware for this route.",
19
+ });
20
+ }
21
+ }
22
+
23
+ // 2. N+1 Query Detection
24
+ if (stats.highQueryCount > stats.count * 0.3) {
25
+ insights.push({
26
+ type: "error",
27
+ title: "Potential N+1 Query Detected",
28
+ message: `Route "${path}" triggers high query counts in ${Math.round((stats.highQueryCount / stats.count) * 100)}% of calls.`,
29
+ action: "Review your database logic and use eager loading (JOINs).",
30
+ });
31
+ }
32
+
33
+ // 3. Payload Size Alerts
34
+ if (stats.avgSize > 1024 * 1024) {
35
+ // > 1MB
36
+ insights.push({
37
+ type: "warning",
38
+ title: "Heavy Payload Detected",
39
+ message: `Route "${path}" sends large responses (avg ${(stats.avgSize / 1024 / 1024).toFixed(1)} MB).`,
40
+ action:
41
+ "Consider pagination or reducing the number of returned fields.",
42
+ });
43
+ }
44
+
45
+ // 4. Rate Limiting Insights
46
+ if (stats.rateLimitHits > stats.count * 0.1) {
47
+ insights.push({
48
+ type: "info",
49
+ title: "High Rate Limiting Activity",
50
+ message: `Route "${path}" has a high block rate (${Math.round((stats.rateLimitHits / stats.count) * 100)}%).`,
51
+ action:
52
+ "Verify if your Rate Limit 'max' is too low or if an IP is scraping you.",
53
+ });
54
+ }
55
+ });
56
+
57
+ // 5. System Health
58
+ if (metrics.eventLoopLag > 100) {
59
+ insights.push({
60
+ type: "error",
61
+ title: "Event Loop Lagging",
62
+ message: `Critical event loop delay detected (${metrics.eventLoopLag}ms).`,
63
+ action: "Identify blocking synchronous operations in your code.",
64
+ });
65
+ }
66
+
67
+ if (metrics.memoryUsage.heapUsed > metrics.memoryUsage.heapLimit * 0.8) {
68
+ insights.push({
69
+ type: "warning",
70
+ title: "High Memory Pressure",
71
+ message: "Node.js heap usage is above 80% of total limit.",
72
+ action:
73
+ "Monitor for memory leaks or increase memory limit (--max-old-space-size).",
74
+ });
75
+ }
76
+
77
+ return insights;
78
+ }
@@ -1,8 +1,15 @@
1
- import { Router, Request, Response } from 'express';
2
- import * as path from 'path';
3
- import * as fs from 'fs';
4
- import { MetricsStore } from '../store';
5
- import { DashboardOptions } from '../types';
1
+ import express, { Router, Request, Response, NextFunction } from "express";
2
+ import * as path from "path";
3
+ import { MetricsStore } from "../store";
4
+ import { DashboardOptions } from "../types";
5
+
6
+ /**
7
+ * Simple session-less auth token based on the secret.
8
+ * In a real-world scenario, you'd use a more robust session manager or JWT.
9
+ */
10
+ const generateToken = (secret: string) => {
11
+ return Buffer.from(`auth:${secret}`).toString("base64");
12
+ };
6
13
 
7
14
  /**
8
15
  * Create the dashboard Express router.
@@ -10,36 +17,94 @@ import { DashboardOptions } from '../types';
10
17
  */
11
18
  export function createDashboardRouter(
12
19
  store: MetricsStore,
13
- _options: DashboardOptions = {}
20
+ options: DashboardOptions = {},
14
21
  ): Router {
15
22
  const router = Router();
16
23
 
17
- // Cache the HTML file in memory
18
- const htmlPath = path.join(__dirname, 'dashboard.html');
19
- let dashboardHtml: string;
24
+ // Default auth settings if none provided (Security by default)
25
+ const auth = options.auth || {
26
+ username: "admin",
27
+ password: "perf-toolkit",
28
+ secret: "toolkit-secret",
29
+ };
30
+
31
+ const mountPath = options.path || "/__perf";
32
+
33
+ // Use JSON parsing for login endpoint
34
+ router.use(express.json());
35
+
36
+ // Authentication Middleware
37
+ const requireAuth = (req: Request, res: Response, next: NextFunction) => {
38
+ if (!auth) return next();
39
+
40
+ const expectedToken = generateToken(auth.secret || "toolkit-secret");
41
+ const cookie = req.headers.cookie || "";
42
+ const hasToken = cookie.includes(`perf-auth=${expectedToken}`);
43
+
44
+ if (hasToken) {
45
+ return next();
46
+ }
47
+
48
+ res.status(401).json({ success: false, message: "Unauthorized" });
49
+ };
20
50
 
21
- try {
22
- dashboardHtml = fs.readFileSync(htmlPath, 'utf-8');
23
- } catch {
24
- dashboardHtml = '<h1>Dashboard HTML not found</h1><p>Ensure dashboard.html is in the dist/dashboard/ directory.</p>';
25
- }
51
+ // Login Endpoint
52
+ router.post("/api/login", (req: Request, res: Response) => {
53
+ if (!auth) {
54
+ return res.json({ success: true, message: "Auth disabled" });
55
+ }
26
56
 
27
- // Serve dashboard HTML
28
- router.get('/', (_req: Request, res: Response) => {
29
- res.set('Content-Type', 'text/html');
30
- res.send(dashboardHtml);
57
+ const { username, password } = req.body;
58
+ const defaultUser = auth.username || "admin";
59
+ const defaultPass = auth.password || "perf-toolkit";
60
+
61
+ if (username === defaultUser && password === defaultPass) {
62
+ const token = generateToken(auth.secret || "toolkit-secret");
63
+ // Set cookie with HttpOnly and reasonable maxAge
64
+ res.setHeader(
65
+ "Set-Cookie",
66
+ `perf-auth=${token}; Path=${mountPath}; HttpOnly; Max-Age=86400; SameSite=Lax`,
67
+ );
68
+ return res.json({ success: true });
69
+ }
70
+
71
+ res.status(401).json({ success: false, message: "Invalid credentials" });
31
72
  });
32
73
 
33
- // JSON metrics API endpoint
34
- router.get('/api/metrics', (_req: Request, res: Response) => {
74
+ // Logout Endpoint
75
+ router.post("/api/logout", (_req: Request, res: Response) => {
76
+ res.setHeader(
77
+ "Set-Cookie",
78
+ `perf-auth=; Path=${mountPath}; HttpOnly; Max-Age=0`,
79
+ );
80
+ res.json({ success: true });
81
+ });
82
+
83
+ // Check auth status endpoint
84
+ router.get("/api/auth-check", (req: Request, res: Response) => {
85
+ if (!auth) return res.json({ authenticated: true, required: false });
86
+
87
+ const expectedToken = generateToken(auth.secret || "toolkit-secret");
88
+ const cookie = req.headers.cookie || "";
89
+ const authenticated = cookie.includes(`perf-auth=${expectedToken}`);
90
+
91
+ res.json({ authenticated, required: true });
92
+ });
93
+
94
+ // JSON metrics API endpoint (Protected)
95
+ router.get("/api/metrics", requireAuth, (_req: Request, res: Response) => {
35
96
  res.json(store.getMetrics());
36
97
  });
37
98
 
38
- // Reset metrics endpoint
39
- router.post('/api/reset', (_req: Request, res: Response) => {
99
+ // Reset metrics endpoint (Protected)
100
+ router.post("/api/reset", requireAuth, (_req: Request, res: Response) => {
40
101
  store.reset();
41
- res.json({ success: true, message: 'Metrics reset' });
102
+ res.json({ success: true, message: "Metrics reset" });
42
103
  });
43
104
 
105
+ // Serve React Dashboard UI bundle
106
+ const uiPath = path.resolve(__dirname, "../../dist/dashboard-ui");
107
+ router.use("/", express.static(uiPath));
108
+
44
109
  return router;
45
110
  }
package/src/index.ts CHANGED
@@ -1,20 +1,22 @@
1
- import { Request, Response, NextFunction, Router } from 'express';
2
- import { MetricsStore } from './store';
3
- import { createCacheMiddleware, LRUCache } from './cache';
4
- import { createCompressionMiddleware } from './compression';
5
- import { createLoggerMiddleware } from './logger';
6
- import { createQueryHelperMiddleware } from './queryHelper';
7
- import { createDashboardRouter } from './dashboard/dashboardRouter';
1
+ import { Request, Response, NextFunction, Router } from "express";
2
+ import { MetricsStore } from "./store";
3
+ import { createCacheMiddleware, LRUCache } from "./cache";
4
+ import { createCompressionMiddleware } from "./compression";
5
+ import { createLoggerMiddleware } from "./logger";
6
+ import { createQueryHelperMiddleware } from "./queryHelper";
7
+ import { createRateLimiter } from "./rateLimit";
8
+ import { createDashboardRouter } from "./dashboard/dashboardRouter";
8
9
  import {
9
10
  ToolkitOptions,
10
11
  CacheOptions,
11
12
  CompressionOptions,
12
13
  LoggerOptions,
13
14
  QueryHelperOptions,
15
+ RateLimitOptions,
14
16
  DashboardOptions,
15
17
  Metrics,
16
18
  CacheMiddleware,
17
- } from './types';
19
+ } from "./types";
18
20
 
19
21
  export interface ToolkitInstance {
20
22
  /** The composed Express middleware */
@@ -50,44 +52,83 @@ export interface ToolkitInstance {
50
52
  * app.use('/__perf', toolkit.dashboardRouter);
51
53
  * ```
52
54
  */
53
- export function performanceToolkit(options: ToolkitOptions = {}): ToolkitInstance {
55
+ export function performanceToolkit(
56
+ options: ToolkitOptions = {},
57
+ ): ToolkitInstance {
54
58
  const store = new MetricsStore({ maxLogs: options.maxLogs || 1000 });
55
59
 
56
- const middlewares: ((req: Request, res: Response, next: NextFunction) => void)[] = [];
60
+ const middlewares: ((
61
+ req: Request,
62
+ res: Response,
63
+ next: NextFunction,
64
+ ) => void)[] = [];
57
65
  let cacheMiddlewareInstance: CacheMiddleware | null = null;
58
66
 
67
+ // ── Dashboard Config ─────────────────────────────────────
68
+ const dashboardConfig = normalizeOption<DashboardOptions>(options.dashboard, {
69
+ enabled: true,
70
+ });
71
+ const dashboardExcludePath = dashboardConfig.path || "/__perf";
72
+
73
+ // ── Rate Limiter ─────────────────────────────────────────
74
+ const rateLimitConfig = normalizeOption<RateLimitOptions>(options.rateLimit, {
75
+ enabled: false,
76
+ });
77
+ if (rateLimitConfig.enabled !== false) {
78
+ // Automatically exclude dashboard from rate limiting to prevent UI lockouts
79
+ rateLimitConfig.exclude = [
80
+ ...(rateLimitConfig.exclude || []),
81
+ dashboardExcludePath,
82
+ ];
83
+ middlewares.push(createRateLimiter(store, rateLimitConfig));
84
+ }
85
+
59
86
  // ── Compression ──────────────────────────────────────────
60
- const compressionConfig = normalizeOption<CompressionOptions>(options.compression, { enabled: true });
87
+ const compressionConfig = normalizeOption<CompressionOptions>(
88
+ options.compression,
89
+ { enabled: true },
90
+ );
61
91
  if (compressionConfig.enabled !== false) {
62
92
  middlewares.push(createCompressionMiddleware(compressionConfig));
63
93
  }
64
94
 
65
95
  // ── Query Helper ─────────────────────────────────────────
66
- const queryConfig = normalizeOption<QueryHelperOptions>(options.queryHelper, { enabled: false });
96
+ const queryConfig = normalizeOption<QueryHelperOptions>(options.queryHelper, {
97
+ enabled: false,
98
+ });
67
99
  if (queryConfig.enabled !== false) {
68
100
  middlewares.push(createQueryHelperMiddleware(queryConfig));
69
101
  }
70
102
 
71
103
  // ── Logger (slow request detection) ──────────────────────
72
- const loggerConfig = normalizeOption<LoggerOptions>(options.logSlowRequests, { enabled: true });
104
+ const loggerConfig = normalizeOption<LoggerOptions>(options.logSlowRequests, {
105
+ enabled: true,
106
+ });
73
107
  if (loggerConfig.enabled !== false) {
74
108
  middlewares.push(createLoggerMiddleware(loggerConfig, store));
75
109
  }
76
110
 
77
111
  // ── Cache ────────────────────────────────────────────────
78
- const cacheConfig = normalizeOption<CacheOptions>(options.cache, { enabled: false });
112
+ const cacheConfig = normalizeOption<CacheOptions>(options.cache, {
113
+ enabled: false,
114
+ });
79
115
  if (cacheConfig.enabled !== false) {
80
116
  cacheMiddlewareInstance = createCacheMiddleware(cacheConfig, store);
81
117
  middlewares.push(cacheMiddlewareInstance);
82
118
  }
83
119
 
84
120
  // ── Dashboard Router ─────────────────────────────────────
85
- const dashboardConfig = normalizeOption<DashboardOptions>(options.dashboard, { enabled: true });
86
- const dashboardPath = dashboardConfig.path || '/__perf';
87
- const dashboardRouter = createDashboardRouter(store, dashboardConfig);
121
+ const dashboardRouter = createDashboardRouter(store, {
122
+ ...dashboardConfig,
123
+ path: "/", // Always serve at the root of the provided router
124
+ });
88
125
 
89
126
  // ── Composed Middleware ──────────────────────────────────
90
- function composedMiddleware(req: Request, res: Response, next: NextFunction): void {
127
+ function composedMiddleware(
128
+ req: Request,
129
+ res: Response,
130
+ next: NextFunction,
131
+ ): void {
91
132
  let index = 0;
92
133
 
93
134
  function runNext(err?: unknown): void {
@@ -117,25 +158,24 @@ export function performanceToolkit(options: ToolkitOptions = {}): ToolkitInstanc
117
158
 
118
159
  /**
119
160
  * Normalize a boolean | object option into a config object.
120
- * - `true` → defaults with enabled: true
121
- * - `false` → defaults with enabled: false
122
- * - object → merged with defaults
123
161
  */
124
162
  function normalizeOption<T extends { enabled?: boolean }>(
125
163
  value: boolean | T | undefined,
126
- defaults: T
164
+ defaults: T,
127
165
  ): T {
128
166
  if (value === true) return { ...defaults, enabled: true };
129
167
  if (value === false) return { ...defaults, enabled: false };
130
- if (typeof value === 'object') return { ...defaults, ...value, enabled: true };
168
+ if (typeof value === "object")
169
+ return { ...defaults, ...value, enabled: true };
131
170
  return defaults;
132
171
  }
133
172
 
134
173
  // ── Re-exports ─────────────────────────────────────────────
135
- export { MetricsStore } from './store';
136
- export { LRUCache, createCacheMiddleware } from './cache';
137
- export { createCompressionMiddleware } from './compression';
138
- export { createLoggerMiddleware } from './logger';
139
- export { createQueryHelperMiddleware } from './queryHelper';
140
- export { createDashboardRouter } from './dashboard/dashboardRouter';
141
- export * from './types';
174
+ export { MetricsStore } from "./store";
175
+ export { LRUCache, createCacheMiddleware } from "./cache";
176
+ export { createCompressionMiddleware } from "./compression";
177
+ export { createLoggerMiddleware } from "./logger";
178
+ export { createQueryHelperMiddleware } from "./queryHelper";
179
+ export { createRateLimiter } from "./rateLimit";
180
+ export { createDashboardRouter } from "./dashboard/dashboardRouter";
181
+ export * from "./types";
package/src/logger.ts CHANGED
@@ -1,42 +1,195 @@
1
- import { Request, Response, NextFunction } from 'express';
2
- import onFinished from 'on-finished';
3
- import { LoggerOptions, LogEntry } from './types';
4
- import { MetricsStore } from './store';
1
+ import { Request, Response, NextFunction } from "express";
2
+ import onFinished from "on-finished";
3
+ import * as fs from "fs";
4
+ import * as path from "path";
5
+ import { LoggerOptions, LogEntry } from "./types";
6
+ import { MetricsStore } from "./store";
5
7
 
6
8
  /**
7
9
  * Default log formatter for console output.
8
10
  */
9
11
  function defaultFormatter(entry: LogEntry): string {
10
- const slow = entry.slow ? ' 🔥 SLOW' : '';
11
- const cached = entry.cached ? ' [CACHED]' : '';
12
+ const slow = entry.slow ? " 🔥 SLOW" : "";
13
+ const cached = entry.cached ? " [CACHED]" : "";
12
14
  const status = entry.statusCode;
13
15
  const time = `${entry.responseTime}ms`;
14
16
 
15
17
  return `[perf] ${entry.method} ${entry.path} → ${status} ${time}${cached}${slow}`;
16
18
  }
17
19
 
20
+ /**
21
+ * Helper class for managing log file rotation and cleanup.
22
+ */
23
+ class LogRotator {
24
+ private currentStream: fs.WriteStream | null = null;
25
+ private currentDateStr: string = "";
26
+ private basePath: string;
27
+ private logDir: string;
28
+ private rotation: boolean;
29
+ private maxDays: number;
30
+
31
+ constructor(filePath: string, rotation: boolean, maxDays: number) {
32
+ this.basePath = path.resolve(process.cwd(), filePath);
33
+ this.logDir = path.dirname(this.basePath);
34
+ this.rotation = rotation;
35
+ this.maxDays = maxDays;
36
+
37
+ if (!fs.existsSync(this.logDir)) {
38
+ fs.mkdirSync(this.logDir, { recursive: true });
39
+ }
40
+
41
+ this.getStream(); // Initialize
42
+ }
43
+
44
+ private getDateStr(date: Date = new Date()): string {
45
+ return date.toISOString().split("T")[0];
46
+ }
47
+
48
+ private getRotatedPath(dateStr: string): string {
49
+ if (!this.rotation) return this.basePath;
50
+ const ext = path.extname(this.basePath);
51
+ const base = path.basename(this.basePath, ext);
52
+ return path.join(this.logDir, `${base}-${dateStr}${ext}`);
53
+ }
54
+
55
+ public getStream(): fs.WriteStream {
56
+ if (!this.rotation) {
57
+ if (!this.currentStream) {
58
+ this.currentStream = fs.createWriteStream(this.basePath, {
59
+ flags: "a",
60
+ });
61
+ }
62
+ return this.currentStream;
63
+ }
64
+
65
+ const todayStr = this.getDateStr();
66
+ if (this.currentDateStr !== todayStr) {
67
+ if (this.currentStream) {
68
+ this.currentStream.end();
69
+ }
70
+ this.currentDateStr = todayStr;
71
+ const newPath = this.getRotatedPath(todayStr);
72
+ this.currentStream = fs.createWriteStream(newPath, { flags: "a" });
73
+
74
+ // Run cleanup asynchronously in the background
75
+ this.cleanupOldLogs();
76
+ }
77
+ return this.currentStream!;
78
+ }
79
+
80
+ private cleanupOldLogs() {
81
+ if (!this.rotation || this.maxDays <= 0) return;
82
+
83
+ fs.readdir(this.logDir, (err, files) => {
84
+ if (err) return;
85
+
86
+ const ext = path.extname(this.basePath);
87
+ const base = path.basename(this.basePath, ext);
88
+ const prefix = `${base}-`;
89
+
90
+ const now = Date.now();
91
+ const maxAgeMs = this.maxDays * 24 * 60 * 60 * 1000;
92
+
93
+ files.forEach((file) => {
94
+ if (file.startsWith(prefix) && file.endsWith(ext)) {
95
+ const datePart = file.slice(prefix.length, -ext.length);
96
+ const fileDate = new Date(datePart).getTime();
97
+
98
+ if (!isNaN(fileDate) && now - fileDate > maxAgeMs) {
99
+ fs.unlink(path.join(this.logDir, file), (unlinkErr) => {
100
+ if (unlinkErr)
101
+ console.error(
102
+ `[perf-toolkit] Failed to delete old log: ${file}`,
103
+ );
104
+ });
105
+ }
106
+ }
107
+ });
108
+ });
109
+ }
110
+ }
111
+
18
112
  /**
19
113
  * Create request logging & slow API detection middleware.
20
114
  */
21
115
  export function createLoggerMiddleware(
22
116
  options: LoggerOptions = {},
23
- store: MetricsStore
117
+ store: MetricsStore,
24
118
  ): (req: Request, res: Response, next: NextFunction) => void {
25
119
  const {
26
120
  slowThreshold = 1000,
27
121
  console: logToConsole = true,
122
+ file: logFilePath,
123
+ rotation = false,
124
+ maxDays = 7,
28
125
  formatter = defaultFormatter,
29
126
  } = options;
30
127
 
128
+ let rotator: LogRotator | null = null;
129
+ if (logFilePath) {
130
+ try {
131
+ rotator = new LogRotator(logFilePath, rotation, maxDays);
132
+ } catch (err) {
133
+ console.error(`[perf-toolkit] Failed to initialize log rotator: ${err}`);
134
+ }
135
+ }
136
+
31
137
  return (req: Request, res: Response, next: NextFunction): void => {
32
138
  const startTime = Date.now();
139
+ const reqPath = req.originalUrl || req.url;
140
+
141
+ // Ignore dashboard API paths
142
+ if (
143
+ reqPath.includes("/api/metrics") ||
144
+ reqPath.includes("/api/reset") ||
145
+ reqPath.includes("/api/__perf")
146
+ ) {
147
+ return next();
148
+ }
149
+
150
+ // Byte counting logic
151
+ let bytesSent = 0;
152
+ const originalWrite = res.write;
153
+ const originalEnd = res.end;
154
+
155
+ res.write = function (
156
+ chunk: any,
157
+ encoding?: any,
158
+ _callback?: any,
159
+ ): boolean {
160
+ if (chunk) {
161
+ bytesSent += Buffer.isBuffer(chunk)
162
+ ? chunk.length
163
+ : Buffer.byteLength(
164
+ chunk,
165
+ (typeof encoding === "string"
166
+ ? encoding
167
+ : "utf8") as BufferEncoding,
168
+ );
169
+ }
170
+ return originalWrite.apply(res, arguments as any);
171
+ };
172
+
173
+ res.end = function (chunk: any, encoding?: any, _callback?: any): Response {
174
+ if (chunk && typeof chunk !== "function") {
175
+ bytesSent += Buffer.isBuffer(chunk)
176
+ ? chunk.length
177
+ : Buffer.byteLength(
178
+ chunk,
179
+ (typeof encoding === "string"
180
+ ? encoding
181
+ : "utf8") as BufferEncoding,
182
+ );
183
+ }
184
+ return originalEnd.apply(res, arguments as any);
185
+ };
33
186
 
34
187
  // Attach perf data to request
35
188
  if (!req.perfToolkit) {
36
189
  req.perfToolkit = {
37
190
  startTime,
38
191
  queryCount: 0,
39
- trackQuery: (label?: string) => {
192
+ trackQuery: () => {
40
193
  req.perfToolkit!.queryCount++;
41
194
  },
42
195
  };
@@ -46,22 +199,27 @@ export function createLoggerMiddleware(
46
199
  const responseTime = Date.now() - startTime;
47
200
  const isSlow = responseTime >= slowThreshold;
48
201
 
202
+ // Extract route pattern if available (e.g. /users/:id)
203
+ const routePattern = (req as any).route?.path;
204
+
49
205
  const entry: LogEntry = {
50
206
  method: req.method,
51
- path: req.originalUrl || req.url,
207
+ path: reqPath,
208
+ routePattern,
52
209
  statusCode: finishedRes.statusCode,
53
210
  responseTime,
54
211
  timestamp: Date.now(),
55
212
  slow: isSlow,
56
- cached: res.getHeader('X-Cache') === 'HIT',
213
+ cached: res.getHeader("X-Cache") === "HIT",
214
+ highQueries: req.perfToolkit?.highQueries || false,
57
215
  queryCount: req.perfToolkit?.queryCount,
58
- contentLength: parseInt(res.getHeader('content-length') as string, 10) || undefined,
59
- userAgent: req.get('user-agent'),
216
+ bytesSent,
217
+ userAgent: req.get("user-agent"),
60
218
  ip: req.ip,
61
219
  };
62
220
 
63
221
  // Record in store
64
- store.addLog(entry);
222
+ store.recordLog(entry);
65
223
 
66
224
  if (isSlow) {
67
225
  store.recordSlowRequest();
@@ -76,6 +234,12 @@ export function createLoggerMiddleware(
76
234
  console.log(message);
77
235
  }
78
236
  }
237
+
238
+ // File output
239
+ if (rotator) {
240
+ const logLine = JSON.stringify(entry) + "\n";
241
+ rotator.getStream().write(logLine);
242
+ }
79
243
  });
80
244
 
81
245
  next();
@@ -31,6 +31,8 @@ export function createQueryHelperMiddleware(
31
31
 
32
32
  // Warn if threshold exceeded
33
33
  if (req.perfToolkit!.queryCount === threshold) {
34
+ req.perfToolkit!.highQueries = true;
35
+
34
36
  console.warn(
35
37
  `[perf] ⚠️ N+1 Alert: ${req.method} ${req.originalUrl || req.url} ` +
36
38
  `has made ${threshold}+ queries. Consider optimizing with batch/join queries.`