Commit 128a5d98 by David Kaltschmidt

Explore: expand recording rules for queries

- load recording rules from prometheus
- map rule name to rule query
- query hint to detect recording rules in query
- click on hint fix expands rule name to query
parent c1b9bbc2
...@@ -169,6 +169,10 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -169,6 +169,10 @@ export class Explore extends React.Component<any, IExploreState> {
const historyKey = `grafana.explore.history.${datasourceId}`; const historyKey = `grafana.explore.history.${datasourceId}`;
const history = store.getObject(historyKey, []); const history = store.getObject(historyKey, []);
if (datasource.init) {
datasource.init();
}
this.setState( this.setState(
{ {
datasource, datasource,
......
...@@ -3,7 +3,7 @@ import Enzyme, { shallow } from 'enzyme'; ...@@ -3,7 +3,7 @@ import Enzyme, { shallow } from 'enzyme';
import Adapter from 'enzyme-adapter-react-16'; import Adapter from 'enzyme-adapter-react-16';
import Plain from 'slate-plain-serializer'; import Plain from 'slate-plain-serializer';
import PromQueryField from './PromQueryField'; import PromQueryField, { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField';
Enzyme.configure({ adapter: new Adapter() }); Enzyme.configure({ adapter: new Adapter() });
...@@ -177,3 +177,43 @@ describe('PromQueryField typeahead handling', () => { ...@@ -177,3 +177,43 @@ describe('PromQueryField typeahead handling', () => {
}); });
}); });
}); });
describe('groupMetricsByPrefix()', () => {
it('returns an empty group for no metrics', () => {
expect(groupMetricsByPrefix([])).toEqual([]);
});
it('returns options grouped by prefix', () => {
expect(groupMetricsByPrefix(['foo_metric'])).toMatchObject([
{
value: 'foo',
children: [
{
value: 'foo_metric',
},
],
},
]);
});
it('returns options without prefix as toplevel option', () => {
expect(groupMetricsByPrefix(['metric'])).toMatchObject([
{
value: 'metric',
},
]);
});
it('returns recording rules grouped separately', () => {
expect(groupMetricsByPrefix([':foo_metric:'])).toMatchObject([
{
value: RECORDING_RULES_GROUP,
children: [
{
value: ':foo_metric:',
},
],
},
]);
});
});
...@@ -28,6 +28,7 @@ const HISTORY_ITEM_COUNT = 5; ...@@ -28,6 +28,7 @@ const HISTORY_ITEM_COUNT = 5;
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
const METRIC_MARK = 'metric'; const METRIC_MARK = 'metric';
const PRISM_LANGUAGE = 'promql'; const PRISM_LANGUAGE = 'promql';
export const RECORDING_RULES_GROUP = '__recording_rules__';
export const wrapLabel = (label: string) => ({ label }); export const wrapLabel = (label: string) => ({ label });
export const setFunctionMove = (suggestion: Suggestion): Suggestion => { export const setFunctionMove = (suggestion: Suggestion): Suggestion => {
...@@ -52,7 +53,22 @@ export function addHistoryMetadata(item: Suggestion, history: any[]): Suggestion ...@@ -52,7 +53,22 @@ export function addHistoryMetadata(item: Suggestion, history: any[]): Suggestion
} }
export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): CascaderOption[] { export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): CascaderOption[] {
return _.chain(metrics) // Filter out recording rules and insert as first option
const ruleRegex = /:\w+:/;
const ruleNames = metrics.filter(metric => ruleRegex.test(metric));
const rulesOption = {
label: 'Recording rules',
value: RECORDING_RULES_GROUP,
children: ruleNames
.slice()
.sort()
.map(name => ({ label: name, value: name })),
};
const options = ruleNames.length > 0 ? [rulesOption] : [];
const metricsOptions = _.chain(metrics)
.filter(metric => !ruleRegex.test(metric))
.groupBy(metric => metric.split(delimiter)[0]) .groupBy(metric => metric.split(delimiter)[0])
.map((metricsForPrefix: string[], prefix: string): CascaderOption => { .map((metricsForPrefix: string[], prefix: string): CascaderOption => {
const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix; const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix;
...@@ -65,6 +81,8 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad ...@@ -65,6 +81,8 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad
}) })
.sortBy('label') .sortBy('label')
.value(); .value();
return [...options, ...metricsOptions];
} }
export function willApplySuggestion( export function willApplySuggestion(
......
...@@ -82,7 +82,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri ...@@ -82,7 +82,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri
return parts.join(''); return parts.join('');
} }
export function determineQueryHints(series: any[]): any[] { export function determineQueryHints(series: any[], datasource?: any): any[] {
const hints = series.map((s, i) => { const hints = series.map((s, i) => {
const query: string = s.query; const query: string = s.query;
const index: number = s.responseIndex; const index: number = s.responseIndex;
...@@ -138,12 +138,56 @@ export function determineQueryHints(series: any[]): any[] { ...@@ -138,12 +138,56 @@ export function determineQueryHints(series: any[]): any[] {
} }
} }
// Check for recording rules expansion
if (datasource && datasource.ruleMappings) {
const mapping = datasource.ruleMappings;
const mappingForQuery = Object.keys(mapping).reduce((acc, ruleName) => {
if (query.search(ruleName) > -1) {
return {
...acc,
[ruleName]: mapping[ruleName],
};
}
return acc;
}, {});
if (_.size(mappingForQuery) > 0) {
const label = 'Query contains recording rules.';
return {
label,
index,
fix: {
label: 'Expand rules',
action: {
type: 'EXPAND_RULES',
query,
index,
mapping: mappingForQuery,
},
},
};
}
}
// No hint found // No hint found
return null; return null;
}); });
return hints; return hints;
} }
export function extractRuleMappingFromGroups(groups: any[]) {
return groups.reduce(
(mapping, group) =>
group.rules.filter(rule => rule.type === 'recording').reduce(
(acc, rule) => ({
...acc,
[rule.name]: rule.query,
}),
mapping
),
{}
);
}
export function prometheusRegularEscape(value) { export function prometheusRegularEscape(value) {
if (typeof value === 'string') { if (typeof value === 'string') {
return value.replace(/'/g, "\\\\'"); return value.replace(/'/g, "\\\\'");
...@@ -162,6 +206,7 @@ export class PrometheusDatasource { ...@@ -162,6 +206,7 @@ export class PrometheusDatasource {
type: string; type: string;
editorSrc: string; editorSrc: string;
name: string; name: string;
ruleMappings: { [index: string]: string };
supportsExplore: boolean; supportsExplore: boolean;
supportMetrics: boolean; supportMetrics: boolean;
url: string; url: string;
...@@ -189,6 +234,11 @@ export class PrometheusDatasource { ...@@ -189,6 +234,11 @@ export class PrometheusDatasource {
this.queryTimeout = instanceSettings.jsonData.queryTimeout; this.queryTimeout = instanceSettings.jsonData.queryTimeout;
this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET'; this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET';
this.resultTransformer = new ResultTransformer(templateSrv); this.resultTransformer = new ResultTransformer(templateSrv);
this.ruleMappings = {};
}
init() {
this.loadRules();
} }
_request(url, data?, options?: any) { _request(url, data?, options?: any) {
...@@ -312,7 +362,7 @@ export class PrometheusDatasource { ...@@ -312,7 +362,7 @@ export class PrometheusDatasource {
result = [...result, ...series]; result = [...result, ...series];
if (queries[index].hinting) { if (queries[index].hinting) {
const queryHints = determineQueryHints(series); const queryHints = determineQueryHints(series, this);
hints = [...hints, ...queryHints]; hints = [...hints, ...queryHints];
} }
}); });
...@@ -525,6 +575,21 @@ export class PrometheusDatasource { ...@@ -525,6 +575,21 @@ export class PrometheusDatasource {
return state; return state;
} }
loadRules() {
this.metadataRequest('/api/v1/rules')
.then(res => res.data || res.json())
.then(body => {
const groups = _.get(body, ['data', 'groups']);
if (groups) {
this.ruleMappings = extractRuleMappingFromGroups(groups);
}
})
.catch(e => {
console.log('Rules API is experimental. Ignore next error.');
console.error(e);
});
}
modifyQuery(query: string, action: any): string { modifyQuery(query: string, action: any): string {
switch (action.type) { switch (action.type) {
case 'ADD_FILTER': { case 'ADD_FILTER': {
...@@ -536,6 +601,14 @@ export class PrometheusDatasource { ...@@ -536,6 +601,14 @@ export class PrometheusDatasource {
case 'ADD_RATE': { case 'ADD_RATE': {
return `rate(${query}[5m])`; return `rate(${query}[5m])`;
} }
case 'EXPAND_RULES': {
const mapping = action.mapping;
if (mapping) {
const ruleNames = Object.keys(mapping);
const rulesRegex = new RegExp(`(\\s|^)(${ruleNames.join('|')})(\\s|$|\\()`, 'ig');
return query.replace(rulesRegex, (match, pre, name, post) => mapping[name]);
}
}
default: default:
return query; return query;
} }
......
...@@ -4,6 +4,7 @@ import q from 'q'; ...@@ -4,6 +4,7 @@ import q from 'q';
import { import {
alignRange, alignRange,
determineQueryHints, determineQueryHints,
extractRuleMappingFromGroups,
PrometheusDatasource, PrometheusDatasource,
prometheusSpecialRegexEscape, prometheusSpecialRegexEscape,
prometheusRegularEscape, prometheusRegularEscape,
...@@ -229,6 +230,36 @@ describe('PrometheusDatasource', () => { ...@@ -229,6 +230,36 @@ describe('PrometheusDatasource', () => {
}); });
}); });
describe('extractRuleMappingFromGroups()', () => {
it('returns empty mapping for no rule groups', () => {
expect(extractRuleMappingFromGroups([])).toEqual({});
});
it('returns a mapping for recording rules only', () => {
const groups = [
{
rules: [
{
name: 'HighRequestLatency',
query: 'job:request_latency_seconds:mean5m{job="myjob"} > 0.5',
type: 'alerting',
},
{
name: 'job:http_inprogress_requests:sum',
query: 'sum(http_inprogress_requests) by (job)',
type: 'recording',
},
],
file: '/rules.yaml',
interval: 60,
name: 'example',
},
];
const mapping = extractRuleMappingFromGroups(groups);
expect(mapping).toEqual({ 'job:http_inprogress_requests:sum': 'sum(http_inprogress_requests) by (job)' });
});
});
describe('Prometheus regular escaping', () => { describe('Prometheus regular escaping', () => {
it('should not escape non-string', () => { it('should not escape non-string', () => {
expect(prometheusRegularEscape(12)).toEqual(12); expect(prometheusRegularEscape(12)).toEqual(12);
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment