Skip to main content
Glama
performance.ts7.9 kB
/** * Performance Monitoring Middleware * * Tracks request latency and logs slow requests for profiling. * Helps identify performance bottlenecks in the REST API. * * Requirements: 17.1 - Memory retrieval response within 200ms at p95 */ import type { NextFunction, Request, Response } from "express"; import { Logger } from "../../utils/logger.js"; /** * Performance metrics for a single request */ export interface RequestMetrics { method: string; path: string; statusCode: number; durationMs: number; timestamp: Date; requestId?: string; } /** * Aggregated performance statistics */ export interface PerformanceStats { totalRequests: number; avgDurationMs: number; p50DurationMs: number; p95DurationMs: number; p99DurationMs: number; maxDurationMs: number; slowRequests: number; byPath: Record<string, PathStats>; } /** * Per-path statistics */ export interface PathStats { count: number; avgDurationMs: number; maxDurationMs: number; slowCount: number; } /** * Performance middleware configuration */ export interface PerformanceConfig { /** Threshold in ms for logging slow requests */ slowThresholdMs: number; /** Maximum number of metrics to retain */ maxMetrics: number; /** Whether to log all requests */ logAllRequests: boolean; /** Paths to exclude from tracking */ excludePaths?: string[]; } /** * Default performance configuration */ export const DEFAULT_PERFORMANCE_CONFIG: PerformanceConfig = { slowThresholdMs: 200, // 200ms threshold per requirement 17.1 maxMetrics: 10000, logAllRequests: false, excludePaths: ["/api/v1/health/live", "/api/v1/health/ready"], }; /** * Performance metrics collector */ class PerformanceCollector { private metrics: RequestMetrics[] = []; private config: PerformanceConfig; constructor(config: PerformanceConfig) { this.config = config; } /** * Record a request metric */ record(metric: RequestMetrics): void { // Evict oldest if at capacity if (this.metrics.length >= this.config.maxMetrics) { this.metrics.shift(); } this.metrics.push(metric); // Log slow requests if (metric.durationMs > this.config.slowThresholdMs) { Logger.warn( `Slow request: ${metric.method} ${metric.path} took ${metric.durationMs}ms (threshold: ${this.config.slowThresholdMs}ms)` ); } else if (this.config.logAllRequests) { Logger.debug(`Request: ${metric.method} ${metric.path} completed in ${metric.durationMs}ms`); } } /** * Get aggregated statistics */ getStats(): PerformanceStats { if (this.metrics.length === 0) { return { totalRequests: 0, avgDurationMs: 0, p50DurationMs: 0, p95DurationMs: 0, p99DurationMs: 0, maxDurationMs: 0, slowRequests: 0, byPath: {}, }; } const durations = this.metrics.map((m) => m.durationMs).sort((a, b) => a - b); const total = durations.reduce((sum, d) => sum + d, 0); // Calculate percentiles const p50Index = Math.floor(durations.length * 0.5); const p95Index = Math.floor(durations.length * 0.95); const p99Index = Math.floor(durations.length * 0.99); // Aggregate by path const byPath: Record<string, PathStats> = {}; for (const metric of this.metrics) { if (!byPath[metric.path]) { byPath[metric.path] = { count: 0, avgDurationMs: 0, maxDurationMs: 0, slowCount: 0, }; } const pathStats = byPath[metric.path]; pathStats.count++; pathStats.avgDurationMs = (pathStats.avgDurationMs * (pathStats.count - 1) + metric.durationMs) / pathStats.count; pathStats.maxDurationMs = Math.max(pathStats.maxDurationMs, metric.durationMs); if (metric.durationMs > this.config.slowThresholdMs) { pathStats.slowCount++; } } return { totalRequests: this.metrics.length, avgDurationMs: Math.round(total / durations.length), p50DurationMs: durations[p50Index] ?? 0, p95DurationMs: durations[p95Index] ?? 0, p99DurationMs: durations[p99Index] ?? 0, maxDurationMs: durations[durations.length - 1] ?? 0, slowRequests: this.metrics.filter((m) => m.durationMs > this.config.slowThresholdMs).length, byPath, }; } /** * Clear all metrics */ clear(): void { this.metrics = []; } /** * Get recent metrics */ getRecentMetrics(count: number = 100): RequestMetrics[] { return this.metrics.slice(-count); } } /** * Global performance collector instance */ let globalCollector: PerformanceCollector | null = null; /** * Get or create the global performance collector */ function getCollector(config: PerformanceConfig): PerformanceCollector { globalCollector ??= new PerformanceCollector(config); return globalCollector; } /** * Get performance statistics */ export function getPerformanceStats(): PerformanceStats { if (!globalCollector) { return { totalRequests: 0, avgDurationMs: 0, p50DurationMs: 0, p95DurationMs: 0, p99DurationMs: 0, maxDurationMs: 0, slowRequests: 0, byPath: {}, }; } return globalCollector.getStats(); } /** * Get recent request metrics */ export function getRecentMetrics(count: number = 100): RequestMetrics[] { if (!globalCollector) { return []; } return globalCollector.getRecentMetrics(count); } /** * Clear performance metrics */ export function clearPerformanceMetrics(): void { if (globalCollector) { globalCollector.clear(); } } /** * Check if path should be excluded from tracking */ function shouldExclude(path: string, config: PerformanceConfig): boolean { if (config.excludePaths) { for (const excludePath of config.excludePaths) { if (path.startsWith(excludePath)) { return true; } } } return false; } /** * Create performance monitoring middleware * * @param config - Performance configuration * @returns Express middleware function */ export function createPerformanceMiddleware( config: Partial<PerformanceConfig> = {} ): (req: Request, res: Response, next: NextFunction) => void { const finalConfig = { ...DEFAULT_PERFORMANCE_CONFIG, ...config }; const collector = getCollector(finalConfig); return (req: Request, res: Response, next: NextFunction): void => { // Skip excluded paths if (shouldExclude(req.path, finalConfig)) { next(); return; } const startTime = process.hrtime.bigint(); // Track response timing using 'close' event for metrics collection // Note: We cannot set headers in 'finish' or 'close' events as response is already sent // The X-Response-Time header must be set before res.send() in the route handler res.on("close", () => { const endTime = process.hrtime.bigint(); const durationNs = Number(endTime - startTime); const durationMs = Math.round(durationNs / 1_000_000); const requestId = (req as Request & { requestId?: string }).requestId; collector.record({ method: req.method, path: req.path, statusCode: res.statusCode, durationMs, timestamp: new Date(), requestId, }); }); // Set response time header before response is sent (if json method exists) if (typeof res.json === "function") { const originalJson = res.json.bind(res); res.json = function (body: unknown) { const endTime = process.hrtime.bigint(); const durationNs = Number(endTime - startTime); const durationMs = Math.round(durationNs / 1_000_000); if (!res.headersSent) { res.setHeader("X-Response-Time", `${durationMs}ms`); } return originalJson(body); }; } next(); }; } export default createPerformanceMiddleware;

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/keyurgolani/ThoughtMcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server