structured-logger.js
1 /** 2 * Structured Logger for Agent System 3 * 4 * Provides JSON-formatted logging with database persistence. 5 * All agent logs are written to agent_logs table AND console as JSON. 6 * Supports log aggregators (Datadog, CloudWatch, etc.) via JSON stdout. 7 * 8 * @example 9 * const logger = new StructuredLogger('developer', taskId); 10 * logger.info('Task started', { file: 'src/scoring.js', action: 'fix_bug' }); 11 * logger.error('Task failed', { error: err.message, stack: err.stack }); 12 */ 13 14 import { run } from '../../utils/db.js'; 15 16 export class StructuredLogger { 17 /** 18 * Create a structured logger 19 * 20 * @param {string} agentName - Agent name ('developer', 'qa', etc.) 21 * @param {number|null} [taskId=null] - Current task ID (optional) 22 */ 23 constructor(agentName, taskId = null) { 24 if (!agentName) { 25 throw new Error('agentName is required'); 26 } 27 28 this.agentName = agentName; 29 this.taskId = taskId; 30 } 31 32 /** 33 * Log a message at specified level 34 * 35 * Writes to both database (agent_logs) and console (JSON format). 36 * Console output is JSON for log aggregator compatibility. 37 * 38 * @param {string} level - Log level ('debug', 'info', 'warn', 'error') 39 * @param {string} message - Log message 40 * @param {Object} [context={}] - Additional context (structured data) 41 */ 42 log(level, message, context = {}) { 43 const validLevels = ['debug', 'info', 'warn', 'error']; 44 if (!validLevels.includes(level)) { 45 throw new Error(`Invalid log level: ${level}. Must be one of: ${validLevels.join(', ')}`); 46 } 47 48 // Merge task_id into context if not already present 49 const fullContext = { 50 ...context, 51 task_id: this.taskId || context.task_id || null, 52 }; 53 54 // Write to database (fire-and-forget; errors fall back to console) 55 this.writeToDatabase(level, message, fullContext); 56 57 // Write to console as JSON (for log aggregators) 58 this.writeToConsole(level, message, fullContext); 59 } 60 61 /** 62 * Write log entry to agent_logs table 63 * 64 * @param {string} level - Log level 65 * @param {string} message - Log message 66 * @param {Object} context - Context data 67 * @private 68 */ 69 writeToDatabase(level, message, context) { 70 run( 71 `INSERT INTO tel.agent_logs (task_id, agent_name, log_level, message, data_json) 72 VALUES ($1, $2, $3, $4, $5)`, 73 [ 74 context.task_id || null, 75 this.agentName, 76 level.toLowerCase(), 77 message, 78 Object.keys(context).length > 0 ? JSON.stringify(context) : null, 79 ] 80 ).catch(error => { 81 // Fallback to console if database write fails 82 console.error('[StructuredLogger] Database write failed:', error.message); 83 }); 84 } 85 86 /** 87 * Write log entry to console as JSON 88 * 89 * @param {string} level - Log level 90 * @param {string} message - Log message 91 * @param {Object} context - Context data 92 * @private 93 */ 94 writeToConsole(level, message, context) { 95 const logEntry = { 96 timestamp: new Date().toISOString(), 97 level: level.toUpperCase(), 98 agent: this.agentName, 99 task_id: context.task_id || null, 100 message, 101 ...context, 102 }; 103 104 // Remove duplicate task_id from context if present 105 if (logEntry.task_id !== null && context.task_id !== undefined) { 106 delete logEntry.context; 107 } 108 109 // Use console methods based on level 110 const consoleMethod = level === 'error' ? 'error' : level === 'warn' ? 'warn' : 'log'; 111 console[consoleMethod](JSON.stringify(logEntry)); 112 } 113 114 /** 115 * Log at DEBUG level 116 * 117 * @param {string} message - Log message 118 * @param {Object} [context={}] - Additional context 119 */ 120 debug(message, context = {}) { 121 this.log('debug', message, context); 122 } 123 124 /** 125 * Log at INFO level 126 * 127 * @param {string} message - Log message 128 * @param {Object} [context={}] - Additional context 129 */ 130 info(message, context = {}) { 131 this.log('info', message, context); 132 } 133 134 /** 135 * Log at WARN level 136 * 137 * @param {string} message - Log message 138 * @param {Object} [context={}] - Additional context 139 */ 140 warn(message, context = {}) { 141 this.log('warn', message, context); 142 } 143 144 /** 145 * Log at ERROR level 146 * 147 * @param {string} message - Log message 148 * @param {Object} [context={}] - Additional context 149 */ 150 error(message, context = {}) { 151 this.log('error', message, context); 152 } 153 154 /** 155 * Update task ID (for when task starts after logger creation) 156 * 157 * @param {number} taskId - New task ID 158 */ 159 setTaskId(taskId) { 160 this.taskId = taskId; 161 } 162 } 163 164 export default StructuredLogger;