check-once.js 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176
  1. #!/usr/bin/env node
  2. import fsp from 'node:fs/promises';
  3. import path from 'node:path';
  4. import { execFile } from 'node:child_process';
  5. import { fileURLToPath } from 'node:url';
  6. const __dirname = path.dirname(fileURLToPath(import.meta.url));
  7. const monitorDir = __dirname;
  8. const dataDir = path.join(monitorDir, 'data');
  9. const envFile = path.join(monitorDir, '.env');
  10. function parseEnv(content) {
  11. const env = {};
  12. for (const line of content.split(/\r?\n/)) {
  13. const trimmed = line.trim();
  14. if (!trimmed || trimmed.startsWith('#')) {
  15. continue;
  16. }
  17. const index = trimmed.indexOf('=');
  18. if (index === -1) {
  19. continue;
  20. }
  21. const key = trimmed.slice(0, index).trim();
  22. const value = trimmed.slice(index + 1).trim().replace(/^['"]|['"]$/g, '');
  23. env[key] = value;
  24. }
  25. return env;
  26. }
  27. async function loadEnv() {
  28. const content = await fsp.readFile(envFile, 'utf8');
  29. return {
  30. ...process.env,
  31. ...parseEnv(content),
  32. };
  33. }
  34. async function readJson(filePath, fallback) {
  35. try {
  36. return JSON.parse(await fsp.readFile(filePath, 'utf8'));
  37. } catch (error) {
  38. return fallback;
  39. }
  40. }
  41. async function writeJson(filePath, value) {
  42. await fsp.mkdir(path.dirname(filePath), { recursive: true });
  43. await fsp.writeFile(filePath, JSON.stringify(value, null, 2));
  44. }
  45. function execFileAsync(command, args, options = {}) {
  46. return new Promise((resolve, reject) => {
  47. execFile(command, args, options, (error, stdout, stderr) => {
  48. if (error) {
  49. error.stdout = stdout;
  50. error.stderr = stderr;
  51. reject(error);
  52. return;
  53. }
  54. resolve({ stdout, stderr });
  55. });
  56. });
  57. }
  58. function shellQuote(value) {
  59. return `'${String(value).replace(/'/g, `'\\''`)}'`;
  60. }
  61. function makeUrl(baseUrl, params) {
  62. const url = new URL(baseUrl);
  63. for (const [key, value] of Object.entries(params)) {
  64. if (value !== undefined && value !== null && value !== '') {
  65. url.searchParams.set(key, value);
  66. }
  67. }
  68. return url.toString();
  69. }
  70. async function fetchViaSsh(env, url) {
  71. const sshHost = env.ERROR_LOG_SSH_HOST;
  72. if (!sshHost) {
  73. throw new Error('ERROR_LOG_SSH_HOST is required for one-click check');
  74. }
  75. const remoteCommand = [
  76. 'curl',
  77. '-sS',
  78. '--max-time',
  79. String(env.ERROR_LOG_CURL_TIMEOUT || 15),
  80. '-H',
  81. `x-internal-token: ${env.ERROR_LOG_API_TOKEN || env.INTERNAL_API_TOKEN || ''}`,
  82. url,
  83. ].map(shellQuote).join(' ');
  84. const sshArgs = [
  85. '-o',
  86. `ConnectTimeout=${env.ERROR_LOG_SSH_CONNECT_TIMEOUT || 8}`,
  87. sshHost,
  88. remoteCommand,
  89. ];
  90. const { stdout } = await execFileAsync('ssh', sshArgs);
  91. return JSON.parse(stdout);
  92. }
  93. function formatLog(item) {
  94. const location = item.FilePath ? `${item.FilePath}:${item.LineNumber || 0}:${item.ColumnNumber || 0}` : 'unknown location';
  95. const request = item.RequestUrl ? `${item.RequestMethod || 'REQUEST'} ${item.RequestUrl}` : '';
  96. const time = item.LastSeenAt || item.CreateTime || '';
  97. return [
  98. `#${item.ID} ${item.Source || 'error'} ${item.Status || ''} x${item.Count || 1} ${time}`,
  99. item.Message || '',
  100. location,
  101. request,
  102. ].filter(Boolean).join('\n');
  103. }
  104. async function main() {
  105. const env = await loadEnv();
  106. const token = env.ERROR_LOG_API_TOKEN || env.INTERNAL_API_TOKEN || '';
  107. if (!token) {
  108. throw new Error('ERROR_LOG_API_TOKEN is required in local-error-monitor/.env');
  109. }
  110. const stateFile = env.ERROR_LOG_CHECK_STATE_FILE || path.join(dataDir, 'check-state.json');
  111. const outputFile = env.ERROR_LOG_CHECK_OUTPUT_FILE || path.join(dataDir, 'recent-error-check.txt');
  112. const state = await readJson(stateFile, { sinceId: Number(env.ERROR_LOG_SINCE_ID || 0) });
  113. const limit = Math.min(Math.max(Number(env.ERROR_LOG_CHECK_LIMIT || 20), 1), 100);
  114. const apiBaseUrl = env.ERROR_LOG_REMOTE_API_URL || 'http://127.0.0.1:3050/internal/error-logs';
  115. const url = makeUrl(apiBaseUrl, {
  116. since_id: state.sinceId || 0,
  117. status: env.ERROR_LOG_CHECK_STATUS || 'open,reopened',
  118. limit,
  119. });
  120. const data = await fetchViaSsh(env, url);
  121. if (data.errcode !== 10000) {
  122. throw new Error(data.errMsg || `Unexpected response: ${JSON.stringify(data)}`);
  123. }
  124. const list = data.result?.list || [];
  125. const checkedAt = new Date().toISOString();
  126. let output;
  127. if (list.length === 0) {
  128. output = `[${checkedAt}] No new server errors since ID ${state.sinceId || 0}.`;
  129. } else {
  130. const nextSinceId = list.reduce((maxId, item) => Math.max(maxId, Number(item.ID || 0)), Number(state.sinceId || 0));
  131. state.sinceId = nextSinceId;
  132. await writeJson(stateFile, state);
  133. output = [
  134. `[${checkedAt}] Found ${list.length} new server error(s). Next since ID: ${nextSinceId}`,
  135. '',
  136. ...list.map(formatLog),
  137. ].join('\n\n');
  138. }
  139. await fsp.mkdir(dataDir, { recursive: true });
  140. await fsp.writeFile(outputFile, output + '\n');
  141. console.log(output);
  142. console.log(`\nSaved to: ${outputFile}`);
  143. }
  144. main().catch((error) => {
  145. console.error('[error-log-check] failed:', error.message || error);
  146. if (error.stderr) {
  147. console.error(error.stderr);
  148. }
  149. process.exit(1);
  150. });