UNPKG

cfn-forge

Version:

CloudFormation deployment automation tool with git-based workflows

383 lines (324 loc) 11.3 kB
/** * Logger utility for consistent CLI output */ const chalk = require('chalk'); const ora = require('ora'); class Logger { constructor() { this.debugEnabled = process.env.DEBUG || process.env.CFN_FORGE_DEBUG; this.errorHistory = []; this.context = {}; this.timestampEnabled = process.env.CFN_FORGE_TIMESTAMPS || false; } formatTimestamp() { if (!this.timestampEnabled) return ''; const now = new Date(); const time = now.toLocaleTimeString('en-US', { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', }); const date = now.toLocaleDateString('en-US', { month: '2-digit', day: '2-digit', }); return `${chalk.gray(`[${date} ${time}]`)} `; } enableTimestamps() { this.timestampEnabled = true; } disableTimestamps() { this.timestampEnabled = false; } info(message) { console.log(`${this.formatTimestamp()}${chalk.white('[INFO]')} ${message}`); } success(message) { console.log(`${this.formatTimestamp()}${chalk.green('[OK]')} ${message}`); } warn(message) { console.log(`${this.formatTimestamp()}${chalk.yellow('[WARN]')} ${message}`); } error(message) { console.error(`${this.formatTimestamp()}${chalk.red('[ERROR]')} ${message}`); } debug(message) { if (this.debugEnabled) { console.log(`${this.formatTimestamp()}${chalk.white('[DEBUG]')} ${chalk.white(message)}`); } } log(message) { console.log(`${this.formatTimestamp()}${message}`); } spinner(text) { return ora({ text, spinner: 'dots', color: 'blue', }); } table(data) { console.table(data); } json(data, pretty = true) { if (pretty) { console.log(JSON.stringify(data, null, 2)); } else { console.log(JSON.stringify(data)); } } box(title, content) { const boxen = require('boxen'); console.log(boxen(content, { title, padding: 1, margin: 1, borderStyle: 'round', borderColor: 'blue', })); } section(title) { console.log(); console.log(chalk.white.bold(`=== ${title} ===`)); console.log(); } subsection(title) { console.log(); console.log(chalk.cyan(`--- ${title} ---`)); } list(items, ordered = false) { items.forEach((item, index) => { const prefix = ordered ? `${index + 1}.` : '•'; console.log(` ${prefix} ${item}`); }); } progress(current, total, message = '') { const percentage = Math.round((current / total) * 100); const filled = Math.round((current / total) * 20); const bar = '█'.repeat(filled) + '░'.repeat(20 - filled); process.stdout.write(`\r${bar} ${percentage}% ${message}`); if (current === total) { process.stdout.write('\n'); } } clear() { process.stdout.write('\x1Bc'); } divider() { console.log(chalk.white('─'.repeat(process.stdout.columns || 80))); } async confirm(message, defaultValue = false) { const inquirer = require('inquirer'); const { confirmed } = await inquirer.prompt([{ type: 'confirm', name: 'confirmed', message, default: defaultValue, }]); return confirmed; } formatError(error) { let message = error.message || 'Unknown error'; // AWS-specific error formatting if (error.code) { switch (error.code) { case 'CredentialsError': message = 'AWS credentials not configured'; break; case 'ValidationError': message = `CloudFormation validation error: ${message}`; break; case 'AccessDenied': message = 'AWS access denied - check your permissions'; break; case 'NoSuchBucket': message = 'S3 bucket not found'; break; case 'StackNotFound': message = 'CloudFormation stack not found'; break; } } return message; } formatDuration(ms) { if (ms < 1000) return `${ms}ms`; if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`; if (ms < 3600000) return `${Math.floor(ms / 60000)}m ${Math.floor((ms % 60000) / 1000)}s`; return `${Math.floor(ms / 3600000)}h ${Math.floor((ms % 3600000) / 60000)}m`; } formatSize(bytes) { const units = ['B', 'KB', 'MB', 'GB']; let size = bytes; let unitIndex = 0; while (size >= 1024 && unitIndex < units.length - 1) { size /= 1024; unitIndex++; } return `${size.toFixed(1)} ${units[unitIndex]}`; } formatCost(amount, currency = 'USD') { const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency, }); return formatter.format(amount); } // Enhanced error handling methods setContext(key, value) { this.context[key] = value; } clearContext() { this.context = {}; } errorWithContext(error, operation = null) { const timestamp = new Date().toISOString(); const errorInfo = { timestamp, operation, message: error.message, code: error.code, stack: this.debugEnabled ? error.stack : null, context: { ...this.context }, }; this.errorHistory.push(errorInfo); // Format and display error const formattedMessage = this.formatDetailedError(error, operation); console.error(`${this.formatTimestamp()}${chalk.red('[ERROR]')} ${formattedMessage}`); // Show context if available if (Object.keys(this.context).length > 0) { try { // Create a safe copy of context without circular references const safeContext = JSON.parse(JSON.stringify(this.context, (key, value) => { if (value && typeof value === 'object' && value.constructor && value.constructor.name === 'Command') { return '[Command Object]'; } return value; })); this.debug(`Context: ${JSON.stringify(safeContext, null, 2)}`); } catch (error) { this.debug('Context: [Unable to serialize - contains circular references]'); } } return errorInfo; } formatDetailedError(error, operation = null) { let message = error.message || 'Unknown error'; const prefix = operation ? `[${operation}] ` : ''; // AWS-specific error handling if (error.code) { const suggestion = this.getErrorSuggestion(error.code, error.message); if (suggestion) { message += `\n\n${chalk.cyan('Suggestion')}: ${suggestion}`; } } // Add stack trace in debug mode if (this.debugEnabled && error.stack) { message += `\n\n${chalk.white('Stack trace:')}\n${chalk.white(error.stack)}`; } return `${prefix}${message}`; } getErrorSuggestion(errorCode, errorMessage) { const suggestions = { CredentialsError: 'Run `aws configure` or `cfn-forge config --aws` to set up credentials', ValidationError: 'Check your CloudFormation template syntax with `cfn-forge validate`', AccessDenied: 'Verify your AWS permissions or try a different AWS profile', NoSuchBucket: 'Create the S3 bucket or check the bucket name configuration', StackNotFound: 'The CloudFormation stack may not exist yet - try deploying first', ThrottlingException: 'AWS is rate limiting requests - wait a moment and try again', LimitExceededException: 'AWS service limits reached - check your account limits', InvalidParameterValue: 'Check parameter values in your .cfn-forge.yaml configuration', AlreadyExistsException: 'Resource already exists - use --force or check for naming conflicts', InsufficientCapabilitiesException: 'Add IAM capabilities to your CloudFormation template', ENOENT: 'File or directory not found - check file paths in your configuration', EACCES: 'Permission denied - check file permissions or run with appropriate privileges', ENOTDIR: 'Expected directory but found file - check your path configuration', EISDIR: 'Expected file but found directory - check your path configuration', }; // Check for specific error message patterns if (errorMessage) { if (errorMessage.includes('template format error')) { return 'Validate your CloudFormation template format and fix syntax errors'; } if (errorMessage.includes('git')) { return 'Initialize git repository: `git init && git add . && git commit -m "Initial commit"`'; } if (errorMessage.includes('AWS_ACCESS_KEY')) { return 'Set AWS credentials: `export AWS_ACCESS_KEY_ID=...` and `export AWS_SECRET_ACCESS_KEY=...`'; } if (errorMessage.includes('region')) { return 'Set AWS region: `export AWS_DEFAULT_REGION=us-east-1` or configure in .cfn-forge.yaml'; } } return suggestions[errorCode]; } reportErrorSummary() { if (this.errorHistory.length === 0) { this.success('No errors encountered during execution'); return; } this.section('Error Summary'); this.errorHistory.forEach((error, index) => { console.log(`${index + 1}. ${chalk.red(error.operation || 'Unknown operation')}`); console.log(` ${error.message}`); console.log(` ${chalk.white(error.timestamp)}`); }); this.subsection('Troubleshooting'); this.info('For detailed debugging, run with DEBUG=1'); this.info('For AWS issues, check: aws sts get-caller-identity'); this.info('For git issues, check: git status'); } fatal(error, operation = null, exitCode = 1) { this.errorWithContext(error, operation); this.reportErrorSummary(); process.exit(exitCode); } async withErrorHandling(operation, asyncFn, context = {}) { this.setContext('operation', operation); Object.entries(context).forEach(([key, value]) => { this.setContext(key, value); }); try { this.debug(`Starting operation: ${operation}`); const startTime = Date.now(); const result = await asyncFn(); const duration = Date.now() - startTime; this.debug(`Completed operation: ${operation} (${this.formatDuration(duration)})`); return result; } catch (error) { this.errorWithContext(error, operation); throw error; } finally { this.clearContext(); } } trackOperation(operation, context = {}) { return { start: () => { this.setContext('operation', operation); Object.entries(context).forEach(([key, value]) => { this.setContext(key, value); }); this.debug(`Starting: ${operation}`); return Date.now(); }, success: (startTime, message = null) => { const duration = Date.now() - startTime; this.debug(`[OK] ${operation} completed (${this.formatDuration(duration)})`); if (message) this.success(message); this.clearContext(); }, error: (error, startTime = null) => { const duration = startTime ? Date.now() - startTime : null; const durationMsg = duration ? ` (failed after ${this.formatDuration(duration)})` : ''; this.errorWithContext(error, `${operation}${durationMsg}`); this.clearContext(); return error; }, }; } } // Create singleton instance const logger = new Logger(); module.exports = { logger, Logger };