#!/usr/bin/env node import fs from 'node:fs'; import fsp from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import { recordError } from '../src/util/errorLogger.js'; const logDir = process.env.PM2_ERROR_LOG_DIR || path.join(os.homedir(), '.pm2', 'logs'); const stateFile = process.env.PM2_ERROR_LOG_STATE_FILE || path.join(process.cwd(), '.pm2-error-log-cursors.json'); const pollIntervalMs = Number(process.env.PM2_ERROR_LOG_POLL_INTERVAL_MS || 30000); const readExisting = process.env.PM2_ERROR_LOG_READ_EXISTING === 'true'; const includeLogNames = parseNameList(process.env.PM2_ERROR_LOG_NAMES || 'app-24'); const excludeLogNames = parseNameList(process.env.PM2_ERROR_LOG_EXCLUDE_NAMES || ''); function parseNameList(value) { return new Set( String(value || '') .split(',') .map((item) => item.trim()) .filter(Boolean) .flatMap((item) => item.endsWith('-error.log') ? [item] : [item, `${item}-error.log`]) ); } function shouldCollectLogFile(file) { if (!file.endsWith('-error.log')) { return false; } if (excludeLogNames.has(file)) { return false; } return includeLogNames.size === 0 || includeLogNames.has(file); } async function loadState() { try { return JSON.parse(await fsp.readFile(stateFile, 'utf8')); } catch (error) { return {}; } } async function saveState(state) { await fsp.writeFile(stateFile, JSON.stringify(state, null, 2)); } async function listPm2ErrorLogs() { try { const files = await fsp.readdir(logDir); return files .filter(shouldCollectLogFile) .map((file) => path.join(logDir, file)); } catch (error) { console.error(`[pm2-error-log-collector] cannot read log dir ${logDir}:`, error.message || error); return []; } } async function readRange(filePath, start, end) { if (end < start) { return ''; } let content = ''; const stream = fs.createReadStream(filePath, { start, end, encoding: 'utf8', }); for await (const chunk of stream) { content += chunk; } return content; } function splitErrorBlocks(content) { const lines = content.replace(/\r\n/g, '\n').split('\n'); const blocks = []; let current = []; const startsNewBlock = (line) => { return ( /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}/.test(line) || /^(Error|TypeError|ReferenceError|SyntaxError|RangeError|UnhandledPromiseRejection|uncaughtException|unhandledRejection)\b/.test(line) ); }; for (const line of lines) { if (startsNewBlock(line) && current.length > 0) { blocks.push(current.join('\n').trim()); current = []; } current.push(line); } if (current.length > 0) { blocks.push(current.join('\n').trim()); } return blocks.filter(Boolean); } function getMessageFromBlock(block) { const line = block .split('\n') .map((item) => item.trim()) .find((item) => item && !item.startsWith('at ')); return line ? line.replace(/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:?\d{2})?\s*/, '') : 'PM2 error log'; } function shouldIgnoreBlock(block) { return ( /ECONNRESET/.test(block) && /write ECONNRESET/.test(block) && /clb-healthcheck/i.test(block) ); } async function collectOnce(state) { const files = await listPm2ErrorLogs(); for (const filePath of files) { let stat; try { stat = await fsp.stat(filePath); } catch (error) { continue; } const previousOffset = state[filePath]; if (previousOffset === undefined && !readExisting) { state[filePath] = stat.size; continue; } const start = Math.min(previousOffset || 0, stat.size); const content = await readRange(filePath, start, stat.size - 1); state[filePath] = stat.size; for (const block of splitErrorBlocks(content)) { if (shouldIgnoreBlock(block)) { continue; } await recordError(new Error(getMessageFromBlock(block)), null, { source: 'pm2-log', stack: block, message: getMessageFromBlock(block), extra: { logFile: filePath }, }); } } await saveState(state); } async function main() { console.log(`[pm2-error-log-collector] watching ${logDir}`); console.log(`[pm2-error-log-collector] include logs: ${includeLogNames.size ? [...includeLogNames].join(', ') : 'all'}`); if (excludeLogNames.size > 0) { console.log(`[pm2-error-log-collector] exclude logs: ${[...excludeLogNames].join(', ')}`); } const state = await loadState(); await collectOnce(state); setInterval(() => { collectOnce(state).catch((error) => { console.error('[pm2-error-log-collector] collect failed:', error); }); }, pollIntervalMs); } main().catch((error) => { console.error('[pm2-error-log-collector] fatal:', error); process.exit(1); });