Prometheus: Fix support of AdHoc filters for specific queries (#46547)

* Use parser to deal with ad hoc filters

* Add comment

* Fix variable naming

* Fix tests
This commit is contained in:
Andrej Ocenas
2022-03-15 17:37:20 +01:00
committed by GitHub
parent 8e13b201ba
commit ecdbcd4941
6 changed files with 112 additions and 157 deletions

View File

@@ -1,4 +1,4 @@
import { addLabelToQuery, addLabelToSelector } from './add_label_to_query'; import { addLabelToQuery } from './add_label_to_query';
describe('addLabelToQuery()', () => { describe('addLabelToQuery()', () => {
it('should add label to simple query', () => { it('should add label to simple query', () => {
@@ -7,13 +7,13 @@ describe('addLabelToQuery()', () => {
}).toThrow(); }).toThrow();
expect(addLabelToQuery('foo', 'bar', 'baz')).toBe('foo{bar="baz"}'); expect(addLabelToQuery('foo', 'bar', 'baz')).toBe('foo{bar="baz"}');
expect(addLabelToQuery('foo{}', 'bar', 'baz')).toBe('foo{bar="baz"}'); expect(addLabelToQuery('foo{}', 'bar', 'baz')).toBe('foo{bar="baz"}');
expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"}'); expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz')).toBe('foo{x="yy", bar="baz"}');
expect(addLabelToQuery('metric > 0.001', 'foo', 'bar')).toBe('metric{foo="bar"} > 0.001'); expect(addLabelToQuery('metric > 0.001', 'foo', 'bar')).toBe('metric{foo="bar"} > 0.001');
}); });
it('should add custom operator', () => { it('should add custom operator', () => {
expect(addLabelToQuery('foo{}', 'bar', 'baz', '!=')).toBe('foo{bar!="baz"}'); expect(addLabelToQuery('foo{}', 'bar', 'baz', '!=')).toBe('foo{bar!="baz"}');
expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz', '!=')).toBe('foo{bar!="baz",x="yy"}'); expect(addLabelToQuery('foo{x="yy"}', 'bar', 'baz', '!=')).toBe('foo{x="yy", bar!="baz"}');
}); });
it('should not modify ranges', () => { it('should not modify ranges', () => {
@@ -32,33 +32,33 @@ describe('addLabelToQuery()', () => {
it('should handle selectors with punctuation', () => { it('should handle selectors with punctuation', () => {
expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe( expect(addLabelToQuery('foo{instance="my-host.com:9100"}', 'bar', 'baz')).toBe(
'foo{bar="baz",instance="my-host.com:9100"}' 'foo{instance="my-host.com:9100", bar="baz"}'
); );
expect(addLabelToQuery('foo:metric:rate1m', 'bar', 'baz')).toBe('foo:metric:rate1m{bar="baz"}'); expect(addLabelToQuery('foo:metric:rate1m', 'bar', 'baz')).toBe('foo:metric:rate1m{bar="baz"}');
expect(addLabelToQuery('avg(foo:metric:rate1m{a="b"})', 'bar', 'baz')).toBe( expect(addLabelToQuery('avg(foo:metric:rate1m{a="b"})', 'bar', 'baz')).toBe(
'avg(foo:metric:rate1m{a="b",bar="baz"})' 'avg(foo:metric:rate1m{a="b", bar="baz"})'
); );
expect(addLabelToQuery('foo{list="a,b,c"}', 'bar', 'baz')).toBe('foo{bar="baz",list="a,b,c"}'); expect(addLabelToQuery('foo{list="a,b,c"}', 'bar', 'baz')).toBe('foo{list="a,b,c", bar="baz"}');
}); });
it('should work on arithmetical expressions', () => { it('should work on arithmetical expressions', () => {
expect(addLabelToQuery('foo + foo', 'bar', 'baz')).toBe('foo{bar="baz"} + foo{bar="baz"}'); expect(addLabelToQuery('foo + foo', 'bar', 'baz')).toBe('foo{bar="baz"} + foo{bar="baz"}');
expect(addLabelToQuery('foo{x="yy"} + metric', 'bar', 'baz')).toBe('foo{bar="baz",x="yy"} + metric{bar="baz"}'); expect(addLabelToQuery('foo{x="yy"} + metric', 'bar', 'baz')).toBe('foo{x="yy", bar="baz"} + metric{bar="baz"}');
expect(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz')).toBe('avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})'); expect(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz')).toBe('avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})');
expect(addLabelToQuery('foo{x="yy"} * metric{y="zz",a="bb"} * metric2', 'bar', 'baz')).toBe( expect(addLabelToQuery('foo{x="yy"} * metric{y="zz",a="bb"} * metric2', 'bar', 'baz')).toBe(
'foo{bar="baz",x="yy"} * metric{a="bb",bar="baz",y="zz"} * metric2{bar="baz"}' 'foo{x="yy", bar="baz"} * metric{y="zz", a="bb", bar="baz"} * metric2{bar="baz"}'
); );
}); });
it('should not add duplicate labels to a query', () => { it('should not add duplicate labels to a query', () => {
expect(addLabelToQuery(addLabelToQuery('foo{x="yy"}', 'bar', 'baz', '!='), 'bar', 'baz', '!=')).toBe( expect(addLabelToQuery(addLabelToQuery('foo{x="yy"}', 'bar', 'baz', '!='), 'bar', 'baz', '!=')).toBe(
'foo{bar!="baz",x="yy"}' 'foo{x="yy", bar!="baz"}'
); );
expect(addLabelToQuery(addLabelToQuery('rate(metric[1m])', 'foo', 'bar'), 'foo', 'bar')).toBe( expect(addLabelToQuery(addLabelToQuery('rate(metric[1m])', 'foo', 'bar'), 'foo', 'bar')).toBe(
'rate(metric{foo="bar"}[1m])' 'rate(metric{foo="bar"}[1m])'
); );
expect(addLabelToQuery(addLabelToQuery('foo{list="a,b,c"}', 'bar', 'baz'), 'bar', 'baz')).toBe( expect(addLabelToQuery(addLabelToQuery('foo{list="a,b,c"}', 'bar', 'baz'), 'bar', 'baz')).toBe(
'foo{bar="baz",list="a,b,c"}' 'foo{list="a,b,c", bar="baz"}'
); );
expect(addLabelToQuery(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz'), 'bar', 'baz')).toBe( expect(addLabelToQuery(addLabelToQuery('avg(foo) + sum(xx_yy)', 'bar', 'baz'), 'bar', 'baz')).toBe(
'avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})' 'avg(foo{bar="baz"}) + sum(xx_yy{bar="baz"})'
@@ -66,14 +66,8 @@ describe('addLabelToQuery()', () => {
}); });
it('should not remove filters', () => { it('should not remove filters', () => {
expect(addLabelToQuery('{x="y"} |="yy"', 'bar', 'baz')).toBe('{bar="baz",x="y"} |="yy"'); expect(addLabelToQuery('{x="y"} |="yy"', 'bar', 'baz')).toBe('{x="y", bar="baz"} |="yy"');
expect(addLabelToQuery('{x="y"} |="yy" !~"xx"', 'bar', 'baz')).toBe('{bar="baz",x="y"} |="yy" !~"xx"'); expect(addLabelToQuery('{x="y"} |="yy" !~"xx"', 'bar', 'baz')).toBe('{x="y", bar="baz"} |="yy" !~"xx"');
});
it('should add label to query properly with Loki datasource', () => {
expect(addLabelToQuery('{job="grafana"} |= "foo-bar"', 'filename', 'test.txt', undefined, true)).toBe(
'{filename="test.txt",job="grafana"} |= "foo-bar"'
);
}); });
it('should add labels to metrics with logical operators', () => { it('should add labels to metrics with logical operators', () => {
@@ -83,13 +77,13 @@ describe('addLabelToQuery()', () => {
it('should not add ad-hoc filter to template variables', () => { it('should not add ad-hoc filter to template variables', () => {
expect(addLabelToQuery('sum(rate({job="foo"}[2m])) by (value $variable)', 'bar', 'baz')).toBe( expect(addLabelToQuery('sum(rate({job="foo"}[2m])) by (value $variable)', 'bar', 'baz')).toBe(
'sum(rate({bar="baz",job="foo"}[2m])) by (value $variable)' 'sum(rate({job="foo", bar="baz"}[2m])) by (value $variable)'
); );
}); });
it('should not add ad-hoc filter to range', () => { it('should not add ad-hoc filter to range', () => {
expect(addLabelToQuery('avg(rate((my_metric{job="foo"} > 0)[3h:])) by (label)', 'bar', 'baz')).toBe( expect(addLabelToQuery('avg(rate((my_metric{job="foo"} > 0)[3h:])) by (label)', 'bar', 'baz')).toBe(
'avg(rate((my_metric{bar="baz",job="foo"} > 0)[3h:])) by (label)' 'avg(rate((my_metric{job="foo", bar="baz"} > 0)[3h:])) by (label)'
); );
}); });
it('should not add ad-hoc filter to labels in label list provided with the group modifier', () => { it('should not add ad-hoc filter to labels in label list provided with the group modifier', () => {
@@ -100,7 +94,7 @@ describe('addLabelToQuery()', () => {
'baz' 'baz'
) )
).toBe( ).toBe(
'max by (id, name, type) (my_metric{bar="baz",type=~"foo|bar|baz-test"}) * on(id) group_right(id, type, name) sum by (id) (my_metric{bar="baz"}) * 1000' 'max by (id, name, type) (my_metric{type=~"foo|bar|baz-test", bar="baz"}) * on(id) group_right(id, type, name) sum by (id) (my_metric{bar="baz"}) * 1000'
); );
}); });
it('should not add ad-hoc filter to labels in label list provided with the group modifier', () => { it('should not add ad-hoc filter to labels in label list provided with the group modifier', () => {
@@ -110,20 +104,11 @@ describe('addLabelToQuery()', () => {
}); });
it('should not add ad-hoc filter to labels to math operations', () => { it('should not add ad-hoc filter to labels to math operations', () => {
expect(addLabelToQuery('count(my_metric{job!="foo"} < (5*1024*1024*1024) or vector(0)) - 1', 'bar', 'baz')).toBe( expect(addLabelToQuery('count(my_metric{job!="foo"} < (5*1024*1024*1024) or vector(0)) - 1', 'bar', 'baz')).toBe(
'count(my_metric{bar="baz",job!="foo"} < (5*1024*1024*1024) or vector(0)) - 1' 'count(my_metric{job!="foo", bar="baz"} < (5*1024*1024*1024) or vector(0)) - 1'
); );
}); });
});
describe('addLabelToSelector()', () => { it('should not add ad-hoc filter bool operator', () => {
test('should add a label to an empty selector', () => { expect(addLabelToQuery('ALERTS < bool 1', 'bar', 'baz')).toBe('ALERTS{bar="baz"} < bool 1');
expect(addLabelToSelector('{}', 'foo', 'bar')).toBe('{foo="bar"}');
expect(addLabelToSelector('', 'foo', 'bar')).toBe('{foo="bar"}');
});
test('should add a label to a selector', () => {
expect(addLabelToSelector('{foo="bar"}', 'baz', '42')).toBe('{baz="42",foo="bar"}');
});
test('should add a label to a selector with custom operator', () => {
expect(addLabelToSelector('{}', 'baz', '42', '!=')).toBe('{baz!="42"}');
}); });
}); });

View File

@@ -1,129 +1,99 @@
import { chain, isEqual } from 'lodash'; import { parser } from 'lezer-promql';
import { OPERATORS, LOGICAL_OPERATORS, PROM_KEYWORDS } from './promql'; import { buildVisualQueryFromString } from './querybuilder/parsing';
import { PromQueryModeller } from './querybuilder/PromQueryModeller';
import { QueryBuilderLabelFilter } from './querybuilder/shared/types';
import { PromVisualQuery } from './querybuilder/types';
const builtInWords = [...PROM_KEYWORDS, ...OPERATORS, ...LOGICAL_OPERATORS]; /**
* Adds label filter to existing query. Useful for query modification for example for ad hoc filters.
// We want to extract all possible metrics and also keywords *
const metricsAndKeywordsRegexp = /([A-Za-z:][\w:]*)\b(?![\]{=!",])/g; * It uses PromQL parser to find instances of metric and labels, alters them and then splices them back into the query.
// Safari currently doesn't support negative lookbehind. When it does, we should refactor this. * Ideally we could use the parse -> change -> render is a simple 3 steps but right now building the visual query
// We are creating 2 matching groups. (\$) is for the Grafana's variables such as ${__rate_s}. We want to ignore * object does not support all possible queries.
// ${__rate_s} and not add variable to it. *
const selectorRegexp = /(\$)?{([^{]*)}/g; * So instead this just operates on substrings of the query with labels and operates just on those. This makes this
* more robust and can alter even invalid queries, and preserves in general the query structure and whitespace.
export function addLabelToQuery( * @param query
query: string, * @param key
key: string, * @param value
value: string | number, * @param operator
operator?: string, */
hasNoMetrics?: boolean export function addLabelToQuery(query: string, key: string, value: string | number, operator = '='): string {
): string {
if (!key || !value) { if (!key || !value) {
throw new Error('Need label to add to query.'); throw new Error('Need label to add to query.');
} }
const vectorSelectorPositions = getVectorSelectorPositions(query);
if (!vectorSelectorPositions.length) {
return query;
}
const filter = toLabelFilter(key, value, operator);
return addFilter(query, vectorSelectorPositions, filter);
}
type VectorSelectorPosition = { from: number; to: number; query: PromVisualQuery };
/**
* Parse the string and get all VectorSelector positions in the query together with parsed representation of the vector
* selector.
* @param query
*/
function getVectorSelectorPositions(query: string): VectorSelectorPosition[] {
const tree = parser.parse(query);
const positions: VectorSelectorPosition[] = [];
tree.iterate({
enter: (type, from, to, get): false | void => {
if (type.name === 'VectorSelector') {
const visQuery = buildVisualQueryFromString(query.substring(from, to));
positions.push({ query: visQuery.query, from, to });
return false;
}
},
});
return positions;
}
function toLabelFilter(key: string, value: string | number, operator: string): QueryBuilderLabelFilter {
// We need to make sure that we convert the value back to string because it may be a number // We need to make sure that we convert the value back to string because it may be a number
const transformedValue = value === Infinity ? '+Inf' : value.toString(); const transformedValue = value === Infinity ? '+Inf' : value.toString();
return { label: key, op: operator, value: transformedValue };
}
// Add empty selectors to bare metric names function addFilter(
let previousWord: string; query: string,
vectorSelectorPositions: VectorSelectorPosition[],
filter: QueryBuilderLabelFilter
): string {
const modeller = new PromQueryModeller();
let newQuery = '';
let prev = 0;
query = query.replace(metricsAndKeywordsRegexp, (match, word, offset) => { for (let i = 0; i < vectorSelectorPositions.length; i++) {
const isMetric = isWordMetric(query, word, offset, previousWord, hasNoMetrics); // This is basically just doing splice on a string for each matched vector selector.
previousWord = word;
return isMetric ? `${word}{}` : word; const match = vectorSelectorPositions[i];
}); const isLast = i === vectorSelectorPositions.length - 1;
// Adding label to existing selectors const start = query.substring(prev, match.from);
let match = selectorRegexp.exec(query); const end = isLast ? query.substring(match.to) : '';
const parts = [];
let lastIndex = 0;
let suffix = '';
while (match) { if (!labelExists(match.query.labels, filter)) {
const prefix = query.slice(lastIndex, match.index); // We don't want to add duplicate labels.
lastIndex = match.index + match[2].length + 2; match.query.labels.push(filter);
suffix = query.slice(match.index + match[0].length);
// If we matched 1st group, we know it is Grafana's variable and we don't want to add labels
if (match[1]) {
parts.push(prefix);
parts.push(match[0]);
} else {
// If we didn't match first group, we are inside selector and we want to add labels
const selector = match[2];
const selectorWithLabel = addLabelToSelector(selector, key, transformedValue, operator);
parts.push(prefix, selectorWithLabel);
} }
const newLabels = modeller.renderQuery(match.query);
match = selectorRegexp.exec(query); newQuery += start + newLabels + end;
prev = match.to;
} }
return newQuery;
parts.push(suffix);
return parts.join('');
} }
const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g; /**
* Check if label exists in the list of labels but ignore the operator.
export function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) { * @param labels
const parsedLabels = []; * @param filter
*/
// Split selector into labels function labelExists(labels: QueryBuilderLabelFilter[], filter: QueryBuilderLabelFilter) {
if (selector) { return labels.find((label) => label.label === filter.label && label.value === filter.value);
let match = labelRegexp.exec(selector);
while (match) {
parsedLabels.push({ key: match[1], operator: match[2], value: match[3] });
match = labelRegexp.exec(selector);
}
}
// Add new label
const operatorForLabelKey = labelOperator || '=';
parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` });
// Sort labels by key and put them together
const formatted = chain(parsedLabels)
.uniqWith(isEqual)
.compact()
.sortBy('key')
.map(({ key, operator, value }) => `${key}${operator}${value}`)
.value()
.join(',');
return `{${formatted}}`;
} }
function isPositionInsideChars(text: string, position: number, openChar: string, closeChar: string) {
const nextSelectorStart = text.slice(position).indexOf(openChar);
const nextSelectorEnd = text.slice(position).indexOf(closeChar);
return nextSelectorEnd > -1 && (nextSelectorStart === -1 || nextSelectorStart > nextSelectorEnd);
}
function isWordMetric(query: string, word: string, offset: number, previousWord: string, hasNoMetrics?: boolean) {
const insideSelector = isPositionInsideChars(query, offset, '{', '}');
// Handle "sum by (key) (metric)"
const previousWordIsKeyWord = previousWord && OPERATORS.indexOf(previousWord) > -1;
// Check for colon as as "word boundary" symbol
const isColonBounded = word.endsWith(':');
// Check for words that start with " which means that they are not metrics
const startsWithQuote = query[offset - 1] === '"';
// Check for template variables
const isTemplateVariable = query[offset - 1] === '$';
// Check for time units
const isTimeUnit = ['s', 'm', 'h', 'd', 'w'].includes(word) && Boolean(Number(query[offset - 1]));
if (
!hasNoMetrics &&
!insideSelector &&
!isColonBounded &&
!previousWordIsKeyWord &&
!startsWithQuote &&
!isTemplateVariable &&
!isTimeUnit &&
builtInWords.indexOf(word) === -1
) {
return true;
}
return false;
}
export default addLabelToQuery;

View File

@@ -247,7 +247,7 @@ describe('PrometheusDatasource', () => {
}, },
]); ]);
const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0); const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0);
expect(result).toMatchObject({ expr: 'metric{job="foo",k1="v1",k2!="v2"} - metric{k1="v1",k2!="v2"}' }); expect(result).toMatchObject({ expr: 'metric{job="foo", k1="v1", k2!="v2"} - metric{k1="v1", k2!="v2"}' });
}); });
it('should add escaping if needed to regex filter expressions', () => { it('should add escaping if needed to regex filter expressions', () => {
@@ -265,7 +265,7 @@ describe('PrometheusDatasource', () => {
]); ]);
const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0); const result = ds.createQuery(target as any, { interval: '15s' } as any, 0, 0);
expect(result).toMatchObject({ expect(result).toMatchObject({
expr: `metric{job="foo",k1=~"v.*",k2=~"v\\\\'.*"} - metric{k1=~"v.*",k2=~"v\\\\'.*"}`, expr: `metric{job="foo", k1=~"v.*", k2=~"v\\\\'.*"} - metric{k1=~"v.*", k2=~"v\\\\'.*"}`,
}); });
}); });
}); });
@@ -637,7 +637,7 @@ describe('PrometheusDatasource', () => {
}; };
const result = ds.applyTemplateVariables(query, {}); const result = ds.applyTemplateVariables(query, {});
expect(result).toMatchObject({ expr: 'test{job="bar",k1="v1",k2!="v2"}' }); expect(result).toMatchObject({ expr: 'test{job="bar", k1="v1", k2!="v2"}' });
}); });
}); });
@@ -2172,7 +2172,7 @@ describe('modifyQuery', () => {
const result = ds.modifyQuery(query, action); const result = ds.modifyQuery(query, action);
expect(result.refId).toEqual('A'); expect(result.refId).toEqual('A');
expect(result.expr).toEqual('go_goroutines{cluster="us-cluster",pod="pod-123"}'); expect(result.expr).toEqual('go_goroutines{cluster="us-cluster", pod="pod-123"}');
}); });
}); });
}); });
@@ -2202,7 +2202,7 @@ describe('modifyQuery', () => {
const result = ds.modifyQuery(query, action); const result = ds.modifyQuery(query, action);
expect(result.refId).toEqual('A'); expect(result.refId).toEqual('A');
expect(result.expr).toEqual('go_goroutines{cluster="us-cluster",pod!="pod-123"}'); expect(result.expr).toEqual('go_goroutines{cluster="us-cluster", pod!="pod-123"}');
}); });
}); });
}); });

View File

@@ -34,7 +34,7 @@ import {
import { safeStringifyValue } from 'app/core/utils/explore'; import { safeStringifyValue } from 'app/core/utils/explore';
import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { getTimeSrv, TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv'; import { getTemplateSrv, TemplateSrv } from 'app/features/templating/template_srv';
import addLabelToQuery from './add_label_to_query'; import { addLabelToQuery } from './add_label_to_query';
import PrometheusLanguageProvider from './language_provider'; import PrometheusLanguageProvider from './language_provider';
import { expandRecordingRules } from './language_utils'; import { expandRecordingRules } from './language_utils';
import { getInitHints, getQueryHints } from './query_hints'; import { getInitHints, getQueryHints } from './query_hints';
@@ -949,15 +949,15 @@ export class PrometheusDatasource
enhanceExprWithAdHocFilters(expr: string) { enhanceExprWithAdHocFilters(expr: string) {
const adhocFilters = this.templateSrv.getAdhocFilters(this.name); const adhocFilters = this.templateSrv.getAdhocFilters(this.name);
let finalQuery = expr;
finalQuery = adhocFilters.reduce((acc: string, filter: { key?: any; operator?: any; value?: any }) => { const finalQuery = adhocFilters.reduce((acc: string, filter: { key?: any; operator?: any; value?: any }) => {
const { key, operator } = filter; const { key, operator } = filter;
let { value } = filter; let { value } = filter;
if (operator === '=~' || operator === '!~') { if (operator === '=~' || operator === '!~') {
value = prometheusRegularEscape(value); value = prometheusRegularEscape(value);
} }
return addLabelToQuery(acc, key, value, operator); return addLabelToQuery(acc, key, value, operator);
}, finalQuery); }, expr);
return finalQuery; return finalQuery;
} }

View File

@@ -163,7 +163,7 @@ describe('expandRecordingRules()', () => {
expandRecordingRules('metricA{label1="value1",label2="value,2"}', { expandRecordingRules('metricA{label1="value1",label2="value,2"}', {
metricA: 'rate(fooA[])', metricA: 'rate(fooA[])',
}) })
).toBe('rate(fooA{label1="value1",label2="value,2"}[])'); ).toBe('rate(fooA{label1="value1", label2="value,2"}[])');
expect( expect(
expandRecordingRules('metricA{label1="value1"} / metricB{label2="value2"}', { expandRecordingRules('metricA{label1="value1"} / metricB{label2="value2"}', {
metricA: 'rate(fooA[])', metricA: 'rate(fooA[])',
@@ -175,7 +175,7 @@ describe('expandRecordingRules()', () => {
metricA: 'rate(fooA[])', metricA: 'rate(fooA[])',
metricB: 'rate(fooB[])', metricB: 'rate(fooB[])',
}) })
).toBe('rate(fooA{label1="value1",label2="value2"}[])/ rate(fooB{label3="value3"}[])'); ).toBe('rate(fooA{label1="value1", label2="value2"}[])/ rate(fooB{label3="value3"}[])');
}); });
}); });

View File

@@ -152,7 +152,7 @@ function addLabelsToExpression(expr: string, invalidLabelsRegexp: RegExp) {
return ''; return '';
}); });
// Loop trough all of the label objects and add them to query. // Loop through all label objects and add them to query.
// As a starting point we have valid query without the labels. // As a starting point we have valid query without the labels.
let result = exprBeforeRegexMatch; let result = exprBeforeRegexMatch;
arrayOfLabelObjects.filter(Boolean).forEach((obj) => { arrayOfLabelObjects.filter(Boolean).forEach((obj) => {