From 6a47ef460173ae31cb7d7d87a957665367b5aaff Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 21 Oct 2025 09:14:58 -0400 Subject: [PATCH 1/2] added formula --- .env | 3 + src/main.ts | 2 +- src/menu/formula.controller.ts | 48 ++++++++++++++++ src/menu/formula.service.ts | 92 +++++++++++++++++++++++++++++++ src/menu/menu.module.ts | 6 +- src/prometheus/metrics.gateway.ts | 7 ++- 6 files changed, 152 insertions(+), 6 deletions(-) create mode 100644 src/menu/formula.controller.ts create mode 100644 src/menu/formula.service.ts diff --git a/.env b/.env index 5ffdcb0..2575902 100644 --- a/.env +++ b/.env @@ -28,6 +28,9 @@ COOKIE_SAME_SITE=lax RANGES_API_URL=http://192.168.2.39:9999 RANGES_API_ENDPOINT=/api/ranges/9999 +FORMULA_API_URL=http://192.168.2.39:9999 +FORMULA_API_ENDPOINT=/api/integration/7777 + # ClickHouse CLICKHOUSE_HOST=http://192.168.2.37:8123 CLICKHOUSE_USER=vlad diff --git a/src/main.ts b/src/main.ts index a97b0c4..e3064aa 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,7 +42,7 @@ async function bootstrap() { }); // Настройка CORS - app.enableCors({//ПОСТАВИТЬ ПРОКСИ, ЧТОБЫ КОРС НЕ РУГАЛСЯ, ИЗМЕНЕНИЕ ПОЛИТИКИ СЕТЕВЫХ ПАКЕТОВ. ПИШУ IP СВОЙ, А ПОРТ ПРОКСИ. REVERSE PROXY. + app.enableCors({ origin: [process.env.FRONTEND_URL, "http://dev.msf.enode"], credentials: true, methods: 'GET,HEAD,PUT,PATCH,POST,DELETE,OPTIONS', diff --git a/src/menu/formula.controller.ts b/src/menu/formula.controller.ts new file mode 100644 index 0000000..65b871a --- /dev/null +++ b/src/menu/formula.controller.ts @@ -0,0 +1,48 @@ +import { Controller, Get, Post, Body, HttpException, HttpStatus, Param } from '@nestjs/common'; +import { FormulaService } from './formula.service'; +import { MenuService } from './menu.service'; + +@Controller('formula') +export class FormulaController { + constructor( + private readonly FormulaService: FormulaService, + private readonly menuService: MenuService + ) { } + + @Get(':id') + async getFormulaData(@Param('id') id: string) { + try { + return await this.FormulaService.getFormulaData(id); + } catch (error) { + throw new HttpException('Failed to fetch Formula data', HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + @Post(':id/update') + async updateFormulaData( + @Param('id') id: string, + @Body() data: any + ) { + if (!data) { + throw new HttpException('Invalid data format', HttpStatus.BAD_REQUEST); + } + + try { + const result = await this.FormulaService.updateFormulaData(id, data); + this.menuService.invalidateCache(); + return result; + } catch (error) { + throw new HttpException(error.message, HttpStatus.INTERNAL_SERVER_ERROR); + } + } + + // OPTIONS метод для получения данных (как в вашем примере curl) + @Get(':id/options') + async getFormulaOptions(@Param('id') id: string) { + try { + return await this.FormulaService.getFormulaOptions(id); + } catch (error) { + throw new HttpException('Failed to fetch Formula options', HttpStatus.INTERNAL_SERVER_ERROR); + } + } +} \ No newline at end of file diff --git a/src/menu/formula.service.ts b/src/menu/formula.service.ts new file mode 100644 index 0000000..e83bd38 --- /dev/null +++ b/src/menu/formula.service.ts @@ -0,0 +1,92 @@ +import { Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; +import { ConfigService } from '@nestjs/config'; + +@Injectable() +export class FormulaService { + private readonly FormulaApiUrl: string; + private readonly FormulaApiEndpoint: string; + + constructor( + private readonly httpService: HttpService, + private readonly configService: ConfigService + ) { + this.FormulaApiUrl = this.configService.get('FORMULA_API_URL', 'http://192.168.2.39:9999'); + this.FormulaApiEndpoint = this.configService.get('FORMULA_API_ENDPOINT', '/api/integration/7777'); + } + + async getFormulaData(id: string): Promise { + try { + const response = await firstValueFrom( + this.httpService.get(`${this.FormulaApiUrl}${this.FormulaApiEndpoint}/${id}`, { + headers: { + 'Accept': 'application/json' + } + }) + ); + + return response.data; + } catch (error) { + console.error('Failed to fetch Formula data:', error); + this.handleError(error); + return {}; + } + } + + async getFormulaOptions(id: string): Promise { + try { + const url = `${this.FormulaApiUrl}${this.FormulaApiEndpoint}`; + console.log('Fetching Formula options via OPTIONS:', url); + + const response = await firstValueFrom( + this.httpService.request({ + method: 'OPTIONS', + url, + headers: { 'Accept': 'application/json' } + }) + ); + + console.log('Response from Formula API:', response.data); + return response.data; + } catch (error) { + console.error('Failed to fetch Formula options:', error); + this.handleError(error); + return []; + } + } + + async updateFormulaData(id: string, data: any) { + try { + const response = await firstValueFrom( + this.httpService.post( + `${this.FormulaApiUrl}${this.FormulaApiEndpoint}/${id}`, + data, + { + headers: { + 'Content-Type': 'application/json' + }, + } + ) + ); + return response.data; + } catch (error) { + console.error('Failed to update Formula data:', error); + this.handleError(error); + throw new Error('Failed to update Formula data'); + } + } + + private handleError(error: any): void { + if (error.response) { + console.error('Server responded with:', { + status: error.response.status, + data: error.response.data + }); + } else if (error.request) { + console.error('No response received:', error.request); + } else { + console.error('Request setup error:', error.message); + } + } +} \ No newline at end of file diff --git a/src/menu/menu.module.ts b/src/menu/menu.module.ts index 403c187..82ad5af 100644 --- a/src/menu/menu.module.ts +++ b/src/menu/menu.module.ts @@ -5,10 +5,12 @@ import { MenuService } from './menu.service'; import { PrometheusModule } from '../prometheus/prometheus.module'; import { RangeService } from './range.service'; import { RangeController } from './range.controller'; +import { FormulaController } from './formula.controller'; +import { FormulaService } from './formula.service'; @Module({ imports: [PrometheusModule, HttpModule], - controllers: [MenuController, RangeController], - providers: [MenuService, RangeService] + controllers: [MenuController, RangeController, FormulaController], + providers: [MenuService, RangeService, FormulaService] }) export class MenuModule { } \ No newline at end of file diff --git a/src/prometheus/metrics.gateway.ts b/src/prometheus/metrics.gateway.ts index af8e1c7..8d9f875 100644 --- a/src/prometheus/metrics.gateway.ts +++ b/src/prometheus/metrics.gateway.ts @@ -56,13 +56,14 @@ export class MetricsGateway implements OnModuleInit, OnModuleDestroy { ); const wsPort = Number(this.configService.get('WS_PORT') || 3001); - this.httpServer.listen(wsPort, () => { + const wsHost = this.configService.get('WS_HOST') || '0.0.0.0'; + + this.httpServer.listen(wsPort, wsHost, () => { this.logger.log( - `WebSocket server running at ws://localhost:${wsPort}/metrics-ws` + `WebSocket server running at ws://${wsHost}:${wsPort}/metrics-ws` ); }); } - onModuleDestroy() { // Очистка всех ресурсов this.clearAllSubscriptions(); From d97a0b95f66fdbb5f1752e54dc3866574f2910f1 Mon Sep 17 00:00:00 2001 From: DmitriyA Date: Tue, 2 Dec 2025 04:36:15 -0500 Subject: [PATCH 2/2] version update --- src/menu/enriched-formula.controller.ts | 38 +++ src/menu/formula-enrichment.service.ts | 307 ++++++++++++++++++++++++ src/menu/formula.controller.ts | 28 ++- src/menu/formula.interface.ts | 36 +++ src/menu/menu.module.ts | 7 +- 5 files changed, 413 insertions(+), 3 deletions(-) create mode 100644 src/menu/enriched-formula.controller.ts create mode 100644 src/menu/formula-enrichment.service.ts create mode 100644 src/menu/formula.interface.ts diff --git a/src/menu/enriched-formula.controller.ts b/src/menu/enriched-formula.controller.ts new file mode 100644 index 0000000..9d8dee1 --- /dev/null +++ b/src/menu/enriched-formula.controller.ts @@ -0,0 +1,38 @@ +// controllers/enriched-formula.controller.ts +import { Controller, Get, Param } from '@nestjs/common'; +import { ApiTags, ApiOperation, ApiResponse, ApiParam } from '@nestjs/swagger'; +import { FormulaEnrichmentService } from './formula-enrichment.service'; +import { EnrichedFormulaMetric } from './formula.interface'; + +@ApiTags('Enriched Formulas') +@Controller('enriched-formulas') +export class EnrichedFormulaController { + constructor( + private readonly formulaEnrichmentService: FormulaEnrichmentService + ) { } + + @Get() + @ApiOperation({ summary: 'Получить все формулы с обогащенными данными' }) + @ApiResponse({ + status: 200, + description: 'Список формул с обогащенными метриками' + }) + async getAllEnrichedFormulas(): Promise { + return this.formulaEnrichmentService.getAllEnrichedFormulas(); + } + + @Get(':id') + @ApiOperation({ summary: 'Получить конкретную формулу с обогащенными данными' }) + @ApiParam({ name: 'id', description: 'ID формулы' }) + @ApiResponse({ + status: 200, + description: 'Формула с обогащенными метриками' + }) + @ApiResponse({ + status: 404, + description: 'Формула не найдена' + }) + async getEnrichedFormula(@Param('id') id: string): Promise { + return this.formulaEnrichmentService.getEnrichedFormula(id); + } +} \ No newline at end of file diff --git a/src/menu/formula-enrichment.service.ts b/src/menu/formula-enrichment.service.ts new file mode 100644 index 0000000..df42516 --- /dev/null +++ b/src/menu/formula-enrichment.service.ts @@ -0,0 +1,307 @@ +// services/formula-enrichment.service.ts +import { Injectable, Logger } from '@nestjs/common'; +import { PrometheusService } from '../prometheus/prometheus.service'; +import { FormulaService } from './formula.service'; +import { + FormulaMetric, + EnrichedFormulaMetric, + EnrichedMetric +} from './formula.interface'; + +@Injectable() +export class FormulaEnrichmentService { + private readonly logger = new Logger(FormulaEnrichmentService.name); + + // Конфигурируемые префиксы + private readonly metricPrefixes = ['zvks_', 'server_', 'application_']; + private readonly defaultPrefix = 'zvks_'; + + constructor( + private readonly prometheusService: PrometheusService, + private readonly formulaService: FormulaService + ) { } + + /** + * Автоматически определяет имя метрики в Prometheus + */ + private resolveMetricName(originalName: string): string { + // Если имя уже содержит префикс, используем как есть + if (this.metricPrefixes.some(prefix => originalName.startsWith(prefix))) { + return originalName; + } + + // Иначе добавляем дефолтный префикс + return `${this.defaultPrefix}${originalName}`; + } + + /** + * Ищет метрику в Prometheus с учетом различных вариантов имен + */ + private async findMetricInPrometheus(originalName: string): Promise<{ + prometheusName: string; + metrics: any[]; + found: boolean; + }> { + const possibleNames = [ + originalName, // как есть + this.resolveMetricName(originalName), // с дефолтным префиксом + // Можно добавить другие варианты если нужно + ]; + + for (const metricName of possibleNames) { + try { + this.logger.debug(`Trying to find metric: ${metricName}`); + const metrics = await this.prometheusService.fetchMetrics(metricName); + + if (metrics && metrics.length > 0) { + this.logger.debug(`Found metric: ${metricName} with ${metrics.length} entries`); + return { + prometheusName: metricName, + metrics, + found: true + }; + } + } catch (error) { + this.logger.debug(`Metric ${metricName} not found: ${error.message}`); + // Продолжаем поиск с следующим вариантом + continue; + } + } + + return { + prometheusName: this.resolveMetricName(originalName), + metrics: [], + found: false + }; + } + + /** + * Получить обогащенные данные формулы по ID + */ + async getEnrichedFormula(id: string): Promise { + const formulaData = await this.formulaService.getFormulaData(id); + + if (!formulaData || !formulaData.values) { + throw new Error(`Formula data not found for id: ${id}`); + } + + const enrichedMetrics = await this.enrichMetrics(formulaData.values.statusarr); + const { parsedFormula, humanReadableFormula } = this.parseFormula( + formulaData.formula, + enrichedMetrics + ); + + return { + ...formulaData, + enrichedMetrics, + parsedFormula, + humanReadableFormula, + metadata: { + totalMetrics: enrichedMetrics.length, + foundMetrics: enrichedMetrics.filter(m => m.found).length, + missingMetrics: enrichedMetrics.filter(m => !m.found).length + } + }; + } + + /** + * Обогащает массив метрик данными из Prometheus + */ + private async enrichMetrics(metricNames: string[]): Promise { + const enrichmentPromises = metricNames.map(async (originalName) => { + try { + const { prometheusName, metrics, found } = await this.findMetricInPrometheus(originalName); + + if (found && metrics.length > 0) { + const metric = metrics[0]; // Берем первую метрику как пример + const description = metric.description || await this.getMetricDescription(prometheusName); + + return { + originalName, + prometheusName, + description, + currentValue: metric.value, + device: metric.device, + source_id: metric.source_id, + timestamp: metric.timestamp, + type: metric.type, + found: true, + valuesCount: metrics.length + }; + } else { + // Метрика не найдена + const description = await this.getMetricDescription(prometheusName); + return { + originalName, + prometheusName, + description: description || 'Метрика не найдена в Prometheus', + currentValue: undefined, + found: false, + valuesCount: 0, + error: `Метрика не найдена. Проверенные имена: ${prometheusName}` + }; + } + } catch (error) { + this.logger.error(`Error enriching metric ${originalName}:`, error); + return { + originalName, + prometheusName: this.resolveMetricName(originalName), + description: `Ошибка при получении данных: ${error.message}`, + currentValue: undefined, + found: false, + valuesCount: 0, + error: error.message + }; + } + }); + + return Promise.all(enrichmentPromises); + } + + /** + * Получает описание метрики + */ + private async getMetricDescription(metricName: string): Promise { + try { + const description = await this.prometheusService.fetchMetricDescription(metricName); + return description || 'Описание недоступно'; + } catch (error) { + return 'Описание недоступно'; + } + } + + /** + * Парсит формулу для лучшего отображения + */ + private parseFormula( + formula: string, + enrichedMetrics: EnrichedMetric[] + ): { parsedFormula: string; humanReadableFormula: string } { + let humanReadableFormula = formula; + + // Заменяем statusarr[index] на описания метрик + enrichedMetrics.forEach((metric, index) => { + const arrayIndex = index + 1; // В формулах индексы с 1 + const statusarrPattern = new RegExp(`statusarr\\[${arrayIndex}\\]`, 'g'); + + // Для humanReadableFormula используем описания метрик + if (metric.found) { + humanReadableFormula = humanReadableFormula.replace( + statusarrPattern, + metric.description + ); + } else { + humanReadableFormula = humanReadableFormula.replace( + statusarrPattern, + `${metric.originalName} (НЕ НАЙДЕНА)` + ); + } + }); + + // Форматируем для лучшей читаемости + humanReadableFormula = humanReadableFormula + .replace(/\*/g, ' × ') + .replace(/\//g, ' ÷ ') + .replace(/\+/g, ' + ') + .replace(/-/g, ' - ') + .replace(/\s+/g, ' ') + .trim(); + + return { + parsedFormula: formula, + humanReadableFormula + }; + } + + /** + * Получить все доступные формулы с обогащенными данными + */ + async getAllEnrichedFormulas(): Promise { + try { + const formulaOptions = await this.formulaService.getFormulaOptions(''); + + if (!Array.isArray(formulaOptions)) { + throw new Error('Invalid formula options response'); + } + + const enrichmentPromises = formulaOptions.map(async (formulaOption) => { + try { + return await this.getEnrichedFormula(formulaOption.id); + } catch (error) { + this.logger.error(`Error enriching formula ${formulaOption.id}:`, error); + return { + ...formulaOption, + enrichedMetrics: [], + parsedFormula: formulaOption.formula || '', + humanReadableFormula: formulaOption.formula || '', + metadata: { + totalMetrics: 0, + foundMetrics: 0, + missingMetrics: 0 + } + }; + } + }); + + return Promise.all(enrichmentPromises); + } catch (error) { + this.logger.error('Error getting all enriched formulas:', error); + return []; + } + } + + /** + * Диагностика - проверка доступности метрик + */ + async diagnoseMetrics(metricNames: string[]): Promise { + const results = await Promise.all( + metricNames.map(async (originalName) => { + const { prometheusName, metrics, found } = await this.findMetricInPrometheus(originalName); + + return { + originalName, + prometheusName, + found, + availableNames: await this.findAvailableMetricNames(originalName), + metricsCount: metrics.length, + sampleValue: found && metrics[0] ? metrics[0].value : null + }; + }) + ); + + return { + diagnosis: results, + summary: { + total: results.length, + found: results.filter(r => r.found).length, + notFound: results.filter(r => !r.found).length + } + }; + } + + /** + * Поиск доступных вариантов имен метрик + */ + private async findAvailableMetricNames(baseName: string): Promise { + const possibleNames = [ + baseName, + `${this.defaultPrefix}${baseName}`, + ...this.metricPrefixes.map(prefix => `${prefix}${baseName}`) + ]; + + const availableNames: string[] = []; + + for (const name of possibleNames) { + try { + const metrics = await this.prometheusService.fetchMetrics(name); + if (metrics && metrics.length > 0) { + availableNames.push(name); + } + } catch (error) { + // Игнорируем ошибки - метрика не найдена + } + } + + return availableNames; + } +} \ No newline at end of file diff --git a/src/menu/formula.controller.ts b/src/menu/formula.controller.ts index 65b871a..58b69d0 100644 --- a/src/menu/formula.controller.ts +++ b/src/menu/formula.controller.ts @@ -1,12 +1,14 @@ import { Controller, Get, Post, Body, HttpException, HttpStatus, Param } from '@nestjs/common'; import { FormulaService } from './formula.service'; import { MenuService } from './menu.service'; +import { FormulaEnrichmentService } from './formula-enrichment.service'; @Controller('formula') export class FormulaController { constructor( private readonly FormulaService: FormulaService, - private readonly menuService: MenuService + private readonly menuService: MenuService, + private readonly formulaEnrichmentService: FormulaEnrichmentService ) { } @Get(':id') @@ -45,4 +47,28 @@ export class FormulaController { throw new HttpException('Failed to fetch Formula options', HttpStatus.INTERNAL_SERVER_ERROR); } } + + @Get(':id/enriched') + async getEnrichedFormulaData(@Param('id') id: string) { + try { + return await this.formulaEnrichmentService.getEnrichedFormula(id); + } catch (error) { + throw new HttpException( + 'Failed to fetch enriched formula data', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } + + @Get('all/enriched') + async getAllEnrichedFormulas() { + try { + return await this.formulaEnrichmentService.getAllEnrichedFormulas(); + } catch (error) { + throw new HttpException( + 'Failed to fetch enriched formulas', + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + } } \ No newline at end of file diff --git a/src/menu/formula.interface.ts b/src/menu/formula.interface.ts new file mode 100644 index 0000000..95d0fe5 --- /dev/null +++ b/src/menu/formula.interface.ts @@ -0,0 +1,36 @@ +// interfaces/formula.interface.ts +export interface FormulaMetric { + id: string; + name: string; + description: string; + values: { + statusarr: string[]; + warr: string[]; + }; + formula: string; +} + +export interface EnrichedFormulaMetric extends FormulaMetric { + enrichedMetrics: EnrichedMetric[]; + parsedFormula: string; + humanReadableFormula: string; + metadata: { + totalMetrics: number; + foundMetrics: number; + missingMetrics: number; + }; +} + +export interface EnrichedMetric { + originalName: string; + prometheusName: string; + description: string; + currentValue?: number; + device?: string; + source_id?: string; + timestamp?: number; + type?: string; + found: boolean; + valuesCount?: number; + error?: string; +} \ No newline at end of file diff --git a/src/menu/menu.module.ts b/src/menu/menu.module.ts index 82ad5af..fc3710b 100644 --- a/src/menu/menu.module.ts +++ b/src/menu/menu.module.ts @@ -7,10 +7,13 @@ import { RangeService } from './range.service'; import { RangeController } from './range.controller'; import { FormulaController } from './formula.controller'; import { FormulaService } from './formula.service'; +import { FormulaEnrichmentService } from './formula-enrichment.service'; +import { EnrichedFormulaController } from './enriched-formula.controller'; @Module({ imports: [PrometheusModule, HttpModule], - controllers: [MenuController, RangeController, FormulaController], - providers: [MenuService, RangeService, FormulaService] + controllers: [MenuController, RangeController, FormulaController, EnrichedFormulaController], + providers: [MenuService, RangeService, FormulaService, FormulaEnrichmentService], + exports: [FormulaEnrichmentService] }) export class MenuModule { } \ No newline at end of file