Compare commits
No commits in common. "86f6614f56e632093c5888d45b16beb00bbbd255" and "8466aa1f93e7c6e5efa1cd57df5dbac030c6d333" have entirely different histories.
86f6614f56
...
8466aa1f93
21
.env
21
.env
|
|
@ -1,9 +1,9 @@
|
||||||
# Прометеус
|
#Прометеус
|
||||||
#PROMETHEUS_API=http://192.168.2.34:9090/api/v1
|
#PROMETHEUS_API=http://192.168.2.34:9090/api/v1
|
||||||
|
|
||||||
#FRONTEND_URL=192.168.2.39:5173
|
#FRONTEND_URL=192.168.2.39:5173
|
||||||
|
|
||||||
# Постгресс
|
#Постгресс
|
||||||
#DB_HOST=192.168.2.37
|
#DB_HOST=192.168.2.37
|
||||||
#DB_PORT=5432
|
#DB_PORT=5432
|
||||||
#DB_USER=trust
|
#DB_USER=trust
|
||||||
|
|
@ -11,28 +11,19 @@
|
||||||
#DB_NAME=trust-db
|
#DB_NAME=trust-db
|
||||||
|
|
||||||
|
|
||||||
# JWT
|
#JWT
|
||||||
#JWT_SECRET=x7F!2p9L#q1$z0*8R5vYgMnBk
|
#JWT_SECRET=x7F!2p9L#q1$z0*8R5vYgMnBk
|
||||||
#JWT_SECRET=x7Fcdp9L#q1$z0*8R5vYgMnBk
|
#JWT_SECRET=x7Fcdp9L#q1$z0*8R5vYgMnBk
|
||||||
|
|
||||||
# COOKIE
|
#COOKIE
|
||||||
# Для production
|
# Для production
|
||||||
#COOKIE_SECURE=true
|
#COOKIE_SECURE=true
|
||||||
#COOKIE_SAME_SITE=strict
|
#COOKIE_SAME_SITE=strict
|
||||||
|
|
||||||
# Для development
|
# Для development
|
||||||
#COOKIE_SECURE=false
|
# COOKIE_SECURE=false
|
||||||
#COOKIE_SAME_SITE=lax
|
# COOKIE_SAME_SITE=lax
|
||||||
|
|
||||||
# Для меню
|
# Для меню
|
||||||
#RANGES_API_URL=http://192.168.2.39:9999
|
#RANGES_API_URL=http://192.168.2.39:9999
|
||||||
#RANGES_API_ENDPOINT=/api/ranges/9999
|
#RANGES_API_ENDPOINT=/api/ranges/9999
|
||||||
|
|
||||||
# ClickHouse
|
|
||||||
#CLICKHOUSE_HOST=http://192.168.2.37:8123
|
|
||||||
#CLICKHOUSE_USER=vlad
|
|
||||||
#CLICKHOUSE_PASSWORD=vlad
|
|
||||||
#CLICKHOUSE_DB=zvks
|
|
||||||
|
|
||||||
# Для ai api
|
|
||||||
#AI_SERVICE_URL=http://192.168.2.39:5134
|
|
||||||
|
|
@ -44,10 +44,7 @@
|
||||||
"@types/cookie-parser": "^1.4.8",
|
"@types/cookie-parser": "^1.4.8",
|
||||||
"@nestjs/jwt": "^11.0.0",
|
"@nestjs/jwt": "^11.0.0",
|
||||||
"@nestjs/passport": "^11.0.5",
|
"@nestjs/passport": "^11.0.5",
|
||||||
"@nestjs/swagger": "11.1.4",
|
"@nestjs/swagger": "11.1.4"
|
||||||
"@clickhouse/client": "^1.11.2",
|
|
||||||
"date-fns": "4.1.0",
|
|
||||||
"@clickhouse/client-web": "^1.11.2"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3.2.0",
|
"@eslint/eslintrc": "^3.2.0",
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,7 @@ import { HttpModule } from '@nestjs/axios';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { AuthModule } from './auth/auth.module';
|
import { AuthModule } from './auth/auth.module';
|
||||||
import { MenuModule } from './menu/menu.module';
|
import { MenuModule } from './menu/menu.module';
|
||||||
import { PrometheusModule } from './prometheus/prometheus.module';
|
import { PrometheusModule } from './prometheus.module';
|
||||||
import { ClickHouseModule } from './clickhouse/clickhouse.module';
|
|
||||||
import { ClickHouseController } from './clickhouse/clickhouse.controller';
|
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [
|
imports: [
|
||||||
|
|
@ -29,8 +27,6 @@ import { ClickHouseController } from './clickhouse/clickhouse.controller';
|
||||||
AuthModule,
|
AuthModule,
|
||||||
PrometheusModule,
|
PrometheusModule,
|
||||||
MenuModule,
|
MenuModule,
|
||||||
ClickHouseModule,
|
|
||||||
],
|
],
|
||||||
controllers: [ClickHouseController],
|
|
||||||
})
|
})
|
||||||
export class AppModule { }
|
export class AppModule {}
|
||||||
|
|
|
||||||
|
|
@ -1,62 +0,0 @@
|
||||||
import { Controller, Get, Post } from '@nestjs/common';
|
|
||||||
import { ClickHouseService } from './clickhouse.service';
|
|
||||||
import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
|
|
||||||
import { HttpService } from '@nestjs/axios';
|
|
||||||
import { firstValueFrom } from 'rxjs';
|
|
||||||
import { ConfigService } from '@nestjs/config';
|
|
||||||
|
|
||||||
@ApiTags('Clickhouse')
|
|
||||||
@Controller('clickhouse')
|
|
||||||
export class ClickHouseController {
|
|
||||||
constructor(
|
|
||||||
private readonly clickhouseService: ClickHouseService,
|
|
||||||
private readonly httpService: HttpService,
|
|
||||||
private readonly configService: ConfigService,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
@Get()
|
|
||||||
@ApiOperation({ summary: 'Get metrics from ClickHouse' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'Metrics data',
|
|
||||||
schema: {
|
|
||||||
type: 'array',
|
|
||||||
items: {
|
|
||||||
type: 'object',
|
|
||||||
properties: {
|
|
||||||
description: { type: 'string' },
|
|
||||||
device: { type: 'number' },
|
|
||||||
id: { type: 'string' },
|
|
||||||
name: { type: 'string' },
|
|
||||||
source: { type: 'string' },
|
|
||||||
status: { type: 'number' },
|
|
||||||
timestamp: { type: 'number' },
|
|
||||||
value: { type: 'string' },
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
})
|
|
||||||
async getClckhouse() {
|
|
||||||
return this.clickhouseService.getClckhouse();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Post('send-to-ai')
|
|
||||||
@ApiOperation({ summary: 'Send metrics to AI service' })
|
|
||||||
@ApiResponse({
|
|
||||||
status: 200,
|
|
||||||
description: 'AI service response',
|
|
||||||
})
|
|
||||||
async sendToAI() {
|
|
||||||
const metrics = await this.clickhouseService.getClckhouse();
|
|
||||||
const aiServiceUrl = this.configService.get('AI_SERVICE_URL/api/metrics/rest');
|
|
||||||
|
|
||||||
try {
|
|
||||||
const response = await firstValueFrom(
|
|
||||||
this.httpService.post(aiServiceUrl, metrics)
|
|
||||||
);
|
|
||||||
return response.data;
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`Failed to send data to AI: ${error.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
||||||
import { Module, Global } from '@nestjs/common';
|
|
||||||
import { createClient, ClickHouseClient } from '@clickhouse/client';
|
|
||||||
import { ClickHouseService } from './clickhouse.service';
|
|
||||||
import { HttpModule } from '@nestjs/axios';
|
|
||||||
|
|
||||||
@Global()
|
|
||||||
@Module({
|
|
||||||
imports: [HttpModule],
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: 'CLICKHOUSE_CLIENT',
|
|
||||||
useFactory: (): ClickHouseClient => {
|
|
||||||
return createClient({
|
|
||||||
host: process.env.CLICKHOUSE_HOST || 'http://localhost:8123',
|
|
||||||
username: process.env.CLICKHOUSE_USER || 'default',
|
|
||||||
password: process.env.CLICKHOUSE_PASSWORD || '',
|
|
||||||
database: process.env.CLICKHOUSE_DB || 'default',
|
|
||||||
});
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ClickHouseService,
|
|
||||||
],
|
|
||||||
exports: ['CLICKHOUSE_CLIENT', ClickHouseService],
|
|
||||||
})
|
|
||||||
export class ClickHouseModule { }
|
|
||||||
|
|
@ -1,72 +0,0 @@
|
||||||
import { Injectable, Inject } from '@nestjs/common';
|
|
||||||
import { ClickHouseClient } from '@clickhouse/client';
|
|
||||||
|
|
||||||
interface ClickHouseRow {
|
|
||||||
EventDataTime: string;
|
|
||||||
ParameterBody: string;
|
|
||||||
CreateDataTime: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface MetricData {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
type: string;
|
|
||||||
addr?: string;
|
|
||||||
value: number | string | null;
|
|
||||||
description: string;
|
|
||||||
status: number;
|
|
||||||
device: number;
|
|
||||||
source: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface ParameterBody {
|
|
||||||
service_name: string;
|
|
||||||
metrics: MetricData[];
|
|
||||||
}
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ClickHouseService {
|
|
||||||
constructor(
|
|
||||||
@Inject('CLICKHOUSE_CLIENT')
|
|
||||||
private readonly clickhouseClient: ClickHouseClient,
|
|
||||||
) { }
|
|
||||||
|
|
||||||
async getClckhouse() {
|
|
||||||
const query = `
|
|
||||||
SELECT
|
|
||||||
EventDataTime,
|
|
||||||
ParameterBody,
|
|
||||||
CreateDataTime
|
|
||||||
FROM zvks.complex_parameters
|
|
||||||
ORDER BY EventDataTime DESC
|
|
||||||
LIMIT 100
|
|
||||||
`;
|
|
||||||
|
|
||||||
const result = await this.clickhouseClient.query({
|
|
||||||
query,
|
|
||||||
format: 'JSONEachRow',
|
|
||||||
});
|
|
||||||
|
|
||||||
const rows = await result.json<ClickHouseRow>();
|
|
||||||
|
|
||||||
// Парсинг данных
|
|
||||||
return rows.flatMap((row: ClickHouseRow) => {
|
|
||||||
try {
|
|
||||||
const parameterBody: ParameterBody = JSON.parse(row.ParameterBody);
|
|
||||||
return parameterBody.metrics.map((metric: MetricData) => ({
|
|
||||||
id: metric.id,
|
|
||||||
name: metric.name,
|
|
||||||
value: metric.value !== null ? metric.value.toString() : 'null',
|
|
||||||
description: metric.description,
|
|
||||||
status: metric.status,
|
|
||||||
device: metric.device,
|
|
||||||
source: metric.source,
|
|
||||||
timestamp: new Date(row.EventDataTime).getTime(),
|
|
||||||
}));
|
|
||||||
} catch (e) {
|
|
||||||
console.error('Error parsing metric:', e);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -42,7 +42,7 @@ async function bootstrap() {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Настройка CORS
|
// Настройка CORS
|
||||||
app.enableCors({//ПОСТАВИТЬ ПРОКСИ, ЧТОБЫ КОРС НЕ РУГАЛСЯ, ИЗМЕНЕНИЕ ПОЛИТИКИ СЕТЕВЫХ ПАКЕТОВ. ПИШУ IP СВОЙ, А ПОРТ ПРОКСИ. REVERSE PROXY.
|
app.enableCors({
|
||||||
origin: [process.env.FRONTEND_URL, "http://dev.msf.enode"],
|
origin: [process.env.FRONTEND_URL, "http://dev.msf.enode"],
|
||||||
credentials: true,
|
credentials: true,
|
||||||
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS',
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ export class MenuController {
|
||||||
|
|
||||||
@Get('full')
|
@Get('full')
|
||||||
async getFullMenu(@Headers('if-modified-since') ifModifiedSince?: string) {
|
async getFullMenu(@Headers('if-modified-since') ifModifiedSince?: string) {
|
||||||
console.log('GET /menu/full requested');
|
|
||||||
try {
|
try {
|
||||||
const result = await this.menuService.getFullMenuWithCache(ifModifiedSince);
|
const result = await this.menuService.getFullMenuWithCache(ifModifiedSince);
|
||||||
|
|
||||||
|
|
@ -27,12 +26,6 @@ export class MenuController {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
@Get('full')
|
|
||||||
async getFullMenu() {
|
|
||||||
console.log('Simplified endpoint called');
|
|
||||||
return { test: 'OK' }; // Простейший ответ
|
|
||||||
} */
|
|
||||||
|
|
||||||
@Get('check-updates')
|
@Get('check-updates')
|
||||||
async checkUpdates(@Headers('if-modified-since') ifModifiedSince: string) {
|
async checkUpdates(@Headers('if-modified-since') ifModifiedSince: string) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ import { Module } from '@nestjs/common';
|
||||||
import { MenuController } from './menu.controller';
|
import { MenuController } from './menu.controller';
|
||||||
import { HttpModule } from '@nestjs/axios';
|
import { HttpModule } from '@nestjs/axios';
|
||||||
import { MenuService } from './menu.service';
|
import { MenuService } from './menu.service';
|
||||||
import { PrometheusModule } from '../prometheus/prometheus.module';
|
import { PrometheusModule } from '../prometheus.module';
|
||||||
import { RangeService } from './range.service';
|
import { RangeService } from './range.service';
|
||||||
import { RangeController } from './range.controller';
|
import { RangeController } from './range.controller';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
import { Injectable, HttpException, HttpStatus } from '@nestjs/common';
|
||||||
import { PrometheusService } from '../prometheus/prometheus.service';
|
import { PrometheusService } from '../prometheus.service';
|
||||||
import { MenuItem } from './menu.interface';
|
import { MenuItem } from './menu.interface';
|
||||||
import * as fs from 'fs/promises';
|
import * as fs from 'fs/promises';
|
||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,6 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
stopUpdates: () => void;
|
stopUpdates: () => void;
|
||||||
clients: Set<string>;
|
clients: Set<string>;
|
||||||
}>();
|
}>();
|
||||||
private lastSentData = new Map<string, any>(); // Кэш последних отправленных данных
|
|
||||||
|
|
||||||
constructor(private readonly prometheusService: PrometheusService) { }
|
constructor(private readonly prometheusService: PrometheusService) { }
|
||||||
|
|
||||||
|
|
@ -49,7 +48,6 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
if (subscription.clients.size === 0) {
|
if (subscription.clients.size === 0) {
|
||||||
subscription.stopUpdates();
|
subscription.stopUpdates();
|
||||||
this.metricSubscriptions.delete(metric);
|
this.metricSubscriptions.delete(metric);
|
||||||
this.lastSentData.delete(metric);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -61,7 +59,6 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
if (subscription.clients.size === 0) {
|
if (subscription.clients.size === 0) {
|
||||||
subscription.stopUpdates();
|
subscription.stopUpdates();
|
||||||
this.metricSubscriptions.delete(metric);
|
this.metricSubscriptions.delete(metric);
|
||||||
this.lastSentData.delete(metric);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -93,28 +90,17 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const subscriptionKey = this.getSubscriptionKey(metric, filters);
|
|
||||||
// Отправляем текущие данные сразу при запросе
|
|
||||||
const initialData = await this.prometheusService.fetchMetricsWithFilters(metric, filters);
|
|
||||||
client.emit('metrics-data', { metric, data: initialData, requestId });
|
|
||||||
this.lastSentData.set(subscriptionKey, initialData);
|
|
||||||
|
|
||||||
const stopUpdates = await this.sendPeriodicUpdates(
|
const stopUpdates = await this.sendPeriodicUpdates(
|
||||||
metric,
|
metric,
|
||||||
step || 5000,
|
step || 5000,
|
||||||
(data) => {
|
(data) => {
|
||||||
const lastData = this.lastSentData.get(subscriptionKey);
|
|
||||||
if (!this.isDataEqual(lastData, data)) {
|
|
||||||
client.emit('metrics-data', { metric, data, requestId });
|
client.emit('metrics-data', { metric, data, requestId });
|
||||||
this.lastSentData.set(subscriptionKey, data);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
filters
|
filters
|
||||||
);
|
);
|
||||||
|
|
||||||
const cleanup = () => {
|
const cleanup = () => {
|
||||||
stopUpdates();
|
stopUpdates();
|
||||||
this.lastSentData.delete(subscriptionKey);
|
|
||||||
client.off('disconnect', cleanup);
|
client.off('disconnect', cleanup);
|
||||||
client.off('unsubscribe-metric', cleanup);
|
client.off('unsubscribe-metric', cleanup);
|
||||||
};
|
};
|
||||||
|
|
@ -136,20 +122,6 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
return `${metric}${filterString ? `?${filterString}` : ''}`;
|
return `${metric}${filterString ? `?${filterString}` : ''}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Сравниваем данные, чтобы избежать лишних отправок
|
|
||||||
private isDataEqual(oldData: any[], newData: any[]): boolean {
|
|
||||||
if (!oldData || !newData || oldData.length !== newData.length) return false;
|
|
||||||
|
|
||||||
return oldData.every((oldItem, index) => {
|
|
||||||
const newItem = newData[index];
|
|
||||||
return (
|
|
||||||
oldItem.value === newItem.value &&
|
|
||||||
oldItem.status === newItem.status &&
|
|
||||||
oldItem.timestamp === newItem.timestamp
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
@SubscribeMessage('subscribe-metric')
|
@SubscribeMessage('subscribe-metric')
|
||||||
async handleSubscribeMetric(
|
async handleSubscribeMetric(
|
||||||
client: Socket,
|
client: Socket,
|
||||||
|
|
@ -159,35 +131,18 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
filters?: Record<string, string>;
|
filters?: Record<string, string>;
|
||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
const { metric, interval = 60000, filters = {} } = payload; // По умолчанию 60 секунд
|
const { metric, interval = 5000, filters = {} } = payload;
|
||||||
const subscriptionKey = this.getSubscriptionKey(metric, filters);
|
const subscriptionKey = this.getSubscriptionKey(metric, filters);
|
||||||
|
|
||||||
if (!this.metricSubscriptions.has(subscriptionKey)) {
|
if (!this.metricSubscriptions.has(subscriptionKey)) {
|
||||||
// Отправляем текущие данные сразу при подписке
|
|
||||||
try {
|
|
||||||
const initialData = await this.prometheusService.fetchMetricsWithFilters(metric, filters);
|
|
||||||
client.emit('metrics-data', {
|
|
||||||
metric: subscriptionKey,
|
|
||||||
data: initialData
|
|
||||||
});
|
|
||||||
this.lastSentData.set(subscriptionKey, initialData);
|
|
||||||
} catch (error) {
|
|
||||||
this.logger.error(`Error fetching initial data for ${metric}:`, error.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stopUpdates = await this.sendPeriodicUpdates(
|
const stopUpdates = await this.sendPeriodicUpdates(
|
||||||
metric,
|
metric,
|
||||||
interval,
|
interval,
|
||||||
(data) => {
|
(data) => {
|
||||||
// Отправляем только если данные изменились
|
|
||||||
const lastData = this.lastSentData.get(subscriptionKey);
|
|
||||||
if (!this.isDataEqual(lastData, data)) {
|
|
||||||
this.server.emit('metrics-data', {
|
this.server.emit('metrics-data', {
|
||||||
metric: subscriptionKey,
|
metric: subscriptionKey,
|
||||||
data
|
data
|
||||||
});
|
});
|
||||||
this.lastSentData.set(subscriptionKey, data);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
filters
|
filters
|
||||||
);
|
);
|
||||||
|
|
@ -198,14 +153,6 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
this.metricSubscriptions.get(subscriptionKey)?.clients.add(client.id);
|
this.metricSubscriptions.get(subscriptionKey)?.clients.add(client.id);
|
||||||
// Отправляем кэшированные данные новому клиенту
|
|
||||||
const cachedData = this.lastSentData.get(subscriptionKey);
|
|
||||||
if (cachedData) {
|
|
||||||
client.emit('metrics-data', {
|
|
||||||
metric: subscriptionKey,
|
|
||||||
data: cachedData
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsubscribe = () => {
|
const unsubscribe = () => {
|
||||||
|
|
@ -215,7 +162,6 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
if (subscription.clients.size === 0) {
|
if (subscription.clients.size === 0) {
|
||||||
subscription.stopUpdates();
|
subscription.stopUpdates();
|
||||||
this.metricSubscriptions.delete(subscriptionKey);
|
this.metricSubscriptions.delete(subscriptionKey);
|
||||||
this.lastSentData.delete(subscriptionKey);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -230,11 +176,6 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
callback: (data: any) => void,
|
callback: (data: any) => void,
|
||||||
filters: Record<string, string> = {}
|
filters: Record<string, string> = {}
|
||||||
) {
|
) {
|
||||||
// Добавляем небольшую случайную задержку, чтобы избежать пиковой нагрузки
|
|
||||||
const initialDelay = Math.floor(Math.random() * 5000);
|
|
||||||
|
|
||||||
await new Promise(resolve => setTimeout(resolve, initialDelay));
|
|
||||||
|
|
||||||
const timer = setInterval(async () => {
|
const timer = setInterval(async () => {
|
||||||
try {
|
try {
|
||||||
const data = await this.prometheusService.fetchMetricsWithFilters(metric, filters);
|
const data = await this.prometheusService.fetchMetricsWithFilters(metric, filters);
|
||||||
|
|
@ -246,7 +187,6 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
clearInterval(timer);
|
clearInterval(timer);
|
||||||
this.lastSentData.delete(this.getSubscriptionKey(metric, filters));
|
|
||||||
this.logger.log(`Stopped updates for ${metric}`);
|
this.logger.log(`Stopped updates for ${metric}`);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
@ -3,13 +3,11 @@ import { HttpService } from '@nestjs/axios';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import { lastValueFrom } from 'rxjs';
|
import { lastValueFrom } from 'rxjs';
|
||||||
import { PrometheusMetric } from './prometheus-metric.interface';
|
import { PrometheusMetric } from './prometheus-metric.interface';
|
||||||
import { MenuItem } from '../menu/menu.interface';
|
import { MenuItem } from './menu/menu.interface';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class PrometheusService {
|
export class PrometheusService {
|
||||||
private readonly prometheusUrl: string;
|
private readonly prometheusUrl: string;
|
||||||
private metricCache = new Map<string, { data: any; timestamp: number }>();
|
|
||||||
private metadataCache = new Map<string, { type: string | null; description: string | undefined; timestamp: number }>();
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private readonly httpService: HttpService,
|
private readonly httpService: HttpService,
|
||||||
|
|
@ -20,13 +18,6 @@ export class PrometheusService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMetricType(metric: string): Promise<string | null> {
|
async fetchMetricType(metric: string): Promise<string | null> {
|
||||||
const cacheKey = `metadata-type-${metric}`;
|
|
||||||
const cacheEntry = this.metadataCache.get(cacheKey);
|
|
||||||
|
|
||||||
if (cacheEntry && Date.now() - cacheEntry.timestamp < 30000) {
|
|
||||||
return cacheEntry.type;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await lastValueFrom(
|
const response = await lastValueFrom(
|
||||||
this.httpService.get(`${this.prometheusUrl}/metadata`, {
|
this.httpService.get(`${this.prometheusUrl}/metadata`, {
|
||||||
|
|
@ -34,29 +25,14 @@ export class PrometheusService {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const metadata = response.data.data[metric];
|
const metadata = response.data.data[metric];
|
||||||
const result = metadata?.length ? metadata[0].type : null;
|
return metadata?.length ? metadata[0].type : null;
|
||||||
|
|
||||||
this.metadataCache.set(cacheKey, {
|
|
||||||
type: result,
|
|
||||||
description: cacheEntry?.description,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Ошибка при получении типа метрики ${metric}:`, error);
|
console.error(`Ошибка при получении типа метрики ${metric}:`, error);
|
||||||
return cacheEntry?.type || null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMetricDescription(metric: string): Promise<string | undefined> {
|
async fetchMetricDescription(metric: string): Promise<string | undefined> {
|
||||||
const cacheKey = `metadata-description-${metric}`;
|
|
||||||
const cacheEntry = this.metadataCache.get(cacheKey);
|
|
||||||
|
|
||||||
if (cacheEntry && Date.now() - cacheEntry.timestamp < 30000) {
|
|
||||||
return cacheEntry.description;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await lastValueFrom(
|
const response = await lastValueFrom(
|
||||||
this.httpService.get(`${this.prometheusUrl}/metadata`, {
|
this.httpService.get(`${this.prometheusUrl}/metadata`, {
|
||||||
|
|
@ -64,29 +40,14 @@ export class PrometheusService {
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
const metadata = response.data.data[metric];
|
const metadata = response.data.data[metric];
|
||||||
const result = metadata?.length ? metadata[0].help : undefined;
|
return metadata?.length ? metadata[0].help : undefined;
|
||||||
|
|
||||||
this.metadataCache.set(cacheKey, {
|
|
||||||
type: cacheEntry?.type ?? null,
|
|
||||||
description: result,
|
|
||||||
timestamp: Date.now()
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Ошибка при получении описания метрики ${metric}:`, error);
|
console.error(`Ошибка при получении описания метрики ${metric}:`, error);
|
||||||
return cacheEntry?.description;
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMetrics(metric: string): Promise<PrometheusMetric[]> {
|
async fetchMetrics(metric: string): Promise<PrometheusMetric[]> {
|
||||||
const cacheKey = `${metric}:{}`;
|
|
||||||
const cacheEntry = this.metricCache.get(cacheKey);
|
|
||||||
|
|
||||||
if (cacheEntry && Date.now() - cacheEntry.timestamp < 5000) {
|
|
||||||
return cacheEntry.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await lastValueFrom(
|
const response = await lastValueFrom(
|
||||||
this.httpService.get(`${this.prometheusUrl}/query`, {
|
this.httpService.get(`${this.prometheusUrl}/query`, {
|
||||||
|
|
@ -97,7 +58,7 @@ export class PrometheusService {
|
||||||
const metricType = await this.fetchMetricType(metric);
|
const metricType = await this.fetchMetricType(metric);
|
||||||
const metricDescription = await this.fetchMetricDescription(metric);
|
const metricDescription = await this.fetchMetricDescription(metric);
|
||||||
|
|
||||||
const result = response.data.data.result.map((entry): PrometheusMetric => ({
|
return response.data.data.result.map((entry): PrometheusMetric => ({
|
||||||
__name__: entry.metric.__name__ || metric,
|
__name__: entry.metric.__name__ || metric,
|
||||||
device: entry.metric.device,
|
device: entry.metric.device,
|
||||||
instance: entry.metric.instance,
|
instance: entry.metric.instance,
|
||||||
|
|
@ -110,24 +71,13 @@ export class PrometheusService {
|
||||||
description: metricDescription,
|
description: metricDescription,
|
||||||
...entry.metric
|
...entry.metric
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.metricCache.set(cacheKey, { data: result, timestamp: Date.now() });
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching metrics for ${metric}:`, error);
|
console.error(`Error fetching metrics for ${metric}:`, error);
|
||||||
if (cacheEntry) return cacheEntry.data;
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMetricsWithFilters(metric: string, filters: Record<string, string>): Promise<PrometheusMetric[]> {
|
async fetchMetricsWithFilters(metric: string, filters: Record<string, string>): Promise<PrometheusMetric[]> {
|
||||||
const cacheKey = `${metric}:${JSON.stringify(filters)}`;
|
|
||||||
const cacheEntry = this.metricCache.get(cacheKey);
|
|
||||||
|
|
||||||
if (cacheEntry && Date.now() - cacheEntry.timestamp < 5000) {
|
|
||||||
return cacheEntry.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const query = this.buildFilteredQuery(metric, filters);
|
const query = this.buildFilteredQuery(metric, filters);
|
||||||
const response = await lastValueFrom(
|
const response = await lastValueFrom(
|
||||||
|
|
@ -139,7 +89,7 @@ export class PrometheusService {
|
||||||
const metricType = await this.fetchMetricType(metric);
|
const metricType = await this.fetchMetricType(metric);
|
||||||
const metricDescription = await this.fetchMetricDescription(metric);
|
const metricDescription = await this.fetchMetricDescription(metric);
|
||||||
|
|
||||||
const result = response.data.data.result.map((entry): PrometheusMetric => ({
|
return response.data.data.result.map((entry): PrometheusMetric => ({
|
||||||
__name__: entry.metric.__name__ || metric,
|
__name__: entry.metric.__name__ || metric,
|
||||||
device: entry.metric.device,
|
device: entry.metric.device,
|
||||||
instance: entry.metric.instance,
|
instance: entry.metric.instance,
|
||||||
|
|
@ -152,12 +102,8 @@ export class PrometheusService {
|
||||||
description: metricDescription,
|
description: metricDescription,
|
||||||
...entry.metric
|
...entry.metric
|
||||||
}));
|
}));
|
||||||
|
|
||||||
this.metricCache.set(cacheKey, { data: result, timestamp: Date.now() });
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error fetching metrics with filters for ${metric}:`, error);
|
console.error(`Error fetching metrics with filters for ${metric}:`, error);
|
||||||
if (cacheEntry) return cacheEntry.data;
|
|
||||||
throw error;
|
throw error;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -166,24 +112,20 @@ export class PrometheusService {
|
||||||
const filterParts = Object.entries(filters)
|
const filterParts = Object.entries(filters)
|
||||||
.filter(([_, value]) => value !== undefined && value !== null && value !== "")
|
.filter(([_, value]) => value !== undefined && value !== null && value !== "")
|
||||||
.map(([key, value]) => {
|
.map(([key, value]) => {
|
||||||
|
// Убираем автоматическое добавление "module$" для source_id
|
||||||
return `${key}="${value}"`;
|
return `${key}="${value}"`;
|
||||||
});
|
});
|
||||||
|
|
||||||
return filterParts.length > 0
|
return filterParts.length > 0
|
||||||
? `${metric}{${filterParts.join(',')}}`
|
? `${metric}{${filterParts.join(',')}}`
|
||||||
: metric;
|
: metric;
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchMetricsRange(metric: string, start: number, end: number, step: number, filters: Record<string, string> = {}): Promise<PrometheusMetric[]> {
|
async fetchMetricsRange(metric: string, start: number, end: number, step: number, filters: Record<string, string> = {}): Promise<PrometheusMetric[]> {
|
||||||
// Рассчитываем оптимальный шаг, если не указан
|
|
||||||
const duration = end - start;
|
|
||||||
const optimalStep = Math.max(Math.floor(duration / 1000), 15); // Минимум 15 секунд
|
|
||||||
|
|
||||||
const query = this.buildFilteredQuery(metric, {
|
const query = this.buildFilteredQuery(metric, {
|
||||||
...filters,
|
...filters,
|
||||||
instance: '192.168.2.34:9050'
|
instance: '192.168.2.34:9050'
|
||||||
});
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await lastValueFrom(
|
const response = await lastValueFrom(
|
||||||
this.httpService.get(`${this.prometheusUrl}/query_range`, {
|
this.httpService.get(`${this.prometheusUrl}/query_range`, {
|
||||||
|
|
@ -191,7 +133,7 @@ export class PrometheusService {
|
||||||
query,
|
query,
|
||||||
start,
|
start,
|
||||||
end,
|
end,
|
||||||
step: optimalStep.toString()
|
step: step.toString()
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
@ -275,40 +217,21 @@ export class PrometheusService {
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchAllMetrics(): Promise<string[]> {
|
async fetchAllMetrics(): Promise<string[]> {
|
||||||
try {
|
|
||||||
const response = await lastValueFrom(
|
const response = await lastValueFrom(
|
||||||
this.httpService.get(`${this.prometheusUrl}/label/__name__/values`)
|
this.httpService.get(`${this.prometheusUrl}/label/__name__/values`)
|
||||||
);
|
);
|
||||||
return response.data.data;
|
return response.data.data;
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching all metrics:', error);
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async fetchAllMetricsWithValues(): Promise<any[]> {
|
async fetchAllMetricsWithValues(): Promise<any[]> {
|
||||||
const metricNames = await this.fetchAllMetrics();
|
const metricNames = await this.fetchAllMetrics();
|
||||||
const zvksMetrics = metricNames.filter(metric =>
|
const zvksMetrics = metricNames.filter(metric => metric.startsWith('zvks'));
|
||||||
metric.startsWith('zvks') ||
|
|
||||||
metric.includes('server_li') ||
|
|
||||||
metric.includes('application_li')
|
|
||||||
);
|
|
||||||
|
|
||||||
const promises = zvksMetrics.map(async (metric) => {
|
const promises = zvksMetrics.map(async (metric) => {
|
||||||
try {
|
|
||||||
const data = await this.fetchMetrics(metric);
|
const data = await this.fetchMetrics(metric);
|
||||||
return { metric, data };
|
return { metric, data };
|
||||||
} catch (error) {
|
|
||||||
console.error(`Error fetching data for metric ${metric}:`, error);
|
|
||||||
return { metric, data: [] };
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
return Promise.all(promises);
|
return Promise.all(promises);
|
||||||
}
|
}
|
||||||
|
|
||||||
clearCache(): void {
|
|
||||||
this.metricCache.clear();
|
|
||||||
this.metadataCache.clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue