#!/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'; 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((file) => file.endsWith('-error.log')) .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'; } 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)) { 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}`); 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); });