vineguard-utils
Version:
Shared utilities for VineGuard - AI-powered testing orchestration
462 lines • 17.5 kB
JavaScript
/**
* 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