mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Merge pull request #12846 from grafana/davkal/explore-rules-expansion
Explore: expand recording rules for queries
This commit is contained in:
commit
b9f4666821
@ -169,6 +169,10 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
const historyKey = `grafana.explore.history.${datasourceId}`;
|
||||
const history = store.getObject(historyKey, []);
|
||||
|
||||
if (datasource.init) {
|
||||
datasource.init();
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
datasource,
|
||||
|
@ -3,7 +3,7 @@ import Enzyme, { shallow } from 'enzyme';
|
||||
import Adapter from 'enzyme-adapter-react-16';
|
||||
import Plain from 'slate-plain-serializer';
|
||||
|
||||
import PromQueryField from './PromQueryField';
|
||||
import PromQueryField, { groupMetricsByPrefix, RECORDING_RULES_GROUP } from './PromQueryField';
|
||||
|
||||
Enzyme.configure({ adapter: new Adapter() });
|
||||
|
||||
@ -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;
|
||||
const HISTORY_COUNT_CUTOFF = 1000 * 60 * 60 * 24; // 24h
|
||||
const METRIC_MARK = 'metric';
|
||||
const PRISM_LANGUAGE = 'promql';
|
||||
export const RECORDING_RULES_GROUP = '__recording_rules__';
|
||||
|
||||
export const wrapLabel = (label: string) => ({ label });
|
||||
export const setFunctionMove = (suggestion: Suggestion): Suggestion => {
|
||||
@ -52,7 +53,22 @@ export function addHistoryMetadata(item: Suggestion, history: any[]): Suggestion
|
||||
}
|
||||
|
||||
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])
|
||||
.map((metricsForPrefix: string[], prefix: string): CascaderOption => {
|
||||
const prefixIsMetric = metricsForPrefix.length === 1 && metricsForPrefix[0] === prefix;
|
||||
@ -65,6 +81,8 @@ export function groupMetricsByPrefix(metrics: string[], delimiter = '_'): Cascad
|
||||
})
|
||||
.sortBy('label')
|
||||
.value();
|
||||
|
||||
return [...options, ...metricsOptions];
|
||||
}
|
||||
|
||||
export function willApplySuggestion(
|
||||
|
@ -82,7 +82,7 @@ export function addLabelToQuery(query: string, key: string, value: string): stri
|
||||
return parts.join('');
|
||||
}
|
||||
|
||||
export function determineQueryHints(series: any[]): any[] {
|
||||
export function determineQueryHints(series: any[], datasource?: any): any[] {
|
||||
const hints = series.map((s, i) => {
|
||||
const query: string = s.query;
|
||||
const index: number = s.responseIndex;
|
||||
@ -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
|
||||
return null;
|
||||
});
|
||||
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) {
|
||||
if (typeof value === 'string') {
|
||||
return value.replace(/'/g, "\\\\'");
|
||||
@ -162,6 +206,7 @@ export class PrometheusDatasource {
|
||||
type: string;
|
||||
editorSrc: string;
|
||||
name: string;
|
||||
ruleMappings: { [index: string]: string };
|
||||
supportsExplore: boolean;
|
||||
supportMetrics: boolean;
|
||||
url: string;
|
||||
@ -189,6 +234,11 @@ export class PrometheusDatasource {
|
||||
this.queryTimeout = instanceSettings.jsonData.queryTimeout;
|
||||
this.httpMethod = instanceSettings.jsonData.httpMethod || 'GET';
|
||||
this.resultTransformer = new ResultTransformer(templateSrv);
|
||||
this.ruleMappings = {};
|
||||
}
|
||||
|
||||
init() {
|
||||
this.loadRules();
|
||||
}
|
||||
|
||||
_request(url, data?, options?: any) {
|
||||
@ -312,7 +362,7 @@ export class PrometheusDatasource {
|
||||
result = [...result, ...series];
|
||||
|
||||
if (queries[index].hinting) {
|
||||
const queryHints = determineQueryHints(series);
|
||||
const queryHints = determineQueryHints(series, this);
|
||||
hints = [...hints, ...queryHints];
|
||||
}
|
||||
});
|
||||
@ -525,6 +575,21 @@ export class PrometheusDatasource {
|
||||
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 {
|
||||
switch (action.type) {
|
||||
case 'ADD_FILTER': {
|
||||
@ -536,6 +601,14 @@ export class PrometheusDatasource {
|
||||
case 'ADD_RATE': {
|
||||
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:
|
||||
return query;
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ import q from 'q';
|
||||
import {
|
||||
alignRange,
|
||||
determineQueryHints,
|
||||
extractRuleMappingFromGroups,
|
||||
PrometheusDatasource,
|
||||
prometheusSpecialRegexEscape,
|
||||
prometheusRegularEscape,
|
||||
@ -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', () => {
|
||||
it('should not escape non-string', () => {
|
||||
expect(prometheusRegularEscape(12)).toEqual(12);
|
||||
|
6
public/vendor/css/rc-cascader.scss
vendored
6
public/vendor/css/rc-cascader.scss
vendored
@ -16,7 +16,7 @@
|
||||
}
|
||||
.rc-cascader-menus.slide-up-enter,
|
||||
.rc-cascader-menus.slide-up-appear {
|
||||
animation-duration: .3s;
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: both;
|
||||
transform-origin: 0 0;
|
||||
opacity: 0;
|
||||
@ -24,7 +24,7 @@
|
||||
animation-play-state: paused;
|
||||
}
|
||||
.rc-cascader-menus.slide-up-leave {
|
||||
animation-duration: .3s;
|
||||
animation-duration: 0.3s;
|
||||
animation-fill-mode: both;
|
||||
transform-origin: 0 0;
|
||||
opacity: 1;
|
||||
@ -66,7 +66,7 @@
|
||||
.rc-cascader-menu-item {
|
||||
height: 32px;
|
||||
line-height: 32px;
|
||||
padding: 0 16px;
|
||||
padding: 0 2.5em 0 16px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
|
Loading…
Reference in New Issue
Block a user