diff --git a/logs.txt b/logs.txt index c0969e7..134fcc4 100644 Binary files a/logs.txt and b/logs.txt differ diff --git a/src/menu/menu.service.ts b/src/menu/menu.service.ts index 7d88842..b9ba54c 100644 --- a/src/menu/menu.service.ts +++ b/src/menu/menu.service.ts @@ -187,41 +187,171 @@ export class MenuService { private normalizeIdPart(part: string): string { return part .replace(/\$/g, '_') - .replace(/[^a-zA-Z0-9-_]/g, ''); + .replace(/,/g, '_') // Заменяем запятые + .replace(/\s+/g, '_') // Заменяем пробелы + .replace(/[^a-zA-Z0-9-_]/g, '') + .toLowerCase(); } + // private async generateModuleItems( + // device: string, + // seriesData: { metric: string; labels: Record }[], + // metadataMap: Map + // ): Promise { + // const modules = new Map(); + + // seriesData.forEach(({ labels }) => { + // if (labels.device === device && labels.source_id) { + // const sourceId = labels.source_id; + // let displayName = sourceId; + + // if (sourceId.startsWith('module$')) { + // displayName = `Module ${sourceId.split('$')[1]}`; + // } else if (sourceId.startsWith('port$')) { + // displayName = `Port ${sourceId.split('$')[1]}`; + // } + + // modules.set(sourceId, displayName); + // } + // }); + + // 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 async generateMetricItems( + // device: string, + // module: string, + // seriesData: { metric: string; labels: Record }[], + // metadataMap: Map + // ): 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 safeDevice = this.normalizeIdPart(device); + // const safeModule = this.normalizeIdPart(module); + + // return Array.from(uniqueMetrics).map(metric => { + // const description = metadataMap.get(metric) || metric; + // const safeMetric = this.normalizeIdPart(metric); + // const metricRanges = ranges[description] || []; + + // return { + // id: `metric_${safeDevice}_${safeModule}_${safeMetric}`, + // title: description, + // metric, + // filters: { + // device, + // source_id: module + // }, + // ranges: metricRanges, + // isDynamic: true, + // meta: { + // originalDevice: device, + // originalModule: module + // } + // }; + // }); + // } + + //ВРЕМЕННЫЙ КОСТЫЛЬ + private async generateModuleItems( device: string, seriesData: { metric: string; labels: Record }[], metadataMap: Map ): Promise { const modules = new Map(); + const specialFolders = new Map>(); seriesData.forEach(({ labels }) => { if (labels.device === device && labels.source_id) { const sourceId = labels.source_id; - let displayName = sourceId; - if (sourceId.startsWith('module$')) { - displayName = `Module ${sourceId.split('$')[1]}`; - } else if (sourceId.startsWith('port$')) { - displayName = `Port ${sourceId.split('$')[1]}`; + // Проверяем наличие специальных тегов (complex, integration) + if (sourceId.includes(', complex') || sourceId.includes(', integration')) { + const [modulePart, folderType] = sourceId.split(', ').map(s => s.trim()); + let displayName = modulePart; + + if (modulePart.startsWith('module$')) { + displayName = `Module ${modulePart.split('$')[1]}`; + } else if (modulePart.startsWith('port$')) { + displayName = `Port ${modulePart.split('$')[1]}`; + } else if (modulePart === 'undefined') { + displayName = 'Unknown Module'; + } + + // Сохраняем в специальные папки + if (!specialFolders.has(folderType)) { + specialFolders.set(folderType, new Map()); + } + specialFolders.get(folderType)!.set(modulePart, displayName); + } + // Проверяем старые некорректные форматы (без запятой) + else if (sourceId.endsWith('complex') || sourceId.endsWith('integration')) { + // Игнорируем старые некорректные форматы + console.warn(`Ignoring legacy format: ${sourceId} for device ${device}`); + } + else { + // Обычная обработка + let displayName = sourceId; + if (sourceId.startsWith('module$')) { + displayName = `Module ${sourceId.split('$')[1]}`; + } else if (sourceId.startsWith('port$')) { + displayName = `Port ${sourceId.split('$')[1]}`; + } + modules.set(sourceId, displayName); } - - modules.set(sourceId, displayName); } }); - const modulePromises = Array.from(modules.entries()).map( + const moduleItems = Array.from(modules.entries()).map( async ([sourceId, displayName]) => ({ - id: `module_${device}_${sourceId}`, + id: `module_${device}_${this.normalizeIdPart(sourceId)}`, title: displayName, items: await this.generateMetricItems(device, sourceId, seriesData, metadataMap), isDynamic: true }) ); - return Promise.all(modulePromises); + // Создаем специальные папки + const specialFolderItems = Array.from(specialFolders.entries()).map( + async ([folderType, folderModules]) => { + const folderItems = await Promise.all( + Array.from(folderModules.entries()).map( + async ([sourceId, displayName]) => ({ + id: `module_${device}_${this.normalizeIdPart(sourceId)}_${this.normalizeIdPart(folderType)}`, + title: displayName, + items: await this.generateMetricItems(device, `${sourceId}, ${folderType}`, seriesData, metadataMap), + isDynamic: true + }) + ) + ); + + return { + id: `folder_${device}_${this.normalizeIdPart(folderType)}`, + title: folderType, + items: folderItems, + isDynamic: true + }; + } + ); + + return [ + ...(await Promise.all(moduleItems)), + ...(await Promise.all(specialFolderItems)) + ]; } private async generateMetricItems( @@ -231,6 +361,8 @@ export class MenuService { metadataMap: Map ): Promise { const ranges = await this.rangeService.getRanges(); + + // Фильтруем по device и точному совпадению source_id const filtered = seriesData.filter( ({ labels }) => labels.device === device && labels.source_id === module ); diff --git a/src/prometheus/prometheus.service.ts b/src/prometheus/prometheus.service.ts index 08876c6..3388095 100644 --- a/src/prometheus/prometheus.service.ts +++ b/src/prometheus/prometheus.service.ts @@ -1,319 +1,3 @@ -// import { Injectable } from '@nestjs/common'; -// 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'; - -// @Injectable() -// export class PrometheusService { -// private readonly prometheusUrl: string; -// private metricCache = new Map(); -// private metadataCache = new Map(); - -// constructor( -// private readonly httpService: HttpService, -// private readonly configService: ConfigService -// ) { -// this.prometheusUrl = this.configService.get('PROMETHEUS_API', 'http://localhost:9090'); -// console.log('Prometheus API URL:', this.prometheusUrl); -// } - -// async fetchMetricType(metric: string): Promise { -// const cacheKey = `metadata-type-${metric}`; -// const cacheEntry = this.metadataCache.get(cacheKey); - -// if (cacheEntry && Date.now() - cacheEntry.timestamp < 30000) { -// return cacheEntry.type; -// } - -// try { -// const response = await lastValueFrom( -// this.httpService.get(`${this.prometheusUrl}/metadata`, { -// params: { metric }, -// }) -// ); -// const metadata = response.data.data[metric]; -// const result = metadata?.length ? metadata[0].type : null; - -// this.metadataCache.set(cacheKey, { -// type: result, -// description: cacheEntry?.description, -// timestamp: Date.now() -// }); - -// return result; -// } catch (error) { -// console.error(`Ошибка при получении типа метрики ${metric}:`, error); -// return cacheEntry?.type || null; -// } -// } - -// async fetchMetricDescription(metric: string): Promise { -// const cacheKey = `metadata-description-${metric}`; -// const cacheEntry = this.metadataCache.get(cacheKey); - -// if (cacheEntry && Date.now() - cacheEntry.timestamp < 30000) { -// return cacheEntry.description; -// } - -// try { -// const response = await lastValueFrom( -// this.httpService.get(`${this.prometheusUrl}/metadata`, { -// params: { metric }, -// }) -// ); -// const metadata = response.data.data[metric]; -// const result = metadata?.length ? metadata[0].help : undefined; - -// this.metadataCache.set(cacheKey, { -// type: cacheEntry?.type ?? null, -// description: result, -// timestamp: Date.now() -// }); - -// return result; -// } catch (error) { -// console.error(`Ошибка при получении описания метрики ${metric}:`, error); -// return cacheEntry?.description; -// } -// } - -// async fetchMetrics(metric: string): Promise { -// const cacheKey = `${metric}:{}`; -// const cacheEntry = this.metricCache.get(cacheKey); - -// if (cacheEntry && Date.now() - cacheEntry.timestamp < 5000) { -// return cacheEntry.data; -// } - -// try { -// const response = await lastValueFrom( -// this.httpService.get(`${this.prometheusUrl}/query`, { -// params: { query: metric }, -// }) -// ); - -// const metricType = await this.fetchMetricType(metric); -// const metricDescription = await this.fetchMetricDescription(metric); - -// const result = response.data.data.result.map((entry): PrometheusMetric => ({ -// __name__: entry.metric.__name__ || metric, -// device: entry.metric.device, -// instance: entry.metric.instance, -// job: entry.metric.job, -// source_id: entry.metric.source_id, -// status: entry.metric.status || '0', -// timestamp: entry.value[0] * 1000, -// value: parseFloat(entry.value[1]), -// type: metricType || 'gauge', -// description: metricDescription, -// ...entry.metric -// })); - -// this.metricCache.set(cacheKey, { data: result, timestamp: Date.now() }); -// return result; -// } catch (error) { -// console.error(`Error fetching metrics for ${metric}:`, error); -// if (cacheEntry) return cacheEntry.data; -// throw error; -// } -// } - -// async fetchMetricsWithFilters(metric: string, filters: Record): Promise { -// const cacheKey = `${metric}:${JSON.stringify(filters)}`; -// const cacheEntry = this.metricCache.get(cacheKey); - -// if (cacheEntry && Date.now() - cacheEntry.timestamp < 5000) { -// return cacheEntry.data; -// } - -// try { -// const query = this.buildFilteredQuery(metric, filters); -// const response = await lastValueFrom( -// this.httpService.get(`${this.prometheusUrl}/query`, { -// params: { query } -// }) -// ); - -// const metricType = await this.fetchMetricType(metric); -// const metricDescription = await this.fetchMetricDescription(metric); - -// const result = response.data.data.result.map((entry): PrometheusMetric => ({ -// __name__: entry.metric.__name__ || metric, -// device: entry.metric.device, -// instance: entry.metric.instance, -// job: entry.metric.job, -// source_id: entry.metric.source_id, -// status: entry.metric.status || '0', -// timestamp: entry.value[0] * 1000, -// value: parseFloat(entry.value[1]), -// type: metricType || 'gauge', -// description: metricDescription, -// ...entry.metric -// })); - -// this.metricCache.set(cacheKey, { data: result, timestamp: Date.now() }); -// return result; -// } catch (error) { -// console.error(`Error fetching metrics with filters for ${metric}:`, error); -// if (cacheEntry) return cacheEntry.data; -// throw error; -// } -// } - -// private buildFilteredQuery(metric: string, filters: Record): string { -// const filterParts = Object.entries(filters) -// .filter(([_, value]) => value !== undefined && value !== null && value !== "") -// .map(([key, value]) => { -// return `${key}="${value}"`; -// }); - -// return filterParts.length > 0 -// ? `${metric}{${filterParts.join(',')}}` -// : metric; -// } - -// async fetchMetricsRange(metric: string, start: number, end: number, step: number, filters: Record = {}): Promise { -// // Рассчитываем оптимальный шаг, если не указан -// const duration = end - start; -// const optimalStep = Math.max(Math.floor(duration / 1000), 15); // Минимум 15 секунд - -// const query = this.buildFilteredQuery(metric, { -// ...filters, -// instance: '192.168.2.34:9050' -// }); - -// try { -// const response = await lastValueFrom( -// this.httpService.get(`${this.prometheusUrl}/query_range`, { -// params: { -// query, -// start, -// end, -// step: optimalStep.toString() -// }, -// }) -// ); - -// const metricType = await this.fetchMetricType(metric); -// const metricDescription = await this.fetchMetricDescription(metric); - -// return response.data.data.result.flatMap((entry) => -// entry.values.map((value): PrometheusMetric => ({ -// __name__: entry.metric.__name__ || metric, -// device: entry.metric.device, -// instance: entry.metric.instance, -// job: entry.metric.job, -// source_id: entry.metric.source_id, -// status: entry.metric.status || '0', -// timestamp: value[0] * 1000, -// value: parseFloat(value[1]), -// type: metricType || 'gauge', -// description: metricDescription, -// ...entry.metric -// })) -// ); -// } catch (error) { -// console.error('Error in fetchMetricsRange:', { -// error: error.response?.data || error.message, -// query, -// filters -// }); -// throw error; -// } -// } - -// async getMetricsForMenuItem(menuItem: MenuItem): Promise { -// if (!menuItem.metric || !menuItem.filters) { -// throw new Error('MenuItem is not a metric item'); -// } - -// return this.fetchMetricsWithFilters(menuItem.metric, menuItem.filters); -// } - -// async fetchMetricMetadata(metric: string): Promise<{ -// name: string; -// help?: string; -// type?: string; -// }> { -// try { -// const response = await lastValueFrom( -// this.httpService.get(`${this.prometheusUrl}/metadata`, { -// params: { metric } -// }) -// ); - -// const data = response.data?.data?.[metric]?.[0]; - -// return { -// name: metric, -// help: data?.help, -// type: data?.type -// }; -// } catch (error) { -// console.error(`Error fetching metadata for ${metric}:`, error); -// return { -// name: metric -// }; -// } -// } - -// async fetchMetricSeries(metric: string): Promise[]> { -// try { -// const response = await lastValueFrom( -// this.httpService.get(`${this.prometheusUrl}/series`, { -// params: { 'match[]': metric } -// }) -// ); - -// return response.data.data || []; -// } catch (error) { -// console.error(`Error fetching series for ${metric}:`, error); -// return []; -// } -// } - -// async fetchAllMetrics(): Promise { -// try { -// const response = await lastValueFrom( -// this.httpService.get(`${this.prometheusUrl}/label/__name__/values`) -// ); -// return response.data.data; -// } catch (error) { -// console.error('Error fetching all metrics:', error); -// return []; -// } -// } - -// async fetchAllMetricsWithValues(): Promise { -// const metricNames = await this.fetchAllMetrics(); -// const zvksMetrics = metricNames.filter(metric => -// metric.startsWith('zvks') || -// metric.includes('server_li') || -// metric.includes('application_li') -// ); - -// const promises = zvksMetrics.map(async (metric) => { -// try { -// const data = await this.fetchMetrics(metric); -// return { metric, data }; -// } catch (error) { -// console.error(`Error fetching data for metric ${metric}:`, error); -// return { metric, data: [] }; -// } -// }); - -// return Promise.all(promises); -// } - -// clearCache(): void { -// this.metricCache.clear(); -// this.metadataCache.clear(); -// } -// } - - import { Injectable } from '@nestjs/common'; import { HttpService } from '@nestjs/axios'; import { ConfigService } from '@nestjs/config';