From 49a5471c014a1b20de7a3225fbd8902f18e6c769 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 1 Apr 2025 11:52:04 -0400 Subject: [PATCH 1/6] Added the ability to establish a connection using a web socket --- package.json | 5 +- src/app.module.ts | 7 ++- src/main.ts | 8 +++- src/metrics.gateway.ts | 101 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 src/metrics.gateway.ts diff --git a/package.json b/package.json index 0fe1fec..6469933 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,10 @@ "pg": "^8.14.1", "typeorm": "^0.3.21", "bcrypt": "^5.1.1", - "@types/bcrypt": "^5.0.2" + "@types/bcrypt": "^5.0.2", + "socket.io": "^4.8.1", + "@nestjs/websockets": "11.0.12", + "@nestjs/platform-socket.io": "11.0.12" }, "devDependencies": { "@eslint/eslintrc": "^3.2.0", diff --git a/src/app.module.ts b/src/app.module.ts index 33b9558..e8ecfad 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -5,6 +5,7 @@ import { PrometheusService } from './prometheus.service'; import { MetricsController } from './metrics.controller'; import { ConfigModule } from '@nestjs/config'; import { AuthModule } from './auth/auth.module'; +import { MetricsGateway } from './metrics.gateway'; @Module({ imports: [ @@ -27,6 +28,10 @@ import { AuthModule } from './auth/auth.module'; AuthModule, ], controllers: [MetricsController], - providers: [PrometheusService], + providers: [ + PrometheusService, + MetricsGateway, + ], + exports: [MetricsGateway], }) export class AppModule { } \ No newline at end of file diff --git a/src/main.ts b/src/main.ts index c76cb40..88fd9d2 100644 --- a/src/main.ts +++ b/src/main.ts @@ -3,11 +3,15 @@ import { AppModule } from './app.module'; async function bootstrap() { - const app = await NestFactory.create(AppModule, { cors: false }); + const app = await NestFactory.create(AppModule); //настройка CORS - + app.enableCors({ + origin: '*', + methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', + allowedHeaders: 'Content-Type, Authorization', + }); await app.listen(process.env.PORT ?? 3000); } bootstrap(); \ No newline at end of file diff --git a/src/metrics.gateway.ts b/src/metrics.gateway.ts new file mode 100644 index 0000000..e2d7002 --- /dev/null +++ b/src/metrics.gateway.ts @@ -0,0 +1,101 @@ +import { WebSocketGateway, WebSocketServer, OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect, SubscribeMessage, } from '@nestjs/websockets'; +import { Server, Socket } from 'socket.io'; +import { PrometheusService } from './prometheus.service'; +import { Logger } from '@nestjs/common'; + +@WebSocketGateway({ + cors: { + origin: '*', // В production укажите конкретные домены + methods: ['GET', 'POST'], + credentials: true + }, + namespace: '/metrics-ws' +}) +export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { + @WebSocketServer() server: Server; + private readonly logger = new Logger(MetricsGateway.name); + private activeSockets: Map = new Map(); + + constructor(private readonly prometheusService: PrometheusService) { } + + afterInit(server: Server) { + this.logger.log('WebSocket Gateway initialized'); + this.logger.log('WebSocket server initialized successfully'); + } + + handleConnection(client: Socket) { + this.logger.log(`Client connected: ${client.id}`); + this.logger.log(`New client connected: ${client.id} from ${client.handshake.address}`); + this.activeSockets.set(client.id, client); + } + + handleDisconnect(client: Socket) { + this.logger.log(`Client disconnected: ${client.id}`); + this.activeSockets.delete(client.id); + } + + @SubscribeMessage('get-metrics') + async handleGetMetrics(client: Socket, payload: any) { + const { metric, start, end, step } = payload; + try { + const data = start && end && step + ? await this.prometheusService.fetchMetricsRange(metric, start, end, step) + : await this.prometheusService.fetchMetrics(metric); + + client.emit('metrics-data', { metric, data }); + } catch (error) { + this.logger.error(`Error fetching metrics: ${error.message}`); + client.emit('metrics-error', { + metric, + error: error.message + }); + } + } + + @SubscribeMessage('get-metric-types') + async handleGetMetricTypes(client: Socket, payload: { metric: string }) { + try { + const type = await this.prometheusService.fetchMetricType(payload.metric); + const description = await this.prometheusService.fetchMetricDescription(payload.metric); + client.emit('metric-types', { + metric: payload.metric, + type, + description + }); + } catch (error) { + this.logger.log(`Error fetching metric types: ${error.message}`); + client.emit('metrics-error', { + metric: payload.metric, + error: error.message + }); + } + } + + @SubscribeMessage('get-all-metrics') + async handleGetAllMetrics(client: Socket) { + try { + const metrics = await this.prometheusService.fetchAllMetrics(); + client.emit('all-metrics', metrics); + } catch (error) { + this.logger.log(`Error fetching all metrics: ${error.message}`); + client.emit('metrics-error', { + error: error.message + }); + } + } + + // Метод для периодической отправки обновлений + async sendPeriodicUpdates(metric: string, interval: number = 5000) { + const timer = setInterval(async () => { + try { + const data = await this.prometheusService.fetchMetrics(metric); + this.server.emit('metrics-update', { metric, data }); + } catch (error) { + this.logger.error(`Error in periodic update for ${metric}: ${error.message}`); + } + }, interval); + + // Возвращаем функцию для остановки обновлений + return () => clearInterval(timer); + } +} \ No newline at end of file -- 2.40.1 From 23438a0e7f6d519428d9577a5349dac9be9e3381 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 2 Apr 2025 19:49:13 -0400 Subject: [PATCH 2/6] fixed bugs with the web socket --- src/metrics.gateway.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/metrics.gateway.ts b/src/metrics.gateway.ts index e2d7002..6916b80 100644 --- a/src/metrics.gateway.ts +++ b/src/metrics.gateway.ts @@ -84,6 +84,15 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat } } + @SubscribeMessage('subscribe-metric') + async handleSubscribeMetric(client: Socket, payload: { metric: string, interval?: number }) { + const stopUpdates = await this.sendPeriodicUpdates(payload.metric, payload.interval); + + // Сохраняем функцию остановки для этого клиента + client.on('disconnect', () => stopUpdates()); + client.on('unsubscribe-metric', () => stopUpdates()); + } + // Метод для периодической отправки обновлений async sendPeriodicUpdates(metric: string, interval: number = 5000) { const timer = setInterval(async () => { -- 2.40.1 From 553b9141d4949edf0e9accf1dc09dc73f45db1e1 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Fri, 4 Apr 2025 15:32:21 -0400 Subject: [PATCH 3/6] fixed bugs --- src/metrics.gateway.ts | 66 ++++++++++++++++++++++++++---------------- 1 file changed, 41 insertions(+), 25 deletions(-) diff --git a/src/metrics.gateway.ts b/src/metrics.gateway.ts index 6916b80..50f4559 100644 --- a/src/metrics.gateway.ts +++ b/src/metrics.gateway.ts @@ -35,22 +35,30 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat } @SubscribeMessage('get-metrics') - async handleGetMetrics(client: Socket, payload: any) { - const { metric, start, end, step } = payload; - try { - const data = start && end && step - ? await this.prometheusService.fetchMetricsRange(metric, start, end, step) - : await this.prometheusService.fetchMetrics(metric); +async handleGetMetrics(client: Socket, payload: any) { + const { metric, start, end, step } = payload; + try { + const data = start && end && step + ? await this.prometheusService.fetchMetricsRange(metric, start, end, step) + : await this.prometheusService.fetchMetrics(metric); - client.emit('metrics-data', { metric, data }); - } catch (error) { - this.logger.error(`Error fetching metrics: ${error.message}`); - client.emit('metrics-error', { - metric, - error: error.message - }); - } + client.emit('metrics-data', { metric, data }); + + // Запускаем автоматические обновления с интервалом по умолчанию + const stopUpdates = await this.sendPeriodicUpdates( + metric, + 5000, // Интервал по умолчанию + client + ); + client.on('disconnect', () => stopUpdates()); + } catch (error) { + this.logger.error(`Error fetching metrics: ${error.message}`); + client.emit('metrics-error', { + metric, + error: error.message + }); } +} @SubscribeMessage('get-metric-types') async handleGetMetricTypes(client: Socket, payload: { metric: string }) { @@ -84,27 +92,35 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat } } - @SubscribeMessage('subscribe-metric') - async handleSubscribeMetric(client: Socket, payload: { metric: string, interval?: number }) { - const stopUpdates = await this.sendPeriodicUpdates(payload.metric, payload.interval); + - // Сохраняем функцию остановки для этого клиента - client.on('disconnect', () => stopUpdates()); - client.on('unsubscribe-metric', () => stopUpdates()); - } + @SubscribeMessage('subscribe-metric') +async handleSubscribeMetric(client: Socket, payload: { metric: string, interval?: number }) { + const stopUpdates = await this.sendPeriodicUpdates( + payload.metric, + payload.interval || 5000, // Добавляем значение по умолчанию + client // Передаем клиента + ); + + // Сохраняем функцию остановки для этого клиента + client.on('disconnect', () => stopUpdates()); + client.on('unsubscribe-metric', () => stopUpdates()); +} // Метод для периодической отправки обновлений - async sendPeriodicUpdates(metric: string, interval: number = 5000) { + async sendPeriodicUpdates(metric: string, interval: number, client: Socket) { const timer = setInterval(async () => { try { const data = await this.prometheusService.fetchMetrics(metric); - this.server.emit('metrics-update', { metric, data }); + client.emit('metrics-data', { metric, data }); } catch (error) { this.logger.error(`Error in periodic update for ${metric}: ${error.message}`); } }, interval); - // Возвращаем функцию для остановки обновлений - return () => clearInterval(timer); + return () => { + clearInterval(timer); + this.logger.log(`Stopped updates for ${metric}`); + }; } } \ No newline at end of file -- 2.40.1 From 23d2fd7eff731224a54b567d8103facdb42f0cb8 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 8 Apr 2025 01:58:15 -0400 Subject: [PATCH 4/6] Added the start, end, and step parameters to solve the update issue on the charts. --- src/metrics.gateway.ts | 70 +++++++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/src/metrics.gateway.ts b/src/metrics.gateway.ts index 50f4559..6fc765b 100644 --- a/src/metrics.gateway.ts +++ b/src/metrics.gateway.ts @@ -35,30 +35,36 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat } @SubscribeMessage('get-metrics') -async handleGetMetrics(client: Socket, payload: any) { - const { metric, start, end, step } = payload; - try { - const data = start && end && step - ? await this.prometheusService.fetchMetricsRange(metric, start, end, step) - : await this.prometheusService.fetchMetrics(metric); + async handleGetMetrics(client: Socket, payload: any) { + const { metric, start, end, step, _t } = payload; + this.logger.log(`Received metrics request: ${metric}, start: ${start}, end: ${end}, step: ${step}`); - client.emit('metrics-data', { metric, data }); + try { + // Для запросов с диапазоном - просто возвращаем данные без подписки + if (start && end) { + const data = await this.prometheusService.fetchMetricsRange(metric, start, end, step); + client.emit('metrics-data', { metric, data }); + return; + } - // Запускаем автоматические обновления с интервалом по умолчанию - const stopUpdates = await this.sendPeriodicUpdates( - metric, - 5000, // Интервал по умолчанию - client - ); - client.on('disconnect', () => stopUpdates()); - } catch (error) { - this.logger.error(`Error fetching metrics: ${error.message}`); - client.emit('metrics-error', { - metric, - error: error.message - }); + // Для запросов без диапазона (realtime) - запускаем подписку + const stopUpdates = await this.sendPeriodicUpdates( + metric, + step || 5000, // Используем переданный шаг или дефолтный + client + ); + + client.on('disconnect', () => stopUpdates()); + client.on('unsubscribe-metric', () => stopUpdates()); + + } catch (error) { + this.logger.error(`Error fetching metrics: ${error.message}`); + client.emit('metrics-error', { + metric, + error: error.message + }); + } } -} @SubscribeMessage('get-metric-types') async handleGetMetricTypes(client: Socket, payload: { metric: string }) { @@ -92,20 +98,20 @@ async handleGetMetrics(client: Socket, payload: any) { } } - + @SubscribeMessage('subscribe-metric') -async handleSubscribeMetric(client: Socket, payload: { metric: string, interval?: number }) { - const stopUpdates = await this.sendPeriodicUpdates( - payload.metric, - payload.interval || 5000, // Добавляем значение по умолчанию - client // Передаем клиента - ); + async handleSubscribeMetric(client: Socket, payload: { metric: string, interval?: number }) { + const stopUpdates = await this.sendPeriodicUpdates( + payload.metric, + payload.interval || 5000, // Добавляем значение по умолчанию + client // Передаем клиента + ); - // Сохраняем функцию остановки для этого клиента - client.on('disconnect', () => stopUpdates()); - client.on('unsubscribe-metric', () => stopUpdates()); -} + // Сохраняем функцию остановки для этого клиента + client.on('disconnect', () => stopUpdates()); + client.on('unsubscribe-metric', () => stopUpdates()); + } // Метод для периодической отправки обновлений async sendPeriodicUpdates(metric: string, interval: number, client: Socket) { -- 2.40.1 From 243a45756a5867c35c9c31987208105a13a7694a Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Wed, 9 Apr 2025 10:02:55 -0400 Subject: [PATCH 5/6] added the api prefix --- src/main.ts | 3 +++ src/metrics.gateway.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main.ts b/src/main.ts index 88fd9d2..85934af 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,9 @@ import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); + // Установка глобального префикса для всех маршрутов + app.setGlobalPrefix('api'); + //настройка CORS app.enableCors({ diff --git a/src/metrics.gateway.ts b/src/metrics.gateway.ts index 6fc765b..510e016 100644 --- a/src/metrics.gateway.ts +++ b/src/metrics.gateway.ts @@ -9,7 +9,7 @@ import { Logger } from '@nestjs/common'; methods: ['GET', 'POST'], credentials: true }, - namespace: '/metrics-ws' + namespace: '/api/metrics-ws' }) export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { @WebSocketServer() server: Server; -- 2.40.1 From 7d8a20772837e54c10acede50646d2d165334467 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Thu, 10 Apr 2025 05:41:41 -0400 Subject: [PATCH 6/6] commented on the section with CORS --- src/metrics.gateway.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/metrics.gateway.ts b/src/metrics.gateway.ts index 510e016..1d1f669 100644 --- a/src/metrics.gateway.ts +++ b/src/metrics.gateway.ts @@ -4,11 +4,12 @@ import { PrometheusService } from './prometheus.service'; import { Logger } from '@nestjs/common'; @WebSocketGateway({ - cors: { - origin: '*', // В production укажите конкретные домены - methods: ['GET', 'POST'], - credentials: true - }, + /* + cors: { + origin: '*', // В production укажите конкретные домены + methods: ['GET', 'POST'], + credentials: true + }, */ namespace: '/api/metrics-ws' }) export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect { -- 2.40.1