rc #27
|
|
@ -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;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
@ -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 { }
|
||||||
|
|
@ -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> {
|
||||||
|
|
|
||||||
|
|
@ -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 {};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue