| 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 |
} |