diff --git a/src/menu/menu.interface.ts b/src/menu/menu.interface.ts index 654e678..6608d56 100644 --- a/src/menu/menu.interface.ts +++ b/src/menu/menu.interface.ts @@ -1,9 +1,14 @@ export interface MenuItem { - title: string; - id: string; - items?: MenuItem[]; - metric?: string; - filters?: Record; - isDynamic?: boolean; - templateId?: string; - } \ No newline at end of file + title: string; + id: string; + items?: MenuItem[]; + metric?: string; + filters?: Record; + isDynamic?: boolean; + templateId?: string; + ranges?: Array<{ + min: number; + max: number; + status: number; + }>; +} \ No newline at end of file diff --git a/src/menu/menu.module.ts b/src/menu/menu.module.ts index dec7701..6eccc8d 100644 --- a/src/menu/menu.module.ts +++ b/src/menu/menu.module.ts @@ -1,11 +1,13 @@ 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'; // Импортируем PrometheusModule +import { PrometheusModule } from '../prometheus.module'; +import { RangeService } from './range.service'; @Module({ - imports: [PrometheusModule], // Добавляем в imports + imports: [PrometheusModule, HttpModule], controllers: [MenuController], - providers: [MenuService] + providers: [MenuService, RangeService] }) -export class MenuModule {} \ No newline at end of file +export class MenuModule { } \ No newline at end of file diff --git a/src/menu/menu.service.ts b/src/menu/menu.service.ts index a7f5d3f..b830d8d 100644 --- a/src/menu/menu.service.ts +++ b/src/menu/menu.service.ts @@ -3,10 +3,14 @@ import { PrometheusService } from '../prometheus.service'; import { MenuItem } from './menu.interface'; import * as fs from 'fs/promises'; import * as path from 'path'; +import { RangeService } from './range.service'; @Injectable() export class MenuService { - constructor(private readonly prometheusService: PrometheusService) { } + constructor( + private readonly prometheusService: PrometheusService, + private readonly rangeService: RangeService + ) { } private readonly menuOverridesPath = path.join(process.cwd(), 'data', 'menu.json'); @@ -17,8 +21,8 @@ export class MenuService { } async getFullMenu(): Promise { - const dynamicItems = await this.generateDynamicItems(); - const baseMenu = this.injectDynamicItems(this.getStaticStructure(), dynamicItems); + const dynamicItemsPromise = this.generateDynamicItems(); + const baseMenu = await this.injectDynamicItems(this.getStaticStructure(), dynamicItemsPromise); const overrides = await this.loadOverrides(); return this.applyOverrides(baseMenu, overrides); } @@ -46,7 +50,7 @@ export class MenuService { const parsed = JSON.parse(content); return parsed.overrides || []; } catch (e) { - return []; // если файл не существует + return []; } } @@ -73,7 +77,6 @@ export class MenuService { private async generateDynamicItems(): Promise { const metricNames = await this.prometheusService.fetchAllMetrics(); - // Получаем все серии для каждой метрики const allSeries = ( await Promise.all( metricNames.map(async name => { @@ -86,9 +89,7 @@ export class MenuService { ) ).flat(); - // Загружаем мета-информацию по каждой метрике - const metadataMap = new Map(); // metric -> help - + const metadataMap = new Map(); await Promise.all( metricNames.map(async metric => { try { @@ -126,18 +127,30 @@ export class MenuService { const filteredSeries = allSeries.filter(({ labels }) => { const device = labels.device; const instance = labels.instance; - return (!device || !isGarbageDevice(device)) && (!instance || !isGarbageInstance(instance)); }); - const devices = this.extractUniqueEntities(filteredSeries, 'device'); - return devices.map(device => ({ + const devices = this.extractUniqueEntities(filteredSeries, 'device'); + const deviceItems = await Promise.all( + devices.map(device => this.createDeviceItem(device, allSeries, metadataMap)) + ); + + return deviceItems; + } + + private async createDeviceItem( + device: string, + seriesData: { metric: string; labels: Record }[], + metadataMap: Map + ): Promise { + const moduleItems = await this.generateModuleItems(device, seriesData, metadataMap); + return { id: `device_${device}`, title: `Graviton S2082I (${device})`, - items: this.generateModuleItems(device, allSeries, metadataMap), + items: moduleItems, isDynamic: true - })); + }; } private extractUniqueEntities(metrics: any[], field: string): string[] { @@ -149,19 +162,19 @@ export class MenuService { }); return Array.from(entities); } - + private normalizeIdPart(part: string): string { return part - .replace(/\$/g, '_') // Заменяем $ на _ - .replace(/[^a-zA-Z0-9-_]/g, ''); // Удаляем другие спецсимволы + .replace(/\$/g, '_') + .replace(/[^a-zA-Z0-9-_]/g, ''); } - private generateModuleItems( + private async generateModuleItems( device: string, seriesData: { metric: string; labels: Record }[], metadataMap: Map - ): MenuItem[] { - const modules = new Map(); // source_id -> display name + ): Promise { + const modules = new Map(); seriesData.forEach(({ labels }) => { if (labels.device === device && labels.source_id) { @@ -178,55 +191,48 @@ export class MenuService { } }); - return Array.from(modules.entries()).map(([sourceId, displayName]) => ({ - id: `module_${device}_${sourceId}`, - title: displayName, - items: this.generateMetricItems(device, sourceId, seriesData, metadataMap), - isDynamic: true - })); + const modulePromises = Array.from(modules.entries()).map( + async ([sourceId, displayName]) => ({ + id: `module_${device}_${sourceId}`, + title: displayName, + items: await this.generateMetricItems(device, sourceId, seriesData, metadataMap), + isDynamic: true + }) + ); + + return Promise.all(modulePromises); } - private generateMetricItems( + private async generateMetricItems( device: string, module: string, seriesData: { metric: string; labels: Record }[], metadataMap: Map - ): MenuItem[] { - // Фильтруем метрики для текущего устройства и модуля + ): Promise { + const ranges = await this.rangeService.getRanges(); const filtered = seriesData.filter( ({ labels }) => labels.device === device && labels.source_id === module ); - - // Получаем уникальные имена метрик + const uniqueMetrics = new Set(filtered.map(entry => entry.metric)); - - // Нормализуем идентификаторы - const normalizeIdPart = (part: string): string => { - return part - .replace(/\$/g, '_') // Заменяем $ на _ - .replace(/[^a-zA-Z0-9-_]/g, '') // Удаляем другие спецсимволы - .replace(/_+/g, '_') // Заменяем множественные _ на один - .replace(/^_|_$/g, ''); // Удаляем _ в начале и конце - }; - - const safeDevice = normalizeIdPart(device); - const safeModule = normalizeIdPart(module); - - // Формируем пункты меню + const safeDevice = this.normalizeIdPart(device); + const safeModule = this.normalizeIdPart(module); + return Array.from(uniqueMetrics).map(metric => { const description = metadataMap.get(metric) || metric; - const safeMetric = normalizeIdPart(metric); - + const safeMetric = this.normalizeIdPart(metric); + const metricRanges = ranges[description] || []; + return { - id: `metric_${safeDevice}_${safeModule}_${safeMetric}`, // Безопасный ID + id: `metric_${safeDevice}_${safeModule}_${safeMetric}`, title: description, metric, filters: { - device, // Оригинальное значение device - source_id: module // Оригинальное значение source_id + device, + source_id: module }, + ranges: metricRanges, isDynamic: true, - // Сохраняем оригинальные значения для отладки meta: { originalDevice: device, originalModule: module @@ -235,15 +241,24 @@ export class MenuService { }); } - private injectDynamicItems(menu: MenuItem, dynamicItems: MenuItem[]): MenuItem { + private async injectDynamicItems( + menu: MenuItem, + dynamicItemsPromise: Promise + ): Promise { + const dynamicItems = await dynamicItemsPromise; + if (menu.id === 'media_servers') { return { ...menu, items: dynamicItems }; } - return { - ...menu, - items: menu.items?.map(item => this.injectDynamicItems(item, dynamicItems)) || [] - }; + if (menu.items) { + const updatedItems = await Promise.all( + menu.items.map(item => this.injectDynamicItems(item, dynamicItemsPromise)) + ); + return { ...menu, items: updatedItems }; + } + + return menu; } async updateMenuItem(id: string, update: Partial): Promise { @@ -272,4 +287,4 @@ export class MenuService { return null; } -} +} \ No newline at end of file diff --git a/src/menu/range.service.ts b/src/menu/range.service.ts new file mode 100644 index 0000000..7a0aa52 --- /dev/null +++ b/src/menu/range.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@nestjs/common'; +import { HttpService } from '@nestjs/axios'; +import { firstValueFrom } from 'rxjs'; + +@Injectable() +export class RangeService { + constructor(private readonly httpService: HttpService) { } + + async getRanges(): Promise>> { + try { + const response = await firstValueFrom( + this.httpService.request({ + method: 'OPTIONS', + url: 'http://192.168.2.39:9999/api/ranges/9999', + headers: { + 'Accept': 'application/json' + } + }) + ); + + // Проверяем, что ответ содержит данные в ожидаемом формате + if (!response.data || !Array.isArray(response.data)) { + console.error('Invalid response format from ranges API', response.data); + return {}; + } + + const rangesMap: Record> = {}; + + response.data.forEach(item => { + if (item.name && Array.isArray(item.ranges)) { + rangesMap[item.name] = item.ranges; + } + }); + + return rangesMap; + } catch (error) { + console.error('Failed to fetch ranges:', error); + + // Детальное логирование ошибки + 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); + } + + return {}; + } + } +} \ No newline at end of file diff --git a/src/prometheus.service.ts b/src/prometheus.service.ts index d7156d6..ac76079 100644 --- a/src/prometheus.service.ts +++ b/src/prometheus.service.ts @@ -174,7 +174,6 @@ export class PrometheusService { return this.fetchMetricsWithFilters(menuItem.metric, menuItem.filters); } - // ✅ Новый метод: получает базовое описание метрики (help, type) async fetchMetricMetadata(metric: string): Promise<{ name: string; help?: string; @@ -202,7 +201,6 @@ export class PrometheusService { } } - // ✅ Новый метод: получает ВСЕ серии метрики (все комбинации label-ов) async fetchMetricSeries(metric: string): Promise[]> { try { const response = await lastValueFrom(