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 { 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 { 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 { 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[]> { 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 { 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(); // 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(); 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 }[], metadataMap: Map ): MenuItem[] { const modules = new Set(); 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 }[], metadataMap: Map ): 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): Promise { 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[]): Promise { 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; } }