Compare commits

..

3 Commits

Author SHA1 Message Date
deployer3000 16ce1da9a2 Merge pull request 'rc' (#27) from rc into main 2025-06-06 14:45:38 +03:00
Vladislav Drozdov f1527abae6 Merge pull request 'added ranges for charts' (#26) from swagger into rc
test-org/trust-module-backend/pipeline/pr-main Build succeeded
Reviewed-on: http://git.enode/deployer3000/trust-module-backend/pulls/26
Reviewed-by: Vladislav Drozdov <ya2@ya.ru>
Reviewed-by: YurijO <ya@ya.ru>
2025-06-06 14:44:37 +03:00
DmitriyA adf24d9b56 added ranges for charts
test-org/trust-module-backend/pipeline/pr-rc This commit looks good Details
2025-06-05 08:09:44 -04:00
5 changed files with 144 additions and 70 deletions

View File

@ -1,9 +1,14 @@
export interface MenuItem { export interface MenuItem {
title: string; title: string;
id: string; id: string;
items?: MenuItem[]; items?: MenuItem[];
metric?: string; metric?: string;
filters?: Record<string, string>; filters?: Record<string, string>;
isDynamic?: boolean; isDynamic?: boolean;
templateId?: string; templateId?: string;
} ranges?: Array<{
min: number;
max: number;
status: number;
}>;
}

View File

@ -1,11 +1,13 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { MenuController } from './menu.controller'; import { MenuController } from './menu.controller';
import { HttpModule } from '@nestjs/axios';
import { MenuService } from './menu.service'; import { MenuService } from './menu.service';
import { PrometheusModule } from '../prometheus.module'; // Импортируем PrometheusModule import { PrometheusModule } from '../prometheus.module';
import { RangeService } from './range.service';
@Module({ @Module({
imports: [PrometheusModule], // Добавляем в imports imports: [PrometheusModule, HttpModule],
controllers: [MenuController], controllers: [MenuController],
providers: [MenuService] providers: [MenuService, RangeService]
}) })
export class MenuModule {} export class MenuModule { }

View File

@ -3,10 +3,14 @@ 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';
import { RangeService } from './range.service';
@Injectable() @Injectable()
export class MenuService { 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'); private readonly menuOverridesPath = path.join(process.cwd(), 'data', 'menu.json');
@ -17,8 +21,8 @@ export class MenuService {
} }
async getFullMenu(): Promise<MenuItem> { async getFullMenu(): Promise<MenuItem> {
const dynamicItems = await this.generateDynamicItems(); const dynamicItemsPromise = this.generateDynamicItems();
const baseMenu = this.injectDynamicItems(this.getStaticStructure(), dynamicItems); const baseMenu = await this.injectDynamicItems(this.getStaticStructure(), dynamicItemsPromise);
const overrides = await this.loadOverrides(); const overrides = await this.loadOverrides();
return this.applyOverrides(baseMenu, overrides); return this.applyOverrides(baseMenu, overrides);
} }
@ -46,7 +50,7 @@ export class MenuService {
const parsed = JSON.parse(content); const parsed = JSON.parse(content);
return parsed.overrides || []; return parsed.overrides || [];
} catch (e) { } catch (e) {
return []; // если файл не существует return [];
} }
} }
@ -73,7 +77,6 @@ export class MenuService {
private async generateDynamicItems(): Promise<MenuItem[]> { private async generateDynamicItems(): Promise<MenuItem[]> {
const metricNames = await this.prometheusService.fetchAllMetrics(); const metricNames = await this.prometheusService.fetchAllMetrics();
// Получаем все серии для каждой метрики
const allSeries = ( const allSeries = (
await Promise.all( await Promise.all(
metricNames.map(async name => { metricNames.map(async name => {
@ -86,9 +89,7 @@ export class MenuService {
) )
).flat(); ).flat();
// Загружаем мета-информацию по каждой метрике const metadataMap = new Map<string, string>();
const metadataMap = new Map<string, string>(); // metric -> help
await Promise.all( await Promise.all(
metricNames.map(async metric => { metricNames.map(async metric => {
try { try {
@ -126,18 +127,30 @@ export class MenuService {
const filteredSeries = allSeries.filter(({ labels }) => { const filteredSeries = allSeries.filter(({ labels }) => {
const device = labels.device; const device = labels.device;
const instance = labels.instance; const instance = labels.instance;
return (!device || !isGarbageDevice(device)) && return (!device || !isGarbageDevice(device)) &&
(!instance || !isGarbageInstance(instance)); (!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<string, string> }[],
metadataMap: Map<string, string>
): Promise<MenuItem> {
const moduleItems = await this.generateModuleItems(device, seriesData, metadataMap);
return {
id: `device_${device}`, id: `device_${device}`,
title: `Graviton S2082I (${device})`, title: `Graviton S2082I (${device})`,
items: this.generateModuleItems(device, allSeries, metadataMap), items: moduleItems,
isDynamic: true isDynamic: true
})); };
} }
private extractUniqueEntities(metrics: any[], field: string): string[] { private extractUniqueEntities(metrics: any[], field: string): string[] {
@ -152,16 +165,16 @@ export class MenuService {
private normalizeIdPart(part: string): string { private normalizeIdPart(part: string): string {
return part return part
.replace(/\$/g, '_') // Заменяем $ на _ .replace(/\$/g, '_')
.replace(/[^a-zA-Z0-9-_]/g, ''); // Удаляем другие спецсимволы .replace(/[^a-zA-Z0-9-_]/g, '');
} }
private generateModuleItems( private async generateModuleItems(
device: string, device: string,
seriesData: { metric: string; labels: Record<string, string> }[], seriesData: { metric: string; labels: Record<string, string> }[],
metadataMap: Map<string, string> metadataMap: Map<string, string>
): MenuItem[] { ): Promise<MenuItem[]> {
const modules = new Map<string, string>(); // source_id -> display name const modules = new Map<string, string>();
seriesData.forEach(({ labels }) => { seriesData.forEach(({ labels }) => {
if (labels.device === device && labels.source_id) { if (labels.device === device && labels.source_id) {
@ -178,55 +191,48 @@ export class MenuService {
} }
}); });
return Array.from(modules.entries()).map(([sourceId, displayName]) => ({ const modulePromises = Array.from(modules.entries()).map(
id: `module_${device}_${sourceId}`, async ([sourceId, displayName]) => ({
title: displayName, id: `module_${device}_${sourceId}`,
items: this.generateMetricItems(device, sourceId, seriesData, metadataMap), title: displayName,
isDynamic: true items: await this.generateMetricItems(device, sourceId, seriesData, metadataMap),
})); isDynamic: true
})
);
return Promise.all(modulePromises);
} }
private generateMetricItems( private async generateMetricItems(
device: string, device: string,
module: string, module: string,
seriesData: { metric: string; labels: Record<string, string> }[], seriesData: { metric: string; labels: Record<string, string> }[],
metadataMap: Map<string, string> metadataMap: Map<string, string>
): MenuItem[] { ): Promise<MenuItem[]> {
// Фильтруем метрики для текущего устройства и модуля const ranges = await this.rangeService.getRanges();
const filtered = seriesData.filter( const filtered = seriesData.filter(
({ labels }) => labels.device === device && labels.source_id === module ({ labels }) => labels.device === device && labels.source_id === module
); );
// Получаем уникальные имена метрик
const uniqueMetrics = new Set(filtered.map(entry => entry.metric)); const uniqueMetrics = new Set(filtered.map(entry => entry.metric));
const safeDevice = this.normalizeIdPart(device);
const safeModule = this.normalizeIdPart(module);
// Нормализуем идентификаторы
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);
// Формируем пункты меню
return Array.from(uniqueMetrics).map(metric => { return Array.from(uniqueMetrics).map(metric => {
const description = metadataMap.get(metric) || metric; const description = metadataMap.get(metric) || metric;
const safeMetric = normalizeIdPart(metric); const safeMetric = this.normalizeIdPart(metric);
const metricRanges = ranges[description] || [];
return { return {
id: `metric_${safeDevice}_${safeModule}_${safeMetric}`, // Безопасный ID id: `metric_${safeDevice}_${safeModule}_${safeMetric}`,
title: description, title: description,
metric, metric,
filters: { filters: {
device, // Оригинальное значение device device,
source_id: module // Оригинальное значение source_id source_id: module
}, },
ranges: metricRanges,
isDynamic: true, isDynamic: true,
// Сохраняем оригинальные значения для отладки
meta: { meta: {
originalDevice: device, originalDevice: device,
originalModule: module originalModule: module
@ -235,15 +241,24 @@ export class MenuService {
}); });
} }
private injectDynamicItems(menu: MenuItem, dynamicItems: MenuItem[]): MenuItem { private async injectDynamicItems(
menu: MenuItem,
dynamicItemsPromise: Promise<MenuItem[]>
): Promise<MenuItem> {
const dynamicItems = await dynamicItemsPromise;
if (menu.id === 'media_servers') { if (menu.id === 'media_servers') {
return { ...menu, items: dynamicItems }; return { ...menu, items: dynamicItems };
} }
return { if (menu.items) {
...menu, const updatedItems = await Promise.all(
items: menu.items?.map(item => this.injectDynamicItems(item, dynamicItems)) || [] menu.items.map(item => this.injectDynamicItems(item, dynamicItemsPromise))
}; );
return { ...menu, items: updatedItems };
}
return menu;
} }
async updateMenuItem(id: string, update: Partial<MenuItem>): Promise<MenuItem> { async updateMenuItem(id: string, update: Partial<MenuItem>): Promise<MenuItem> {

54
src/menu/range.service.ts Normal file
View File

@ -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<Record<string, Array<{ min: number; max: number; status: number }>>> {
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<string, Array<{ min: number; max: number; status: number }>> = {};
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 {};
}
}
}

View File

@ -174,7 +174,6 @@ export class PrometheusService {
return this.fetchMetricsWithFilters(menuItem.metric, menuItem.filters); return this.fetchMetricsWithFilters(menuItem.metric, menuItem.filters);
} }
// ✅ Новый метод: получает базовое описание метрики (help, type)
async fetchMetricMetadata(metric: string): Promise<{ async fetchMetricMetadata(metric: string): Promise<{
name: string; name: string;
help?: string; help?: string;
@ -202,7 +201,6 @@ export class PrometheusService {
} }
} }
// ✅ Новый метод: получает ВСЕ серии метрики (все комбинации label-ов)
async fetchMetricSeries(metric: string): Promise<Record<string, string>[]> { async fetchMetricSeries(metric: string): Promise<Record<string, string>[]> {
try { try {
const response = await lastValueFrom( const response = await lastValueFrom(