diff --git a/.env b/.env index d94718a..71e5cb2 100644 --- a/.env +++ b/.env @@ -1,9 +1,9 @@ -#Прометеус +# Прометеус #PROMETHEUS_API=http://192.168.2.34:9090/api/v1 #FRONTEND_URL=192.168.2.39:5173 -#Постгресс +# Постгресс #DB_HOST=192.168.2.37 #DB_PORT=5432 #DB_USER=trust @@ -11,19 +11,28 @@ #DB_NAME=trust-db -#JWT +# JWT #JWT_SECRET=x7F!2p9L#q1$z0*8R5vYgMnBk #JWT_SECRET=x7Fcdp9L#q1$z0*8R5vYgMnBk -#COOKIE +# COOKIE # Для production #COOKIE_SECURE=true #COOKIE_SAME_SITE=strict # Для development -# COOKIE_SECURE=false -# COOKIE_SAME_SITE=lax +#COOKIE_SECURE=false +#COOKIE_SAME_SITE=lax # Для меню #RANGES_API_URL=http://192.168.2.39: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 +#ANALYSIS_API_URL=http://192.168.2.39:5134/models/api/metrics/rest \ No newline at end of file diff --git a/package.json b/package.json index 7e75df4..05bfc84 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,10 @@ "@types/cookie-parser": "^1.4.8", "@nestjs/jwt": "^11.0.0", "@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": { "@eslint/eslintrc": "^3.2.0", diff --git a/src/Subscription.interface.ts b/src/ai/ai.module.ts similarity index 100% rename from src/Subscription.interface.ts rename to src/ai/ai.module.ts diff --git a/src/ai/ai.service.ts b/src/ai/ai.service.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/app.module.ts b/src/app.module.ts index c224821..aea5e61 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -4,7 +4,9 @@ import { HttpModule } from '@nestjs/axios'; import { ConfigModule } from '@nestjs/config'; import { AuthModule } from './auth/auth.module'; import { MenuModule } from './menu/menu.module'; -import { PrometheusModule } from './prometheus.module'; +import { PrometheusModule } from './prometheus/prometheus.module'; +import { ClickHouseModule } from './clickhouse/clickhouse.module'; +import { ClickHouseController } from './clickhouse/clickhouse.controller'; @Module({ imports: [ @@ -27,6 +29,8 @@ import { PrometheusModule } from './prometheus.module'; AuthModule, PrometheusModule, MenuModule, + ClickHouseModule, ], + controllers: [ClickHouseController], }) -export class AppModule {} +export class AppModule { } diff --git a/src/clickhouse/clickhouse.controller.ts b/src/clickhouse/clickhouse.controller.ts new file mode 100644 index 0000000..6a13f41 --- /dev/null +++ b/src/clickhouse/clickhouse.controller.ts @@ -0,0 +1,35 @@ +import { Controller, Get } from '@nestjs/common'; +import { ClickHouseService } from './clickhouse.service'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; + +@ApiTags('Clickhouse') +@Controller('clickhouse') +export class ClickHouseController { + constructor(private readonly clickhouseService: ClickHouseService) { } + + @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(); + } +} \ No newline at end of file diff --git a/src/clickhouse/clickhouse.module.ts b/src/clickhouse/clickhouse.module.ts new file mode 100644 index 0000000..64e5ac4 --- /dev/null +++ b/src/clickhouse/clickhouse.module.ts @@ -0,0 +1,23 @@ +import { Module, Global } from '@nestjs/common'; +import { createClient, ClickHouseClient } from '@clickhouse/client'; +import { ClickHouseService } from './clickhouse.service'; + +@Global() +@Module({ + 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 { } \ No newline at end of file diff --git a/src/clickhouse/clickhouse.service.ts b/src/clickhouse/clickhouse.service.ts new file mode 100644 index 0000000..beaf424 --- /dev/null +++ b/src/clickhouse/clickhouse.service.ts @@ -0,0 +1,72 @@ +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(); + + // Парсинг данных + 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 []; + } + }); + } +} \ No newline at end of file diff --git a/src/menu/menu.module.ts b/src/menu/menu.module.ts index 926b955..403c187 100644 --- a/src/menu/menu.module.ts +++ b/src/menu/menu.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { MenuController } from './menu.controller'; import { HttpModule } from '@nestjs/axios'; import { MenuService } from './menu.service'; -import { PrometheusModule } from '../prometheus.module'; +import { PrometheusModule } from '../prometheus/prometheus.module'; import { RangeService } from './range.service'; import { RangeController } from './range.controller'; diff --git a/src/menu/menu.service.ts b/src/menu/menu.service.ts index 583a843..7d88842 100644 --- a/src/menu/menu.service.ts +++ b/src/menu/menu.service.ts @@ -1,5 +1,5 @@ import { Injectable, HttpException, HttpStatus } from '@nestjs/common'; -import { PrometheusService } from '../prometheus.service'; +import { PrometheusService } from '../prometheus/prometheus.service'; import { MenuItem } from './menu.interface'; import * as fs from 'fs/promises'; import * as path from 'path'; diff --git a/src/prometheus/Subscription.interface.ts b/src/prometheus/Subscription.interface.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/metrics.controller.ts b/src/prometheus/metrics.controller.ts similarity index 100% rename from src/metrics.controller.ts rename to src/prometheus/metrics.controller.ts diff --git a/src/metrics.gateway.ts b/src/prometheus/metrics.gateway.ts similarity index 100% rename from src/metrics.gateway.ts rename to src/prometheus/metrics.gateway.ts diff --git a/src/prometheus-metric.interface.ts b/src/prometheus/prometheus-metric.interface.ts similarity index 100% rename from src/prometheus-metric.interface.ts rename to src/prometheus/prometheus-metric.interface.ts diff --git a/src/prometheus.module.ts b/src/prometheus/prometheus.module.ts similarity index 100% rename from src/prometheus.module.ts rename to src/prometheus/prometheus.module.ts diff --git a/src/prometheus.service.ts b/src/prometheus/prometheus.service.ts similarity index 86% rename from src/prometheus.service.ts rename to src/prometheus/prometheus.service.ts index 2932f56..136d766 100644 --- a/src/prometheus.service.ts +++ b/src/prometheus/prometheus.service.ts @@ -3,7 +3,7 @@ import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config'; import { lastValueFrom } from 'rxjs'; import { PrometheusMetric } from './prometheus-metric.interface'; -import { MenuItem } from './menu/menu.interface'; +import { MenuItem } from '../menu/menu.interface'; @Injectable() export class PrometheusService { @@ -109,22 +109,22 @@ export class PrometheusService { } private buildFilteredQuery(metric: string, filters: Record): string { - const filterParts = Object.entries(filters) - .filter(([_, value]) => value !== undefined && value !== null && value !== "") - .map(([key, value]) => { - // Убираем автоматическое добавление "module$" для source_id - return `${key}="${value}"`; - }); + const filterParts = Object.entries(filters) + .filter(([_, value]) => value !== undefined && value !== null && value !== "") + .map(([key, value]) => { + // Убираем автоматическое добавление "module$" для source_id + return `${key}="${value}"`; + }); - return filterParts.length > 0 - ? `${metric}{${filterParts.join(',')}}` - : metric; -} + return filterParts.length > 0 + ? `${metric}{${filterParts.join(',')}}` + : metric; + } async fetchMetricsRange(metric: string, start: number, end: number, step: number, filters: Record = {}): Promise { const query = this.buildFilteredQuery(metric, { ...filters, - instance: '192.168.2.34:9050' + instance: '192.168.2.34:9050' }); try { const response = await lastValueFrom( @@ -224,14 +224,14 @@ export class PrometheusService { } async fetchAllMetricsWithValues(): Promise { - const metricNames = await this.fetchAllMetrics(); - const zvksMetrics = metricNames.filter(metric => metric.startsWith('zvks')); - - const promises = zvksMetrics.map(async (metric) => { - const data = await this.fetchMetrics(metric); - return { metric, data }; - }); - - return Promise.all(promises); -} + const metricNames = await this.fetchAllMetrics(); + const zvksMetrics = metricNames.filter(metric => metric.startsWith('zvks')); + + const promises = zvksMetrics.map(async (metric) => { + const data = await this.fetchMetrics(metric); + return { metric, data }; + }); + + return Promise.all(promises); + } }