websocket fix #24

Merged
Ghost merged 1 commits from swagger into rc 2025-06-03 13:10:59 +03:00
3 changed files with 61 additions and 29 deletions

View File

@ -150,23 +150,38 @@ export class MenuService {
return Array.from(entities); return Array.from(entities);
} }
private normalizeIdPart(part: string): string {
return part
.replace(/\$/g, '_') // Заменяем $ на _
.replace(/[^a-zA-Z0-9-_]/g, ''); // Удаляем другие спецсимволы
}
private generateModuleItems( private 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[] { ): MenuItem[] {
const modules = new Set<string>(); const modules = new Map<string, string>(); // source_id -> display name
seriesData.forEach(({ labels }) => { seriesData.forEach(({ labels }) => {
if (labels.device === device && labels.source_id) { 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 => ({ return Array.from(modules.entries()).map(([sourceId, displayName]) => ({
id: `module_${device}_${module}`, id: `module_${device}_${sourceId}`,
title: `Module ${module.replace('module$', '')}`, title: displayName,
items: this.generateMetricItems(device, module, seriesData, metadataMap), items: this.generateMetricItems(device, sourceId, seriesData, metadataMap),
isDynamic: true isDynamic: true
})); }));
} }
@ -177,24 +192,45 @@ export class MenuService {
seriesData: { metric: string; labels: Record<string, string> }[], seriesData: { metric: string; labels: Record<string, string> }[],
metadataMap: Map<string, string> metadataMap: Map<string, string>
): MenuItem[] { ): MenuItem[] {
// Фильтруем метрики для текущего устройства и модуля
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 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);
return { return {
id: `metric_${device}_${module}_${metric}`, id: `metric_${safeDevice}_${safeModule}_${safeMetric}`, // Безопасный ID
title: description, title: description,
metric, metric,
filters: { filters: {
device, device, // Оригинальное значение device
source_id: module source_id: module // Оригинальное значение source_id
}, },
isDynamic: true isDynamic: true,
// Сохраняем оригинальные значения для отладки
meta: {
originalDevice: device,
originalModule: module
}
}; };
}); });
} }

View File

@ -117,9 +117,8 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
} }
private getSubscriptionKey(metric: string, filters: Record<string, string>): string { private getSubscriptionKey(metric: string, filters: Record<string, string>): string {
// Создаём уникальный ключ на основе метрики и фильтров
const filterKeys = Object.keys(filters).sort(); 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}` : ''}`; return `${metric}${filterString ? `?${filterString}` : ''}`;
} }
@ -140,7 +139,6 @@ export class MetricsGateway implements OnGatewayInit, OnGatewayConnection, OnGat
metric, metric,
interval, interval,
(data) => { (data) => {
// Отправляем только подписчикам этой конкретной метрики с фильтрами
this.server.emit('metrics-data', { this.server.emit('metrics-data', {
metric: subscriptionKey, metric: subscriptionKey,
data data

View File

@ -112,16 +112,14 @@ export class PrometheusService {
const filterParts = Object.entries(filters) const filterParts = Object.entries(filters)
.filter(([_, value]) => value !== undefined && value !== null && value !== "") .filter(([_, value]) => value !== undefined && value !== null && value !== "")
.map(([key, value]) => { .map(([key, value]) => {
if (key === 'source_id' && !value.startsWith('module$')) { // Убираем автоматическое добавление "module$" для source_id
return `${key}="module$${value}"`;
}
return `${key}="${value}"`; return `${key}="${value}"`;
}); });
return filterParts.length > 0 return filterParts.length > 0
? `${metric}{${filterParts.join(',')}}` ? `${metric}{${filterParts.join(',')}}`
: metric; : metric;
} }
async fetchMetricsRange(metric: string, start: number, end: number, step: number, filters: Record<string, string> = {}): Promise<PrometheusMetric[]> { async fetchMetricsRange(metric: string, start: number, end: number, step: number, filters: Record<string, string> = {}): Promise<PrometheusMetric[]> {
const query = this.buildFilteredQuery(metric, { const query = this.buildFilteredQuery(metric, {