|
@@ -0,0 +1,233 @@
|
|
|
+#!/usr/bin/env node
|
|
|
+
|
|
|
+/**
|
|
|
+ * 健康检查脚本
|
|
|
+ * 监控交易系统的健康状态并发送告警
|
|
|
+ */
|
|
|
+
|
|
|
+const fs = require('fs');
|
|
|
+const path = require('path');
|
|
|
+
|
|
|
+// 配置
|
|
|
+const CONFIG = {
|
|
|
+ logPath: path.join(__dirname, '../logs/trading.log'),
|
|
|
+ statsPath: path.join(__dirname, '../logs/stats.json'),
|
|
|
+ maxLogAge: 5 * 60 * 1000, // 5分钟
|
|
|
+ minTradingVolume: 0.001, // 最小交易量
|
|
|
+ maxErrorRate: 0.1, // 10%错误率
|
|
|
+ alertWebhook: process.env.ALERT_WEBHOOK_URL // Slack/Discord webhook
|
|
|
+};
|
|
|
+
|
|
|
+// 健康状态
|
|
|
+const HealthStatus = {
|
|
|
+ HEALTHY: '🟢 健康',
|
|
|
+ WARNING: '🟡 警告',
|
|
|
+ CRITICAL: '🔴 危急',
|
|
|
+ UNKNOWN: '⚫ 未知'
|
|
|
+};
|
|
|
+
|
|
|
+/**
|
|
|
+ * 检查日志文件最后更新时间
|
|
|
+ */
|
|
|
+function checkLogActivity() {
|
|
|
+ try {
|
|
|
+ if (!fs.existsSync(CONFIG.logPath)) {
|
|
|
+ return {
|
|
|
+ status: HealthStatus.CRITICAL,
|
|
|
+ message: '日志文件不存在'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const stats = fs.statSync(CONFIG.logPath);
|
|
|
+ const lastModified = stats.mtime.getTime();
|
|
|
+ const now = Date.now();
|
|
|
+ const age = now - lastModified;
|
|
|
+
|
|
|
+ if (age > CONFIG.maxLogAge) {
|
|
|
+ return {
|
|
|
+ status: HealthStatus.WARNING,
|
|
|
+ message: `日志文件${Math.floor(age / 60000)}分钟未更新`
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ status: HealthStatus.HEALTHY,
|
|
|
+ message: '日志更新正常'
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ return {
|
|
|
+ status: HealthStatus.UNKNOWN,
|
|
|
+ message: `检查日志失败: ${error.message}`
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 检查交易统计
|
|
|
+ */
|
|
|
+function checkTradingStats() {
|
|
|
+ try {
|
|
|
+ if (!fs.existsSync(CONFIG.statsPath)) {
|
|
|
+ return {
|
|
|
+ status: HealthStatus.WARNING,
|
|
|
+ message: '统计文件不存在'
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ const statsContent = fs.readFileSync(CONFIG.statsPath, 'utf8');
|
|
|
+ const stats = JSON.parse(statsContent);
|
|
|
+
|
|
|
+ // 检查错误率
|
|
|
+ if (stats.totalOrders > 0) {
|
|
|
+ const errorRate = stats.failedOrders / stats.totalOrders;
|
|
|
+ if (errorRate > CONFIG.maxErrorRate) {
|
|
|
+ return {
|
|
|
+ status: HealthStatus.CRITICAL,
|
|
|
+ message: `错误率过高: ${(errorRate * 100).toFixed(1)}%`
|
|
|
+ };
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查交易量
|
|
|
+ if (stats.totalVolume < CONFIG.minTradingVolume) {
|
|
|
+ return {
|
|
|
+ status: HealthStatus.WARNING,
|
|
|
+ message: `交易量过低: ${stats.totalVolume} BTC`
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查连续错误
|
|
|
+ if (stats.consecutiveErrors > 5) {
|
|
|
+ return {
|
|
|
+ status: HealthStatus.CRITICAL,
|
|
|
+ message: `连续错误: ${stats.consecutiveErrors}次`
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ status: HealthStatus.HEALTHY,
|
|
|
+ message: `交易正常 - 成交量: ${stats.totalVolume.toFixed(4)} BTC`
|
|
|
+ };
|
|
|
+ } catch (error) {
|
|
|
+ return {
|
|
|
+ status: HealthStatus.UNKNOWN,
|
|
|
+ message: `检查统计失败: ${error.message}`
|
|
|
+ };
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 检查进程内存使用
|
|
|
+ */
|
|
|
+function checkMemoryUsage() {
|
|
|
+ const used = process.memoryUsage();
|
|
|
+ const heapUsedMB = Math.round(used.heapUsed / 1024 / 1024);
|
|
|
+ const heapTotalMB = Math.round(used.heapTotal / 1024 / 1024);
|
|
|
+ const rssMB = Math.round(used.rss / 1024 / 1024);
|
|
|
+
|
|
|
+ if (rssMB > 2048) {
|
|
|
+ return {
|
|
|
+ status: HealthStatus.CRITICAL,
|
|
|
+ message: `内存使用过高: ${rssMB}MB`
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ if (rssMB > 1024) {
|
|
|
+ return {
|
|
|
+ status: HealthStatus.WARNING,
|
|
|
+ message: `内存使用较高: ${rssMB}MB`
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ status: HealthStatus.HEALTHY,
|
|
|
+ message: `内存使用: ${rssMB}MB (堆: ${heapUsedMB}/${heapTotalMB}MB)`
|
|
|
+ };
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 发送告警
|
|
|
+ */
|
|
|
+async function sendAlert(message) {
|
|
|
+ if (!CONFIG.alertWebhook) {
|
|
|
+ console.log('告警Webhook未配置');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ const fetch = (await import('node-fetch')).default;
|
|
|
+ await fetch(CONFIG.alertWebhook, {
|
|
|
+ method: 'POST',
|
|
|
+ headers: { 'Content-Type': 'application/json' },
|
|
|
+ body: JSON.stringify({
|
|
|
+ text: `🚨 交易系统告警\n${message}\n时间: ${new Date().toLocaleString('zh-CN')}`
|
|
|
+ })
|
|
|
+ });
|
|
|
+ } catch (error) {
|
|
|
+ console.error('发送告警失败:', error.message);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+/**
|
|
|
+ * 执行健康检查
|
|
|
+ */
|
|
|
+async function performHealthCheck() {
|
|
|
+ console.log('\n=== 健康检查 ===');
|
|
|
+ console.log(`时间: ${new Date().toLocaleString('zh-CN')}`);
|
|
|
+ console.log('');
|
|
|
+
|
|
|
+ const checks = [
|
|
|
+ { name: '日志活动', result: checkLogActivity() },
|
|
|
+ { name: '交易统计', result: checkTradingStats() },
|
|
|
+ { name: '内存使用', result: checkMemoryUsage() }
|
|
|
+ ];
|
|
|
+
|
|
|
+ let overallStatus = HealthStatus.HEALTHY;
|
|
|
+ const criticalIssues = [];
|
|
|
+
|
|
|
+ for (const check of checks) {
|
|
|
+ console.log(`${check.name}: ${check.result.status}`);
|
|
|
+ console.log(` └─ ${check.result.message}`);
|
|
|
+
|
|
|
+ // 记录严重问题
|
|
|
+ if (check.result.status === HealthStatus.CRITICAL) {
|
|
|
+ overallStatus = HealthStatus.CRITICAL;
|
|
|
+ criticalIssues.push(`${check.name}: ${check.result.message}`);
|
|
|
+ } else if (check.result.status === HealthStatus.WARNING && overallStatus === HealthStatus.HEALTHY) {
|
|
|
+ overallStatus = HealthStatus.WARNING;
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('\n总体状态:', overallStatus);
|
|
|
+
|
|
|
+ // 发送告警
|
|
|
+ if (overallStatus === HealthStatus.CRITICAL && criticalIssues.length > 0) {
|
|
|
+ const alertMessage = criticalIssues.join('\n');
|
|
|
+ console.log('\n⚠️ 发送告警...');
|
|
|
+ await sendAlert(alertMessage);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 写入健康状态文件
|
|
|
+ const healthStatus = {
|
|
|
+ timestamp: new Date().toISOString(),
|
|
|
+ status: overallStatus,
|
|
|
+ checks: checks.map(c => ({
|
|
|
+ name: c.name,
|
|
|
+ status: c.result.status,
|
|
|
+ message: c.result.message
|
|
|
+ }))
|
|
|
+ };
|
|
|
+
|
|
|
+ fs.writeFileSync(
|
|
|
+ path.join(__dirname, '../logs/health.json'),
|
|
|
+ JSON.stringify(healthStatus, null, 2)
|
|
|
+ );
|
|
|
+
|
|
|
+ console.log('\n=================\n');
|
|
|
+}
|
|
|
+
|
|
|
+// 执行健康检查
|
|
|
+performHealthCheck().catch(error => {
|
|
|
+ console.error('健康检查失败:', error);
|
|
|
+ process.exit(1);
|
|
|
+});
|