trust-module-backend/src/menu/menu.service.ts

290 lines
8.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

import { Injectable } from '@nestjs/common';
import { PrometheusService } from '../prometheus.service';
import { MenuItem } from './menu.interface';
import * as fs from 'fs/promises';
import * as path from 'path';
import { RangeService } from './range.service';
@Injectable()
export class MenuService {
constructor(
private readonly prometheusService: PrometheusService,
private readonly rangeService: RangeService
) { }
private readonly menuOverridesPath = path.join(process.cwd(), 'data', 'menu.json');
async saveMenuToFile(): Promise<void> {
const menu = await this.getFullMenu();
await fs.mkdir(path.dirname(this.menuOverridesPath), { recursive: true });
await fs.writeFile(this.menuOverridesPath, JSON.stringify(menu, null, 2), 'utf-8');
}
async getFullMenu(): Promise<MenuItem> {
const dynamicItemsPromise = this.generateDynamicItems();
const baseMenu = await this.injectDynamicItems(this.getStaticStructure(), dynamicItemsPromise);
const overrides = await this.loadOverrides();
return this.applyOverrides(baseMenu, overrides);
}
private applyOverrides(menu: MenuItem, overrides: Partial<MenuItem>[]): MenuItem {
const overrideMap = new Map(overrides.map(o => [o.id, o]));
const apply = (item: MenuItem): MenuItem => {
const override = overrideMap.get(item.id);
const updated = override ? { ...item, ...override } : item;
if (updated.items) {
updated.items = updated.items.map(apply);
}
return updated;
};
return apply(menu);
}
private async loadOverrides(): Promise<Partial<MenuItem>[]> {
try {
const content = await fs.readFile(this.menuOverridesPath, 'utf-8');
const parsed = JSON.parse(content);
return parsed.overrides || [];
} catch (e) {
return [];
}
}
private getStaticStructure(): MenuItem {
return {
title: "ЗВКС",
id: "root",
items: [
{
title: "ВКС",
id: "vks",
items: [
{
title: "Медиа серверы",
id: "media_servers",
items: []
}
]
}
]
};
}
private async generateDynamicItems(): Promise<MenuItem[]> {
const metricNames = await this.prometheusService.fetchAllMetrics();
const allSeries = (
await Promise.all(
metricNames.map(async name => {
const series = await this.prometheusService.fetchMetricSeries(name);
return series.map(s => ({
metric: name,
labels: s
}));
})
)
).flat();
const metadataMap = new Map<string, string>();
await Promise.all(
metricNames.map(async metric => {
try {
const meta = await this.prometheusService.fetchMetricMetadata(metric);
if (meta?.help) {
metadataMap.set(metric, meta.help);
}
} catch (e) {
console.warn(`No metadata for metric ${metric}`);
}
})
);
const isGarbageDevice = (device: string) =>
device.startsWith('/dev') ||
device.startsWith('/proc') ||
device.startsWith('/sys') ||
device.startsWith('/rootfs') ||
device.startsWith('/var') ||
device.startsWith('overlay') ||
device.startsWith('br') ||
device.startsWith('docker0') ||
device.startsWith('ens18') ||
device.startsWith('sda') ||
device.startsWith('sr0') ||
device.startsWith('tmpfs') ||
device.startsWith('veth') ||
device.startsWith('gvfsd') ||
device.startsWith('lo') ||
device.startsWith('/run');
const isGarbageInstance = (instance: string) =>
instance.includes('192.168.2.34:9049');
const filteredSeries = allSeries.filter(({ labels }) => {
const device = labels.device;
const instance = labels.instance;
return (!device || !isGarbageDevice(device)) &&
(!instance || !isGarbageInstance(instance));
});
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}`,
title: `Graviton S2082I (${device})`,
items: moduleItems,
isDynamic: true
};
}
private extractUniqueEntities(metrics: any[], field: string): string[] {
const entities = new Set<string>();
metrics.forEach(meta => {
if (meta.labels?.[field]) {
entities.add(meta.labels[field]);
}
});
return Array.from(entities);
}
private normalizeIdPart(part: string): string {
return part
.replace(/\$/g, '_')
.replace(/[^a-zA-Z0-9-_]/g, '');
}
private async generateModuleItems(
device: string,
seriesData: { metric: string; labels: Record<string, string> }[],
metadataMap: Map<string, string>
): Promise<MenuItem[]> {
const modules = new Map<string, string>();
seriesData.forEach(({ labels }) => {
if (labels.device === device && 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);
}
});
const modulePromises = Array.from(modules.entries()).map(
async ([sourceId, displayName]) => ({
id: `module_${device}_${sourceId}`,
title: displayName,
items: await this.generateMetricItems(device, sourceId, seriesData, metadataMap),
isDynamic: true
})
);
return Promise.all(modulePromises);
}
private async generateMetricItems(
device: string,
module: string,
seriesData: { metric: string; labels: Record<string, string> }[],
metadataMap: Map<string, string>
): Promise<MenuItem[]> {
const ranges = await this.rangeService.getRanges();
const filtered = seriesData.filter(
({ labels }) => labels.device === device && labels.source_id === module
);
const uniqueMetrics = new Set(filtered.map(entry => entry.metric));
const safeDevice = this.normalizeIdPart(device);
const safeModule = this.normalizeIdPart(module);
return Array.from(uniqueMetrics).map(metric => {
const description = metadataMap.get(metric) || metric;
const safeMetric = this.normalizeIdPart(metric);
const metricRanges = ranges[description] || [];
return {
id: `metric_${safeDevice}_${safeModule}_${safeMetric}`,
title: description,
metric,
filters: {
device,
source_id: module
},
ranges: metricRanges,
isDynamic: true,
meta: {
originalDevice: device,
originalModule: module
}
};
});
}
private async injectDynamicItems(
menu: MenuItem,
dynamicItemsPromise: Promise<MenuItem[]>
): Promise<MenuItem> {
const dynamicItems = await dynamicItemsPromise;
if (menu.id === 'media_servers') {
return { ...menu, items: dynamicItems };
}
if (menu.items) {
const updatedItems = await Promise.all(
menu.items.map(item => this.injectDynamicItems(item, dynamicItemsPromise))
);
return { ...menu, items: updatedItems };
}
return menu;
}
async updateMenuItem(id: string, update: Partial<MenuItem>): Promise<MenuItem> {
const fullMenu = await this.getFullMenu();
const item = this.findMenuItem(fullMenu, id);
if (!item) throw new Error('Menu item not found');
Object.assign(item, update);
return item;
}
async saveOverrides(overrides: Partial<MenuItem>[]): Promise<void> {
await fs.writeFile(this.menuOverridesPath, JSON.stringify({ overrides }, null, 2), 'utf-8');
}
private findMenuItem(menu: MenuItem, id: string): MenuItem | null {
if (menu.id === id) return menu;
if (menu.items) {
for (const item of menu.items) {
const found = this.findMenuItem(item, id);
if (found) return found;
}
}
return null;
}
}