observability-analyzer
Version:
Production-ready MCP Server for intelligent Loki/Tempo observability dashboard analysis and generation
370 lines (357 loc) • 14.6 kB
JavaScript
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
;