mirror of
https://github.com/grafana/grafana.git
synced 2025-01-09 15:43:23 -06:00
Prometheus: Improve expanding ruleIs with identifier label checking (#90336)
* define rule mapping type * introduce a new mapping type * add type comments * add identifier check * remove tests from wrong file * add tests to the right file * define function body * unit tests and logic for getQueryLabelsForRuleName * update logic of getRecordingRuleIdentifierIdx * update logic and tests getRecordingRuleIdentifierIdx * fix unit tests * fix tests * update how we return the options * update message * update type * update expandRecordingRules unit tests * remove identifier from end result * fix unit tests once more * remove fix action from expand rules warning * remove generic type * update warning text * betterer
This commit is contained in:
parent
32232e44d2
commit
f8645f73ea
@ -451,9 +451,8 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"]
|
||||
[0, 0, 0, "Do not use any type assertions.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"]
|
||||
],
|
||||
"packages/grafana-prometheus/src/language_provider.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
|
@ -34,7 +34,14 @@ import {
|
||||
fetchMockCalledWith,
|
||||
getMockTimeRange,
|
||||
} from './test/__mocks__/datasource';
|
||||
import { PromApplication, PrometheusCacheLevel, PromOptions, PromQuery, PromQueryRequest } from './types';
|
||||
import {
|
||||
PromApplication,
|
||||
PrometheusCacheLevel,
|
||||
PromOptions,
|
||||
PromQuery,
|
||||
PromQueryRequest,
|
||||
RawRecordingRules,
|
||||
} from './types';
|
||||
|
||||
const fetchMock = jest.fn().mockReturnValue(of(createDefaultPromResponse()));
|
||||
|
||||
@ -395,7 +402,7 @@ describe('PrometheusDatasource', () => {
|
||||
});
|
||||
|
||||
it('returns a mapping for recording rules only', () => {
|
||||
const groups = [
|
||||
const groups: RawRecordingRules[] = [
|
||||
{
|
||||
rules: [
|
||||
{
|
||||
@ -415,7 +422,50 @@ describe('PrometheusDatasource', () => {
|
||||
},
|
||||
];
|
||||
const mapping = extractRuleMappingFromGroups(groups);
|
||||
expect(mapping).toEqual({ 'job:http_inprogress_requests:sum': 'sum(http_inprogress_requests) by (job)' });
|
||||
expect(mapping).toEqual({
|
||||
'job:http_inprogress_requests:sum': [{ query: 'sum(http_inprogress_requests) by (job)' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should extract rules with same name respecting its labels', () => {
|
||||
const groups: RawRecordingRules[] = [
|
||||
{
|
||||
name: 'nameOfTheGroup:uid11',
|
||||
file: 'the_file_123',
|
||||
rules: [
|
||||
{
|
||||
name: 'metric_5m',
|
||||
query: 'super_duper_query',
|
||||
labels: {
|
||||
uuid: 'uuid111',
|
||||
},
|
||||
type: 'recording',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'nameOfTheGroup:uid22',
|
||||
file: 'the_file_456',
|
||||
rules: [
|
||||
{
|
||||
name: 'metric_5m',
|
||||
query: 'another_super_duper_query',
|
||||
labels: {
|
||||
uuid: 'uuid222',
|
||||
},
|
||||
type: 'recording',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
const mapping = extractRuleMappingFromGroups(groups);
|
||||
expect(mapping['metric_5m']).toBeDefined();
|
||||
expect(mapping['metric_5m'].length).toEqual(2);
|
||||
expect(mapping['metric_5m'][0].query).toEqual('super_duper_query');
|
||||
expect(mapping['metric_5m'][0].labels).toEqual({ uuid: 'uuid111' });
|
||||
expect(mapping['metric_5m'][1].query).toEqual('another_super_duper_query');
|
||||
expect(mapping['metric_5m'][1].labels).toEqual({ uuid: 'uuid222' });
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -68,6 +68,8 @@ import {
|
||||
PromOptions,
|
||||
PromQuery,
|
||||
PromQueryRequest,
|
||||
RawRecordingRules,
|
||||
RuleQueryMapping,
|
||||
} from './types';
|
||||
import { PrometheusVariableSupport } from './variables';
|
||||
|
||||
@ -81,7 +83,7 @@ export class PrometheusDatasource
|
||||
implements DataSourceWithQueryImportSupport<PromQuery>, DataSourceWithQueryExportSupport<PromQuery>
|
||||
{
|
||||
type: string;
|
||||
ruleMappings: { [index: string]: string };
|
||||
ruleMappings: RuleQueryMapping;
|
||||
hasIncrementalQuery: boolean;
|
||||
url: string;
|
||||
id: number;
|
||||
@ -773,7 +775,7 @@ export class PrometheusDatasource
|
||||
}
|
||||
case 'EXPAND_RULES': {
|
||||
if (action.options) {
|
||||
expression = expandRecordingRules(expression, action.options);
|
||||
expression = expandRecordingRules(expression, action.options as any);
|
||||
}
|
||||
break;
|
||||
}
|
||||
@ -969,18 +971,22 @@ export function alignRange(
|
||||
};
|
||||
}
|
||||
|
||||
export function extractRuleMappingFromGroups(groups: any[]) {
|
||||
return groups.reduce(
|
||||
export function extractRuleMappingFromGroups(groups: RawRecordingRules[]): RuleQueryMapping {
|
||||
return groups.reduce<RuleQueryMapping>(
|
||||
(mapping, group) =>
|
||||
group.rules
|
||||
.filter((rule: any) => rule.type === 'recording')
|
||||
.reduce(
|
||||
(acc: { [key: string]: string }, rule: any) => ({
|
||||
...acc,
|
||||
[rule.name]: rule.query,
|
||||
}),
|
||||
mapping
|
||||
),
|
||||
.filter((rule) => rule.type === 'recording')
|
||||
.reduce((acc, rule) => {
|
||||
// retrieve existing record
|
||||
const existingRule = acc[rule.name] ?? [];
|
||||
// push a new query with labels
|
||||
existingRule.push({
|
||||
query: rule.query,
|
||||
labels: rule.labels,
|
||||
});
|
||||
acc[rule.name] = existingRule;
|
||||
return acc;
|
||||
}, mapping),
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
@ -153,52 +153,65 @@ describe('expandRecordingRules()', () => {
|
||||
});
|
||||
|
||||
it('does not modify recording rules name in label values', () => {
|
||||
expect(expandRecordingRules('{__name__="metric"} + bar', { metric: 'foo', bar: 'super' })).toBe(
|
||||
'{__name__="metric"} + super'
|
||||
);
|
||||
expect(
|
||||
expandRecordingRules('{__name__="metric"} + bar', {
|
||||
metric: { expandedQuery: 'foo' },
|
||||
bar: { expandedQuery: 'super' },
|
||||
})
|
||||
).toBe('{__name__="metric"} + super');
|
||||
});
|
||||
|
||||
it('returns query with expanded recording rules', () => {
|
||||
expect(expandRecordingRules('metric', { metric: 'foo' })).toBe('foo');
|
||||
expect(expandRecordingRules('metric + metric', { metric: 'foo' })).toBe('foo + foo');
|
||||
expect(expandRecordingRules('metric{}', { metric: 'foo' })).toBe('foo{}');
|
||||
expect(expandRecordingRules('metric[]', { metric: 'foo' })).toBe('foo[]');
|
||||
expect(expandRecordingRules('metric + foo', { metric: 'foo', foo: 'bar' })).toBe('foo + bar');
|
||||
expect(expandRecordingRules('metric', { metric: { expandedQuery: 'foo' } })).toBe('foo');
|
||||
expect(expandRecordingRules('metric + metric', { metric: { expandedQuery: 'foo' } })).toBe('foo + foo');
|
||||
expect(expandRecordingRules('metric{}', { metric: { expandedQuery: 'foo' } })).toBe('foo{}');
|
||||
expect(expandRecordingRules('metric[]', { metric: { expandedQuery: 'foo' } })).toBe('foo[]');
|
||||
expect(
|
||||
expandRecordingRules('metric + foo', {
|
||||
metric: { expandedQuery: 'foo' },
|
||||
foo: { expandedQuery: 'bar' },
|
||||
})
|
||||
).toBe('foo + bar');
|
||||
});
|
||||
|
||||
it('returns query with labels with expanded recording rules', () => {
|
||||
expect(
|
||||
expandRecordingRules('metricA{label1="value1"} / metricB{label2="value2"}', { metricA: 'fooA', metricB: 'fooB' })
|
||||
expandRecordingRules('metricA{label1="value1"} / metricB{label2="value2"}', {
|
||||
metricA: { expandedQuery: 'fooA' },
|
||||
metricB: { expandedQuery: 'fooB' },
|
||||
})
|
||||
).toBe('fooA{label1="value1"} / fooB{label2="value2"}');
|
||||
expect(
|
||||
expandRecordingRules('metricA{label1="value1",label2="value,2"}', {
|
||||
metricA: 'rate(fooA[])',
|
||||
metricA: { expandedQuery: 'rate(fooA[])' },
|
||||
})
|
||||
).toBe('rate(fooA{label1="value1", label2="value,2"}[])');
|
||||
expect(
|
||||
expandRecordingRules('metricA{label1="value1"} / metricB{label2="value2"}', {
|
||||
metricA: 'rate(fooA[])',
|
||||
metricB: 'rate(fooB[])',
|
||||
metricA: { expandedQuery: 'rate(fooA[])' },
|
||||
metricB: { expandedQuery: 'rate(fooB[])' },
|
||||
})
|
||||
).toBe('rate(fooA{label1="value1"}[]) / rate(fooB{label2="value2"}[])');
|
||||
expect(
|
||||
expandRecordingRules('metricA{label1="value1",label2="value2"} / metricB{label3="value3"}', {
|
||||
metricA: 'rate(fooA[])',
|
||||
metricB: 'rate(fooB[])',
|
||||
metricA: { expandedQuery: 'rate(fooA[])' },
|
||||
metricB: { expandedQuery: 'rate(fooB[])' },
|
||||
})
|
||||
).toBe('rate(fooA{label1="value1", label2="value2"}[]) / rate(fooB{label3="value3"}[])');
|
||||
});
|
||||
|
||||
it('expands the query even it is wrapped with parentheses', () => {
|
||||
expect(
|
||||
expandRecordingRules('sum (metric{label1="value1"}) by (env)', { metric: 'foo{labelInside="valueInside"}' })
|
||||
expandRecordingRules('sum (metric{label1="value1"}) by (env)', {
|
||||
metric: { expandedQuery: 'foo{labelInside="valueInside"}' },
|
||||
})
|
||||
).toBe('sum (foo{labelInside="valueInside", label1="value1"}) by (env)');
|
||||
});
|
||||
|
||||
it('expands the query with regex match', () => {
|
||||
expect(
|
||||
expandRecordingRules('sum (metric{label1=~"/value1/(sa|sb)"}) by (env)', {
|
||||
metric: 'foo{labelInside="valueInside"}',
|
||||
metric: { expandedQuery: 'foo{labelInside="valueInside"}' },
|
||||
})
|
||||
).toBe('sum (foo{labelInside="valueInside", label1=~"/value1/(sa|sb)"}) by (env)');
|
||||
});
|
||||
@ -206,14 +219,49 @@ describe('expandRecordingRules()', () => {
|
||||
it('ins:metric:per{pid="val-42", comp="api"}', () => {
|
||||
const query = `aaa:111{pid="val-42", comp="api"} + bbb:222{pid="val-42"}`;
|
||||
const mapping = {
|
||||
'aaa:111':
|
||||
'(max without (mp) (targetMetric{device=~"/dev/(sda1|sdb)"}) / max without (mp) (targetMetric2{device=~"/dev/(sda1|sdb)"}))',
|
||||
'bbb:222': '(targetMetric2{device=~"/dev/(sda1|sdb)"})',
|
||||
'aaa:111': {
|
||||
expandedQuery:
|
||||
'(max without (mp) (targetMetric{device=~"/dev/(sda1|sdb)"}) / max without (mp) (targetMetric2{device=~"/dev/(sda1|sdb)"}))',
|
||||
},
|
||||
'bbb:222': { expandedQuery: '(targetMetric2{device=~"/dev/(sda1|sdb)"})' },
|
||||
};
|
||||
const expected = `(max without (mp) (targetMetric{device=~"/dev/(sda1|sdb)", pid="val-42", comp="api"}) / max without (mp) (targetMetric2{device=~"/dev/(sda1|sdb)", pid="val-42", comp="api"})) + (targetMetric2{device=~"/dev/(sda1|sdb)", pid="val-42"})`;
|
||||
const result = expandRecordingRules(query, mapping);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('when there is an identifier, identifier must be removed from expanded query', () => {
|
||||
const query = `ins:metric:per{uuid="111", comp="api"}`;
|
||||
const mapping = {
|
||||
'ins:metric:per': {
|
||||
expandedQuery: 'targetMetric{device="some_device"}',
|
||||
identifier: 'uuid',
|
||||
identifierValue: '111',
|
||||
},
|
||||
};
|
||||
const expected = `targetMetric{device="some_device", comp="api"}`;
|
||||
const result = expandRecordingRules(query, mapping);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
|
||||
it('when there is an identifier, identifier must be removed from complex expanded query', () => {
|
||||
const query = `instance_path:requests:rate5m{uuid="111", four="tops"} + instance_path:requests:rate15m{second="album", uuid="222"}`;
|
||||
const mapping = {
|
||||
'instance_path:requests:rate5m': {
|
||||
expandedQuery: `rate(prometheus_http_requests_total{job="prometheus"}`,
|
||||
identifier: 'uuid',
|
||||
identifierValue: '111',
|
||||
},
|
||||
'instance_path:requests:rate15m': {
|
||||
expandedQuery: `prom_http_requests_sum{job="prometheus"}`,
|
||||
identifier: 'uuid',
|
||||
identifierValue: '222',
|
||||
},
|
||||
};
|
||||
const expected = `rate(prometheus_http_requests_total{job="prometheus", four="tops"} + prom_http_requests_sum{job="prometheus", second="album"}`;
|
||||
const result = expandRecordingRules(query, mapping);
|
||||
expect(result).toBe(expected);
|
||||
});
|
||||
});
|
||||
|
||||
describe('escapeLabelValueInExactSelector()', () => {
|
||||
|
@ -16,7 +16,7 @@ import {
|
||||
import { addLabelToQuery } from './add_label_to_query';
|
||||
import { SUGGESTIONS_LIMIT } from './language_provider';
|
||||
import { PROMETHEUS_QUERY_BUILDER_MAX_RESULTS } from './querybuilder/components/MetricSelect';
|
||||
import { PrometheusCacheLevel, PromMetricsMetadata, PromMetricsMetadataItem } from './types';
|
||||
import { PrometheusCacheLevel, PromMetricsMetadata, PromMetricsMetadataItem, RecordingRuleIdentifier } from './types';
|
||||
|
||||
export const processHistogramMetrics = (metrics: string[]) => {
|
||||
const resultSet: Set<string> = new Set();
|
||||
@ -139,7 +139,7 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: str
|
||||
return { labelKeys, selector: selectorString };
|
||||
}
|
||||
|
||||
export function expandRecordingRules(query: string, mapping: { [name: string]: string }): string {
|
||||
export function expandRecordingRules(query: string, mapping: { [name: string]: RecordingRuleIdentifier }): string {
|
||||
const getRuleRegex = (ruleName: string) => new RegExp(`(\\s|\\(|^)(${ruleName})(\\s|$|\\(|\\[|\\{)`, 'ig');
|
||||
|
||||
// For each mapping key we iterate over the query and split them in parts.
|
||||
@ -202,12 +202,13 @@ export function expandRecordingRules(query: string, mapping: { [name: string]: s
|
||||
|
||||
// check if the mapping is there
|
||||
if (mapping[tsp]) {
|
||||
const recordingRule = mapping[tsp];
|
||||
const { expandedQuery: recordingRule, identifierValue, identifier } = mapping[tsp];
|
||||
// it is a recording rule. if the following is a label then apply it
|
||||
if (i + 1 !== tmpSplitParts.length && tmpSplitParts[i + 1].match(labelRegexp)) {
|
||||
// the next value in the loop is label. Let's apply labels to the metric
|
||||
labelFound = true;
|
||||
const labels = tmpSplitParts[i + 1];
|
||||
const regexp = new RegExp(`(,)?(\\s)?(${identifier}=\\"${identifierValue}\\")(,)?(\\s)?`, 'g');
|
||||
const labels = tmpSplitParts[i + 1].replace(regexp, '');
|
||||
const invalidLabelsRegex = /(\)\{|\}\{|\]\{)/;
|
||||
return addLabelsToExpression(recordingRule + labels, invalidLabelsRegex);
|
||||
} else {
|
||||
|
@ -1,6 +1,17 @@
|
||||
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/query_hints.test.ts
|
||||
import { QueryHint } from '@grafana/data';
|
||||
import { QueryBuilderLabelFilter } from '@grafana/experimental';
|
||||
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import { getQueryHints, SUM_HINT_THRESHOLD_COUNT } from './query_hints';
|
||||
import {
|
||||
getExpandRulesHints,
|
||||
getQueryHints,
|
||||
getQueryLabelsForRuleName,
|
||||
getRecordingRuleIdentifierIdx,
|
||||
SUM_HINT_THRESHOLD_COUNT,
|
||||
} from './query_hints';
|
||||
import { buildVisualQueryFromString } from './querybuilder/parsing';
|
||||
import { RuleQueryMapping } from './types';
|
||||
|
||||
describe('getQueryHints()', () => {
|
||||
it('returns no hints for no series', () => {
|
||||
@ -231,3 +242,268 @@ describe('getQueryHints()', () => {
|
||||
expect(hints!.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExpandRulesHints', () => {
|
||||
it('should return no hint when no rule is present in query', () => {
|
||||
const extractedMapping: RuleQueryMapping = {};
|
||||
const hints = getExpandRulesHints('metric_5m', extractedMapping);
|
||||
const expected: QueryHint[] = [];
|
||||
expect(hints).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return expand rule hint, single rules', () => {
|
||||
const extractedMapping: RuleQueryMapping = {
|
||||
metric_5m: [
|
||||
{
|
||||
query: 'expanded_metric_query[5m]',
|
||||
labels: {},
|
||||
},
|
||||
],
|
||||
metric_15m: [
|
||||
{
|
||||
query: 'expanded_metric_query[15m]',
|
||||
labels: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const query = `metric_5m`;
|
||||
const hints = getExpandRulesHints('metric_5m', extractedMapping);
|
||||
const expected = expect.arrayContaining([expect.objectContaining({ type: 'EXPAND_RULES' })]);
|
||||
expect(hints).toEqual(expected);
|
||||
expect(hints).toEqual([
|
||||
{
|
||||
type: 'EXPAND_RULES',
|
||||
label: 'Query contains recording rules.',
|
||||
fix: {
|
||||
label: 'Expand rules',
|
||||
action: {
|
||||
type: 'EXPAND_RULES',
|
||||
query,
|
||||
options: {
|
||||
metric_5m: {
|
||||
expandedQuery: 'expanded_metric_query[5m]',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('should return no expand rule hint, if the given query does not have a label', () => {
|
||||
const extractedMapping: RuleQueryMapping = {
|
||||
metric_5m: [
|
||||
{
|
||||
query: 'expanded_metric_query_111[5m]',
|
||||
labels: {
|
||||
uuid: '111',
|
||||
},
|
||||
},
|
||||
{
|
||||
query: 'expanded_metric_query_222[5m]',
|
||||
labels: {
|
||||
uuid: '222',
|
||||
},
|
||||
},
|
||||
],
|
||||
metric_15m: [
|
||||
{
|
||||
query: 'expanded_metric_query[15m]',
|
||||
labels: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const hints = getExpandRulesHints(
|
||||
`sum(metric_5m{uuid="5m"} + metric_10m{uuid="10m"}) + metric_66m{uuid="66m"}`,
|
||||
extractedMapping
|
||||
);
|
||||
const expected = expect.arrayContaining([expect.objectContaining({ type: 'EXPAND_RULES_WARNING' })]);
|
||||
expect(hints).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return expand rule warning hint, if the given query *does* have a label', () => {
|
||||
const extractedMapping: RuleQueryMapping = {
|
||||
metric_5m: [
|
||||
{
|
||||
query: 'expanded_metric_query_111[5m]',
|
||||
labels: {
|
||||
uuid: '111',
|
||||
},
|
||||
},
|
||||
{
|
||||
query: 'expanded_metric_query_222[5m]',
|
||||
labels: {
|
||||
uuid: '222',
|
||||
},
|
||||
},
|
||||
],
|
||||
metric_15m: [
|
||||
{
|
||||
query: 'expanded_metric_query[15m]',
|
||||
labels: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
const query = `metric_5m{uuid="111"}`;
|
||||
const hints = getExpandRulesHints('metric_5m{uuid="111"}', extractedMapping);
|
||||
expect(hints).toEqual([
|
||||
{
|
||||
type: 'EXPAND_RULES',
|
||||
label: 'Query contains recording rules.',
|
||||
fix: {
|
||||
label: 'Expand rules',
|
||||
action: {
|
||||
type: 'EXPAND_RULES',
|
||||
query,
|
||||
options: {
|
||||
metric_5m: {
|
||||
expandedQuery: 'expanded_metric_query_111[5m]',
|
||||
identifier: 'uuid',
|
||||
identifierValue: '111',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRecordingRuleIdentifierIdx', () => {
|
||||
it('should return the matching identifier', () => {
|
||||
const mapping: RuleQueryMapping[string] = [
|
||||
{
|
||||
query: 'expanded_metric_query_111[5m]',
|
||||
labels: {
|
||||
uuid: '111',
|
||||
},
|
||||
},
|
||||
{
|
||||
query: 'expanded_metric_query_222[5m]',
|
||||
labels: {
|
||||
uuid: '222',
|
||||
},
|
||||
},
|
||||
];
|
||||
const ruleName = `metric_5m`;
|
||||
const query = `metric_5m{uuid="111"}`;
|
||||
const { idx, identifier, identifierValue, expandedQuery } = getRecordingRuleIdentifierIdx(query, ruleName, mapping);
|
||||
expect(idx).toEqual(0);
|
||||
expect(identifier).toEqual(`uuid`);
|
||||
expect(identifierValue).toEqual('111');
|
||||
expect(expandedQuery).toEqual(`expanded_metric_query_111[5m]`);
|
||||
});
|
||||
|
||||
it('should not return the matching identifier', () => {
|
||||
const mapping: RuleQueryMapping[string] = [
|
||||
{
|
||||
query: 'expanded_metric_query_111[5m]',
|
||||
labels: {
|
||||
uuid: '111',
|
||||
},
|
||||
},
|
||||
{
|
||||
query: 'expanded_metric_query_222[5m]',
|
||||
labels: {
|
||||
uuid: '222',
|
||||
},
|
||||
},
|
||||
];
|
||||
const ruleName = `metric_5m`;
|
||||
const query = `metric_5m{uuid="999"}`;
|
||||
const { idx } = getRecordingRuleIdentifierIdx(query, ruleName, mapping);
|
||||
expect(idx).toEqual(-1);
|
||||
});
|
||||
|
||||
it('should return the matching identifier index for a complex query', () => {
|
||||
const mapping: RuleQueryMapping[string] = [
|
||||
{
|
||||
query: 'expanded_metric_query_111[5m]',
|
||||
labels: {
|
||||
uuid: '111',
|
||||
},
|
||||
},
|
||||
{
|
||||
query: 'expanded_metric_query_222[5m]',
|
||||
labels: {
|
||||
uuid: '222',
|
||||
},
|
||||
},
|
||||
];
|
||||
const ruleName = `metric_55m`;
|
||||
const query = `metric_5m{uuid="111"} + metric_55m{uuid="222"}`;
|
||||
const { idx, identifier, identifierValue, expandedQuery } = getRecordingRuleIdentifierIdx(query, ruleName, mapping);
|
||||
expect(idx).toEqual(1);
|
||||
expect(identifier).toEqual(`uuid`);
|
||||
expect(identifierValue).toEqual('222');
|
||||
expect(expandedQuery).toEqual(`expanded_metric_query_222[5m]`);
|
||||
});
|
||||
|
||||
it('should return the matching identifier index for a complex query with binary operators', () => {
|
||||
const mapping: RuleQueryMapping[string] = [
|
||||
{
|
||||
query: 'expanded_metric_query_111[5m]',
|
||||
labels: {
|
||||
uuid: '111',
|
||||
},
|
||||
},
|
||||
{
|
||||
query: 'expanded_metric_query_222[5m]',
|
||||
labels: {
|
||||
uuid: '222',
|
||||
},
|
||||
},
|
||||
{
|
||||
query: 'expanded_metric_query_333[5m]',
|
||||
labels: {
|
||||
uuid: '333',
|
||||
},
|
||||
},
|
||||
];
|
||||
const ruleName = `metric_5m`;
|
||||
const query = `metric_7n{} + (metric_5m{uuid="333"} + metric_55m{uuid="222"})`;
|
||||
const { idx, identifier, identifierValue, expandedQuery } = getRecordingRuleIdentifierIdx(query, ruleName, mapping);
|
||||
expect(idx).toEqual(2);
|
||||
expect(identifier).toEqual(`uuid`);
|
||||
expect(identifierValue).toEqual('333');
|
||||
expect(expandedQuery).toEqual(`expanded_metric_query_333[5m]`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getQueryLabelsForRuleName', () => {
|
||||
it('should return labels for the metric name', () => {
|
||||
const metricName = `metric_5m`;
|
||||
const query = `metric_5m{uuid="111"}`;
|
||||
const { query: visualQuery } = buildVisualQueryFromString(query);
|
||||
const result = getQueryLabelsForRuleName(metricName, visualQuery);
|
||||
const expected: QueryBuilderLabelFilter[] = [{ label: 'uuid', op: '=', value: '111' }];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return labels from a query with binary operations', () => {
|
||||
const metricName = `metric_5m`;
|
||||
const query = `metric_55m{uuid="222"} + metric_33m{uuid="333"} + metric_5m{uuid="111"}`;
|
||||
const { query: visualQuery } = buildVisualQueryFromString(query);
|
||||
const result = getQueryLabelsForRuleName(metricName, visualQuery);
|
||||
const expected: QueryBuilderLabelFilter[] = [{ label: 'uuid', op: '=', value: '111' }];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return labels from a query with binary operations with parentheses', () => {
|
||||
const metricName = `metric_5m`;
|
||||
const query = `(metric_55m{uuid="222"} + metric_33m{uuid="333"}) + metric_5m{uuid="111"}`;
|
||||
const { query: visualQuery } = buildVisualQueryFromString(query);
|
||||
const result = getQueryLabelsForRuleName(metricName, visualQuery);
|
||||
const expected: QueryBuilderLabelFilter[] = [{ label: 'uuid', op: '=', value: '111' }];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
|
||||
it('should return labels from a query for the first metricName match', () => {
|
||||
const metricName = `metric_5m`;
|
||||
const query = `(metric_55m{uuid="222"} + metric_33m{uuid="333"}) + metric_5m{uuid="999"} + metric_5m{uuid="555"}`;
|
||||
const { query: visualQuery } = buildVisualQueryFromString(query);
|
||||
const result = getQueryLabelsForRuleName(metricName, visualQuery);
|
||||
const expected: QueryBuilderLabelFilter[] = [{ label: 'uuid', op: '=', value: '999' }];
|
||||
expect(result).toEqual(expected);
|
||||
});
|
||||
});
|
||||
|
@ -4,7 +4,10 @@ import { size } from 'lodash';
|
||||
import { QueryFix, QueryHint } from '@grafana/data';
|
||||
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import { PromMetricsMetadata } from './types';
|
||||
import { buildVisualQueryFromString } from './querybuilder/parsing';
|
||||
import { QueryBuilderLabelFilter } from './querybuilder/shared/types';
|
||||
import { PromVisualQuery } from './querybuilder/types';
|
||||
import { PromMetricsMetadata, RecordingRuleIdentifier, RuleQueryMapping } from './types';
|
||||
|
||||
/**
|
||||
* Number of time series results needed before starting to suggest sum aggregation hints
|
||||
@ -168,31 +171,8 @@ export function getQueryHints(query: string, series?: unknown[], datasource?: Pr
|
||||
|
||||
// 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.';
|
||||
hints.push({
|
||||
type: 'EXPAND_RULES',
|
||||
label,
|
||||
fix: {
|
||||
label: 'Expand rules',
|
||||
action: {
|
||||
type: 'EXPAND_RULES',
|
||||
query,
|
||||
options: mappingForQuery,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const expandQueryHints = getExpandRulesHints(query, datasource.ruleMappings);
|
||||
hints.push(...expandQueryHints);
|
||||
}
|
||||
|
||||
if (series && series.length >= SUM_HINT_THRESHOLD_COUNT) {
|
||||
@ -230,6 +210,126 @@ export function getInitHints(datasource: PrometheusDatasource): QueryHint[] {
|
||||
return hints;
|
||||
}
|
||||
|
||||
export function getExpandRulesHints(query: string, mapping: RuleQueryMapping): QueryHint[] {
|
||||
const hints: QueryHint[] = [];
|
||||
const mappingForQuery = Object.keys(mapping).reduce((acc, ruleName) => {
|
||||
if (query.search(ruleName) === -1) {
|
||||
return acc;
|
||||
}
|
||||
|
||||
if (mapping[ruleName].length > 1) {
|
||||
const { idx, expandedQuery, identifier, identifierValue } = getRecordingRuleIdentifierIdx(
|
||||
query,
|
||||
ruleName,
|
||||
mapping[ruleName]
|
||||
);
|
||||
|
||||
// No identifier detected add warning
|
||||
if (idx === -1) {
|
||||
hints.push({
|
||||
type: 'EXPAND_RULES_WARNING',
|
||||
label:
|
||||
'We found multiple recording rules that match in this query. To expand the recording rule, add an identifier label/value.',
|
||||
});
|
||||
return acc;
|
||||
} else {
|
||||
// Identifier found.
|
||||
return {
|
||||
...acc,
|
||||
[ruleName]: {
|
||||
expandedQuery,
|
||||
identifier,
|
||||
identifierValue,
|
||||
},
|
||||
};
|
||||
}
|
||||
} else {
|
||||
return {
|
||||
...acc,
|
||||
[ruleName]: {
|
||||
expandedQuery: mapping[ruleName][0].query,
|
||||
},
|
||||
};
|
||||
}
|
||||
}, {});
|
||||
|
||||
if (size(mappingForQuery) > 0) {
|
||||
const label = 'Query contains recording rules.';
|
||||
hints.push({
|
||||
type: 'EXPAND_RULES',
|
||||
label,
|
||||
fix: {
|
||||
label: 'Expand rules',
|
||||
action: {
|
||||
type: 'EXPAND_RULES',
|
||||
query,
|
||||
options: mappingForQuery,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return hints;
|
||||
}
|
||||
|
||||
export function getRecordingRuleIdentifierIdx(
|
||||
queryStr: string,
|
||||
ruleName: string,
|
||||
mapping: RuleQueryMapping[string]
|
||||
): RecordingRuleIdentifier & { idx: number } {
|
||||
const { query } = buildVisualQueryFromString(queryStr);
|
||||
const queryMetricLabels: QueryBuilderLabelFilter[] = getQueryLabelsForRuleName(ruleName, query);
|
||||
if (queryMetricLabels.length === 0) {
|
||||
return { idx: -1, identifier: '', identifierValue: '', expandedQuery: '' };
|
||||
}
|
||||
|
||||
let uuidLabel = '';
|
||||
let uuidLabelValue = '';
|
||||
let uuidLabelIdx = -1;
|
||||
|
||||
queryMetricLabels.forEach((qml) => {
|
||||
if (uuidLabelIdx === -1 && qml.label.search('uuid') !== -1) {
|
||||
uuidLabel = qml.label;
|
||||
uuidLabelValue = qml.value;
|
||||
}
|
||||
});
|
||||
|
||||
mapping.forEach((mp, idx) => {
|
||||
if (mp.labels) {
|
||||
Object.entries(mp.labels).forEach(([key, value]) => {
|
||||
if (uuidLabelIdx === -1 && key === uuidLabel && value === uuidLabelValue) {
|
||||
uuidLabelIdx = idx;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
idx: uuidLabelIdx,
|
||||
identifier: uuidLabel,
|
||||
identifierValue: uuidLabelValue,
|
||||
expandedQuery: mapping[uuidLabelIdx]?.query ?? '',
|
||||
};
|
||||
}
|
||||
|
||||
// returns the labels of matching metric
|
||||
// metricName is the ruleName in query
|
||||
export function getQueryLabelsForRuleName(metricName: string, query: PromVisualQuery): QueryBuilderLabelFilter[] {
|
||||
if (query.metric === metricName) {
|
||||
return query.labels;
|
||||
} else {
|
||||
if (query.binaryQueries) {
|
||||
for (let i = 0; i < query.binaryQueries.length; i++) {
|
||||
const labels = getQueryLabelsForRuleName(metricName, query.binaryQueries[i].query);
|
||||
if (labels && labels.length > 0) {
|
||||
return labels;
|
||||
}
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function getQueryTokens(query: string) {
|
||||
return (
|
||||
Array.from(query.matchAll(/\$?[a-zA-Z_:][a-zA-Z0-9_:]*/g))
|
||||
|
@ -141,3 +141,48 @@ export type StandardPromVariableQuery = {
|
||||
query: string;
|
||||
refId: string;
|
||||
};
|
||||
|
||||
// Rules that we fetch from Prometheus
|
||||
export type RawRecordingRules = {
|
||||
name: string;
|
||||
file: string;
|
||||
rules: Rule[];
|
||||
interval?: number;
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
// A single recording rule with its labels and the query it represents
|
||||
// In this object, there may be other fields but those are the ones we care for now
|
||||
export type Rule = {
|
||||
name: string;
|
||||
query: string;
|
||||
duration?: number;
|
||||
labels?: Record<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
alerts?: AlertInfo[];
|
||||
type: 'alerting' | 'recording';
|
||||
};
|
||||
|
||||
export type AlertInfo = {
|
||||
labels: Record<string, string>;
|
||||
annotations: Record<string, string>;
|
||||
state: string;
|
||||
activeAt: string;
|
||||
value: string;
|
||||
};
|
||||
|
||||
// Extracted recording rules with labels
|
||||
// We parse and extract the rules because
|
||||
// there might be multiple rules with same name but different labels and queries
|
||||
export type RuleQueryMapping = {
|
||||
[key: string]: Array<{
|
||||
query: string;
|
||||
labels?: Record<string, string>;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type RecordingRuleIdentifier = {
|
||||
expandedQuery: string;
|
||||
identifier?: string;
|
||||
identifierValue?: string;
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user