UNPKG

observability-analyzer

Version:

Production-ready MCP Server for intelligent Loki/Tempo observability dashboard analysis and generation

370 lines (357 loc) 14.6 kB
"use strict"; var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.GrafanaExporter = void 0; const fs = __importStar(require("fs")); const path = __importStar(require("path")); class GrafanaExporter { exportDashboard(dashboard, outputPath, options) { // Ensure the dashboard is properly formatted const formattedDashboard = this.formatDashboardForExport(dashboard); // Create export object const exportData = { dashboard: formattedDashboard, overwrite: options?.overwrite ?? true, message: options?.message ?? `Exported ${dashboard.title} dashboard` }; if (options?.folderName) { exportData.folderUid = this.generateFolderUid(options.folderName); } // Write to file this.writeJSONFile(outputPath, exportData); } exportMultipleDashboards(dashboards, outputDirectory, options) { // Ensure output directory exists if (!fs.existsSync(outputDirectory)) { fs.mkdirSync(outputDirectory, { recursive: true }); } const exportedFiles = []; // Export each dashboard for (const dashboard of dashboards) { const filename = this.sanitizeFilename(dashboard.title) + '.json'; const filepath = path.join(outputDirectory, filename); this.exportDashboard(dashboard, filepath, { overwrite: options?.overwrite, folderName: options?.folderName, message: `Batch export: ${dashboard.title}` }); exportedFiles.push(filename); } // Create index file if requested if (options?.createIndex) { this.createIndexFile(outputDirectory, dashboards, exportedFiles); } } generateDataSources(config) { const dataSources = []; // Loki data source dataSources.push({ uid: 'loki', name: 'Loki', type: 'loki', url: config.loki.url, access: 'proxy', isDefault: false, jsonData: { timeout: config.loki.timeout ?? 30000, maxLines: 1000 } }); return dataSources; } exportDataSources(config, outputPath) { const dataSources = this.generateDataSources(config); this.writeJSONFile(outputPath, { datasources: dataSources }); } createProvisioningFiles(dashboards, config, outputDirectory) { const provisioningDir = path.join(outputDirectory, 'provisioning'); const dashboardsDir = path.join(provisioningDir, 'dashboards'); const datasourcesDir = path.join(provisioningDir, 'datasources'); // Create directories fs.mkdirSync(dashboardsDir, { recursive: true }); fs.mkdirSync(datasourcesDir, { recursive: true }); // Export dashboards this.exportMultipleDashboards(dashboards, path.join(dashboardsDir, 'observability'), { overwrite: true, folderName: 'Observability', createIndex: true }); // Create dashboard provisioning config const dashboardProvisioning = { apiVersion: 1, providers: [ { name: 'observability-analyzer', orgId: 1, folder: 'Observability', folderUid: this.generateFolderUid('Observability'), type: 'file', disableDeletion: false, updateIntervalSeconds: 10, allowUiUpdates: true, options: { path: '/etc/grafana/provisioning/dashboards/observability' } } ] }; this.writeJSONFile(path.join(dashboardsDir, 'observability.yml'), dashboardProvisioning); // Export data sources this.exportDataSources(config, path.join(datasourcesDir, 'observability.yml')); // Create README this.createProvisioningReadme(outputDirectory, dashboards.length); } validateDashboard(dashboard) { const errors = []; const warnings = []; // Required fields validation if (!dashboard.title || dashboard.title.trim().length === 0) { errors.push('Dashboard title is required'); } if (!dashboard.panels || dashboard.panels.length === 0) { warnings.push('Dashboard has no panels'); } // Panel validation if (dashboard.panels) { const panelIds = new Set(); for (let i = 0; i < dashboard.panels.length; i++) { const panel = dashboard.panels[i]; // Check for duplicate panel IDs if (panelIds.has(panel.id)) { errors.push(`Duplicate panel ID: ${panel.id}`); } else { panelIds.add(panel.id); } // Panel title validation if (!panel.title || panel.title.trim().length === 0) { warnings.push(`Panel ${panel.id} has no title`); } // Target validation if (!panel.targets || panel.targets.length === 0) { warnings.push(`Panel "${panel.title}" has no query targets`); } else { for (const target of panel.targets) { if (!target.expr || target.expr.trim().length === 0) { warnings.push(`Panel "${panel.title}" has empty query expression`); } if (!target.datasource) { warnings.push(`Panel "${panel.title}" target missing datasource`); } } } // Grid position validation if (panel.gridPos) { if (panel.gridPos.w <= 0 || panel.gridPos.h <= 0) { errors.push(`Panel "${panel.title}" has invalid dimensions`); } if (panel.gridPos.x < 0 || panel.gridPos.y < 0) { errors.push(`Panel "${panel.title}" has negative position`); } if (panel.gridPos.x + panel.gridPos.w > 24) { warnings.push(`Panel "${panel.title}" extends beyond grid width (24)`); } } } } // Template validation if (dashboard.templating?.list) { const variableNames = new Set(); for (const variable of dashboard.templating.list) { if (variableNames.has(variable.name)) { errors.push(`Duplicate variable name: ${variable.name}`); } else { variableNames.add(variable.name); } if (!variable.query && variable.type !== 'textbox') { warnings.push(`Variable "${variable.name}" has no query`); } } } // Time range validation if (dashboard.time) { if (!this.isValidTimeRange(dashboard.time.from)) { errors.push(`Invalid 'from' time range: ${dashboard.time.from}`); } if (!this.isValidTimeRange(dashboard.time.to)) { errors.push(`Invalid 'to' time range: ${dashboard.time.to}`); } } return { valid: errors.length === 0, errors, warnings }; } optimizeDashboard(dashboard) { const optimized = JSON.parse(JSON.stringify(dashboard)); // Remove empty or default values to reduce size if (optimized.links && optimized.links.length === 0) { delete optimized.links; } // Optimize panels if (optimized.panels) { for (const panel of optimized.panels) { // Remove empty transformations if (panel.transformations && panel.transformations.length === 0) { delete panel.transformations; } // Remove default field config overrides if (panel.fieldConfig.overrides && panel.fieldConfig.overrides.length === 0) { delete panel.fieldConfig.overrides; } // Optimize targets if (panel.targets) { for (const target of panel.targets) { // Remove empty legend format if (target.legendFormat === '') { delete target.legendFormat; } // Remove default hide value if (target.hide === false) { delete target.hide; } } } } } // Remove empty template variables if (optimized.templating.list.length === 0) { optimized.templating = { list: [] }; } return optimized; } formatDashboardForExport(dashboard) { const formatted = JSON.parse(JSON.stringify(dashboard)); // Remove or reset fields that shouldn't be in exports delete formatted.id; formatted.version = 1; // Ensure UID is set for import consistency if (!formatted.uid) { formatted.uid = this.generateDashboardUid(dashboard.title); } // Ensure schema version is current formatted.schemaVersion = 36; return formatted; } generateDashboardUid(title) { // Create a simple UID based on title return title .toLowerCase() .replace(/[^a-z0-9]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .substring(0, 40); } generateFolderUid(folderName) { return 'folder-' + folderName .toLowerCase() .replace(/[^a-z0-9]/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .substring(0, 30); } sanitizeFilename(filename) { return filename .replace(/[<>:"/\\|?*]/g, '-') .replace(/\s+/g, '-') .replace(/-+/g, '-') .replace(/^-|-$/g, '') .toLowerCase(); } writeJSONFile(filePath, data) { const jsonContent = JSON.stringify(data, null, 2); const directory = path.dirname(filePath); // Ensure directory exists if (!fs.existsSync(directory)) { fs.mkdirSync(directory, { recursive: true }); } fs.writeFileSync(filePath, jsonContent, 'utf8'); } createIndexFile(outputDirectory, dashboards, exportedFiles) { const indexData = { exportedAt: new Date().toISOString(), dashboardCount: dashboards.length, dashboards: dashboards.map((dashboard, index) => ({ title: dashboard.title, uid: dashboard.uid, tags: dashboard.tags, filename: exportedFiles[index], panelCount: dashboard.panels?.length ?? 0 })) }; this.writeJSONFile(path.join(outputDirectory, 'index.json'), indexData); } createProvisioningReadme(outputDirectory, dashboardCount) { const readmeContent = `# Observability Analyzer - Grafana Provisioning This directory contains Grafana provisioning files generated by the Observability Dashboard Analyzer. ## Contents - **dashboards/**: Dashboard JSON files (${dashboardCount} dashboards) - **datasources/**: Data source configuration files ## Setup Instructions 1. Copy the entire \`provisioning\` directory to your Grafana provisioning path: \`\`\`bash cp -r provisioning/ /etc/grafana/provisioning/ \`\`\` 2. Restart Grafana to load the new dashboards and data sources: \`\`\`bash sudo systemctl restart grafana-server \`\`\` 3. Navigate to Grafana UI and check the "Observability" folder for imported dashboards. ## Data Source Configuration Make sure your Loki instance is accessible from Grafana using the URL specified in the data source file. Update the URL in \`datasources/observability.yml\` if needed. ## Dashboard Customization The imported dashboards can be customized through the Grafana UI. Changes will be preserved as the provisioning is configured to allow UI updates. ## Generated by Observability Dashboard Analyzer - ${new Date().toISOString()} `; fs.writeFileSync(path.join(outputDirectory, 'README.md'), readmeContent, 'utf8'); } isValidTimeRange(timeRange) { // Check for relative time ranges if (timeRange.startsWith('now')) { return /^now(-\d+[smhdMy])?$/.test(timeRange); } // Check for absolute time ranges (ISO format) if (timeRange.match(/^\d{4}-\d{2}-\d{2}/)) { return !isNaN(Date.parse(timeRange)); } return false; } } exports.GrafanaExporter = GrafanaExporter; //# sourceMappingURL=GrafanaExporter.js.map