UNPKG

vineguard-utils

Version:

Shared utilities for VineGuard - AI-powered testing orchestration

462 lines 17.5 kB
/** * Health monitoring and status checks for VineGuard * Provides comprehensive health checks, metrics collection, and monitoring endpoints */ import { createPackageErrorHandler, ValidationError } from './errors.js'; import { existsSync } from 'fs'; import { join } from 'path'; const errorHandler = createPackageErrorHandler('utils'); // Health check status enum export var HealthStatus; (function (HealthStatus) { HealthStatus["HEALTHY"] = "healthy"; HealthStatus["DEGRADED"] = "degraded"; HealthStatus["UNHEALTHY"] = "unhealthy"; })(HealthStatus || (HealthStatus = {})); // Health monitor class export class HealthMonitor { packageName; checks = new Map(); customMetrics = new Map(); static globalStartTime = Date.now(); // Global start time for proper uptime tracking constructor(packageName) { this.packageName = packageName; } // Register a health check registerCheck(name, checkFn) { errorHandler.safeExecuteSync(() => { errorHandler.validator.string(name, 'name'); errorHandler.validator.required(checkFn, 'checkFn'); if (this.checks.has(name)) { throw new ValidationError(`Health check '${name}' is already registered`, 'name', name, this.packageName); } this.checks.set(name, checkFn); }); } // Remove a health check unregisterCheck(name) { this.checks.delete(name); } // Register custom metric registerMetric(name, metricFn) { errorHandler.safeExecuteSync(() => { errorHandler.validator.string(name, 'name'); errorHandler.validator.required(metricFn, 'metricFn'); this.customMetrics.set(name, metricFn); }); } // Run a single health check async runCheck(name) { const checkFn = this.checks.get(name); if (!checkFn) { throw new ValidationError(`Health check '${name}' not found`, 'name', name, this.packageName); } const startTime = Date.now(); try { const result = await checkFn(); const duration = Date.now() - startTime; return { ...result, timestamp: new Date(), duration }; } catch (error) { const duration = Date.now() - startTime; return { name, status: HealthStatus.UNHEALTHY, message: error instanceof Error ? error.message : 'Unknown error', details: { error: error instanceof Error ? error.message : String(error) }, timestamp: new Date(), duration }; } } // Run all health checks async runAllChecks() { const checkPromises = Array.from(this.checks.keys()).map(name => this.runCheck(name).catch(error => ({ name, status: HealthStatus.UNHEALTHY, message: error instanceof Error ? error.message : 'Failed to run check', timestamp: new Date(), duration: 0 }))); return Promise.all(checkPromises); } // Get system metrics getSystemMetrics() { const process = globalThis.process; if (!process) { return { memory: { used: 0, total: 0, percentage: 0 }, cpu: { loadAverage: [] }, process: { pid: 0, uptime: 0, version: 'unknown' } }; } const memUsage = process.memoryUsage(); const loadAverage = process.platform !== 'win32' ? process.loadavg?.() || [] : []; const customMetrics = {}; for (const [name, metricFn] of this.customMetrics.entries()) { try { customMetrics[name] = metricFn(); } catch (error) { // Log error but don't fail metrics collection console.warn(`Failed to collect metric '${name}':`, error); } } return { memory: { used: memUsage.heapUsed, total: memUsage.heapTotal, percentage: (memUsage.heapUsed / memUsage.heapTotal) * 100 }, cpu: { loadAverage }, process: { pid: process.pid, uptime: process.uptime(), version: process.version }, custom: Object.keys(customMetrics).length > 0 ? customMetrics : undefined }; } // Generate complete health report async getHealthReport() { const checks = await this.runAllChecks(); const metrics = this.getSystemMetrics(); // Determine overall status let overallStatus = HealthStatus.HEALTHY; const hasUnhealthy = checks.some(check => check.status === HealthStatus.UNHEALTHY); const hasDegraded = checks.some(check => check.status === HealthStatus.DEGRADED); if (hasUnhealthy) { overallStatus = HealthStatus.UNHEALTHY; } else if (hasDegraded) { overallStatus = HealthStatus.DEGRADED; } return { status: overallStatus, version: process.env.VINEGUARD_VERSION || '1.0.0', timestamp: new Date(), uptime: Date.now() - HealthMonitor.globalStartTime, checks, metrics }; } // Get readiness status (simpler check for container readiness) async isReady() { const checks = await this.runAllChecks(); return checks.every(check => check.status !== HealthStatus.UNHEALTHY); } // Get liveness status (simpler check for container liveness) async isAlive() { try { // Basic liveness check - ensure process is responsive const startTime = Date.now(); await new Promise(resolve => setTimeout(resolve, 1)); const responseTime = Date.now() - startTime; // If it takes more than 100ms for a simple timeout, something's wrong return responseTime < 100; } catch { return false; } } } // Default health checks export const createDefaultHealthChecks = () => ({ // Memory usage check memory: async () => { const metrics = new HealthMonitor().getSystemMetrics(); const memoryPercentage = metrics.memory.percentage; let status = HealthStatus.HEALTHY; let message = `Memory usage: ${memoryPercentage.toFixed(1)}%`; if (memoryPercentage > 90) { status = HealthStatus.UNHEALTHY; message = `High memory usage: ${memoryPercentage.toFixed(1)}%`; } else if (memoryPercentage > 75) { status = HealthStatus.DEGRADED; message = `Elevated memory usage: ${memoryPercentage.toFixed(1)}%`; } return { name: 'memory', status, message, details: metrics.memory }; }, // Process uptime check uptime: async () => { const uptime = process.uptime(); return { name: 'uptime', status: HealthStatus.HEALTHY, message: `Process uptime: ${Math.floor(uptime)}s`, details: { uptime } }; }, // Environment variables check environment: async () => { const requiredEnvVars = ['NODE_ENV']; const optionalEnvVars = ['VINEGUARD_VINEYARD_ROOT', 'VINEGUARD_LOG_LEVEL']; const missing = requiredEnvVars.filter(env => !process.env[env]); const present = optionalEnvVars.filter(env => process.env[env]); if (missing.length > 0) { return { name: 'environment', status: HealthStatus.UNHEALTHY, message: `Missing environment variables: ${missing.join(', ')}`, details: { missing, present } }; } return { name: 'environment', status: HealthStatus.HEALTHY, message: `Environment configured (${present.length} optional vars set)`, details: { nodeEnv: process.env.NODE_ENV, vineyardRoot: process.env.VINEGUARD_VINEYARD_ROOT, logLevel: process.env.VINEGUARD_LOG_LEVEL, present } }; }, // Project structure check projectStructure: async () => { const projectRoot = process.env.VINEGUARD_VINEYARD_ROOT || process.cwd(); const importantFiles = [ 'package.json', 'node_modules', 'src', 'test', 'tests', '__tests__', 'spec', 'jest.config.js', 'jest.config.ts', 'vitest.config.js', 'vitest.config.ts', 'playwright.config.js', 'playwright.config.ts', 'cypress.config.js', 'cypress.config.ts' ]; const found = importantFiles.filter(file => existsSync(join(projectRoot, file))); const hasPackageJson = found.includes('package.json'); const hasTestDir = found.some(f => ['test', 'tests', '__tests__', 'spec'].includes(f)); const hasTestConfig = found.some(f => f.includes('config')); let status = HealthStatus.HEALTHY; let message = `Project structure: ${found.length} key files found`; if (!hasPackageJson) { status = HealthStatus.UNHEALTHY; message = 'No package.json found in project root'; } else if (!hasTestDir && !hasTestConfig) { status = HealthStatus.DEGRADED; message = 'No test directories or config files found'; } return { name: 'projectStructure', status, message, details: { projectRoot, found, hasPackageJson, hasTestDir, hasTestConfig } }; }, // Dependencies check dependencies: async () => { const projectRoot = process.env.VINEGUARD_VINEYARD_ROOT || process.cwd(); const nodeModulesPath = join(projectRoot, 'node_modules'); const packageJsonPath = join(projectRoot, 'package.json'); if (!existsSync(packageJsonPath)) { return { name: 'dependencies', status: HealthStatus.UNHEALTHY, message: 'No package.json found', details: { projectRoot } }; } const hasNodeModules = existsSync(nodeModulesPath); if (!hasNodeModules) { return { name: 'dependencies', status: HealthStatus.DEGRADED, message: 'Dependencies not installed (no node_modules)', details: { projectRoot, packageJsonPath } }; } // Check for common testing frameworks const testingFrameworks = [ 'jest', 'vitest', 'mocha', 'jasmine', 'ava', 'tape', 'playwright', 'cypress', 'puppeteer', '@testing-library/react' ]; const installedFrameworks = testingFrameworks.filter(framework => existsSync(join(nodeModulesPath, framework))); return { name: 'dependencies', status: HealthStatus.HEALTHY, message: `Dependencies OK (${installedFrameworks.length} testing frameworks found)`, details: { projectRoot, hasNodeModules, installedFrameworks } }; }, // Disk space check diskSpace: async () => { const projectRoot = process.env.VINEGUARD_VINEYARD_ROOT || process.cwd(); try { const nodeModulesPath = join(projectRoot, 'node_modules'); const hasNodeModules = existsSync(nodeModulesPath); // Rough estimation of disk usage let status = HealthStatus.HEALTHY; let message = 'Disk space appears adequate'; return { name: 'diskSpace', status, message, details: { projectRoot, hasNodeModules, warning: 'Detailed disk space metrics require native modules' } }; } catch (error) { return { name: 'diskSpace', status: HealthStatus.DEGRADED, message: 'Could not check disk space', details: { error: error instanceof Error ? error.message : String(error) } }; } } }); // Global health monitor instance let globalHealthMonitor = null; export const getGlobalHealthMonitor = () => { if (!globalHealthMonitor) { globalHealthMonitor = new HealthMonitor('global'); // Register default health checks const defaultChecks = createDefaultHealthChecks(); Object.entries(defaultChecks).forEach(([name, checkFn]) => { globalHealthMonitor.registerCheck(name, checkFn); }); } return globalHealthMonitor; }; // Helper function to create HTTP handlers for common frameworks export const createHealthHandlers = (monitor = getGlobalHealthMonitor()) => ({ // Express.js style handler for health endpoint health: async (_req, res) => { try { const report = await monitor.getHealthReport(); const statusCode = report.status === HealthStatus.UNHEALTHY ? 503 : 200; res.status(statusCode).json(report); } catch (error) { res.status(500).json({ status: HealthStatus.UNHEALTHY, message: 'Health check failed', error: error instanceof Error ? error.message : String(error) }); } }, // Express.js style handler for readiness probe ready: async (_req, res) => { try { const isReady = await monitor.isReady(); const statusCode = isReady ? 200 : 503; res.status(statusCode).json({ ready: isReady, timestamp: new Date().toISOString() }); } catch (error) { res.status(500).json({ ready: false, error: error instanceof Error ? error.message : String(error) }); } }, // Express.js style handler for liveness probe live: async (_req, res) => { try { const isAlive = await monitor.isAlive(); const statusCode = isAlive ? 200 : 503; res.status(statusCode).json({ alive: isAlive, timestamp: new Date().toISOString() }); } catch (error) { res.status(500).json({ alive: false, error: error instanceof Error ? error.message : String(error) }); } }, // Metrics endpoint metrics: async (_req, res) => { try { const metrics = monitor.getSystemMetrics(); res.status(200).json({ metrics, timestamp: new Date().toISOString() }); } catch (error) { res.status(500).json({ error: error instanceof Error ? error.message : String(error) }); } } }); // Utility function to format health report as Prometheus metrics export const formatPrometheusMetrics = (report) => { const lines = []; // Overall health status lines.push(`# HELP vineguard_health_status Overall health status (0=unhealthy, 1=degraded, 2=healthy)`); lines.push(`# TYPE vineguard_health_status gauge`); const statusValue = report.status === HealthStatus.HEALTHY ? 2 : report.status === HealthStatus.DEGRADED ? 1 : 0; lines.push(`vineguard_health_status ${statusValue}`); // Memory metrics lines.push(`# HELP vineguard_memory_usage_bytes Memory usage in bytes`); lines.push(`# TYPE vineguard_memory_usage_bytes gauge`); lines.push(`vineguard_memory_usage_bytes ${report.metrics.memory.used}`); lines.push(`# HELP vineguard_memory_usage_percentage Memory usage percentage`); lines.push(`# TYPE vineguard_memory_usage_percentage gauge`); lines.push(`vineguard_memory_usage_percentage ${report.metrics.memory.percentage}`); // Uptime lines.push(`# HELP vineguard_uptime_seconds Process uptime in seconds`); lines.push(`# TYPE vineguard_uptime_seconds counter`); lines.push(`vineguard_uptime_seconds ${Math.floor(report.uptime / 1000)}`); // Health check results lines.push(`# HELP vineguard_health_check_status Individual health check status`); lines.push(`# TYPE vineguard_health_check_status gauge`); report.checks.forEach(check => { const value = check.status === HealthStatus.HEALTHY ? 1 : 0; lines.push(`vineguard_health_check_status{check="${check.name}"} ${value}`); }); // Custom metrics if (report.metrics.custom) { Object.entries(report.metrics.custom).forEach(([name, value]) => { lines.push(`# HELP vineguard_custom_${name} Custom metric: ${name}`); lines.push(`# TYPE vineguard_custom_${name} gauge`); lines.push(`vineguard_custom_${name} ${value}`); }); } return lines.join('\n') + '\n'; }; //# sourceMappingURL=health.js.map