/ src / agents / utils / structured-logger.js
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;