@calmhive/calmhive-cli
Version:
🐝 Intelligent Claude CLI wrapper with rule injection, AFk background processing, voice interface, and adaptive retry
473 lines (401 loc) • 11.8 kB
JavaScript
/**
* Structured Error Logging Framework for Calmhive
*
* Provides consistent, structured logging throughout the application with:
* - JSON-structured log entries for machine parsing
* - Error correlation and tracking
* - Performance metrics integration
* - Log level management
* - Multiple output targets (console, file, external systems)
*/
const fs = require('fs');
const path = require('path');
const os = require('os');
class StructuredLogger {
constructor(context = {}, options = {}) {
this.context = context;
this.logLevel = options.logLevel || process.env.CALMHIVE_LOG_LEVEL || 'info';
this.enableConsole = options.enableConsole !== false;
this.enableFile = options.enableFile !== false;
this.enableMetrics = options.enableMetrics !== false;
// Log file configuration
this.logDir = options.logDir || path.join(os.homedir(), '.claude/calmhive/logs');
this.errorLogFile = path.join(this.logDir, 'errors.jsonl');
this.generalLogFile = path.join(this.logDir, 'general.jsonl');
this.performanceLogFile = path.join(this.logDir, 'performance.jsonl');
// Correlation tracking
this.correlationId = this.generateCorrelationId();
this.sessionMetrics = new Map();
// Log level hierarchy
this.logLevels = {
error: 0,
warn: 1,
info: 2,
debug: 3,
trace: 4
};
this.ensureLogDirectory();
}
/**
* Ensure log directory exists
*/
ensureLogDirectory() {
try {
if (!fs.existsSync(this.logDir)) {
fs.mkdirSync(this.logDir, { recursive: true });
}
} catch (error) {
console.error(`Failed to create log directory ${this.logDir}:`, error.message);
}
}
/**
* Generate correlation ID for tracking related log entries
*/
generateCorrelationId() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
/**
* Check if log level should be output
*/
shouldLog(level) {
const currentLevel = this.logLevels[this.logLevel] || 2;
const messageLevel = this.logLevels[level] || 2;
return messageLevel <= currentLevel;
}
/**
* Create structured log entry
*/
createLogEntry(level, message, data = {}, error = null) {
const timestamp = new Date().toISOString();
const entry = {
timestamp,
level,
message,
correlationId: this.correlationId,
context: this.context,
pid: process.pid,
...data
};
// Add error details if provided
if (error) {
entry.error = this.serializeError(error);
}
// Add performance data if available
if (data.performance) {
entry.performance = data.performance;
}
// Add stack trace for errors
if (level === 'error' && !error) {
entry.stack = new Error().stack;
}
return entry;
}
/**
* Serialize error object for logging
*/
serializeError(error) {
return {
name: error.name,
message: error.message,
code: error.code,
context: error.context,
stack: error.stack,
timestamp: error.timestamp,
// Include custom error properties
...(error.toJSON ? error.toJSON() : {})
};
}
/**
* Write log entry to file
*/
writeToFile(entry, filename) {
if (!this.enableFile) {return;}
try {
const logLine = JSON.stringify(entry) + '\n';
fs.appendFileSync(filename, logLine);
} catch (error) {
// Fallback to console if file writing fails
console.error('Failed to write to log file:', error.message);
console.error('Original log entry:', entry);
}
}
/**
* Output to console with formatting
*/
writeToConsole(entry) {
if (!this.enableConsole) {return;}
const { timestamp, level, message, context, error } = entry;
const contextStr = context.component ? `[${context.component}]` : '';
const timeStr = timestamp.substring(11, 19); // Extract HH:MM:SS
// Color coding for different log levels
const colors = {
error: '\x1b[31m', // Red
warn: '\x1b[33m', // Yellow
info: '\x1b[36m', // Cyan
debug: '\x1b[32m', // Green
trace: '\x1b[90m' // Gray
};
const resetColor = '\x1b[0m';
const color = colors[level] || '';
// Format console output
let output = `${color}[${timeStr}] ${level.toUpperCase()} ${contextStr}: ${message}${resetColor}`;
// Add error details for console
if (error) {
output += `\n${color} Error: ${error.name} - ${error.message}${resetColor}`;
if (error.context) {
output += `\n${color} Context: ${JSON.stringify(error.context)}${resetColor}`;
}
}
// Output based on level
if (level === 'error') {
console.error(output);
} else if (level === 'warn') {
console.warn(output);
} else {
console.log(output);
}
}
/**
* Error logging with enhanced context
*/
error(message, error = null, metadata = {}) {
if (!this.shouldLog('error')) {return;}
const entry = this.createLogEntry('error', message, metadata, error);
this.writeToConsole(entry);
this.writeToFile(entry, this.errorLogFile);
this.writeToFile(entry, this.generalLogFile);
// Track error metrics
if (this.enableMetrics) {
this.recordErrorMetric(entry);
}
}
/**
* Warning logging
*/
warn(message, metadata = {}) {
if (!this.shouldLog('warn')) {return;}
const entry = this.createLogEntry('warn', message, metadata);
this.writeToConsole(entry);
this.writeToFile(entry, this.generalLogFile);
}
/**
* Info logging
*/
info(message, metadata = {}) {
if (!this.shouldLog('info')) {return;}
const entry = this.createLogEntry('info', message, metadata);
this.writeToConsole(entry);
this.writeToFile(entry, this.generalLogFile);
}
/**
* Debug logging
*/
debug(message, metadata = {}) {
if (!this.shouldLog('debug')) {return;}
const entry = this.createLogEntry('debug', message, metadata);
this.writeToConsole(entry);
this.writeToFile(entry, this.generalLogFile);
}
/**
* Trace logging
*/
trace(message, metadata = {}) {
if (!this.shouldLog('trace')) {return;}
const entry = this.createLogEntry('trace', message, metadata);
this.writeToConsole(entry);
this.writeToFile(entry, this.generalLogFile);
}
/**
* Performance logging
*/
performance(operation, duration, metadata = {}) {
const entry = this.createLogEntry('info', `Performance: ${operation}`, {
...metadata,
performance: {
operation,
duration,
timestamp: Date.now()
}
});
this.writeToFile(entry, this.performanceLogFile);
// Also log slow operations as warnings
const slowThreshold = metadata.slowThreshold || 5000; // 5 seconds
if (duration > slowThreshold) {
this.warn(`Slow operation detected: ${operation} took ${duration}ms`, {
performance: { operation, duration },
...metadata
});
}
}
/**
* Record error metrics
*/
recordErrorMetric(entry) {
const key = `${entry.context.component || 'unknown'}:${entry.error?.name || 'unknown'}`;
if (!this.sessionMetrics.has(key)) {
this.sessionMetrics.set(key, {
count: 0,
firstSeen: entry.timestamp,
lastSeen: entry.timestamp,
examples: []
});
}
const metric = this.sessionMetrics.get(key);
metric.count++;
metric.lastSeen = entry.timestamp;
// Keep last 3 examples
metric.examples.push({
message: entry.message,
timestamp: entry.timestamp,
context: entry.error?.context
});
if (metric.examples.length > 3) {
metric.examples.shift();
}
}
/**
* Get error metrics summary
*/
getErrorMetrics() {
const summary = {
totalUniqueErrors: this.sessionMetrics.size,
errors: {}
};
for (const [key, metric] of this.sessionMetrics) {
summary.errors[key] = {
count: metric.count,
firstSeen: metric.firstSeen,
lastSeen: metric.lastSeen,
recentExamples: metric.examples
};
}
return summary;
}
/**
* Create child logger with additional context
*/
child(additionalContext = {}) {
return new StructuredLogger(
{ ...this.context, ...additionalContext },
{
logLevel: this.logLevel,
enableConsole: this.enableConsole,
enableFile: this.enableFile,
enableMetrics: this.enableMetrics,
logDir: this.logDir
}
);
}
/**
* Create logger for specific operation with performance tracking
*/
withOperation(operationName, metadata = {}) {
const startTime = Date.now();
const operationLogger = this.child({
operation: operationName,
operationId: this.generateCorrelationId(),
...metadata
});
return {
logger: operationLogger,
complete: (result = 'success', additionalMetadata = {}) => {
const duration = Date.now() - startTime;
operationLogger.performance(operationName, duration, {
result,
...metadata,
...additionalMetadata
});
operationLogger.info(`Operation completed: ${operationName}`, {
duration,
result,
...additionalMetadata
});
},
fail: (error, additionalMetadata = {}) => {
const duration = Date.now() - startTime;
operationLogger.error(`Operation failed: ${operationName}`, error, {
duration,
...metadata,
...additionalMetadata
});
}
};
}
/**
* Flush any pending log writes
*/
flush() {
// For now, we're using synchronous writes, so no flushing needed
// In a production system, you might want to implement async writes with buffering
}
/**
* Close logger and clean up resources
*/
close() {
this.flush();
// Write final metrics summary
if (this.enableMetrics && this.sessionMetrics.size > 0) {
const summary = this.getErrorMetrics();
this.info('Session error metrics summary', { errorMetrics: summary });
}
}
}
/**
* Logger factory for creating loggers with consistent configuration
*/
class LoggerFactory {
constructor(globalOptions = {}) {
this.globalOptions = globalOptions;
this.loggers = new Map();
}
/**
* Get or create logger for component
*/
getLogger(component, additionalContext = {}) {
const key = `${component}:${JSON.stringify(additionalContext)}`;
if (!this.loggers.has(key)) {
const logger = new StructuredLogger(
{ component, ...additionalContext },
this.globalOptions
);
this.loggers.set(key, logger);
}
return this.loggers.get(key);
}
/**
* Close all loggers
*/
closeAll() {
for (const logger of this.loggers.values()) {
logger.close();
}
this.loggers.clear();
}
/**
* Get all error metrics from all loggers
*/
getAllErrorMetrics() {
const allMetrics = {};
for (const [key, logger] of this.loggers) {
allMetrics[key] = logger.getErrorMetrics();
}
return allMetrics;
}
}
// Global logger factory instance
const globalLoggerFactory = new LoggerFactory({
logLevel: process.env.CALMHIVE_LOG_LEVEL || 'info',
enableConsole: process.env.CALMHIVE_DISABLE_CONSOLE_LOGS !== 'true',
enableFile: process.env.CALMHIVE_DISABLE_FILE_LOGS !== 'true',
enableMetrics: process.env.CALMHIVE_DISABLE_METRICS !== 'true'
});
// Convenience function to get logger
function getLogger(component, context = {}) {
return globalLoggerFactory.getLogger(component, context);
}
module.exports = {
StructuredLogger,
LoggerFactory,
globalLoggerFactory,
getLogger
};