UNPKG

observability-analyzer

Version:

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

461 lines 19.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.REDMethodGenerator = void 0; class REDMethodGenerator { generateLokiDashboard(services, lokiQueries, datasourceConfig) { const panels = []; let panelId = 1; // Handle both string and object datasource config for backward compatibility const dsConfig = typeof datasourceConfig === 'string' ? { datasourceUid: datasourceConfig, datasourceName: datasourceConfig, datasourceType: 'loki' } : datasourceConfig; // Datasource variable const datasourceVariable = { name: 'datasource', type: 'datasource', label: 'Data Source', query: dsConfig.datasourceType || 'loki', current: { text: dsConfig.datasourceName || dsConfig.datasourceUid, value: dsConfig.datasourceUid, selected: true }, refresh: 1, sort: 0 }; // Service variable const serviceVariable = { name: 'service', type: 'custom', label: 'Service', query: services.join(','), options: [ { text: 'All', value: '$__all', selected: true }, ...services.map(service => ({ text: service, value: service, selected: false })) ], includeAll: true, allValue: '.*', current: services.length === 1 ? { text: services[0], value: services[0], selected: true } : { text: 'All', value: '$__all', selected: true }, refresh: 1, sort: 1, multi: false }; // Request Rate Panel if (lokiQueries.requestRate.length > 0) { panels.push(this.createRequestRatePanel(lokiQueries.requestRate, panelId++, undefined, dsConfig.datasourceType || 'loki')); } // Error Rate Panel if (lokiQueries.errorRate.length > 0) { panels.push(this.createErrorRatePanel(lokiQueries.errorRate, panelId++, undefined, dsConfig.datasourceType || 'loki')); } // Duration Panel if (lokiQueries.duration.length > 0) { panels.push(this.createDurationPanel(lokiQueries.duration, panelId++, undefined, dsConfig.datasourceType || 'loki')); } return { id: null, uid: `loki-red-${Date.now()}`, title: 'Loki RED Method Dashboard', tags: ['loki', 'logs', 'monitoring', 'red'], timezone: 'browser', time: { from: 'now-1h', to: 'now' }, timepicker: { refresh_intervals: ['5s', '10s', '30s', '1m', '5m', '15m', '30m', '1h', '2h', '1d'], time_options: ['5m', '15m', '1h', '6h', '12h', '24h', '2d', '7d', '30d'] }, refresh: '30s', panels, templating: { list: [datasourceVariable, serviceVariable] }, annotations: { list: [] }, editable: true, gnetId: null, graphTooltip: 1, hideControls: false, style: 'dark', links: [], version: 1, schemaVersion: 30 }; } generateServiceREDDashboard(services, lokiQueries, datasourceUid) { const dashboardOptions = { theme: 'dark', refreshInterval: '30s', timeRange: '-1h', includeLegend: true, includeTooltips: true, includeAlerts: false, panelHeight: 8, panelWidth: 8 }; const panels = []; let panelId = 1; // Create panels for each service panels.push(this.createRequestRatePanel(lokiQueries.requestRate, panelId++, dashboardOptions, datasourceUid)); panels.push(this.createErrorRatePanel(lokiQueries.errorRate, panelId++, dashboardOptions, datasourceUid)); panels.push(this.createDurationPanel(lokiQueries.duration, panelId++, dashboardOptions, datasourceUid)); // Datasource template variable const datasourceVariable = { name: 'datasource', type: 'datasource', label: 'Data Source', query: 'loki', current: { text: datasourceUid || 'loki', value: datasourceUid || 'loki', selected: true }, refresh: 1, sort: 0 }; // Service template variable const serviceVariable = { name: 'service', type: 'custom', label: 'Service', query: services.join(','), options: services.map((service, index) => ({ text: service, value: service, selected: index === 0 })), includeAll: true, allValue: '*', current: services.length > 0 ? { text: services[0], value: services[0], selected: true } : { text: 'All', value: '*', selected: false }, refresh: 1, sort: 1 }; // Time variable const timeVariable = { name: 'interval', type: 'interval', label: 'Interval', query: '1m,5m,10m,15m,30m,1h,6h,12h,1d', current: { text: '5m', value: '5m', selected: true }, options: [ { text: '1m', value: '1m', selected: false }, { text: '5m', value: '5m', selected: true }, { text: '10m', value: '10m', selected: false }, { text: '15m', value: '15m', selected: false }, { text: '30m', value: '30m', selected: false }, { text: '1h', value: '1h', selected: false }, { text: '6h', value: '6h', selected: false }, { text: '12h', value: '12h', selected: false }, { text: '1d', value: '1d', selected: false } ], refresh: 2, sort: 2 }; return { uid: `loki-service-red-${Date.now()}`, title: `${services.length > 1 ? 'Multi-Service' : services[0]} RED Method Monitoring`, tags: ['loki', 'red', 'monitoring', 'service'], time: { from: dashboardOptions.timeRange, to: 'now' }, refresh: dashboardOptions.refreshInterval, panels, templating: { list: [datasourceVariable, serviceVariable, timeVariable] }, schemaVersion: 36 }; } validateREDQueries(queries) { const errors = []; const warnings = []; const executedQueries = []; for (const queryObj of queries) { if (queryObj.lokiQuery) { const lokiValidation = this.validateLogQLQuery(queryObj.lokiQuery); if (!lokiValidation.valid) { errors.push(`Loki query for ${queryObj.service} ${queryObj.type}: ${lokiValidation.error}`); } executedQueries.push({ query: queryObj.lokiQuery, type: 'loki', success: lokiValidation.valid, error: lokiValidation.error }); } if (!queryObj.lokiQuery) { errors.push(`Missing query for ${queryObj.service} ${queryObj.type}`); } } return { success: errors.length === 0, errors, warnings, executedQueries }; } generateServiceSpecificQueries(service, analysis) { const lokiQueries = this.generateLokiREDQueries(service, analysis); return { loki: lokiQueries }; } generateLokiREDQueries(service, analysis) { const requestRate = [ `sum by (service) (count_over_time({service="${service}"}[$interval]))`, `sum by (service) (rate({service="${service}"}[$interval]))` ]; const errorRate = [ `sum by (service) (count_over_time({service="${service}"} |~ "(?i)error|exception|failed"[$interval]))`, `sum by (service) (rate({service="${service}"} |~ "(?i)error|exception|failed"[$interval]))` ]; const duration = []; if (analysis.logStructure.hasDurationFields) { duration.push(`histogram_quantile(0.95, sum by (service, le) (rate({service="${service}"} | json | duration != "" | unwrap duration [$interval])))`, `avg_over_time({service="${service}"} | json | duration != "" | unwrap duration [$interval])`); } return { requestRate, errorRate, duration }; } createRequestRatePanel(lokiQueries, panelId, options, datasourceType = 'loki') { const targets = []; // Add Loki targets for (const [index, query] of lokiQueries.slice(0, 3).entries()) { const processedQuery = query.includes('$service') ? query : query.replace(/service="([^"]*)"/, 'service=~"$service"'); targets.push({ expr: processedQuery, refId: String.fromCharCode(65 + index), datasource: { type: datasourceType, uid: '$datasource' }, legendFormat: `Request Rate - ${String.fromCharCode(65 + index)}`, hide: false }); } return { id: panelId, title: 'Request Rate', type: 'timeseries', gridPos: { h: options?.panelHeight || 8, w: options?.panelWidth || 8, x: 0, y: 0 }, targets, fieldConfig: { defaults: { color: { mode: 'palette-classic' }, unit: 'reqps', custom: { drawStyle: 'line', lineInterpolation: 'linear', lineWidth: 1, fillOpacity: 10, gradientMode: 'none', hideFrom: { legend: false, tooltip: false, vis: false }, axisPlacement: 'auto', axisLabel: '', scaleDistribution: { type: 'linear' }, thresholdsStyle: { mode: 'off' }, stacking: { group: 'A', mode: 'none' }, showPoints: 'never', pointSize: 5, spanNulls: false, barAlignment: 0 }, thresholds: { mode: 'absolute', steps: [ { color: 'green', value: null }, { color: 'yellow', value: 100 }, { color: 'red', value: 500 } ] } }, overrides: [] }, options: { legend: { displayMode: 'visible', placement: 'bottom', calcs: [] }, tooltip: { mode: 'single', sort: 'none' } }, pluginVersion: '8.0.0' }; } createErrorRatePanel(lokiQueries, panelId, options, datasourceType = 'loki') { const targets = []; // Add primary Loki error rate query if (lokiQueries.length > 0) { const processedQuery = lokiQueries[0].includes('$service') ? lokiQueries[0] : lokiQueries[0].replace(/service="([^"]*)"/, 'service=~"$service"'); targets.push({ expr: processedQuery, refId: 'A', datasource: { type: datasourceType, uid: '$datasource' }, legendFormat: 'Error Rate', hide: false }); } return { id: panelId, title: 'Error Rate', type: 'stat', gridPos: { h: options?.panelHeight || 8, w: options?.panelWidth || 8, x: 8, y: 0 }, targets, fieldConfig: { defaults: { color: { mode: 'thresholds' }, unit: 'short', custom: { displayMode: 'basic', align: 'auto' }, thresholds: { mode: 'absolute', steps: [ { color: 'green', value: null }, { color: 'yellow', value: 1 }, { color: 'red', value: 5 } ] } }, overrides: [] }, options: { reduceOptions: { values: false, calcs: ['lastNotNull'], fields: '' }, orientation: 'auto', textMode: 'auto', colorMode: 'background', graphMode: 'area', justifyMode: 'auto' }, pluginVersion: '8.0.0' }; } createDurationPanel(lokiQueries, panelId, options, datasourceType = 'loki') { const targets = []; for (const [index, query] of lokiQueries.slice(0, 3).entries()) { const processedQuery = query.includes('$service') ? query : query.replace(/service="([^"]*)"/, 'service=~"$service"'); targets.push({ expr: processedQuery, refId: String.fromCharCode(65 + index), datasource: { type: datasourceType, uid: '$datasource' }, legendFormat: `Duration P${index === 0 ? '95' : index === 1 ? '50' : '99'}`, hide: false }); } return { id: panelId, title: 'Response Time / Duration', type: 'timeseries', gridPos: { h: options?.panelHeight || 8, w: options?.panelWidth || 8, x: 16, y: 0 }, targets, fieldConfig: { defaults: { color: { mode: 'palette-classic' }, unit: 'ms', custom: { drawStyle: 'line', lineInterpolation: 'linear', lineWidth: 1, fillOpacity: 10, gradientMode: 'none', hideFrom: { legend: false, tooltip: false, vis: false }, axisPlacement: 'auto', axisLabel: '', scaleDistribution: { type: 'linear' }, thresholdsStyle: { mode: 'off' }, stacking: { group: 'A', mode: 'none' }, showPoints: 'never', pointSize: 5, spanNulls: false, barAlignment: 0 }, thresholds: { mode: 'absolute', steps: [ { color: 'green', value: null }, { color: 'yellow', value: 200 }, { color: 'red', value: 500 } ] } }, overrides: [] }, options: { legend: { displayMode: 'visible', placement: 'bottom', calcs: [] }, tooltip: { mode: 'single' } }, pluginVersion: '8.0.0' }; } validateLogQLQuery(query) { // Enhanced LogQL syntax validation const trimmedQuery = query.trim(); if (!trimmedQuery) { return { valid: false, error: 'Query cannot be empty' }; } // Must contain label selectors if (!trimmedQuery.includes('{') || !trimmedQuery.includes('}')) { return { valid: false, error: 'LogQL queries must contain label selectors in braces {}' }; } // Check for unmatched parentheses const openParens = (trimmedQuery.match(/\(/g) || []).length; const closeParens = (trimmedQuery.match(/\)/g) || []).length; if (openParens !== closeParens) { return { valid: false, error: 'Unmatched parentheses in LogQL query' }; } // Check for unmatched brackets (range selectors) const openBrackets = (trimmedQuery.match(/\[/g) || []).length; const closeBrackets = (trimmedQuery.match(/\]/g) || []).length; if (openBrackets !== closeBrackets) { return { valid: false, error: 'Unmatched brackets in LogQL range vector selector' }; } // Check for unmatched braces (label selectors) const openBraces = (trimmedQuery.match(/\{/g) || []).length; const closeBraces = (trimmedQuery.match(/\}/g) || []).length; if (openBraces !== closeBraces) { return { valid: false, error: 'Unmatched braces in LogQL label selectors' }; } // Validate time range format in brackets const timeRangePattern = /\[(\d+[smhdMy])\]/g; const timeRanges = trimmedQuery.match(/\[[^\]]+\]/g); if (timeRanges) { for (const range of timeRanges) { if (!timeRangePattern.test(range)) { return { valid: false, error: `Invalid time range format: ${range}. Use formats like [5m], [1h], [24h]` }; } } } // Check for common LogQL functions and operators const hasValidLogQLStructure = trimmedQuery.includes('rate(') || trimmedQuery.includes('sum(') || trimmedQuery.includes('count_over_time(') || trimmedQuery.includes('avg_over_time(') || trimmedQuery.includes('histogram_quantile(') || trimmedQuery.match(/\{[^}]+\}/) || // Simple label selector trimmedQuery.includes('|~') || trimmedQuery.includes('|=') || trimmedQuery.includes('| json') || trimmedQuery.includes('| unwrap'); if (!hasValidLogQLStructure) { return { valid: false, error: 'Query does not appear to contain valid LogQL syntax or functions' }; } // Check for common syntax errors if (trimmedQuery.includes('service=""')) { return { valid: false, error: 'Empty service selector. Use service="service-name" or remove the filter' }; } if (trimmedQuery.match(/\|\s*\|/)) { return { valid: false, error: 'Invalid pipe operator usage. Use single pipes like |= or |~' }; } return { valid: true }; } } exports.REDMethodGenerator = REDMethodGenerator; //# sourceMappingURL=REDMethodGenerator.js.map