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

240 lines
7.0 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';
@Injectable()
export class MenuService {
constructor(private readonly prometheusService: PrometheusService) { }
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 dynamicItems = await this.generateDynamicItems();
const baseMenu = this.injectDynamicItems(this.getStaticStructure(), dynamicItems);
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>(); // metric -> help
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');
return devices.map(device => ({
id: `device_${device}`,
title: `Graviton S2082I (${device})`,
items: this.generateModuleItems(device, allSeries, metadataMap),
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 generateModuleItems(
device: string,
seriesData: { metric: string; labels: Record<string, string> }[],
metadataMap: Map<string, string>
): MenuItem[] {
const modules = new Set<string>();
seriesData.forEach(({ labels }) => {
if (labels.device === device && labels.source_id) {
modules.add(labels.source_id);
}
});
return Array.from(modules).map(module => ({
id: `module_${device}_${module}`,
title: `Module ${module.replace('module$', '')}`,
items: this.generateMetricItems(device, module, seriesData, metadataMap),
isDynamic: true
}));
}
private generateMetricItems(
device: string,
module: string,
seriesData: { metric: string; labels: Record<string, string> }[],
metadataMap: Map<string, string>
): MenuItem[] {
const filtered = seriesData.filter(
({ labels }) => labels.device === device && labels.source_id === module
);
const uniqueMetrics = new Set(filtered.map(entry => entry.metric));
return Array.from(uniqueMetrics).map(metric => {
const description = metadataMap.get(metric) || metric;
return {
id: `metric_${device}_${module}_${metric}`,
title: description,
metric,
filters: {
device,
source_id: module
},
isDynamic: true
};
});
}
private injectDynamicItems(menu: MenuItem, dynamicItems: MenuItem[]): MenuItem {
if (menu.id === 'media_servers') {
return { ...menu, items: dynamicItems };
}
return {
...menu,
items: menu.items?.map(item => this.injectDynamicItems(item, dynamicItems)) || []
};
}
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;
}
}