mullvad-servers-ping-tester
Version:
Инструмент для тестирования пинга серверов Mullvad VPN с расширенной аналитикой
255 lines • 13.4 kB
JavaScript
;
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