/ src / utils / logger.js
logger.js
  1  /**
  2   * Logger utility for console and file output
  3   * Formats messages for Cline monitoring via page.on('console') and page.on('pageerror')
  4   * Supports daily log rotation with configurable retention
  5   */
  6  
  7  import fs from 'fs';
  8  import path from 'path';
  9  
 10  const colors = {
 11    reset: '\x1b[0m',
 12    bright: '\x1b[1m',
 13    red: '\x1b[31m',
 14    green: '\x1b[32m',
 15    yellow: '\x1b[33m',
 16    blue: '\x1b[34m',
 17    magenta: '\x1b[35m',
 18    cyan: '\x1b[36m',
 19  };
 20  
 21  class Logger {
 22    constructor(context = '333', options = {}) {
 23      this.context = context;
 24      this.logToFile = options.logToFile !== false && process.env.NODE_ENV !== 'test'; // default true, disabled in tests
 25      this.logDir = options.logDir || process.env.LOGS_DIR || './logs';
 26      this.logFile = null;
 27      this.logStream = null;
 28  
 29      if (this.logToFile) {
 30        this._initializeLogFile();
 31      }
 32    }
 33  
 34    /**
 35     * Map module contexts to consolidated domain log files
 36     * This keeps log files organized by domain (pipeline, outreach, etc.)
 37     * rather than creating a separate file for each module
 38     */
 39    _getLogDomain(context) {
 40      const normalized = context.toLowerCase().replace(/[^a-z0-9-_]/g, '');
 41  
 42      const domainMap = {
 43        // Pipeline stages
 44        pipeline: 'pipeline',
 45        pipelineservice: 'pipeline',
 46        keywords: 'keywords',
 47        keywordmanager: 'keywords',
 48        keywordcounters: 'keywords',
 49        serps: 'serps',
 50        scraper: 'serps',
 51        assets: 'assets',
 52        capture: 'assets',
 53        scoring: 'scoring',
 54        score: 'scoring',
 55        scoringcli: 'scoring',
 56        rescoring: 'rescoring',
 57        rescoringcli: 'rescoring',
 58        enrich: 'enrich',
 59        proposals: 'proposals',
 60        proposalgeneratorv2: 'proposals',
 61  
 62        // Outreach channels
 63        outreach: 'outreach',
 64        smsoutreach: 'outreach',
 65        emailoutreach: 'outreach',
 66        formoutreach: 'outreach',
 67        xoutreach: 'outreach',
 68        linkedinoutreach: 'outreach',
 69  
 70        // Inbound processing
 71        replies: 'inbound',
 72        inboundsms: 'inbound',
 73        inboundemail: 'inbound',
 74        inboundprocessor: 'inbound',
 75  
 76        // Dashboard
 77        dashboard: 'dashboard',
 78        overview: 'dashboard',
 79        pipelinehealth: 'dashboard',
 80        pipelinewidgets: 'dashboard',
 81        systemhealth: 'dashboard',
 82        cronjobs: 'dashboard',
 83        coverage: 'dashboard',
 84        compliance: 'dashboard',
 85        conversations: 'dashboard',
 86  
 87        // Agent system
 88        agents: 'agents',
 89        agentrunner: 'agents',
 90        agentsystem: 'agents',
 91        developer: 'agents',
 92        qa: 'agents',
 93        security: 'agents',
 94        architect: 'agents',
 95        monitor: 'agents',
 96        triage: 'agents',
 97        agentrunnerbackground: 'agents',
 98        agentrunnerasync: 'agents',
 99  
100        // Cron jobs
101        cron: 'cron',
102        dailylogrotation: 'cron',
103        weeklyrepricing: 'cron',
104        syncemailevents: 'cron',
105        syncunsubscribes: 'cron',
106  
107        // Utilities
108        dedupe: 'utils',
109        dedupelocale: 'utils',
110        dedupelocaleaware: 'utils',
111        backfillscreenshots: 'utils',
112        imageoptimizer: 'utils',
113        errorhandler: 'utils',
114        sitefilters: 'utils',
115        errorpagedetector: 'utils',
116        tlddetector: 'utils',
117        timezonedetector: 'utils',
118        domcropanalyzer: 'utils',
119        circuitbreaker: 'utils',
120        contactprioritizer: 'utils',
121        ratelimiter: 'utils',
122        stealthbrowser: 'utils',
123        summary: 'utils',
124  
125        // Tests
126        test: 'tests',
127        testlogging: 'tests',
128      };
129  
130      return domainMap[normalized] || 'app';
131    }
132  
133    _initializeLogFile() {
134      // Create logs directory if it doesn't exist
135      if (!fs.existsSync(this.logDir)) {
136        fs.mkdirSync(this.logDir, { recursive: true });
137      }
138  
139      // Generate log filename with domain and date
140      const d = new Date();
141      const date = `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`; // YYYY-MM-DD (local timezone)
142      const logDomain = this._getLogDomain(this.context);
143      this.logFile = path.join(this.logDir, `${logDomain}-${date}.log`);
144  
145      // Open write stream in append mode
146      this.logStream = fs.createWriteStream(this.logFile, { flags: 'a' });
147  
148      // Handle stream errors
149      this.logStream.on('error', err => {
150        console.error(`[Logger] Failed to write to log file: ${err.message}`);
151      });
152    }
153  
154    _writeToFile(message) {
155      if (this.logStream && !this.logStream.destroyed) {
156        // Strip ANSI color codes for file output
157        // eslint-disable-next-line no-control-regex
158        const cleanMessage = message.replace(/\x1b\[[0-9;]*m/g, '');
159        this.logStream.write(`${cleanMessage}\n`);
160      }
161    }
162  
163    _format(level, message, data = null) {
164      const d = new Date();
165      const pad = (n, w = 2) => String(n).padStart(w, '0');
166      const offset = -d.getTimezoneOffset();
167      const sign = offset >= 0 ? '+' : '-';
168      const oh = Math.floor(Math.abs(offset) / 60);
169      const om = Math.abs(offset) % 60;
170      const timestamp = `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())}T${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}.${pad(d.getMilliseconds(), 3)}${sign}${pad(oh)}:${pad(om)}`;
171      const prefix = `[${timestamp}] [${this.context}] [${level}]`;
172  
173      if (data) {
174        return `${prefix} ${message}\n${JSON.stringify(data, null, 2)}`;
175      }
176      return `${prefix} ${message}`;
177    }
178  
179    close() {
180      if (this.logStream && !this.logStream.destroyed) {
181        this.logStream.end();
182      }
183    }
184  
185    info(message, data = null) {
186      const formatted = this._format('INFO', message, data);
187      console.log(colors.blue + formatted + colors.reset);
188      this._writeToFile(formatted);
189    }
190  
191    success(message, data = null) {
192      const formatted = this._format('SUCCESS', message, data);
193      console.log(colors.green + formatted + colors.reset);
194      this._writeToFile(formatted);
195    }
196  
197    warn(message, data = null) {
198      const formatted = this._format('WARN', message, data);
199      console.warn(colors.yellow + formatted + colors.reset);
200      this._writeToFile(formatted);
201    }
202  
203    error(message, error = null) {
204      const errorData = error
205        ? {
206            message: error.message,
207            stack: error.stack,
208          }
209        : null;
210      const formatted = this._format('ERROR', message, errorData);
211      console.error(colors.red + formatted + colors.reset);
212      this._writeToFile(formatted);
213    }
214  
215    debug(message, data = null) {
216      const formatted = this._format('DEBUG', message, data);
217      if (process.env.DEBUG === 'true') {
218        console.log(colors.cyan + formatted + colors.reset);
219      }
220      this._writeToFile(formatted);
221    }
222  
223    // Progress indicator for batch operations
224    progress(current, total, message = '') {
225      const percentage = Math.round((current / total) * 100);
226      const bar =
227        '█'.repeat(Math.floor(percentage / 2)) + '░'.repeat(50 - Math.floor(percentage / 2));
228      process.stdout.write(`\r${colors.cyan}[${bar}] ${percentage}% ${message}${colors.reset}`);
229  
230      if (current === total) {
231        console.log(''); // New line when complete
232      }
233    }
234  }
235  
236  export default Logger;