Compare commits

..

No commits in common. "066a1e4eba5d5ee8f5a8975f81494c8ee36199be" and "054fc46a01bcf764510d7e645925f144cb943f3f" have entirely different histories.

3 changed files with 29 additions and 61 deletions

View File

@ -149,39 +149,24 @@ 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 Map<string, string>(); // source_id -> display name const modules = new Set<string>();
seriesData.forEach(({ labels }) => { seriesData.forEach(({ labels }) => {
if (labels.device === device && labels.source_id) { if (labels.device === device && labels.source_id) {
const sourceId = labels.source_id; modules.add(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.entries()).map(([sourceId, displayName]) => ({ return Array.from(modules).map(module => ({
id: `module_${device}_${sourceId}`, id: `module_${device}_${module}`,
title: displayName, title: `Module ${module.replace('module$', '')}`,
items: this.generateMetricItems(device, sourceId, seriesData, metadataMap), items: this.generateMetricItems(device, module, seriesData, metadataMap),
isDynamic: true isDynamic: true
})); }));
} }
@ -192,45 +177,24 @@ 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_${safeDevice}_${safeModule}_${safeMetric}`, // Безопасный ID id: `metric_${device}_${module}_${metric}`,
title: description, title: description,
metric, metric,
filters: { filters: {
device, // Оригинальное значение device device,
source_id: module // Оригинальное значение source_id source_id: module
}, },
isDynamic: true, isDynamic: true
// Сохраняем оригинальные значения для отладки
meta: {
originalDevice: device,
originalModule: module
}
}; };
}); });
} }

View File

@ -117,10 +117,11 @@ 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}=${encodeURIComponent(filters[k])}`).join('&'); const filterString = filterKeys.map(k => `${k}=${filters[k]}`).join('&');
return `${metric}${filterString ? `?${filterString}` : ''}`; return `${metric}${filterString ? `?${filterString}` : ''}`;
} }
@SubscribeMessage('subscribe-metric') @SubscribeMessage('subscribe-metric')
async handleSubscribeMetric( async handleSubscribeMetric(
@ -139,6 +140,7 @@ 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

@ -109,17 +109,19 @@ export class PrometheusService {
} }
private buildFilteredQuery(metric: string, filters: Record<string, string>): string { private buildFilteredQuery(metric: string, filters: Record<string, string>): string {
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]) => {
// Убираем автоматическое добавление "module$" для source_id if (key === 'source_id' && !value.startsWith('module$')) {
return `${key}="${value}"`; return `${key}="module$${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, {