| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176 |
- #!/usr/bin/env node
- import fsp from 'node:fs/promises';
- import path from 'node:path';
- import { execFile } from 'node:child_process';
- import { fileURLToPath } from 'node:url';
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
- const monitorDir = __dirname;
- const dataDir = path.join(monitorDir, 'data');
- const envFile = path.join(monitorDir, '.env');
- function parseEnv(content) {
- const env = {};
- for (const line of content.split(/\r?\n/)) {
- const trimmed = line.trim();
- if (!trimmed || trimmed.startsWith('#')) {
- continue;
- }
- const index = trimmed.indexOf('=');
- if (index === -1) {
- continue;
- }
- const key = trimmed.slice(0, index).trim();
- const value = trimmed.slice(index + 1).trim().replace(/^['"]|['"]$/g, '');
- env[key] = value;
- }
- return env;
- }
- async function loadEnv() {
- const content = await fsp.readFile(envFile, 'utf8');
- return {
- ...process.env,
- ...parseEnv(content),
- };
- }
- async function readJson(filePath, fallback) {
- try {
- return JSON.parse(await fsp.readFile(filePath, 'utf8'));
- } catch (error) {
- return fallback;
- }
- }
- async function writeJson(filePath, value) {
- await fsp.mkdir(path.dirname(filePath), { recursive: true });
- await fsp.writeFile(filePath, JSON.stringify(value, null, 2));
- }
- function execFileAsync(command, args, options = {}) {
- return new Promise((resolve, reject) => {
- execFile(command, args, options, (error, stdout, stderr) => {
- if (error) {
- error.stdout = stdout;
- error.stderr = stderr;
- reject(error);
- return;
- }
- resolve({ stdout, stderr });
- });
- });
- }
- function shellQuote(value) {
- return `'${String(value).replace(/'/g, `'\\''`)}'`;
- }
- function makeUrl(baseUrl, params) {
- const url = new URL(baseUrl);
- for (const [key, value] of Object.entries(params)) {
- if (value !== undefined && value !== null && value !== '') {
- url.searchParams.set(key, value);
- }
- }
- return url.toString();
- }
- async function fetchViaSsh(env, url) {
- const sshHost = env.ERROR_LOG_SSH_HOST;
- if (!sshHost) {
- throw new Error('ERROR_LOG_SSH_HOST is required for one-click check');
- }
- const remoteCommand = [
- 'curl',
- '-sS',
- '--max-time',
- String(env.ERROR_LOG_CURL_TIMEOUT || 15),
- '-H',
- `x-internal-token: ${env.ERROR_LOG_API_TOKEN || env.INTERNAL_API_TOKEN || ''}`,
- url,
- ].map(shellQuote).join(' ');
- const sshArgs = [
- '-o',
- `ConnectTimeout=${env.ERROR_LOG_SSH_CONNECT_TIMEOUT || 8}`,
- sshHost,
- remoteCommand,
- ];
- const { stdout } = await execFileAsync('ssh', sshArgs);
- return JSON.parse(stdout);
- }
- function formatLog(item) {
- const location = item.FilePath ? `${item.FilePath}:${item.LineNumber || 0}:${item.ColumnNumber || 0}` : 'unknown location';
- const request = item.RequestUrl ? `${item.RequestMethod || 'REQUEST'} ${item.RequestUrl}` : '';
- const time = item.LastSeenAt || item.CreateTime || '';
- return [
- `#${item.ID} ${item.Source || 'error'} ${item.Status || ''} x${item.Count || 1} ${time}`,
- item.Message || '',
- location,
- request,
- ].filter(Boolean).join('\n');
- }
- async function main() {
- const env = await loadEnv();
- const token = env.ERROR_LOG_API_TOKEN || env.INTERNAL_API_TOKEN || '';
- if (!token) {
- throw new Error('ERROR_LOG_API_TOKEN is required in local-error-monitor/.env');
- }
- const stateFile = env.ERROR_LOG_CHECK_STATE_FILE || path.join(dataDir, 'check-state.json');
- const outputFile = env.ERROR_LOG_CHECK_OUTPUT_FILE || path.join(dataDir, 'recent-error-check.txt');
- const state = await readJson(stateFile, { sinceId: Number(env.ERROR_LOG_SINCE_ID || 0) });
- const limit = Math.min(Math.max(Number(env.ERROR_LOG_CHECK_LIMIT || 20), 1), 100);
- const apiBaseUrl = env.ERROR_LOG_REMOTE_API_URL || 'http://127.0.0.1:3050/internal/error-logs';
- const url = makeUrl(apiBaseUrl, {
- since_id: state.sinceId || 0,
- status: env.ERROR_LOG_CHECK_STATUS || 'open,reopened',
- limit,
- });
- const data = await fetchViaSsh(env, url);
- if (data.errcode !== 10000) {
- throw new Error(data.errMsg || `Unexpected response: ${JSON.stringify(data)}`);
- }
- const list = data.result?.list || [];
- const checkedAt = new Date().toISOString();
- let output;
- if (list.length === 0) {
- output = `[${checkedAt}] No new server errors since ID ${state.sinceId || 0}.`;
- } else {
- const nextSinceId = list.reduce((maxId, item) => Math.max(maxId, Number(item.ID || 0)), Number(state.sinceId || 0));
- state.sinceId = nextSinceId;
- await writeJson(stateFile, state);
- output = [
- `[${checkedAt}] Found ${list.length} new server error(s). Next since ID: ${nextSinceId}`,
- '',
- ...list.map(formatLog),
- ].join('\n\n');
- }
- await fsp.mkdir(dataDir, { recursive: true });
- await fsp.writeFile(outputFile, output + '\n');
- console.log(output);
- console.log(`\nSaved to: ${outputFile}`);
- }
- main().catch((error) => {
- console.error('[error-log-check] failed:', error.message || error);
- if (error.stderr) {
- console.error(error.stderr);
- }
- process.exit(1);
- });
|