observability-analyzer
Version:
Production-ready MCP Server for intelligent Loki/Tempo observability dashboard analysis and generation
461 lines • 19.3 kB
JavaScript
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
;