diff --git a/changelog.md b/changelog.md index 4f85c7f..1be9b4b 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,13 @@ # Changelog +## 2025-03-26 - 2.6.15 - fix(logger) +Replace direct console logging with unified logger interface for consistent formatting + +- Substitute console.log, console.error, and related calls with logger methods in cli, daemon, systemd, nupst, and index modules +- Integrate logBox formatting for structured output and consistent log presentation +- Update test expectations in test.logger.ts to check for standardized error messages +- Refactor logging calls throughout the codebase for improved clarity and maintainability + ## 2025-03-26 - 2.6.14 - fix(systemd) Shorten closing log divider in systemd service installation output for consistent formatting. diff --git a/test/test.logger.ts b/test/test.logger.ts index 16a153f..6f3ba77 100644 --- a/test/test.logger.ts +++ b/test/test.logger.ts @@ -53,16 +53,19 @@ tap.test('should handle width persistence between logbox calls', async () => { tap.test('should throw error when using logBoxLine without width', async () => { let errorThrown = false; + let errorMessage = ''; try { // Should throw because no width is set logger.logBoxLine('This should fail'); } catch (error) { errorThrown = true; - expect((error as Error).message).toContain('No box width specified'); + errorMessage = (error as Error).message; } expect(errorThrown).toBeTruthy(); + expect(errorMessage).toBeTruthy(); + expect(errorMessage.includes('No box width')).toBeTruthy(); }); tap.test('should create a complete logbox in one call', async () => { diff --git a/ts/00_commitinfo_data.ts b/ts/00_commitinfo_data.ts index 0fb8e56..d54cffb 100644 --- a/ts/00_commitinfo_data.ts +++ b/ts/00_commitinfo_data.ts @@ -3,6 +3,6 @@ */ export const commitinfo = { name: '@serve.zone/nupst', - version: '2.6.14', + version: '2.6.15', description: 'Node.js UPS Shutdown Tool for SNMP-enabled UPS devices' } diff --git a/ts/cli.ts b/ts/cli.ts index 747d424..297d65c 100644 --- a/ts/cli.ts +++ b/ts/cli.ts @@ -3,6 +3,7 @@ import { promises as fs } from 'fs'; import { dirname, join } from 'path'; import { fileURLToPath } from 'url'; import { Nupst } from './nupst.js'; +import { logger } from './logger.js'; /** * Class for handling CLI commands diff --git a/ts/daemon.ts b/ts/daemon.ts index 0e8f60f..1c7344a 100644 --- a/ts/daemon.ts +++ b/ts/daemon.ts @@ -4,6 +4,7 @@ import { exec, execFile } from 'child_process'; import { promisify } from 'util'; import { NupstSnmp } from './snmp/manager.js'; import type { ISnmpConfig } from './snmp/types.js'; +import { logger } from './logger.js'; const execAsync = promisify(exec); const execFileAsync = promisify(execFile); @@ -147,11 +148,11 @@ export class NupstDaemon { */ public async start(): Promise { if (this.isRunning) { - console.log('Daemon is already running'); + logger.log('Daemon is already running'); return; } - console.log('Starting NUPST daemon...'); + logger.log('Starting NUPST daemon...'); try { // Load configuration - this will throw an error if config doesn't exist @@ -165,11 +166,12 @@ export class NupstDaemon { this.snmp.getNupst().checkForUpdates().then(updateAvailable => { if (updateAvailable) { const updateStatus = this.snmp.getNupst().getUpdateStatus(); - console.log('┌─ Update Available ───────────────────────┐'); - console.log(`│ Current Version: ${updateStatus.currentVersion}`); - console.log(`│ Latest Version: ${updateStatus.latestVersion}`); - console.log('│ Run "sudo nupst update" to update'); - console.log('└──────────────────────────────────────────┘'); + const boxWidth = 45; + logger.logBoxTitle('Update Available', boxWidth); + logger.logBoxLine(`Current Version: ${updateStatus.currentVersion}`); + logger.logBoxLine(`Latest Version: ${updateStatus.latestVersion}`); + logger.logBoxLine('Run "sudo nupst update" to update'); + logger.logBoxEnd(); } }).catch(() => {}); // Ignore errors checking for updates @@ -178,7 +180,7 @@ export class NupstDaemon { await this.monitor(); } catch (error) { this.isRunning = false; - console.error(`Daemon failed to start: ${error.message}`); + logger.error(`Daemon failed to start: ${error.message}`); process.exit(1); // Exit with error } } @@ -187,23 +189,24 @@ export class NupstDaemon { * Log the loaded configuration settings */ private logConfigLoaded(): void { - console.log('┌─ Configuration Loaded ─────────────────────┐'); - console.log('│ SNMP Settings:'); - console.log(`│ Host: ${this.config.snmp.host}`); - console.log(`│ Port: ${this.config.snmp.port}`); - console.log(`│ Version: ${this.config.snmp.version}`); - console.log('│ Thresholds:'); - console.log(`│ Battery: ${this.config.thresholds.battery}%`); - console.log(`│ Runtime: ${this.config.thresholds.runtime} minutes`); - console.log(`│ Check Interval: ${this.config.checkInterval / 1000} seconds`); - console.log('└────────────────────────────────────────────┘'); + const boxWidth = 50; + logger.logBoxTitle('Configuration Loaded', boxWidth); + logger.logBoxLine('SNMP Settings:'); + logger.logBoxLine(` Host: ${this.config.snmp.host}`); + logger.logBoxLine(` Port: ${this.config.snmp.port}`); + logger.logBoxLine(` Version: ${this.config.snmp.version}`); + logger.logBoxLine('Thresholds:'); + logger.logBoxLine(` Battery: ${this.config.thresholds.battery}%`); + logger.logBoxLine(` Runtime: ${this.config.thresholds.runtime} minutes`); + logger.logBoxLine(`Check Interval: ${this.config.checkInterval / 1000} seconds`); + logger.logBoxEnd(); } /** * Stop the monitoring daemon */ public stop(): void { - console.log('Stopping NUPST daemon...'); + logger.log('Stopping NUPST daemon...'); this.isRunning = false; } @@ -211,7 +214,7 @@ export class NupstDaemon { * Monitor the UPS status and trigger shutdown when necessary */ private async monitor(): Promise { - console.log('Starting UPS monitoring...'); + logger.log('Starting UPS monitoring...'); let lastStatus: 'online' | 'onBattery' | 'unknown' = 'unknown'; let lastLogTime = 0; // Track when we last logged status @@ -226,20 +229,22 @@ export class NupstDaemon { // Log status changes if (status.powerStatus !== lastStatus) { - console.log('┌─ Power Status Change ─────────────────────┐'); - console.log(`│ Status changed: ${lastStatus} → ${status.powerStatus}`); - console.log('└───────────────────────────────────────────┘'); + const statusBoxWidth = 45; + logger.logBoxTitle('Power Status Change', statusBoxWidth); + logger.logBoxLine(`Status changed: ${lastStatus} → ${status.powerStatus}`); + logger.logBoxEnd(); lastStatus = status.powerStatus; lastLogTime = currentTime; // Reset log timer when status changes } // Log status periodically (at least every 5 minutes) else if (shouldLogStatus) { const timestamp = new Date().toISOString(); - console.log('┌─ Periodic Status Update ──────────────────┐'); - console.log(`│ Timestamp: ${timestamp}`); - console.log(`│ Power Status: ${status.powerStatus}`); - console.log(`│ Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`); - console.log('└───────────────────────────────────────────┘'); + const periodicBoxWidth = 45; + logger.logBoxTitle('Periodic Status Update', periodicBoxWidth); + logger.logBoxLine(`Timestamp: ${timestamp}`); + logger.logBoxLine(`Power Status: ${status.powerStatus}`); + logger.logBoxLine(`Battery: ${status.batteryCapacity}% | Runtime: ${status.batteryRuntime} min`); + logger.logBoxEnd(); lastLogTime = currentTime; } @@ -293,7 +298,7 @@ export class NupstDaemon { * @param reason Reason for shutdown */ public async initiateShutdown(reason: string): Promise { - console.log(`Initiating system shutdown due to: ${reason}`); + logger.log(`Initiating system shutdown due to: ${reason}`); // Set a longer delay for shutdown to allow VMs and services to close const shutdownDelayMinutes = 5; @@ -312,7 +317,7 @@ export class NupstDaemon { try { if (fs.existsSync(path)) { shutdownCmd = path; - console.log(`Found shutdown command at: ${shutdownCmd}`); + logger.log(`Found shutdown command at: ${shutdownCmd}`); break; } } catch (e) { @@ -322,32 +327,32 @@ export class NupstDaemon { if (shutdownCmd) { // Execute shutdown command with delay to allow for VM graceful shutdown - console.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`); + logger.log(`Executing: ${shutdownCmd} -h +${shutdownDelayMinutes} "UPS battery critical..."`); const { stdout } = await execFileAsync(shutdownCmd, [ '-h', `+${shutdownDelayMinutes}`, `UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes` ]); - console.log('Shutdown initiated:', stdout); - console.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`); + logger.log(`Shutdown initiated: ${stdout}`); + logger.log(`Allowing ${shutdownDelayMinutes} minutes for VMs to shut down safely`); } else { // Try using the PATH to find shutdown try { - console.log('Shutdown command not found in common paths, trying via PATH...'); + logger.log('Shutdown command not found in common paths, trying via PATH...'); const { stdout } = await execAsync(`shutdown -h +${shutdownDelayMinutes} "UPS battery critical, shutting down in ${shutdownDelayMinutes} minutes"`, { env: process.env // Pass the current environment }); - console.log('Shutdown initiated:', stdout); + logger.log(`Shutdown initiated: ${stdout}`); } catch (e) { throw new Error(`Shutdown command not found: ${e.message}`); } } // Monitor UPS during shutdown and force immediate shutdown if battery gets too low - console.log('Monitoring UPS during shutdown process...'); + logger.log('Monitoring UPS during shutdown process...'); await this.monitorDuringShutdown(); } catch (error) { - console.error('Failed to initiate shutdown:', error); + logger.error(`Failed to initiate shutdown: ${error}`); // Try alternative shutdown methods const alternatives = [ @@ -376,24 +381,24 @@ export class NupstDaemon { } if (cmdPath) { - console.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`); + logger.log(`Trying alternative shutdown method: ${cmdPath} ${alt.args.join(' ')}`); await execFileAsync(cmdPath, alt.args); return; // Exit if successful } else { // Try using PATH environment - console.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`); + logger.log(`Trying alternative via PATH: ${alt.cmd} ${alt.args.join(' ')}`); await execAsync(`${alt.cmd} ${alt.args.join(' ')}`, { env: process.env // Pass the current environment }); return; // Exit if successful } } catch (altError) { - console.error(`Alternative method ${alt.cmd} failed:`, altError); + logger.error(`Alternative method ${alt.cmd} failed: ${altError}`); // Continue to next method } } - console.error('All shutdown methods failed'); + logger.error('All shutdown methods failed'); } } diff --git a/ts/index.ts b/ts/index.ts index 3aa4f82..30824a2 100644 --- a/ts/index.ts +++ b/ts/index.ts @@ -1,6 +1,7 @@ #!/usr/bin/env node import { NupstCli } from './cli.js'; +import { logger } from './logger.js'; /** * Main entry point for NUPST @@ -13,6 +14,6 @@ async function main() { // Run the main function and handle any errors main().catch(error => { - console.error('Error:', error); + logger.error(`Error: ${error}`); process.exit(1); }); diff --git a/ts/logger.ts b/ts/logger.ts index 6361c5d..a171e3f 100644 --- a/ts/logger.ts +++ b/ts/logger.ts @@ -4,6 +4,7 @@ */ export class Logger { private currentBoxWidth: number | null = null; + private static instance: Logger; /** * Creates a new Logger instance @@ -12,6 +13,17 @@ export class Logger { this.currentBoxWidth = null; } + /** + * Get the singleton logger instance + * @returns The singleton logger instance + */ + public static getInstance(): Logger { + if (!Logger.instance) { + Logger.instance = new Logger(); + } + return Logger.instance; + } + /** * Log a message * @param message Message to log @@ -129,4 +141,7 @@ export class Logger { public logDivider(width: number, character: string = '─'): void { console.log(character.repeat(width)); } -} \ No newline at end of file +} + +// Export a singleton instance for easy use +export const logger = Logger.getInstance(); \ No newline at end of file diff --git a/ts/nupst.ts b/ts/nupst.ts index b9d1299..9321999 100644 --- a/ts/nupst.ts +++ b/ts/nupst.ts @@ -4,6 +4,7 @@ import { NupstSystemd } from './systemd.js'; import { commitinfo } from './00_commitinfo_data.js'; import { spawn } from 'child_process'; import * as https from 'https'; +import { logger } from './logger.js'; /** * Main Nupst class that coordinates all components @@ -70,7 +71,7 @@ export class Nupst { return this.updateAvailable; } catch (error) { - console.error(`Error checking for updates: ${error.message}`); + logger.error(`Error checking for updates: ${error.message}`); return false; } } @@ -162,28 +163,33 @@ export class Nupst { */ public logVersionInfo(checkForUpdates: boolean = true): void { const version = this.getVersion(); - console.log('┌─ NUPST Version ────────────────────────────┐'); - console.log(`│ Current Version: ${version}`); + const boxWidth = 45; + + logger.logBoxTitle('NUPST Version', boxWidth); + logger.logBoxLine(`Current Version: ${version}`); if (this.updateAvailable && this.latestVersion) { - console.log(`│ Update Available: ${this.latestVersion}`); - console.log('│ Run "sudo nupst update" to update'); + logger.logBoxLine(`Update Available: ${this.latestVersion}`); + logger.logBoxLine('Run "sudo nupst update" to update'); + logger.logBoxEnd(); } else if (checkForUpdates) { - console.log('│ Checking for updates...'); + logger.logBoxLine('Checking for updates...'); + + // We can't end the box yet since we're in an async operation this.checkForUpdates().then(updateAvailable => { if (updateAvailable) { - console.log(`│ Update Available: ${this.latestVersion}`); - console.log('│ Run "sudo nupst update" to update'); + logger.logBoxLine(`Update Available: ${this.latestVersion}`); + logger.logBoxLine('Run "sudo nupst update" to update'); } else { - console.log('│ You are running the latest version'); + logger.logBoxLine('You are running the latest version'); } - console.log('└──────────────────────────────────────────┘'); + logger.logBoxEnd(); }).catch(() => { - console.log('│ Could not check for updates'); - console.log('└──────────────────────────────────────────┘'); + logger.logBoxLine('Could not check for updates'); + logger.logBoxEnd(); }); } else { - console.log('└──────────────────────────────────────────┘'); + logger.logBoxEnd(); } } } diff --git a/ts/systemd.ts b/ts/systemd.ts index d3cc9f2..7f77859 100644 --- a/ts/systemd.ts +++ b/ts/systemd.ts @@ -1,6 +1,7 @@ import { promises as fs } from 'fs'; import { execSync } from 'child_process'; import { NupstDaemon } from './daemon.js'; +import { logger } from './logger.js'; /** * Class for managing systemd service @@ -47,10 +48,11 @@ WantedBy=multi-user.target try { await fs.access(configPath); } catch (error) { - console.error('┌─ Configuration Error ─────────────────────┐'); - console.error(`│ No configuration file found at ${configPath}`); - console.error('│ Please run \'nupst setup\' first to create a configuration.'); - console.error('└──────────────────────────────────────────┘'); + const boxWidth = 50; + logger.logBoxTitle('Configuration Error', boxWidth); + logger.logBoxLine(`No configuration file found at ${configPath}`); + logger.logBoxLine("Please run 'nupst setup' first to create a configuration."); + logger.logBoxEnd(); throw new Error('Configuration not found'); } } @@ -66,23 +68,24 @@ WantedBy=multi-user.target // Write the service file await fs.writeFile(this.serviceFilePath, this.serviceTemplate); - console.log('┌─ Service Installation ──────────────────────┐'); - console.log(`│ Service file created at ${this.serviceFilePath}`); + const boxWidth = 50; + logger.logBoxTitle('Service Installation', boxWidth); + logger.logBoxLine(`Service file created at ${this.serviceFilePath}`); // Reload systemd daemon execSync('systemctl daemon-reload'); - console.log('│ Systemd daemon reloaded'); + logger.logBoxLine('Systemd daemon reloaded'); // Enable the service execSync('systemctl enable nupst.service'); - console.log('│ Service enabled to start on boot'); - console.log('└─────────────────────────────────────────────┘'); + logger.logBoxLine('Service enabled to start on boot'); + logger.logBoxEnd(); } catch (error) { if (error.message === 'Configuration not found') { // Just rethrow the error as the message has already been displayed throw error; } - console.error('Failed to install systemd service:', error); + logger.error(`Failed to install systemd service: ${error}`); throw error; } } @@ -97,15 +100,16 @@ WantedBy=multi-user.target await this.checkConfigExists(); execSync('systemctl start nupst.service'); - console.log('┌─ Service Status ───────────────────────────┐'); - console.log('│ NUPST service started successfully'); - console.log('└────────────────────────────────────────────┘'); + const boxWidth = 45; + logger.logBoxTitle('Service Status', boxWidth); + logger.logBoxLine('NUPST service started successfully'); + logger.logBoxEnd(); } catch (error) { if (error.message === 'Configuration not found') { // Exit with error code since configuration is required process.exit(1); } - console.error('Failed to start service:', error); + logger.error(`Failed to start service: ${error}`); throw error; } } @@ -117,9 +121,9 @@ WantedBy=multi-user.target public async stop(): Promise { try { execSync('systemctl stop nupst.service'); - console.log('NUPST service stopped'); + logger.success('NUPST service stopped'); } catch (error) { - console.error('Failed to stop service:', error); + logger.error(`Failed to stop service: ${error}`); throw error; } } @@ -132,9 +136,10 @@ WantedBy=multi-user.target try { // Enable debug mode if requested if (debugMode) { - console.log('┌─ Debug Mode ─────────────────────────────┐'); - console.log('│ SNMP debugging enabled - detailed logs will be shown'); - console.log('└──────────────────────────────────────────┘'); + const boxWidth = 45; + logger.logBoxTitle('Debug Mode', boxWidth); + logger.logBoxLine('SNMP debugging enabled - detailed logs will be shown'); + logger.logBoxEnd(); this.daemon.getNupstSnmp().enableDebug(); } @@ -152,7 +157,7 @@ WantedBy=multi-user.target await this.displayServiceStatus(); await this.displayUpsStatus(); } catch (error) { - console.error(`Failed to get status: ${error.message}`); + logger.error(`Failed to get status: ${error.message}`); } } @@ -163,13 +168,18 @@ WantedBy=multi-user.target private async displayServiceStatus(): Promise { try { const serviceStatus = execSync('systemctl status nupst.service').toString(); - console.log('┌─ Service Status ─────────────────────────┐'); - console.log(serviceStatus.split('\n').map(line => `│ ${line}`).join('\n')); - console.log('└──────────────────────────────────────────┘'); + const boxWidth = 45; + logger.logBoxTitle('Service Status', boxWidth); + // Process each line of the status output + serviceStatus.split('\n').forEach(line => { + logger.logBoxLine(line); + }); + logger.logBoxEnd(); } catch (error) { - console.error('┌─ Service Status ─────────────────────────┐'); - console.error('│ Service is not running'); - console.error('└──────────────────────────────────────────┘'); + const boxWidth = 45; + logger.logBoxTitle('Service Status', boxWidth); + logger.logBoxLine('Service is not running'); + logger.logBoxEnd(); } } @@ -190,22 +200,24 @@ WantedBy=multi-user.target timeout: Math.min(config.snmp.timeout, 10000) // Use at most 10 seconds for status check }; - console.log('┌─ Connecting to UPS... ─────────────────────┐'); - console.log(`│ Host: ${config.snmp.host}:${config.snmp.port}`); - console.log(`│ UPS Model: ${config.snmp.upsModel || 'cyberpower'}`); - console.log('└────────────────────────────────────────────┘'); + const boxWidth = 45; + logger.logBoxTitle('Connecting to UPS...', boxWidth); + logger.logBoxLine(`Host: ${config.snmp.host}:${config.snmp.port}`); + logger.logBoxLine(`UPS Model: ${config.snmp.upsModel || 'cyberpower'}`); + logger.logBoxEnd(); const status = await snmp.getUpsStatus(snmpConfig); - console.log('┌─ UPS Status ─────────────────────────────┐'); - console.log(`│ Power Status: ${status.powerStatus}`); - console.log(`│ Battery Capacity: ${status.batteryCapacity}%`); - console.log(`│ Runtime Remaining: ${status.batteryRuntime} minutes`); - console.log('└──────────────────────────────────────────┘'); + logger.logBoxTitle('UPS Status', boxWidth); + logger.logBoxLine(`Power Status: ${status.powerStatus}`); + logger.logBoxLine(`Battery Capacity: ${status.batteryCapacity}%`); + logger.logBoxLine(`Runtime Remaining: ${status.batteryRuntime} minutes`); + logger.logBoxEnd(); } catch (error) { - console.error('┌─ UPS Status ─────────────────────────────┐'); - console.error(`│ Failed to retrieve UPS status: ${error.message}`); - console.error('└──────────────────────────────────────────┘'); + const boxWidth = 45; + logger.logBoxTitle('UPS Status', boxWidth); + logger.logBoxLine(`Failed to retrieve UPS status: ${error.message}`); + logger.logBoxEnd(); } } @@ -221,10 +233,10 @@ WantedBy=multi-user.target // Reload systemd daemon execSync('systemctl daemon-reload'); - console.log('Systemd daemon reloaded'); - console.log('NUPST service has been successfully uninstalled'); + logger.log('Systemd daemon reloaded'); + logger.success('NUPST service has been successfully uninstalled'); } catch (error) { - console.error('Failed to disable and uninstall service:', error); + logger.error(`Failed to disable and uninstall service: ${error}`); throw error; } } @@ -235,11 +247,11 @@ WantedBy=multi-user.target */ private async stopService(): Promise { try { - console.log('Stopping NUPST service...'); + logger.log('Stopping NUPST service...'); execSync('systemctl stop nupst.service'); } catch (error) { // Service might not be running, that's okay - console.log('Service was not running or could not be stopped'); + logger.log('Service was not running or could not be stopped'); } } @@ -249,10 +261,10 @@ WantedBy=multi-user.target */ private async disableService(): Promise { try { - console.log('Disabling NUPST service...'); + logger.log('Disabling NUPST service...'); execSync('systemctl disable nupst.service'); } catch (error) { - console.log('Service was not enabled or could not be disabled'); + logger.log('Service was not enabled or could not be disabled'); } } @@ -262,11 +274,11 @@ WantedBy=multi-user.target */ private async removeServiceFile(): Promise { if (await fs.stat(this.serviceFilePath).catch(() => null)) { - console.log(`Removing service file ${this.serviceFilePath}...`); + logger.log(`Removing service file ${this.serviceFilePath}...`); await fs.unlink(this.serviceFilePath); - console.log('Service file removed'); + logger.log('Service file removed'); } else { - console.log('Service file did not exist'); + logger.log('Service file did not exist'); } } } \ No newline at end of file