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:
ismail simsek 2024-07-18 13:17:33 +02:00 committed by GitHub
parent 32232e44d2
commit f8645f73ea
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 593 additions and 68 deletions

View File

@ -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"],

View File

@ -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' });
});
});

View File

@ -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),
{}
);
}

View File

@ -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()', () => {

View File

@ -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 {

View File

@ -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);
});
});

View File

@ -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))

View File

@ -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;
};