forked from datawhale/whale-town
创建新工程
This commit is contained in:
310
server/src/api/AdminAPI.ts
Normal file
310
server/src/api/AdminAPI.ts
Normal file
@@ -0,0 +1,310 @@
|
||||
import * as http from 'http';
|
||||
import * as url from 'url';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { HealthChecker } from '../monitoring/HealthChecker';
|
||||
import { BackupManager } from '../backup/BackupManager';
|
||||
import { LogManager } from '../logging/LogManager';
|
||||
import { MaintenanceManager } from '../maintenance/MaintenanceManager';
|
||||
|
||||
export class AdminAPI {
|
||||
private server: http.Server;
|
||||
private healthChecker: HealthChecker;
|
||||
private backupManager: BackupManager;
|
||||
private logManager: LogManager;
|
||||
private maintenanceManager: MaintenanceManager;
|
||||
private getActiveConnections: () => number;
|
||||
private getTotalConnections: () => number;
|
||||
|
||||
constructor(
|
||||
port: number,
|
||||
healthChecker: HealthChecker,
|
||||
backupManager: BackupManager,
|
||||
logManager: LogManager,
|
||||
maintenanceManager: MaintenanceManager,
|
||||
getActiveConnections: () => number,
|
||||
getTotalConnections: () => number
|
||||
) {
|
||||
this.healthChecker = healthChecker;
|
||||
this.backupManager = backupManager;
|
||||
this.logManager = logManager;
|
||||
this.maintenanceManager = maintenanceManager;
|
||||
this.getActiveConnections = getActiveConnections;
|
||||
this.getTotalConnections = getTotalConnections;
|
||||
|
||||
this.server = http.createServer((req, res) => {
|
||||
this.handleRequest(req, res);
|
||||
});
|
||||
|
||||
this.server.listen(port, () => {
|
||||
this.logManager.info('ADMIN_API', `Admin API server started on port ${port}`);
|
||||
console.log(`🔧 Admin API server started on port ${port}`);
|
||||
});
|
||||
}
|
||||
|
||||
private async handleRequest(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
// 设置CORS头
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
|
||||
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
|
||||
|
||||
if (req.method === 'OPTIONS') {
|
||||
res.writeHead(200);
|
||||
res.end();
|
||||
return;
|
||||
}
|
||||
|
||||
const parsedUrl = url.parse(req.url || '', true);
|
||||
const pathname = parsedUrl.pathname || '';
|
||||
const method = req.method || 'GET';
|
||||
|
||||
// 处理静态文件请求
|
||||
if (pathname === '/' || pathname === '/admin' || pathname === '/admin/') {
|
||||
await this.serveStaticFile(res, 'admin/index.html');
|
||||
return;
|
||||
}
|
||||
|
||||
if (pathname.startsWith('/admin/') && method === 'GET') {
|
||||
const filePath = pathname.substring(1); // 移除开头的 /
|
||||
await this.serveStaticFile(res, filePath);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 简单的认证检查(在生产环境中应该使用更安全的方法)
|
||||
const authHeader = req.headers.authorization;
|
||||
if (!this.isAuthorized(authHeader)) {
|
||||
this.sendResponse(res, 401, { error: 'Unauthorized' });
|
||||
return;
|
||||
}
|
||||
|
||||
// 路由处理
|
||||
if (pathname === '/health' && method === 'GET') {
|
||||
await this.handleHealthCheck(req, res);
|
||||
} else if (pathname === '/backups' && method === 'GET') {
|
||||
await this.handleListBackups(req, res);
|
||||
} else if (pathname === '/backups' && method === 'POST') {
|
||||
await this.handleCreateBackup(req, res);
|
||||
} else if (pathname.startsWith('/backups/') && method === 'POST') {
|
||||
await this.handleRestoreBackup(req, res, pathname);
|
||||
} else if (pathname.startsWith('/backups/') && method === 'DELETE') {
|
||||
await this.handleDeleteBackup(req, res, pathname);
|
||||
} else if (pathname === '/logs/analyze' && method === 'GET') {
|
||||
await this.handleAnalyzeLogs(req, res, parsedUrl.query);
|
||||
} else if (pathname === '/logs/files' && method === 'GET') {
|
||||
await this.handleListLogFiles(req, res);
|
||||
} else if (pathname === '/maintenance/tasks' && method === 'GET') {
|
||||
await this.handleListTasks(req, res);
|
||||
} else if (pathname === '/maintenance/tasks' && method === 'PUT') {
|
||||
await this.handleUpdateTask(req, res);
|
||||
} else if (pathname.startsWith('/maintenance/tasks/') && method === 'POST') {
|
||||
await this.handleRunTask(req, res, pathname);
|
||||
} else if (pathname === '/maintenance/mode' && method === 'POST') {
|
||||
await this.handleMaintenanceMode(req, res);
|
||||
} else if (pathname === '/system/info' && method === 'GET') {
|
||||
await this.handleSystemInfo(req, res);
|
||||
} else {
|
||||
this.sendResponse(res, 404, { error: 'Not found' });
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
this.logManager.error('ADMIN_API', 'API request error', {
|
||||
error: error instanceof Error ? error.message : String(error),
|
||||
pathname,
|
||||
method
|
||||
});
|
||||
this.sendResponse(res, 500, { error: 'Internal server error' });
|
||||
}
|
||||
}
|
||||
|
||||
private isAuthorized(authHeader?: string): boolean {
|
||||
// 简单的认证 - 在生产环境中应该使用更安全的方法
|
||||
const expectedToken = process.env.ADMIN_TOKEN || 'admin123';
|
||||
return authHeader === `Bearer ${expectedToken}`;
|
||||
}
|
||||
|
||||
private async handleHealthCheck(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const health = await this.healthChecker.getHealthStatus(
|
||||
this.getActiveConnections(),
|
||||
this.getTotalConnections()
|
||||
);
|
||||
|
||||
const serviceHealth = await this.healthChecker.checkServiceHealth();
|
||||
|
||||
this.sendResponse(res, 200, {
|
||||
...health,
|
||||
serviceHealth
|
||||
});
|
||||
}
|
||||
|
||||
private async handleListBackups(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const backups = await this.backupManager.listBackups();
|
||||
this.sendResponse(res, 200, { backups });
|
||||
}
|
||||
|
||||
private async handleCreateBackup(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const body = await this.readRequestBody(req);
|
||||
const { description } = JSON.parse(body || '{}');
|
||||
|
||||
const backup = await this.backupManager.createBackup({
|
||||
description: description || 'Manual backup via API'
|
||||
});
|
||||
|
||||
this.sendResponse(res, 201, { backup });
|
||||
}
|
||||
|
||||
private async handleRestoreBackup(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
|
||||
const backupId = pathname.split('/')[2];
|
||||
const success = await this.backupManager.restoreBackup(backupId);
|
||||
|
||||
this.sendResponse(res, success ? 200 : 400, {
|
||||
success,
|
||||
message: success ? 'Backup restored successfully' : 'Failed to restore backup'
|
||||
});
|
||||
}
|
||||
|
||||
private async handleDeleteBackup(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
|
||||
const backupId = pathname.split('/')[2];
|
||||
const success = await this.backupManager.deleteBackup(backupId);
|
||||
|
||||
this.sendResponse(res, success ? 200 : 400, {
|
||||
success,
|
||||
message: success ? 'Backup deleted successfully' : 'Failed to delete backup'
|
||||
});
|
||||
}
|
||||
|
||||
private async handleAnalyzeLogs(req: http.IncomingMessage, res: http.ServerResponse, query: any): Promise<void> {
|
||||
const options: any = {};
|
||||
|
||||
if (query.startTime) options.startTime = parseInt(query.startTime);
|
||||
if (query.endTime) options.endTime = parseInt(query.endTime);
|
||||
if (query.level) options.level = parseInt(query.level);
|
||||
if (query.category) options.category = query.category;
|
||||
if (query.limit) options.limit = parseInt(query.limit);
|
||||
|
||||
const analytics = await this.logManager.analyzeLogs(options);
|
||||
this.sendResponse(res, 200, { analytics });
|
||||
}
|
||||
|
||||
private async handleListLogFiles(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const logFiles = this.logManager.getLogFiles();
|
||||
this.sendResponse(res, 200, { logFiles });
|
||||
}
|
||||
|
||||
private async handleListTasks(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const tasks = this.maintenanceManager.getTasks();
|
||||
this.sendResponse(res, 200, { tasks });
|
||||
}
|
||||
|
||||
private async handleUpdateTask(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const body = await this.readRequestBody(req);
|
||||
const { taskId, updates } = JSON.parse(body || '{}');
|
||||
|
||||
const success = this.maintenanceManager.updateTask(taskId, updates);
|
||||
|
||||
this.sendResponse(res, success ? 200 : 400, {
|
||||
success,
|
||||
message: success ? 'Task updated successfully' : 'Failed to update task'
|
||||
});
|
||||
}
|
||||
|
||||
private async handleRunTask(req: http.IncomingMessage, res: http.ServerResponse, pathname: string): Promise<void> {
|
||||
const taskId = pathname.split('/')[3];
|
||||
const result = await this.maintenanceManager.runTask(taskId);
|
||||
|
||||
this.sendResponse(res, result.success ? 200 : 400, result);
|
||||
}
|
||||
|
||||
private async handleMaintenanceMode(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const body = await this.readRequestBody(req);
|
||||
const { enabled, reason } = JSON.parse(body || '{}');
|
||||
|
||||
if (enabled) {
|
||||
await this.maintenanceManager.enterMaintenanceMode(reason || 'Manual maintenance mode');
|
||||
} else {
|
||||
await this.maintenanceManager.exitMaintenanceMode();
|
||||
}
|
||||
|
||||
this.sendResponse(res, 200, {
|
||||
maintenanceMode: this.maintenanceManager.isInMaintenanceMode(),
|
||||
message: enabled ? 'Entered maintenance mode' : 'Exited maintenance mode'
|
||||
});
|
||||
}
|
||||
|
||||
private async handleSystemInfo(req: http.IncomingMessage, res: http.ServerResponse): Promise<void> {
|
||||
const systemInfo = this.healthChecker.getSystemInfo();
|
||||
const health = await this.healthChecker.getHealthStatus(
|
||||
this.getActiveConnections(),
|
||||
this.getTotalConnections()
|
||||
);
|
||||
|
||||
this.sendResponse(res, 200, {
|
||||
systemInfo,
|
||||
health,
|
||||
connections: {
|
||||
active: this.getActiveConnections(),
|
||||
total: this.getTotalConnections()
|
||||
},
|
||||
maintenanceMode: this.maintenanceManager.isInMaintenanceMode()
|
||||
});
|
||||
}
|
||||
|
||||
private async readRequestBody(req: http.IncomingMessage): Promise<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
let body = '';
|
||||
req.on('data', chunk => {
|
||||
body += chunk.toString();
|
||||
});
|
||||
req.on('end', () => {
|
||||
resolve(body);
|
||||
});
|
||||
req.on('error', reject);
|
||||
});
|
||||
}
|
||||
|
||||
private sendResponse(res: http.ServerResponse, statusCode: number, data: any): void {
|
||||
res.writeHead(statusCode, { 'Content-Type': 'application/json' });
|
||||
res.end(JSON.stringify(data, null, 2));
|
||||
}
|
||||
|
||||
private async serveStaticFile(res: http.ServerResponse, filePath: string): Promise<void> {
|
||||
try {
|
||||
const fullPath = path.join(__dirname, '../../', filePath);
|
||||
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
res.writeHead(404, { 'Content-Type': 'text/plain' });
|
||||
res.end('File not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(fullPath);
|
||||
const ext = path.extname(filePath).toLowerCase();
|
||||
|
||||
let contentType = 'text/plain';
|
||||
switch (ext) {
|
||||
case '.html':
|
||||
contentType = 'text/html';
|
||||
break;
|
||||
case '.css':
|
||||
contentType = 'text/css';
|
||||
break;
|
||||
case '.js':
|
||||
contentType = 'application/javascript';
|
||||
break;
|
||||
case '.json':
|
||||
contentType = 'application/json';
|
||||
break;
|
||||
}
|
||||
|
||||
res.writeHead(200, { 'Content-Type': contentType });
|
||||
res.end(content);
|
||||
} catch (error) {
|
||||
res.writeHead(500, { 'Content-Type': 'text/plain' });
|
||||
res.end('Internal server error');
|
||||
}
|
||||
}
|
||||
|
||||
public close(): void {
|
||||
this.server.close();
|
||||
}
|
||||
}
|
||||
384
server/src/backup/BackupManager.ts
Normal file
384
server/src/backup/BackupManager.ts
Normal file
@@ -0,0 +1,384 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as zlib from 'zlib';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const gzip = promisify(zlib.gzip);
|
||||
const gunzip = promisify(zlib.gunzip);
|
||||
|
||||
export interface BackupInfo {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
size: number;
|
||||
compressed: boolean;
|
||||
description?: string;
|
||||
files: string[];
|
||||
}
|
||||
|
||||
export interface BackupOptions {
|
||||
compress?: boolean;
|
||||
description?: string;
|
||||
maxBackups?: number;
|
||||
}
|
||||
|
||||
export class BackupManager {
|
||||
private dataDir: string;
|
||||
private backupDir: string;
|
||||
|
||||
constructor(dataDir: string) {
|
||||
this.dataDir = dataDir;
|
||||
this.backupDir = path.join(dataDir, 'backups');
|
||||
this.ensureBackupDirectory();
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保备份目录存在
|
||||
*/
|
||||
private ensureBackupDirectory(): void {
|
||||
if (!fs.existsSync(this.backupDir)) {
|
||||
fs.mkdirSync(this.backupDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建备份
|
||||
*/
|
||||
async createBackup(options: BackupOptions = {}): Promise<BackupInfo> {
|
||||
const {
|
||||
compress = true,
|
||||
description = 'Automatic backup',
|
||||
maxBackups = 10
|
||||
} = options;
|
||||
|
||||
const backupId = `backup_${Date.now()}`;
|
||||
const backupPath = path.join(this.backupDir, backupId);
|
||||
|
||||
// 创建备份目录
|
||||
fs.mkdirSync(backupPath, { recursive: true });
|
||||
|
||||
// 获取需要备份的文件
|
||||
const filesToBackup = await this.getFilesToBackup();
|
||||
const backedUpFiles: string[] = [];
|
||||
|
||||
console.log(`📦 Creating backup: ${backupId}`);
|
||||
|
||||
for (const file of filesToBackup) {
|
||||
try {
|
||||
const relativePath = path.relative(this.dataDir, file);
|
||||
const backupFilePath = path.join(backupPath, relativePath);
|
||||
|
||||
// 确保目标目录存在
|
||||
const backupFileDir = path.dirname(backupFilePath);
|
||||
if (!fs.existsSync(backupFileDir)) {
|
||||
fs.mkdirSync(backupFileDir, { recursive: true });
|
||||
}
|
||||
|
||||
// 读取原文件
|
||||
const fileData = await fs.promises.readFile(file);
|
||||
|
||||
if (compress && path.extname(file) === '.json') {
|
||||
// 压缩JSON文件
|
||||
const compressed = await gzip(fileData);
|
||||
await fs.promises.writeFile(backupFilePath + '.gz', compressed);
|
||||
backedUpFiles.push(relativePath + '.gz');
|
||||
} else {
|
||||
// 直接复制文件
|
||||
await fs.promises.writeFile(backupFilePath, fileData);
|
||||
backedUpFiles.push(relativePath);
|
||||
}
|
||||
|
||||
console.log(` ✅ Backed up: ${relativePath}`);
|
||||
} catch (error) {
|
||||
console.error(` ❌ Failed to backup ${file}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
// 计算备份大小
|
||||
const backupSize = await this.calculateDirectorySize(backupPath);
|
||||
|
||||
// 创建备份信息文件
|
||||
const backupInfo: BackupInfo = {
|
||||
id: backupId,
|
||||
timestamp: Date.now(),
|
||||
size: backupSize,
|
||||
compressed: compress,
|
||||
description,
|
||||
files: backedUpFiles
|
||||
};
|
||||
|
||||
await fs.promises.writeFile(
|
||||
path.join(backupPath, 'backup_info.json'),
|
||||
JSON.stringify(backupInfo, null, 2)
|
||||
);
|
||||
|
||||
console.log(`📦 Backup created: ${backupId} (${this.formatBytes(backupSize)})`);
|
||||
|
||||
// 清理旧备份
|
||||
await this.cleanupOldBackups(maxBackups);
|
||||
|
||||
return backupInfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* 恢复备份
|
||||
*/
|
||||
async restoreBackup(backupId: string): Promise<boolean> {
|
||||
const backupPath = path.join(this.backupDir, backupId);
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
console.error(`❌ Backup not found: ${backupId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 读取备份信息
|
||||
const backupInfoPath = path.join(backupPath, 'backup_info.json');
|
||||
if (!fs.existsSync(backupInfoPath)) {
|
||||
console.error(`❌ Backup info not found: ${backupId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const backupInfo: BackupInfo = JSON.parse(
|
||||
await fs.promises.readFile(backupInfoPath, 'utf-8')
|
||||
);
|
||||
|
||||
console.log(`🔄 Restoring backup: ${backupId}`);
|
||||
|
||||
// 创建当前数据的备份(以防恢复失败)
|
||||
const emergencyBackup = await this.createBackup({
|
||||
description: `Emergency backup before restore ${backupId}`,
|
||||
maxBackups: 50 // 保留更多紧急备份
|
||||
});
|
||||
|
||||
try {
|
||||
// 恢复文件
|
||||
for (const file of backupInfo.files) {
|
||||
const backupFilePath = path.join(backupPath, file);
|
||||
const isCompressed = file.endsWith('.gz');
|
||||
const originalFileName = isCompressed ? file.slice(0, -3) : file;
|
||||
const targetPath = path.join(this.dataDir, originalFileName);
|
||||
|
||||
// 确保目标目录存在
|
||||
const targetDir = path.dirname(targetPath);
|
||||
if (!fs.existsSync(targetDir)) {
|
||||
fs.mkdirSync(targetDir, { recursive: true });
|
||||
}
|
||||
|
||||
try {
|
||||
if (isCompressed) {
|
||||
// 解压缩文件
|
||||
const compressedData = await fs.promises.readFile(backupFilePath);
|
||||
const decompressed = await gunzip(compressedData);
|
||||
await fs.promises.writeFile(targetPath, decompressed);
|
||||
} else {
|
||||
// 直接复制文件
|
||||
const fileData = await fs.promises.readFile(backupFilePath);
|
||||
await fs.promises.writeFile(targetPath, fileData);
|
||||
}
|
||||
|
||||
console.log(` ✅ Restored: ${originalFileName}`);
|
||||
} catch (error) {
|
||||
console.error(` ❌ Failed to restore ${file}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`🔄 Backup restored successfully: ${backupId}`);
|
||||
return true;
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Restore failed, attempting to restore emergency backup:`, error);
|
||||
|
||||
// 尝试恢复紧急备份
|
||||
const emergencyRestoreSuccess = await this.restoreBackup(emergencyBackup.id);
|
||||
if (emergencyRestoreSuccess) {
|
||||
console.log(`✅ Emergency backup restored successfully`);
|
||||
} else {
|
||||
console.error(`❌ Emergency backup restore also failed!`);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有备份列表
|
||||
*/
|
||||
async listBackups(): Promise<BackupInfo[]> {
|
||||
const backups: BackupInfo[] = [];
|
||||
|
||||
try {
|
||||
const backupDirs = await fs.promises.readdir(this.backupDir);
|
||||
|
||||
for (const dir of backupDirs) {
|
||||
const backupPath = path.join(this.backupDir, dir);
|
||||
const backupInfoPath = path.join(backupPath, 'backup_info.json');
|
||||
|
||||
if (fs.existsSync(backupInfoPath)) {
|
||||
try {
|
||||
const backupInfo: BackupInfo = JSON.parse(
|
||||
await fs.promises.readFile(backupInfoPath, 'utf-8')
|
||||
);
|
||||
backups.push(backupInfo);
|
||||
} catch (error) {
|
||||
console.error(`Error reading backup info for ${dir}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 按时间戳排序(最新的在前)
|
||||
backups.sort((a, b) => b.timestamp - a.timestamp);
|
||||
|
||||
} catch (error) {
|
||||
console.error('Error listing backups:', error);
|
||||
}
|
||||
|
||||
return backups;
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除备份
|
||||
*/
|
||||
async deleteBackup(backupId: string): Promise<boolean> {
|
||||
const backupPath = path.join(this.backupDir, backupId);
|
||||
|
||||
if (!fs.existsSync(backupPath)) {
|
||||
console.error(`❌ Backup not found: ${backupId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.deleteDirectory(backupPath);
|
||||
console.log(`🗑️ Backup deleted: ${backupId}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`❌ Failed to delete backup ${backupId}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取需要备份的文件列表
|
||||
*/
|
||||
private async getFilesToBackup(): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
|
||||
// 递归获取数据目录中的所有文件(除了备份目录)
|
||||
const scanDirectory = async (dir: string) => {
|
||||
const items = await fs.promises.readdir(dir);
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dir, item);
|
||||
|
||||
// 跳过备份目录
|
||||
if (itemPath === this.backupDir) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const stat = await fs.promises.stat(itemPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
await scanDirectory(itemPath);
|
||||
} else if (stat.isFile()) {
|
||||
// 只备份特定类型的文件
|
||||
const ext = path.extname(item).toLowerCase();
|
||||
if (['.json', '.txt', '.log', '.config'].includes(ext)) {
|
||||
files.push(itemPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
await scanDirectory(this.dataDir);
|
||||
return files;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算目录大小
|
||||
*/
|
||||
private async calculateDirectorySize(dir: string): Promise<number> {
|
||||
let totalSize = 0;
|
||||
|
||||
const items = await fs.promises.readdir(dir);
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dir, item);
|
||||
const stat = await fs.promises.stat(itemPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
totalSize += await this.calculateDirectorySize(itemPath);
|
||||
} else {
|
||||
totalSize += stat.size;
|
||||
}
|
||||
}
|
||||
|
||||
return totalSize;
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧备份
|
||||
*/
|
||||
private async cleanupOldBackups(maxBackups: number): Promise<void> {
|
||||
const backups = await this.listBackups();
|
||||
|
||||
if (backups.length > maxBackups) {
|
||||
const backupsToDelete = backups.slice(maxBackups);
|
||||
|
||||
for (const backup of backupsToDelete) {
|
||||
await this.deleteBackup(backup.id);
|
||||
}
|
||||
|
||||
console.log(`🧹 Cleaned up ${backupsToDelete.length} old backups`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除目录及其内容
|
||||
*/
|
||||
private async deleteDirectory(dir: string): Promise<void> {
|
||||
const items = await fs.promises.readdir(dir);
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dir, item);
|
||||
const stat = await fs.promises.stat(itemPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
await this.deleteDirectory(itemPath);
|
||||
} else {
|
||||
await fs.promises.unlink(itemPath);
|
||||
}
|
||||
}
|
||||
|
||||
await fs.promises.rmdir(dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化字节数
|
||||
*/
|
||||
private formatBytes(bytes: number): string {
|
||||
if (bytes === 0) return '0 Bytes';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
}
|
||||
|
||||
/**
|
||||
* 自动备份调度
|
||||
*/
|
||||
startAutoBackup(intervalHours: number = 6): NodeJS.Timeout {
|
||||
console.log(`⏰ Auto backup scheduled every ${intervalHours} hours`);
|
||||
|
||||
return setInterval(async () => {
|
||||
try {
|
||||
await this.createBackup({
|
||||
description: `Automatic backup - ${new Date().toISOString()}`,
|
||||
maxBackups: 20
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('❌ Auto backup failed:', error);
|
||||
}
|
||||
}, intervalHours * 60 * 60 * 1000);
|
||||
}
|
||||
}
|
||||
484
server/src/logging/LogManager.ts
Normal file
484
server/src/logging/LogManager.ts
Normal file
@@ -0,0 +1,484 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
|
||||
export enum LogLevel {
|
||||
DEBUG = 0,
|
||||
INFO = 1,
|
||||
WARN = 2,
|
||||
ERROR = 3,
|
||||
CRITICAL = 4
|
||||
}
|
||||
|
||||
export interface LogEntry {
|
||||
timestamp: number;
|
||||
level: LogLevel;
|
||||
category: string;
|
||||
message: string;
|
||||
data?: any;
|
||||
clientId?: string;
|
||||
characterId?: string;
|
||||
}
|
||||
|
||||
export interface LogAnalytics {
|
||||
totalEntries: number;
|
||||
levelCounts: { [key in LogLevel]: number };
|
||||
categoryCounts: { [category: string]: number };
|
||||
timeRange: { start: number; end: number };
|
||||
topErrors: Array<{ message: string; count: number }>;
|
||||
connectionStats: {
|
||||
totalConnections: number;
|
||||
totalDisconnections: number;
|
||||
averageSessionDuration: number;
|
||||
};
|
||||
}
|
||||
|
||||
export class LogManager {
|
||||
private logDir: string;
|
||||
private currentLogFile: string;
|
||||
private logLevel: LogLevel;
|
||||
private maxLogFileSize: number;
|
||||
private maxLogFiles: number;
|
||||
private logBuffer: LogEntry[] = [];
|
||||
private flushInterval: NodeJS.Timeout;
|
||||
|
||||
constructor(dataDir: string, options: {
|
||||
logLevel?: LogLevel;
|
||||
maxLogFileSize?: number;
|
||||
maxLogFiles?: number;
|
||||
flushIntervalMs?: number;
|
||||
} = {}) {
|
||||
this.logDir = path.join(dataDir, 'logs');
|
||||
this.logLevel = options.logLevel ?? LogLevel.INFO;
|
||||
this.maxLogFileSize = options.maxLogFileSize ?? 10 * 1024 * 1024; // 10MB
|
||||
this.maxLogFiles = options.maxLogFiles ?? 30; // 30 files
|
||||
|
||||
this.ensureLogDirectory();
|
||||
this.currentLogFile = this.getCurrentLogFileName();
|
||||
|
||||
// 定期刷新日志缓冲区
|
||||
this.flushInterval = setInterval(() => {
|
||||
this.flushLogs();
|
||||
}, options.flushIntervalMs ?? 5000); // 5秒
|
||||
|
||||
// 程序退出时刷新日志
|
||||
process.on('exit', () => this.flushLogs());
|
||||
process.on('SIGINT', () => this.flushLogs());
|
||||
process.on('SIGTERM', () => this.flushLogs());
|
||||
}
|
||||
|
||||
/**
|
||||
* 确保日志目录存在
|
||||
*/
|
||||
private ensureLogDirectory(): void {
|
||||
if (!fs.existsSync(this.logDir)) {
|
||||
fs.mkdirSync(this.logDir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前日志文件名
|
||||
*/
|
||||
private getCurrentLogFileName(): string {
|
||||
const date = new Date();
|
||||
const dateStr = date.toISOString().split('T')[0]; // YYYY-MM-DD
|
||||
return path.join(this.logDir, `server_${dateStr}.log`);
|
||||
}
|
||||
|
||||
/**
|
||||
* 记录日志
|
||||
*/
|
||||
log(level: LogLevel, category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
|
||||
if (level < this.logLevel) {
|
||||
return;
|
||||
}
|
||||
|
||||
const entry: LogEntry = {
|
||||
timestamp: Date.now(),
|
||||
level,
|
||||
category,
|
||||
message,
|
||||
data,
|
||||
clientId,
|
||||
characterId
|
||||
};
|
||||
|
||||
this.logBuffer.push(entry);
|
||||
|
||||
// 如果是错误或关键级别,立即刷新
|
||||
if (level >= LogLevel.ERROR) {
|
||||
this.flushLogs();
|
||||
}
|
||||
|
||||
// 控制台输出
|
||||
this.outputToConsole(entry);
|
||||
}
|
||||
|
||||
/**
|
||||
* 便捷方法
|
||||
*/
|
||||
debug(category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
|
||||
this.log(LogLevel.DEBUG, category, message, data, clientId, characterId);
|
||||
}
|
||||
|
||||
info(category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
|
||||
this.log(LogLevel.INFO, category, message, data, clientId, characterId);
|
||||
}
|
||||
|
||||
warn(category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
|
||||
this.log(LogLevel.WARN, category, message, data, clientId, characterId);
|
||||
}
|
||||
|
||||
error(category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
|
||||
this.log(LogLevel.ERROR, category, message, data, clientId, characterId);
|
||||
}
|
||||
|
||||
critical(category: string, message: string, data?: any, clientId?: string, characterId?: string): void {
|
||||
this.log(LogLevel.CRITICAL, category, message, data, clientId, characterId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新日志缓冲区到文件
|
||||
*/
|
||||
private flushLogs(): void {
|
||||
if (this.logBuffer.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// 检查是否需要轮转日志文件
|
||||
this.rotateLogFileIfNeeded();
|
||||
|
||||
// 将缓冲区中的日志写入文件
|
||||
const logLines = this.logBuffer.map(entry => this.formatLogEntry(entry));
|
||||
const logContent = logLines.join('\n') + '\n';
|
||||
|
||||
fs.appendFileSync(this.currentLogFile, logContent);
|
||||
|
||||
// 清空缓冲区
|
||||
this.logBuffer = [];
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to flush logs:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日志条目
|
||||
*/
|
||||
private formatLogEntry(entry: LogEntry): string {
|
||||
const timestamp = new Date(entry.timestamp).toISOString();
|
||||
const levelStr = LogLevel[entry.level].padEnd(8);
|
||||
const category = entry.category.padEnd(15);
|
||||
|
||||
let line = `${timestamp} [${levelStr}] ${category} ${entry.message}`;
|
||||
|
||||
if (entry.clientId) {
|
||||
line += ` [Client: ${entry.clientId}]`;
|
||||
}
|
||||
|
||||
if (entry.characterId) {
|
||||
line += ` [Character: ${entry.characterId}]`;
|
||||
}
|
||||
|
||||
if (entry.data) {
|
||||
line += ` [Data: ${JSON.stringify(entry.data)}]`;
|
||||
}
|
||||
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* 控制台输出
|
||||
*/
|
||||
private outputToConsole(entry: LogEntry): void {
|
||||
const formatted = this.formatLogEntry(entry);
|
||||
|
||||
switch (entry.level) {
|
||||
case LogLevel.DEBUG:
|
||||
console.debug(formatted);
|
||||
break;
|
||||
case LogLevel.INFO:
|
||||
console.info(formatted);
|
||||
break;
|
||||
case LogLevel.WARN:
|
||||
console.warn(formatted);
|
||||
break;
|
||||
case LogLevel.ERROR:
|
||||
case LogLevel.CRITICAL:
|
||||
console.error(formatted);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 轮转日志文件
|
||||
*/
|
||||
private rotateLogFileIfNeeded(): void {
|
||||
try {
|
||||
// 检查当前日志文件是否存在且大小是否超过限制
|
||||
if (fs.existsSync(this.currentLogFile)) {
|
||||
const stats = fs.statSync(this.currentLogFile);
|
||||
|
||||
if (stats.size >= this.maxLogFileSize) {
|
||||
// 重命名当前文件
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
||||
const rotatedFileName = this.currentLogFile.replace('.log', `_${timestamp}.log`);
|
||||
fs.renameSync(this.currentLogFile, rotatedFileName);
|
||||
|
||||
console.log(`📋 Log file rotated: ${path.basename(rotatedFileName)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// 更新当前日志文件名(可能是新的日期)
|
||||
this.currentLogFile = this.getCurrentLogFileName();
|
||||
|
||||
// 清理旧日志文件
|
||||
this.cleanupOldLogFiles();
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to rotate log file:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理旧日志文件
|
||||
*/
|
||||
private cleanupOldLogFiles(): void {
|
||||
try {
|
||||
const logFiles = fs.readdirSync(this.logDir)
|
||||
.filter(file => file.endsWith('.log'))
|
||||
.map(file => ({
|
||||
name: file,
|
||||
path: path.join(this.logDir, file),
|
||||
mtime: fs.statSync(path.join(this.logDir, file)).mtime
|
||||
}))
|
||||
.sort((a, b) => b.mtime.getTime() - a.mtime.getTime());
|
||||
|
||||
if (logFiles.length > this.maxLogFiles) {
|
||||
const filesToDelete = logFiles.slice(this.maxLogFiles);
|
||||
|
||||
for (const file of filesToDelete) {
|
||||
fs.unlinkSync(file.path);
|
||||
console.log(`🗑️ Deleted old log file: ${file.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to cleanup old log files:', error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 分析日志
|
||||
*/
|
||||
async analyzeLogs(options: {
|
||||
startTime?: number;
|
||||
endTime?: number;
|
||||
level?: LogLevel;
|
||||
category?: string;
|
||||
limit?: number;
|
||||
} = {}): Promise<LogAnalytics> {
|
||||
const {
|
||||
startTime = Date.now() - 24 * 60 * 60 * 1000, // 默认24小时
|
||||
endTime = Date.now(),
|
||||
level,
|
||||
category,
|
||||
limit = 1000
|
||||
} = options;
|
||||
|
||||
const entries = await this.readLogEntries(startTime, endTime, limit);
|
||||
|
||||
// 过滤条目
|
||||
const filteredEntries = entries.filter(entry => {
|
||||
if (level !== undefined && entry.level !== level) return false;
|
||||
if (category && entry.category !== category) return false;
|
||||
return true;
|
||||
});
|
||||
|
||||
// 统计分析
|
||||
const levelCounts = {
|
||||
[LogLevel.DEBUG]: 0,
|
||||
[LogLevel.INFO]: 0,
|
||||
[LogLevel.WARN]: 0,
|
||||
[LogLevel.ERROR]: 0,
|
||||
[LogLevel.CRITICAL]: 0
|
||||
};
|
||||
|
||||
const categoryCounts: { [category: string]: number } = {};
|
||||
const errorMessages: { [message: string]: number } = {};
|
||||
|
||||
let connectionCount = 0;
|
||||
let disconnectionCount = 0;
|
||||
const sessionDurations: number[] = [];
|
||||
|
||||
for (const entry of filteredEntries) {
|
||||
levelCounts[entry.level]++;
|
||||
|
||||
categoryCounts[entry.category] = (categoryCounts[entry.category] || 0) + 1;
|
||||
|
||||
if (entry.level >= LogLevel.ERROR) {
|
||||
errorMessages[entry.message] = (errorMessages[entry.message] || 0) + 1;
|
||||
}
|
||||
|
||||
// 连接统计
|
||||
if (entry.message.includes('connected')) {
|
||||
connectionCount++;
|
||||
} else if (entry.message.includes('disconnected')) {
|
||||
disconnectionCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// 排序错误消息
|
||||
const topErrors = Object.entries(errorMessages)
|
||||
.map(([message, count]) => ({ message, count }))
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.slice(0, 10);
|
||||
|
||||
return {
|
||||
totalEntries: filteredEntries.length,
|
||||
levelCounts,
|
||||
categoryCounts,
|
||||
timeRange: { start: startTime, end: endTime },
|
||||
topErrors,
|
||||
connectionStats: {
|
||||
totalConnections: connectionCount,
|
||||
totalDisconnections: disconnectionCount,
|
||||
averageSessionDuration: sessionDurations.length > 0
|
||||
? sessionDurations.reduce((a, b) => a + b, 0) / sessionDurations.length
|
||||
: 0
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取日志条目
|
||||
*/
|
||||
private async readLogEntries(startTime: number, endTime: number, limit: number): Promise<LogEntry[]> {
|
||||
const entries: LogEntry[] = [];
|
||||
|
||||
try {
|
||||
const logFiles = fs.readdirSync(this.logDir)
|
||||
.filter(file => file.endsWith('.log'))
|
||||
.map(file => path.join(this.logDir, file))
|
||||
.sort();
|
||||
|
||||
for (const logFile of logFiles) {
|
||||
const content = fs.readFileSync(logFile, 'utf-8');
|
||||
const lines = content.split('\n').filter(line => line.trim());
|
||||
|
||||
for (const line of lines) {
|
||||
try {
|
||||
const entry = this.parseLogLine(line);
|
||||
if (entry && entry.timestamp >= startTime && entry.timestamp <= endTime) {
|
||||
entries.push(entry);
|
||||
|
||||
if (entries.length >= limit) {
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略解析错误的行
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('Failed to read log entries:', error);
|
||||
}
|
||||
|
||||
return entries.sort((a, b) => a.timestamp - b.timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析日志行
|
||||
*/
|
||||
private parseLogLine(line: string): LogEntry | null {
|
||||
try {
|
||||
// 格式: 2023-12-05T10:30:00.000Z [INFO ] CONNECTION Client connected [Client: abc123] [Data: {...}]
|
||||
const timestampMatch = line.match(/^(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z)/);
|
||||
if (!timestampMatch) return null;
|
||||
|
||||
const timestamp = new Date(timestampMatch[1]).getTime();
|
||||
|
||||
const levelMatch = line.match(/\[(\w+)\s*\]/);
|
||||
if (!levelMatch) return null;
|
||||
|
||||
const levelStr = levelMatch[1];
|
||||
const level = LogLevel[levelStr as keyof typeof LogLevel];
|
||||
if (level === undefined) return null;
|
||||
|
||||
const parts = line.split('] ');
|
||||
if (parts.length < 3) return null;
|
||||
|
||||
const category = parts[1].trim();
|
||||
const messagePart = parts.slice(2).join('] ');
|
||||
|
||||
// 提取客户端ID和角色ID
|
||||
const clientMatch = messagePart.match(/\[Client: ([^\]]+)\]/);
|
||||
const characterMatch = messagePart.match(/\[Character: ([^\]]+)\]/);
|
||||
const dataMatch = messagePart.match(/\[Data: (.+)\]$/);
|
||||
|
||||
let message = messagePart;
|
||||
let data: any = undefined;
|
||||
|
||||
// 清理消息文本
|
||||
message = message.replace(/\[Client: [^\]]+\]/, '').trim();
|
||||
message = message.replace(/\[Character: [^\]]+\]/, '').trim();
|
||||
|
||||
if (dataMatch) {
|
||||
message = message.replace(/\[Data: .+\]$/, '').trim();
|
||||
try {
|
||||
data = JSON.parse(dataMatch[1]);
|
||||
} catch {
|
||||
// 忽略JSON解析错误
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
timestamp,
|
||||
level,
|
||||
category,
|
||||
message,
|
||||
data,
|
||||
clientId: clientMatch ? clientMatch[1] : undefined,
|
||||
characterId: characterMatch ? characterMatch[1] : undefined
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取日志文件列表
|
||||
*/
|
||||
getLogFiles(): Array<{ name: string; size: number; modified: number }> {
|
||||
try {
|
||||
return fs.readdirSync(this.logDir)
|
||||
.filter(file => file.endsWith('.log'))
|
||||
.map(file => {
|
||||
const filePath = path.join(this.logDir, file);
|
||||
const stats = fs.statSync(filePath);
|
||||
return {
|
||||
name: file,
|
||||
size: stats.size,
|
||||
modified: stats.mtime.getTime()
|
||||
};
|
||||
})
|
||||
.sort((a, b) => b.modified - a.modified);
|
||||
} catch (error) {
|
||||
console.error('Failed to get log files:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清理资源
|
||||
*/
|
||||
destroy(): void {
|
||||
if (this.flushInterval) {
|
||||
clearInterval(this.flushInterval);
|
||||
}
|
||||
this.flushLogs();
|
||||
}
|
||||
}
|
||||
720
server/src/maintenance/MaintenanceManager.ts
Normal file
720
server/src/maintenance/MaintenanceManager.ts
Normal file
@@ -0,0 +1,720 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { HealthChecker, HealthStatus } from '../monitoring/HealthChecker';
|
||||
import { BackupManager } from '../backup/BackupManager';
|
||||
import { LogManager, LogLevel } from '../logging/LogManager';
|
||||
|
||||
export interface MaintenanceTask {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
schedule: string; // cron-like schedule
|
||||
lastRun?: number;
|
||||
nextRun?: number;
|
||||
enabled: boolean;
|
||||
priority: 'low' | 'medium' | 'high' | 'critical';
|
||||
}
|
||||
|
||||
export interface MaintenanceReport {
|
||||
timestamp: number;
|
||||
tasks: Array<{
|
||||
task: MaintenanceTask;
|
||||
status: 'success' | 'failed' | 'skipped';
|
||||
duration: number;
|
||||
error?: string;
|
||||
details?: any;
|
||||
}>;
|
||||
systemHealth: HealthStatus;
|
||||
recommendations: string[];
|
||||
}
|
||||
|
||||
export class MaintenanceManager {
|
||||
private dataDir: string;
|
||||
private healthChecker: HealthChecker;
|
||||
private backupManager: BackupManager;
|
||||
private logManager: LogManager;
|
||||
private tasks: Map<string, MaintenanceTask> = new Map();
|
||||
private maintenanceInterval: NodeJS.Timeout | null = null;
|
||||
private isMaintenanceMode: boolean = false;
|
||||
|
||||
constructor(
|
||||
dataDir: string,
|
||||
healthChecker: HealthChecker,
|
||||
backupManager: BackupManager,
|
||||
logManager: LogManager
|
||||
) {
|
||||
this.dataDir = dataDir;
|
||||
this.healthChecker = healthChecker;
|
||||
this.backupManager = backupManager;
|
||||
this.logManager = logManager;
|
||||
|
||||
this.initializeDefaultTasks();
|
||||
this.loadTasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 初始化默认维护任务
|
||||
*/
|
||||
private initializeDefaultTasks(): void {
|
||||
const defaultTasks: MaintenanceTask[] = [
|
||||
{
|
||||
id: 'health_check',
|
||||
name: 'System Health Check',
|
||||
description: 'Check system health metrics and alert on issues',
|
||||
schedule: '*/5 * * * *', // Every 5 minutes
|
||||
enabled: true,
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 'backup_data',
|
||||
name: 'Data Backup',
|
||||
description: 'Create backup of all game data',
|
||||
schedule: '0 */6 * * *', // Every 6 hours
|
||||
enabled: true,
|
||||
priority: 'high'
|
||||
},
|
||||
{
|
||||
id: 'cleanup_logs',
|
||||
name: 'Log Cleanup',
|
||||
description: 'Clean up old log files and rotate current logs',
|
||||
schedule: '0 2 * * *', // Daily at 2 AM
|
||||
enabled: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
id: 'cleanup_temp_files',
|
||||
name: 'Temporary Files Cleanup',
|
||||
description: 'Remove temporary files and clean up cache',
|
||||
schedule: '0 3 * * *', // Daily at 3 AM
|
||||
enabled: true,
|
||||
priority: 'low'
|
||||
},
|
||||
{
|
||||
id: 'validate_data',
|
||||
name: 'Data Validation',
|
||||
description: 'Validate integrity of character and world data',
|
||||
schedule: '0 4 * * 0', // Weekly on Sunday at 4 AM
|
||||
enabled: true,
|
||||
priority: 'medium'
|
||||
},
|
||||
{
|
||||
id: 'performance_analysis',
|
||||
name: 'Performance Analysis',
|
||||
description: 'Analyze system performance and generate reports',
|
||||
schedule: '0 1 * * *', // Daily at 1 AM
|
||||
enabled: true,
|
||||
priority: 'low'
|
||||
},
|
||||
{
|
||||
id: 'security_scan',
|
||||
name: 'Security Scan',
|
||||
description: 'Scan for security issues and suspicious activities',
|
||||
schedule: '0 0 * * *', // Daily at midnight
|
||||
enabled: true,
|
||||
priority: 'high'
|
||||
}
|
||||
];
|
||||
|
||||
for (const task of defaultTasks) {
|
||||
this.tasks.set(task.id, task);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动维护调度器
|
||||
*/
|
||||
startScheduler(): void {
|
||||
if (this.maintenanceInterval) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.logManager.info('MAINTENANCE', 'Starting maintenance scheduler');
|
||||
|
||||
// 每分钟检查一次是否有任务需要执行
|
||||
this.maintenanceInterval = setInterval(() => {
|
||||
this.checkAndRunTasks();
|
||||
}, 60000); // 1 minute
|
||||
|
||||
// 立即执行一次检查
|
||||
this.checkAndRunTasks();
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止维护调度器
|
||||
*/
|
||||
stopScheduler(): void {
|
||||
if (this.maintenanceInterval) {
|
||||
clearInterval(this.maintenanceInterval);
|
||||
this.maintenanceInterval = null;
|
||||
this.logManager.info('MAINTENANCE', 'Maintenance scheduler stopped');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查并运行到期的任务
|
||||
*/
|
||||
private async checkAndRunTasks(): Promise<void> {
|
||||
const now = Date.now();
|
||||
const tasksToRun: MaintenanceTask[] = [];
|
||||
|
||||
for (const task of this.tasks.values()) {
|
||||
if (!task.enabled) continue;
|
||||
|
||||
const nextRun = this.calculateNextRun(task);
|
||||
if (nextRun <= now) {
|
||||
tasksToRun.push(task);
|
||||
}
|
||||
}
|
||||
|
||||
if (tasksToRun.length > 0) {
|
||||
// 按优先级排序
|
||||
tasksToRun.sort((a, b) => {
|
||||
const priorityOrder = { critical: 4, high: 3, medium: 2, low: 1 };
|
||||
return priorityOrder[b.priority] - priorityOrder[a.priority];
|
||||
});
|
||||
|
||||
await this.runMaintenanceTasks(tasksToRun);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 运行维护任务
|
||||
*/
|
||||
async runMaintenanceTasks(tasks: MaintenanceTask[]): Promise<MaintenanceReport> {
|
||||
const startTime = Date.now();
|
||||
this.logManager.info('MAINTENANCE', `Starting maintenance run with ${tasks.length} tasks`);
|
||||
|
||||
const report: MaintenanceReport = {
|
||||
timestamp: startTime,
|
||||
tasks: [],
|
||||
systemHealth: await this.healthChecker.getHealthStatus(0, 0), // Will be updated
|
||||
recommendations: []
|
||||
};
|
||||
|
||||
for (const task of tasks) {
|
||||
const taskStartTime = Date.now();
|
||||
|
||||
try {
|
||||
this.logManager.info('MAINTENANCE', `Running task: ${task.name}`, { taskId: task.id });
|
||||
|
||||
const result = await this.executeTask(task);
|
||||
|
||||
const duration = Date.now() - taskStartTime;
|
||||
task.lastRun = taskStartTime;
|
||||
|
||||
report.tasks.push({
|
||||
task,
|
||||
status: result.success ? 'success' : 'failed',
|
||||
duration,
|
||||
error: result.error,
|
||||
details: result.details
|
||||
});
|
||||
|
||||
if (result.success) {
|
||||
this.logManager.info('MAINTENANCE', `Task completed: ${task.name}`, {
|
||||
taskId: task.id,
|
||||
duration
|
||||
});
|
||||
} else {
|
||||
this.logManager.error('MAINTENANCE', `Task failed: ${task.name}`, {
|
||||
taskId: task.id,
|
||||
error: result.error
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
const duration = Date.now() - taskStartTime;
|
||||
task.lastRun = taskStartTime;
|
||||
|
||||
report.tasks.push({
|
||||
task,
|
||||
status: 'failed',
|
||||
duration,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
|
||||
this.logManager.error('MAINTENANCE', `Task error: ${task.name}`, {
|
||||
taskId: task.id,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 更新系统健康状态
|
||||
report.systemHealth = await this.healthChecker.getHealthStatus(0, 0);
|
||||
|
||||
// 生成建议
|
||||
report.recommendations = this.generateRecommendations(report);
|
||||
|
||||
// 保存任务状态
|
||||
this.saveTasks();
|
||||
|
||||
const totalDuration = Date.now() - startTime;
|
||||
this.logManager.info('MAINTENANCE', `Maintenance run completed`, {
|
||||
totalTasks: tasks.length,
|
||||
successfulTasks: report.tasks.filter(t => t.status === 'success').length,
|
||||
failedTasks: report.tasks.filter(t => t.status === 'failed').length,
|
||||
totalDuration
|
||||
});
|
||||
|
||||
return report;
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行单个维护任务
|
||||
*/
|
||||
private async executeTask(task: MaintenanceTask): Promise<{ success: boolean; error?: string; details?: any }> {
|
||||
switch (task.id) {
|
||||
case 'health_check':
|
||||
return await this.executeHealthCheck();
|
||||
|
||||
case 'backup_data':
|
||||
return await this.executeBackup();
|
||||
|
||||
case 'cleanup_logs':
|
||||
return await this.executeLogCleanup();
|
||||
|
||||
case 'cleanup_temp_files':
|
||||
return await this.executeTempCleanup();
|
||||
|
||||
case 'validate_data':
|
||||
return await this.executeDataValidation();
|
||||
|
||||
case 'performance_analysis':
|
||||
return await this.executePerformanceAnalysis();
|
||||
|
||||
case 'security_scan':
|
||||
return await this.executeSecurityScan();
|
||||
|
||||
default:
|
||||
return { success: false, error: `Unknown task: ${task.id}` };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行健康检查
|
||||
*/
|
||||
private async executeHealthCheck(): Promise<{ success: boolean; error?: string; details?: any }> {
|
||||
try {
|
||||
const health = await this.healthChecker.getHealthStatus(0, 0);
|
||||
const serviceHealth = await this.healthChecker.checkServiceHealth();
|
||||
|
||||
if (health.status === 'critical' || !serviceHealth) {
|
||||
this.logManager.critical('HEALTH', 'System health critical', { health, serviceHealth });
|
||||
} else if (health.status === 'warning') {
|
||||
this.logManager.warn('HEALTH', 'System health warning', { health });
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
details: {
|
||||
health,
|
||||
serviceHealth,
|
||||
issues: health.issues
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行备份
|
||||
*/
|
||||
private async executeBackup(): Promise<{ success: boolean; error?: string; details?: any }> {
|
||||
try {
|
||||
const backup = await this.backupManager.createBackup({
|
||||
description: 'Scheduled maintenance backup',
|
||||
maxBackups: 20
|
||||
});
|
||||
|
||||
return {
|
||||
success: true,
|
||||
details: {
|
||||
backupId: backup.id,
|
||||
size: backup.size,
|
||||
files: backup.files.length
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行日志清理
|
||||
*/
|
||||
private async executeLogCleanup(): Promise<{ success: boolean; error?: string; details?: any }> {
|
||||
try {
|
||||
const logFiles = this.logManager.getLogFiles();
|
||||
const oldFiles = logFiles.filter(file =>
|
||||
Date.now() - file.modified > 30 * 24 * 60 * 60 * 1000 // 30 days
|
||||
);
|
||||
|
||||
let deletedFiles = 0;
|
||||
let deletedSize = 0;
|
||||
|
||||
for (const file of oldFiles) {
|
||||
try {
|
||||
const filePath = path.join(this.dataDir, 'logs', file.name);
|
||||
fs.unlinkSync(filePath);
|
||||
deletedFiles++;
|
||||
deletedSize += file.size;
|
||||
} catch (error) {
|
||||
this.logManager.warn('MAINTENANCE', `Failed to delete log file: ${file.name}`, { error });
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
details: {
|
||||
totalFiles: logFiles.length,
|
||||
deletedFiles,
|
||||
deletedSize
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行临时文件清理
|
||||
*/
|
||||
private async executeTempCleanup(): Promise<{ success: boolean; error?: string; details?: any }> {
|
||||
try {
|
||||
let deletedFiles = 0;
|
||||
let deletedSize = 0;
|
||||
|
||||
// 清理临时文件
|
||||
const tempPatterns = ['.tmp', '.temp', '.cache', '.lock'];
|
||||
|
||||
const cleanDirectory = async (dir: string) => {
|
||||
try {
|
||||
const items = fs.readdirSync(dir);
|
||||
|
||||
for (const item of items) {
|
||||
const itemPath = path.join(dir, item);
|
||||
const stat = fs.statSync(itemPath);
|
||||
|
||||
if (stat.isDirectory()) {
|
||||
await cleanDirectory(itemPath);
|
||||
} else {
|
||||
const shouldDelete = tempPatterns.some(pattern => item.endsWith(pattern)) ||
|
||||
item.startsWith('.health_check_temp');
|
||||
|
||||
if (shouldDelete) {
|
||||
fs.unlinkSync(itemPath);
|
||||
deletedFiles++;
|
||||
deletedSize += stat.size;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// 忽略访问错误
|
||||
}
|
||||
};
|
||||
|
||||
await cleanDirectory(this.dataDir);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
details: {
|
||||
deletedFiles,
|
||||
deletedSize
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行数据验证
|
||||
*/
|
||||
private async executeDataValidation(): Promise<{ success: boolean; error?: string; details?: any }> {
|
||||
try {
|
||||
const charactersFile = path.join(this.dataDir, 'characters.json');
|
||||
let validationResults = {
|
||||
charactersValid: false,
|
||||
charactersCount: 0,
|
||||
issues: [] as string[]
|
||||
};
|
||||
|
||||
if (fs.existsSync(charactersFile)) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(charactersFile, 'utf-8'));
|
||||
|
||||
if (Array.isArray(data)) {
|
||||
validationResults.charactersValid = true;
|
||||
validationResults.charactersCount = data.length;
|
||||
|
||||
// 验证每个角色数据
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
const char = data[i];
|
||||
if (!char.id || !char.name || !char.ownerId) {
|
||||
validationResults.issues.push(`Character ${i} missing required fields`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
validationResults.issues.push('Characters data is not an array');
|
||||
}
|
||||
} catch (error) {
|
||||
validationResults.issues.push('Characters file is corrupted');
|
||||
}
|
||||
} else {
|
||||
validationResults.issues.push('Characters file does not exist');
|
||||
}
|
||||
|
||||
return {
|
||||
success: validationResults.issues.length === 0,
|
||||
details: validationResults
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行性能分析
|
||||
*/
|
||||
private async executePerformanceAnalysis(): Promise<{ success: boolean; error?: string; details?: any }> {
|
||||
try {
|
||||
const analytics = await this.logManager.analyzeLogs({
|
||||
startTime: Date.now() - 24 * 60 * 60 * 1000 // Last 24 hours
|
||||
});
|
||||
|
||||
const systemInfo = this.healthChecker.getSystemInfo();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
details: {
|
||||
logAnalytics: analytics,
|
||||
systemInfo
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行安全扫描
|
||||
*/
|
||||
private async executeSecurityScan(): Promise<{ success: boolean; error?: string; details?: any }> {
|
||||
try {
|
||||
// 检查可疑活动
|
||||
const analytics = await this.logManager.analyzeLogs({
|
||||
startTime: Date.now() - 24 * 60 * 60 * 1000,
|
||||
level: LogLevel.ERROR
|
||||
});
|
||||
|
||||
const securityIssues: string[] = [];
|
||||
|
||||
// 检查错误率
|
||||
if (analytics.totalEntries > 100 &&
|
||||
analytics.levelCounts[LogLevel.ERROR] / analytics.totalEntries > 0.1) {
|
||||
securityIssues.push('High error rate detected');
|
||||
}
|
||||
|
||||
// 检查频繁的失败尝试
|
||||
for (const error of analytics.topErrors) {
|
||||
if (error.message.includes('failed') && error.count > 50) {
|
||||
securityIssues.push(`Frequent failures: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
success: securityIssues.length === 0,
|
||||
details: {
|
||||
securityIssues,
|
||||
errorAnalytics: analytics
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成建议
|
||||
*/
|
||||
private generateRecommendations(report: MaintenanceReport): string[] {
|
||||
const recommendations: string[] = [];
|
||||
|
||||
// 基于系统健康状态的建议
|
||||
if (report.systemHealth.status === 'critical') {
|
||||
recommendations.push('System is in critical state - immediate attention required');
|
||||
} else if (report.systemHealth.status === 'warning') {
|
||||
recommendations.push('System has warnings - monitor closely');
|
||||
}
|
||||
|
||||
// 基于内存使用的建议
|
||||
if (report.systemHealth.metrics.memory.percentage > 80) {
|
||||
recommendations.push('High memory usage - consider restarting or optimizing');
|
||||
}
|
||||
|
||||
// 基于存储空间的建议
|
||||
if (report.systemHealth.metrics.storage.percentage > 90) {
|
||||
recommendations.push('Low disk space - clean up old files or expand storage');
|
||||
}
|
||||
|
||||
// 基于任务失败的建议
|
||||
const failedTasks = report.tasks.filter(t => t.status === 'failed');
|
||||
if (failedTasks.length > 0) {
|
||||
recommendations.push(`${failedTasks.length} maintenance tasks failed - check logs for details`);
|
||||
}
|
||||
|
||||
return recommendations;
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算下次运行时间
|
||||
*/
|
||||
private calculateNextRun(task: MaintenanceTask): number {
|
||||
// 简化的调度计算 - 在实际项目中应该使用完整的cron解析器
|
||||
const now = Date.now();
|
||||
const lastRun = task.lastRun || 0;
|
||||
|
||||
// 解析简单的调度格式
|
||||
if (task.schedule.startsWith('*/')) {
|
||||
const minutes = parseInt(task.schedule.split(' ')[0].substring(2));
|
||||
return lastRun + (minutes * 60 * 1000);
|
||||
} else if (task.schedule.startsWith('0 */')) {
|
||||
const hours = parseInt(task.schedule.split(' ')[1].substring(2));
|
||||
return lastRun + (hours * 60 * 60 * 1000);
|
||||
} else if (task.schedule.startsWith('0 ')) {
|
||||
// Daily tasks
|
||||
const hour = parseInt(task.schedule.split(' ')[1]);
|
||||
const nextRun = new Date();
|
||||
nextRun.setHours(hour, 0, 0, 0);
|
||||
|
||||
if (nextRun.getTime() <= now) {
|
||||
nextRun.setDate(nextRun.getDate() + 1);
|
||||
}
|
||||
|
||||
return nextRun.getTime();
|
||||
}
|
||||
|
||||
// 默认:如果没有运行过,立即运行
|
||||
return lastRun === 0 ? now : now + 60000; // 1 minute
|
||||
}
|
||||
|
||||
/**
|
||||
* 加载任务配置
|
||||
*/
|
||||
private loadTasks(): void {
|
||||
const tasksFile = path.join(this.dataDir, 'maintenance_tasks.json');
|
||||
|
||||
if (fs.existsSync(tasksFile)) {
|
||||
try {
|
||||
const data = JSON.parse(fs.readFileSync(tasksFile, 'utf-8'));
|
||||
|
||||
for (const taskData of data) {
|
||||
if (this.tasks.has(taskData.id)) {
|
||||
// 更新现有任务
|
||||
const existingTask = this.tasks.get(taskData.id)!;
|
||||
Object.assign(existingTask, taskData);
|
||||
} else {
|
||||
// 添加新任务
|
||||
this.tasks.set(taskData.id, taskData);
|
||||
}
|
||||
}
|
||||
|
||||
this.logManager.info('MAINTENANCE', `Loaded ${data.length} maintenance tasks`);
|
||||
} catch (error) {
|
||||
this.logManager.error('MAINTENANCE', 'Failed to load maintenance tasks', { error });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存任务配置
|
||||
*/
|
||||
private saveTasks(): void {
|
||||
const tasksFile = path.join(this.dataDir, 'maintenance_tasks.json');
|
||||
|
||||
try {
|
||||
const tasksArray = Array.from(this.tasks.values());
|
||||
fs.writeFileSync(tasksFile, JSON.stringify(tasksArray, null, 2));
|
||||
} catch (error) {
|
||||
this.logManager.error('MAINTENANCE', 'Failed to save maintenance tasks', { error });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 进入维护模式
|
||||
*/
|
||||
async enterMaintenanceMode(reason: string): Promise<void> {
|
||||
this.isMaintenanceMode = true;
|
||||
this.logManager.warn('MAINTENANCE', `Entering maintenance mode: ${reason}`);
|
||||
|
||||
// 这里可以添加通知所有客户端的逻辑
|
||||
// 例如:广播维护模式消息,拒绝新连接等
|
||||
}
|
||||
|
||||
/**
|
||||
* 退出维护模式
|
||||
*/
|
||||
async exitMaintenanceMode(): Promise<void> {
|
||||
this.isMaintenanceMode = false;
|
||||
this.logManager.info('MAINTENANCE', 'Exiting maintenance mode');
|
||||
|
||||
// 这里可以添加恢复正常服务的逻辑
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查是否在维护模式
|
||||
*/
|
||||
isInMaintenanceMode(): boolean {
|
||||
return this.isMaintenanceMode;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取所有任务
|
||||
*/
|
||||
getTasks(): MaintenanceTask[] {
|
||||
return Array.from(this.tasks.values());
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新任务
|
||||
*/
|
||||
updateTask(taskId: string, updates: Partial<MaintenanceTask>): boolean {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (!task) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Object.assign(task, updates);
|
||||
this.saveTasks();
|
||||
|
||||
this.logManager.info('MAINTENANCE', `Task updated: ${taskId}`, { updates });
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* 手动运行任务
|
||||
*/
|
||||
async runTask(taskId: string): Promise<{ success: boolean; error?: string; details?: any }> {
|
||||
const task = this.tasks.get(taskId);
|
||||
if (!task) {
|
||||
return { success: false, error: `Task not found: ${taskId}` };
|
||||
}
|
||||
|
||||
this.logManager.info('MAINTENANCE', `Manually running task: ${task.name}`, { taskId });
|
||||
|
||||
try {
|
||||
const result = await this.executeTask(task);
|
||||
task.lastRun = Date.now();
|
||||
this.saveTasks();
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
221
server/src/monitoring/HealthChecker.ts
Normal file
221
server/src/monitoring/HealthChecker.ts
Normal file
@@ -0,0 +1,221 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
|
||||
export interface HealthStatus {
|
||||
status: 'healthy' | 'warning' | 'critical';
|
||||
timestamp: number;
|
||||
metrics: {
|
||||
memory: {
|
||||
used: number;
|
||||
total: number;
|
||||
percentage: number;
|
||||
};
|
||||
cpu: {
|
||||
usage: number;
|
||||
};
|
||||
connections: {
|
||||
active: number;
|
||||
total: number;
|
||||
};
|
||||
storage: {
|
||||
used: number;
|
||||
available: number;
|
||||
percentage: number;
|
||||
};
|
||||
uptime: number;
|
||||
};
|
||||
issues: string[];
|
||||
}
|
||||
|
||||
export class HealthChecker {
|
||||
private startTime: number;
|
||||
private lastCpuUsage: NodeJS.CpuUsage;
|
||||
private dataDir: string;
|
||||
|
||||
constructor(dataDir: string) {
|
||||
this.startTime = Date.now();
|
||||
this.lastCpuUsage = process.cpuUsage();
|
||||
this.dataDir = dataDir;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统健康状态
|
||||
*/
|
||||
async getHealthStatus(activeConnections: number, totalConnections: number): Promise<HealthStatus> {
|
||||
const issues: string[] = [];
|
||||
|
||||
// 内存使用情况
|
||||
const memoryUsage = process.memoryUsage();
|
||||
const totalMemory = os.totalmem();
|
||||
const memoryPercentage = (memoryUsage.rss / totalMemory) * 100;
|
||||
|
||||
if (memoryPercentage > 80) {
|
||||
issues.push(`High memory usage: ${memoryPercentage.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
// CPU使用情况
|
||||
const currentCpuUsage = process.cpuUsage(this.lastCpuUsage);
|
||||
const cpuPercentage = (currentCpuUsage.user + currentCpuUsage.system) / 1000000 * 100;
|
||||
this.lastCpuUsage = process.cpuUsage();
|
||||
|
||||
if (cpuPercentage > 80) {
|
||||
issues.push(`High CPU usage: ${cpuPercentage.toFixed(1)}%`);
|
||||
}
|
||||
|
||||
// 存储空间检查
|
||||
const storageInfo = await this.getStorageInfo();
|
||||
if (storageInfo.percentage > 90) {
|
||||
issues.push(`Low disk space: ${storageInfo.percentage.toFixed(1)}% used`);
|
||||
}
|
||||
|
||||
// 连接数检查
|
||||
if (activeConnections > 1000) {
|
||||
issues.push(`High connection count: ${activeConnections} active connections`);
|
||||
}
|
||||
|
||||
// 运行时间
|
||||
const uptime = Date.now() - this.startTime;
|
||||
|
||||
// 确定整体状态
|
||||
let status: 'healthy' | 'warning' | 'critical' = 'healthy';
|
||||
if (issues.length > 0) {
|
||||
status = memoryPercentage > 90 || cpuPercentage > 90 || storageInfo.percentage > 95 ? 'critical' : 'warning';
|
||||
}
|
||||
|
||||
return {
|
||||
status,
|
||||
timestamp: Date.now(),
|
||||
metrics: {
|
||||
memory: {
|
||||
used: memoryUsage.rss,
|
||||
total: totalMemory,
|
||||
percentage: memoryPercentage
|
||||
},
|
||||
cpu: {
|
||||
usage: cpuPercentage
|
||||
},
|
||||
connections: {
|
||||
active: activeConnections,
|
||||
total: totalConnections
|
||||
},
|
||||
storage: storageInfo,
|
||||
uptime
|
||||
},
|
||||
issues
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取存储空间信息
|
||||
*/
|
||||
private async getStorageInfo(): Promise<{ used: number; available: number; percentage: number }> {
|
||||
try {
|
||||
const stats = await fs.promises.stat(this.dataDir);
|
||||
const diskUsage = await this.getDiskUsage(this.dataDir);
|
||||
|
||||
return {
|
||||
used: diskUsage.used,
|
||||
available: diskUsage.available,
|
||||
percentage: (diskUsage.used / (diskUsage.used + diskUsage.available)) * 100
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error getting storage info:', error);
|
||||
return { used: 0, available: 0, percentage: 0 };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取磁盘使用情况(跨平台)
|
||||
*/
|
||||
private async getDiskUsage(dirPath: string): Promise<{ used: number; available: number }> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const { exec } = require('child_process');
|
||||
|
||||
// Windows
|
||||
if (process.platform === 'win32') {
|
||||
const drive = path.parse(dirPath).root;
|
||||
exec(`wmic logicaldisk where caption="${drive}" get size,freespace /value`, (error: any, stdout: string) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = stdout.split('\n').filter(line => line.includes('='));
|
||||
let freeSpace = 0;
|
||||
let totalSize = 0;
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.includes('FreeSpace=')) {
|
||||
freeSpace = parseInt(line.split('=')[1]);
|
||||
} else if (line.includes('Size=')) {
|
||||
totalSize = parseInt(line.split('=')[1]);
|
||||
}
|
||||
});
|
||||
|
||||
resolve({
|
||||
used: totalSize - freeSpace,
|
||||
available: freeSpace
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Unix/Linux/macOS
|
||||
exec(`df -k "${dirPath}"`, (error: any, stdout: string) => {
|
||||
if (error) {
|
||||
reject(error);
|
||||
return;
|
||||
}
|
||||
|
||||
const lines = stdout.split('\n');
|
||||
if (lines.length < 2) {
|
||||
reject(new Error('Invalid df output'));
|
||||
return;
|
||||
}
|
||||
|
||||
const parts = lines[1].split(/\s+/);
|
||||
const used = parseInt(parts[2]) * 1024; // Convert from KB to bytes
|
||||
const available = parseInt(parts[3]) * 1024;
|
||||
|
||||
resolve({ used, available });
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查服务是否响应
|
||||
*/
|
||||
async checkServiceHealth(): Promise<boolean> {
|
||||
try {
|
||||
// 检查数据目录是否可访问
|
||||
await fs.promises.access(this.dataDir, fs.constants.R_OK | fs.constants.W_OK);
|
||||
|
||||
// 检查是否可以创建临时文件
|
||||
const tempFile = path.join(this.dataDir, '.health_check_temp');
|
||||
await fs.promises.writeFile(tempFile, 'health_check');
|
||||
await fs.promises.unlink(tempFile);
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Service health check failed:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取系统信息
|
||||
*/
|
||||
getSystemInfo() {
|
||||
return {
|
||||
platform: os.platform(),
|
||||
arch: os.arch(),
|
||||
nodeVersion: process.version,
|
||||
hostname: os.hostname(),
|
||||
cpus: os.cpus().length,
|
||||
totalMemory: os.totalmem(),
|
||||
freeMemory: os.freemem(),
|
||||
loadAverage: os.loadavg(),
|
||||
uptime: os.uptime()
|
||||
};
|
||||
}
|
||||
}
|
||||
1071
server/src/server.ts
Normal file
1071
server/src/server.ts
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user