UNPKG

mullvad-servers-ping-tester

Version:

Инструмент для тестирования пинга серверов Mullvad VPN с расширенной аналитикой

255 lines 13.4 kB
"use strict"; var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __metadata = (this && this.__metadata) || function (k, v) { if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v); }; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.HistoryAnalyzerService = void 0; const inversify_1 = require("inversify"); const promises_1 = __importDefault(require("fs/promises")); const path_1 = __importDefault(require("path")); const logger_1 = require("@/utils/logger"); const config_1 = __importDefault(require("@/config/config")); let HistoryAnalyzerService = class HistoryAnalyzerService { constructor() { this.logger = new logger_1.Logger('HistoryAnalyzerService'); this.historyPath = path_1.default.join(process.cwd(), 'history'); } /** * Сохраняет текущие результаты в историю * @param results Результаты пинга * @param statistics Статистика пинга */ async saveToHistory(results, statistics) { try { // Создаем директорию истории, если она не существует await promises_1.default.mkdir(this.historyPath, { recursive: true }); const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, ''); const historyFile = path_1.default.join(this.historyPath, `history_${timestamp}.json`); const historyData = { date: new Date().toISOString(), results, statistics }; await promises_1.default.writeFile(historyFile, JSON.stringify(historyData, null, 2), 'utf8'); this.logger.info(`History saved to ${historyFile}`); // Очищаем старые файлы истории, если их слишком много await this.cleanupOldHistoryFiles(); } catch (error) { this.logger.error(`Failed to save history: ${error instanceof Error ? error.message : String(error)}`); } } /** * Загружает исторические данные */ async loadHistory() { try { // Создаем директорию истории, если она не существует await promises_1.default.mkdir(this.historyPath, { recursive: true }); const files = await promises_1.default.readdir(this.historyPath); const historyFiles = files .filter(file => file.startsWith('history_') && file.endsWith('.json')) .sort(); // Сортируем по имени файла (которое содержит временную метку) const historyData = []; for (const file of historyFiles) { try { const filePath = path_1.default.join(this.historyPath, file); const fileContent = await promises_1.default.readFile(filePath, 'utf8'); const data = JSON.parse(fileContent); historyData.push(data); } catch (error) { this.logger.warn(`Failed to parse history file ${file}: ${error instanceof Error ? error.message : String(error)}`); } } this.logger.info(`Loaded ${historyData.length} history records`); return historyData; } catch (error) { this.logger.error(`Failed to load history: ${error instanceof Error ? error.message : String(error)}`); return []; } } /** * Анализирует изменения в производительности серверов * @param history Исторические данные */ analyzePerformanceTrends(history) { if (history.length < 2) { this.logger.warn('Not enough history data for trend analysis'); return { overallTrend: 'unknown', averagePingTrend: [], reachabilityTrend: [], serverTrends: [], timeframe: [] }; } // Сортируем историю по дате const sortedHistory = [...history].sort((a, b) => new Date(a.date).getTime() - new Date(b.date).getTime()); // Извлекаем временные метки для графиков const timeframe = sortedHistory.map(h => { const date = new Date(h.date); return `${date.toLocaleDateString()} ${date.toLocaleTimeString()}`; }); // Анализируем тренды средних значений пинга const averagePingTrend = sortedHistory.map(h => h.statistics.averagePing); // Анализируем тренды доступности const reachabilityTrend = sortedHistory.map(h => h.statistics.reachableServers / h.statistics.totalServers * 100); // Анализируем тренды для отдельных серверов const serverMap = new Map(); // Инициализируем данные для каждого сервера for (const historyItem of sortedHistory) { for (const result of historyItem.results) { const { hostname } = result.server; if (!serverMap.has(hostname)) { serverMap.set(hostname, { hostname, country: result.server.country_name, city: result.server.city_name, pingHistory: new Array(sortedHistory.length).fill(null), averagePing: 0, standardDeviation: 0, trend: 'unknown', reachabilityRate: 0 }); } } } // Заполняем историю пингов для каждого сервера sortedHistory.forEach((historyItem, historyIndex) => { for (const result of historyItem.results) { const { hostname } = result.server; const serverData = serverMap.get(hostname); if (serverData) { serverData.pingHistory[historyIndex] = result.ping; } } }); // Вычисляем статистику для каждого сервера for (const serverData of serverMap.values()) { const validPings = serverData.pingHistory.filter(p => p !== null); // Вычисляем среднее значение пинга serverData.averagePing = validPings.length > 0 ? Math.round(validPings.reduce((sum, ping) => sum + ping, 0) / validPings.length) : 0; // Вычисляем стандартное отклонение if (validPings.length > 1) { const variance = validPings.reduce((sum, ping) => sum + Math.pow(ping - serverData.averagePing, 2), 0) / validPings.length; serverData.standardDeviation = Math.round(Math.sqrt(variance)); } // Вычисляем процент доступности serverData.reachabilityRate = serverData.pingHistory.length > 0 ? (validPings.length / serverData.pingHistory.length) * 100 : 0; // Определяем тренд if (validPings.length >= 2) { const firstHalf = validPings.slice(0, Math.floor(validPings.length / 2)); const secondHalf = validPings.slice(Math.floor(validPings.length / 2)); const firstHalfAvg = firstHalf.reduce((sum, ping) => sum + ping, 0) / firstHalf.length; const secondHalfAvg = secondHalf.reduce((sum, ping) => sum + ping, 0) / secondHalf.length; const difference = firstHalfAvg - secondHalfAvg; if (Math.abs(difference) < 5) { serverData.trend = 'stable'; } else if (difference > 0) { serverData.trend = 'improving'; // Пинг уменьшается } else { serverData.trend = 'degrading'; // Пинг увеличивается } } } // Преобразуем Map в массив и сортируем по среднему пингу const serverTrends = Array.from(serverMap.values()) .sort((a, b) => a.averagePing - b.averagePing); // Определяем общий тренд let overallTrend = 'unknown'; if (averagePingTrend.length >= 2) { const firstHalf = averagePingTrend.slice(0, Math.floor(averagePingTrend.length / 2)); const secondHalf = averagePingTrend.slice(Math.floor(averagePingTrend.length / 2)); const firstHalfAvg = firstHalf.reduce((sum, ping) => sum + ping, 0) / firstHalf.length; const secondHalfAvg = secondHalf.reduce((sum, ping) => sum + ping, 0) / secondHalf.length; const difference = firstHalfAvg - secondHalfAvg; if (Math.abs(difference) < 5) { overallTrend = 'stable'; } else if (difference > 0) { overallTrend = 'improving'; // Пинг уменьшается } else { overallTrend = 'degrading'; // Пинг увеличивается } } return { overallTrend, averagePingTrend, reachabilityTrend, serverTrends, timeframe }; } /** * Находит наиболее стабильные серверы * @param history Исторические данные * @param count Количество серверов для вывода */ findMostStableServers(history, count = 10) { const trends = this.analyzePerformanceTrends(history); // Сортируем серверы по стабильности (низкое стандартное отклонение и высокая доступность) return trends.serverTrends .filter(server => server.reachabilityRate > 80) // Минимум 80% доступности .sort((a, b) => { // Сначала сортируем по доступности if (Math.abs(a.reachabilityRate - b.reachabilityRate) > 10) { return b.reachabilityRate - a.reachabilityRate; } // Затем по стандартному отклонению (меньше = стабильнее) if (a.standardDeviation !== b.standardDeviation) { return a.standardDeviation - b.standardDeviation; } // Наконец, по среднему пингу return a.averagePing - b.averagePing; }) .slice(0, count); } /** * Очищает старые файлы истории */ async cleanupOldHistoryFiles() { try { const maxHistoryFiles = config_1.default.MAX_HISTORY_FILES || 100; const files = await promises_1.default.readdir(this.historyPath); const historyFiles = files .filter(file => file.startsWith('history_') && file.endsWith('.json')) .sort(); // Сортируем по имени файла (которое содержит временную метку) if (historyFiles.length > maxHistoryFiles) { const filesToDelete = historyFiles.slice(0, historyFiles.length - maxHistoryFiles); for (const file of filesToDelete) { const filePath = path_1.default.join(this.historyPath, file); await promises_1.default.unlink(filePath); this.logger.debug(`Deleted old history file: ${file}`); } this.logger.info(`Cleaned up ${filesToDelete.length} old history files`); } } catch (error) { this.logger.error(`Failed to cleanup old history files: ${error instanceof Error ? error.message : String(error)}`); } } }; exports.HistoryAnalyzerService = HistoryAnalyzerService; exports.HistoryAnalyzerService = HistoryAnalyzerService = __decorate([ (0, inversify_1.injectable)(), __metadata("design:paramtypes", []) ], HistoryAnalyzerService); //# sourceMappingURL=historyAnalyzer.service.js.map