diff --git a/src/menu/menu.service.ts b/src/menu/menu.service.ts index 502b295..a7f5d3f 100644 --- a/src/menu/menu.service.ts +++ b/src/menu/menu.service.ts @@ -149,24 +149,39 @@ export class MenuService { }); return Array.from(entities); } + + private normalizeIdPart(part: string): string { + return part + .replace(/\$/g, '_') // Заменяем $ на _ + .replace(/[^a-zA-Z0-9-_]/g, ''); // Удаляем другие спецсимволы + } private generateModuleItems( device: string, seriesData: { metric: string; labels: Record }[], metadataMap: Map ): MenuItem[] { - const modules = new Set(); + const modules = new Map(); // source_id -> display name seriesData.forEach(({ labels }) => { if (labels.device === device && labels.source_id) { - modules.add(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); } }); - return Array.from(modules).map(module => ({ - id: `module_${device}_${module}`, - title: `Module ${module.replace('module$', '')}`, - items: this.generateMetricItems(device, module, seriesData, metadataMap), + return Array.from(modules.entries()).map(([sourceId, displayName]) => ({ + id: `module_${device}_${sourceId}`, + title: displayName, + items: this.generateMetricItems(device, sourceId, seriesData, metadataMap), isDynamic: true })); } @@ -177,24 +192,45 @@ export class MenuService { seriesData: { metric: string; labels: Record }[], metadataMap: Map ): MenuItem[] { + // Фильтруем метрики для текущего устройства и модуля 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); + + // Формируем пункты меню return Array.from(uniqueMetrics).map(metric => { const description = metadataMap.get(metric) || metric; - + const safeMetric = normalizeIdPart(metric); + return { - id: `metric_${device}_${module}_${metric}`, + id: `metric_${safeDevice}_${safeModule}_${safeMetric}`, // Безопасный ID title: description, metric, filters: { - device, - source_id: module + device, // Оригинальное значение device + source_id: module // Оригинальное значение source_id }, - isDynamic: true + isDynamic: true, + // Сохраняем оригинальные значения для отладки + meta: { + originalDevice: device, + originalModule: module + } }; }); } diff --git a/src/metrics.gateway.ts b/src/metrics.gateway.ts index 684d5f3..f545f27 100644 --- a/src/metrics.gateway.ts +++ b/src/metrics.gateway.ts @@ -117,11 +117,10 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat } private getSubscriptionKey(metric: string, filters: Record): string { - // Создаём уникальный ключ на основе метрики и фильтров const filterKeys = Object.keys(filters).sort(); - const filterString = filterKeys.map(k => `${k}=${filters[k]}`).join('&'); + const filterString = filterKeys.map(k => `${k}=${encodeURIComponent(filters[k])}`).join('&'); return `${metric}${filterString ? `?${filterString}` : ''}`; - } + } @SubscribeMessage('subscribe-metric') async handleSubscribeMetric( @@ -140,7 +139,6 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat metric, interval, (data) => { - // Отправляем только подписчикам этой конкретной метрики с фильтрами this.server.emit('metrics-data', { metric: subscriptionKey, data diff --git a/src/prometheus.service.ts b/src/prometheus.service.ts index c54315f..d7156d6 100644 --- a/src/prometheus.service.ts +++ b/src/prometheus.service.ts @@ -109,19 +109,17 @@ export class PrometheusService { } private buildFilteredQuery(metric: string, filters: Record): string { - const filterParts = Object.entries(filters) - .filter(([_, value]) => value !== undefined && value !== null && value !== "") - .map(([key, value]) => { - if (key === 'source_id' && !value.startsWith('module$')) { - return `${key}="module$${value}"`; - } - return `${key}="${value}"`; - }); + const filterParts = Object.entries(filters) + .filter(([_, value]) => value !== undefined && value !== null && value !== "") + .map(([key, value]) => { + // Убираем автоматическое добавление "module$" для source_id + return `${key}="${value}"`; + }); - return filterParts.length > 0 - ? `${metric}{${filterParts.join(',')}}` - : metric; - } + return filterParts.length > 0 + ? `${metric}{${filterParts.join(',')}}` + : metric; +} async fetchMetricsRange(metric: string, start: number, end: number, step: number, filters: Record = {}): Promise { const query = this.buildFilteredQuery(metric, {