/** * Metrics Plugin * * Automatically collects metrics for requests and responses */ import { defaultMetricsCollector } from '../../observability/metrics.js'; import type { Plugin, PluginContext, PluginResponse, MetricsPluginConfig } from '../types.js'; /** * Default configuration */ const DEFAULT_CONFIG: Required = { enabled: true, prefix: 'elasticsearch', recordDuration: true, recordSize: true, recordResponseSize: true, }; /** * Create metrics plugin */ export function createMetricsPlugin(config: MetricsPluginConfig = {}): Plugin { const pluginConfig = { ...DEFAULT_CONFIG, ...config }; const metrics = defaultMetricsCollector; return { name: 'metrics', version: '1.0.0', priority: 20, // Execute early, after logging beforeRequest: (context: PluginContext) => { if (!pluginConfig.enabled) { return context; } // Record request counter metrics.recordCounter(`${pluginConfig.prefix}.requests`, 1, { method: context.request.method, path: extractIndexFromPath(context.request.path), }); // Record request size if enabled if (pluginConfig.recordSize && context.request.body) { const size = Buffer.byteLength(JSON.stringify(context.request.body), 'utf8'); metrics.recordHistogram(`${pluginConfig.prefix}.request.size`, size, { method: context.request.method, }); } return context; }, afterResponse: (context: PluginContext, response: PluginResponse) => { if (!pluginConfig.enabled) { return response; } const duration = Date.now() - context.request.startTime; // Record request duration if enabled if (pluginConfig.recordDuration) { metrics.recordHistogram(`${pluginConfig.prefix}.request.duration`, duration, { method: context.request.method, path: extractIndexFromPath(context.request.path), status: response.statusCode.toString(), }); } // Record response size if enabled if (pluginConfig.recordResponseSize && response.body) { const size = Buffer.byteLength(JSON.stringify(response.body), 'utf8'); metrics.recordHistogram(`${pluginConfig.prefix}.response.size`, size, { method: context.request.method, status: response.statusCode.toString(), }); } // Record success/failure const success = response.statusCode >= 200 && response.statusCode < 300; metrics.recordCounter( `${pluginConfig.prefix}.requests.${success ? 'success' : 'failure'}`, 1, { method: context.request.method, status: response.statusCode.toString(), } ); return response; }, onError: (context) => { if (!pluginConfig.enabled) { return null; } const duration = Date.now() - context.request.startTime; // Record error metrics.recordCounter(`${pluginConfig.prefix}.errors`, 1, { method: context.request.method, path: extractIndexFromPath(context.request.path), error: context.error.name, }); // Record error duration if (pluginConfig.recordDuration) { metrics.recordHistogram(`${pluginConfig.prefix}.error.duration`, duration, { method: context.request.method, error: context.error.name, }); } // Don't handle error return null; }, }; } /** * Extract index name from path */ function extractIndexFromPath(path: string): string { // Remove leading slash const cleanPath = path.startsWith('/') ? path.slice(1) : path; // Split by slash and get first segment const segments = cleanPath.split('/'); // Common patterns: // /{index}/_search // /{index}/_doc/{id} // /_cat/indices if (segments[0].startsWith('_')) { return segments[0]; // API endpoint like _cat, _search } return segments[0] || 'unknown'; }