TypeScript · 6799 bytes Raw Blame History
1 import type { FastifyRequest, FastifyReply, FastifyInstance } from 'fastify';
2 import { performance } from 'perf_hooks';
3
4 interface PerformanceMetrics {
5 requestCount: number;
6 averageResponseTime: number;
7 p95ResponseTime: number;
8 p99ResponseTime: number;
9 errorRate: number;
10 activeConnections: number;
11 memoryUsage: NodeJS.MemoryUsage;
12 cpuUsage: NodeJS.CpuUsage;
13 responseTimes: number[];
14 errors: number;
15 lastReset: Date;
16 }
17
18 const metrics: PerformanceMetrics = {
19 requestCount: 0,
20 averageResponseTime: 0,
21 p95ResponseTime: 0,
22 p99ResponseTime: 0,
23 errorRate: 0,
24 activeConnections: 0,
25 memoryUsage: process.memoryUsage(),
26 cpuUsage: process.cpuUsage(),
27 responseTimes: [],
28 errors: 0,
29 lastReset: new Date(),
30 };
31
32 // Keep last 1000 response times for percentile calculations
33 const MAX_RESPONSE_TIMES = 1000;
34
35 export async function performanceMiddleware(fastify: FastifyInstance) {
36 // Request timing
37 fastify.addHook('onRequest', async (request: FastifyRequest) => {
38 request.startTime = performance.now();
39 metrics.activeConnections++;
40 });
41
42 fastify.addHook('onResponse', async (request: FastifyRequest, reply: FastifyReply) => {
43 const responseTime = performance.now() - (request.startTime || 0);
44
45 metrics.requestCount++;
46 metrics.activeConnections = Math.max(0, metrics.activeConnections - 1);
47
48 // Track response times
49 metrics.responseTimes.push(responseTime);
50 if (metrics.responseTimes.length > MAX_RESPONSE_TIMES) {
51 metrics.responseTimes.shift();
52 }
53
54 // Track errors
55 if (reply.statusCode >= 400) {
56 metrics.errors++;
57 }
58
59 // Update metrics
60 updateMetrics();
61
62 // Log slow requests
63 if (responseTime > 5000) {
64 fastify.log.warn({
65 url: request.url,
66 method: request.method,
67 responseTime,
68 statusCode: reply.statusCode,
69 }, 'Slow request detected');
70 }
71 });
72
73 fastify.addHook('onError', async (request: FastifyRequest, reply: FastifyReply, error: Error) => {
74 metrics.errors++;
75 metrics.activeConnections = Math.max(0, metrics.activeConnections - 1);
76 updateMetrics();
77 });
78
79 // Metrics endpoint
80 fastify.get('/api/metrics', async () => {
81 return {
82 ...metrics,
83 uptime: process.uptime(),
84 timestamp: new Date(),
85 };
86 });
87
88 // Health check with performance data
89 fastify.get('/api/health/detailed', async () => {
90 const memUsage = process.memoryUsage();
91 const cpuUsage = process.cpuUsage();
92
93 return {
94 status: 'healthy',
95 performance: {
96 responseTime: {
97 average: metrics.averageResponseTime,
98 p95: metrics.p95ResponseTime,
99 p99: metrics.p99ResponseTime,
100 },
101 throughput: {
102 requestsPerSecond: calculateRequestsPerSecond(),
103 activeConnections: metrics.activeConnections,
104 },
105 errors: {
106 rate: metrics.errorRate,
107 total: metrics.errors,
108 },
109 system: {
110 memory: {
111 used: memUsage.heapUsed,
112 total: memUsage.heapTotal,
113 external: memUsage.external,
114 rss: memUsage.rss,
115 },
116 cpu: {
117 user: cpuUsage.user,
118 system: cpuUsage.system,
119 },
120 uptime: process.uptime(),
121 },
122 },
123 timestamp: new Date(),
124 };
125 });
126
127 // Start periodic metrics collection
128 startMetricsCollection(fastify);
129 }
130
131 function updateMetrics() {
132 if (metrics.responseTimes.length === 0) return;
133
134 // Calculate average
135 const sum = metrics.responseTimes.reduce((a, b) => a + b, 0);
136 metrics.averageResponseTime = sum / metrics.responseTimes.length;
137
138 // Calculate percentiles
139 const sorted = [...metrics.responseTimes].sort((a, b) => a - b);
140 metrics.p95ResponseTime = percentile(sorted, 0.95);
141 metrics.p99ResponseTime = percentile(sorted, 0.99);
142
143 // Calculate error rate
144 metrics.errorRate = metrics.requestCount > 0 ? (metrics.errors / metrics.requestCount) * 100 : 0;
145
146 // Update system metrics
147 metrics.memoryUsage = process.memoryUsage();
148 metrics.cpuUsage = process.cpuUsage();
149 }
150
151 function percentile(sortedArray: number[], p: number): number {
152 if (sortedArray.length === 0) return 0;
153
154 const index = Math.ceil(sortedArray.length * p) - 1;
155 return sortedArray[Math.max(0, index)];
156 }
157
158 function calculateRequestsPerSecond(): number {
159 const now = new Date();
160 const timeDiff = (now.getTime() - metrics.lastReset.getTime()) / 1000;
161
162 if (timeDiff === 0) return 0;
163
164 return metrics.requestCount / timeDiff;
165 }
166
167 function startMetricsCollection(fastify: FastifyInstance) {
168 // Reset metrics periodically to prevent memory leaks
169 setInterval(() => {
170 // Keep recent data but reset counters
171 const recentResponseTimes = metrics.responseTimes.slice(-100);
172
173 metrics.responseTimes = recentResponseTimes;
174 metrics.requestCount = Math.floor(metrics.requestCount * 0.1); // Keep 10% for trend
175 metrics.errors = Math.floor(metrics.errors * 0.1);
176 metrics.lastReset = new Date();
177
178 updateMetrics();
179 }, 5 * 60 * 1000); // Reset every 5 minutes
180
181 // Log metrics periodically
182 setInterval(() => {
183 fastify.log.info({
184 metrics: {
185 requestCount: metrics.requestCount,
186 averageResponseTime: Math.round(metrics.averageResponseTime),
187 p95ResponseTime: Math.round(metrics.p95ResponseTime),
188 errorRate: Math.round(metrics.errorRate * 100) / 100,
189 activeConnections: metrics.activeConnections,
190 memoryUsageMB: Math.round(metrics.memoryUsage.heapUsed / 1024 / 1024),
191 requestsPerSecond: Math.round(calculateRequestsPerSecond() * 100) / 100,
192 },
193 }, 'Performance metrics');
194 }, 60 * 1000); // Log every minute
195 }
196
197 // Declare module augmentation for request
198 declare module 'fastify' {
199 interface FastifyRequest {
200 startTime?: number;
201 }
202 }
203
204 // Response time tracking for caching decisions
205 export function shouldCache(responseTime: number, statusCode: number): boolean {
206 // Cache successful responses that are reasonably fast
207 if (statusCode >= 200 && statusCode < 300) {
208 return responseTime < 1000; // Cache responses under 1 second
209 }
210
211 return false;
212 }
213
214 // Memory pressure detection
215 export function isMemoryPressureHigh(): boolean {
216 const usage = process.memoryUsage();
217 const heapUsedMB = usage.heapUsed / 1024 / 1024;
218 const heapTotalMB = usage.heapTotal / 1024 / 1024;
219
220 // Consider memory pressure high if heap usage > 80%
221 return (heapUsedMB / heapTotalMB) > 0.8;
222 }
223
224 // CPU usage detection
225 export function isCpuUsageHigh(): boolean {
226 const usage = process.cpuUsage();
227 const totalUsage = usage.user + usage.system;
228
229 // This is a simplified check - in production you'd want more sophisticated monitoring
230 return totalUsage > 500000; // 500ms of CPU time indicates high usage
231 }