mirror of https://github.com/grafana/grafana
Loki: Fix support of ad-hoc filters for specific queries (#51232)
* Loki: Refactor ad-hoc filters to use parser * Remove renaming of files for easier review * Update * Update * Add previously buggy test * Fix tests * Fix typos * Update, improve typing * Move reused code up * Update order * Update betterer statsspull/51293/head
parent
4ace36ba01
commit
d3dd3042d6
@ -1,121 +1,155 @@ |
||||
import { addLabelToQuery, addLabelToSelector } from './add_label_to_query'; |
||||
import { addLabelToQuery } from './add_label_to_query'; |
||||
|
||||
describe('addLabelToQuery()', () => { |
||||
it('should add label to simple query', () => { |
||||
expect(() => { |
||||
addLabelToQuery('foo', '', ''); |
||||
addLabelToQuery('foo', '', '=', ''); |
||||
}).toThrow(); |
||||
expect(addLabelToQuery('{}', 'bar', 'baz')).toBe('{bar="baz"}'); |
||||
expect(addLabelToQuery('{x="yy"}', 'bar', 'baz')).toBe('{bar="baz",x="yy"}'); |
||||
expect(addLabelToQuery('{}', 'bar', '=', 'baz')).toBe('{bar="baz"}'); |
||||
expect(addLabelToQuery('{x="yy"}', 'bar', '=', 'baz')).toBe('{x="yy", bar="baz"}'); |
||||
}); |
||||
|
||||
it('should add custom operator', () => { |
||||
expect(addLabelToQuery('{}', 'bar', 'baz', '!=')).toBe('{bar!="baz"}'); |
||||
expect(addLabelToQuery('{x="yy"}', 'bar', 'baz', '!=')).toBe('{bar!="baz",x="yy"}'); |
||||
expect(addLabelToQuery('{}', 'bar', '!=', 'baz')).toBe('{bar!="baz"}'); |
||||
expect(addLabelToQuery('{x="yy"}', 'bar', '!=', 'baz')).toBe('{x="yy", bar!="baz"}'); |
||||
}); |
||||
|
||||
it('should not modify ranges', () => { |
||||
expect(addLabelToQuery('rate({}[1m])', 'foo', 'bar')).toBe('rate({foo="bar"}[1m])'); |
||||
expect(addLabelToQuery('rate({}[1m])', 'foo', '=', 'bar')).toBe('rate({foo="bar"}[1m])'); |
||||
}); |
||||
|
||||
it('should detect in-order function use', () => { |
||||
expect(addLabelToQuery('sum by (xx) ({})', 'bar', 'baz')).toBe('sum by (xx) ({bar="baz"})'); |
||||
}); |
||||
|
||||
it('should convert number Infinity to +Inf', () => { |
||||
expect(addLabelToQuery('sum(rate({}[5m])) by (le)', 'le', Infinity)).toBe('sum(rate({le="+Inf"}[5m])) by (le)'); |
||||
expect(addLabelToQuery('sum by (host) (rate({} [1m]))', 'bar', '=', 'baz')).toBe( |
||||
'sum by (host) (rate({bar="baz"} [1m]))' |
||||
); |
||||
}); |
||||
|
||||
it('should handle selectors with punctuation', () => { |
||||
expect(addLabelToQuery('{instance="my-host.com:9100"}', 'bar', 'baz')).toBe( |
||||
'{bar="baz",instance="my-host.com:9100"}' |
||||
expect(addLabelToQuery('{instance="my-host.com:9100"}', 'bar', '=', 'baz')).toBe( |
||||
'{instance="my-host.com:9100", bar="baz"}' |
||||
); |
||||
expect(addLabelToQuery('{list="a,b,c"}', 'bar', 'baz')).toBe('{bar="baz",list="a,b,c"}'); |
||||
expect(addLabelToQuery('{list="a,b,c"}', 'bar', '=', 'baz')).toBe('{list="a,b,c", bar="baz"}'); |
||||
}); |
||||
|
||||
it('should work on arithmetical expressions', () => { |
||||
expect(addLabelToQuery('{} + {}', 'bar', 'baz')).toBe('{bar="baz"} + {bar="baz"}'); |
||||
expect(addLabelToQuery('avg({}) + sum({})', 'bar', 'baz')).toBe('avg({bar="baz"}) + sum({bar="baz"})'); |
||||
expect(addLabelToQuery('{x="yy"} * {y="zz",a="bb"} * {}', 'bar', 'baz')).toBe( |
||||
'{bar="baz",x="yy"} * {a="bb",bar="baz",y="zz"} * {bar="baz"}' |
||||
expect(addLabelToQuery('{} + {}', 'bar', '=', 'baz')).toBe('{bar="baz"} + {bar="baz"}'); |
||||
expect(addLabelToQuery('avg(rate({x="y"} [$__interval]))+ sum(rate({}[5m]))', 'bar', '=', 'baz')).toBe( |
||||
'avg(rate({x="y", bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))' |
||||
); |
||||
expect(addLabelToQuery('{x="yy"} * {y="zz",a="bb"} * {}', 'bar', '=', 'baz')).toBe( |
||||
'{x="yy", bar="baz"} * {y="zz", a="bb", bar="baz"} * {bar="baz"}' |
||||
); |
||||
}); |
||||
|
||||
it('should not add duplicate labels to a query', () => { |
||||
expect(addLabelToQuery(addLabelToQuery('{x="yy"}', 'bar', 'baz', '!='), 'bar', 'baz', '!=')).toBe( |
||||
'{bar!="baz",x="yy"}' |
||||
expect(addLabelToQuery(addLabelToQuery('{x="yy"}', 'bar', '!=', 'baz'), 'bar', '!=', 'baz')).toBe( |
||||
'{x="yy", bar!="baz"}' |
||||
); |
||||
expect(addLabelToQuery(addLabelToQuery('rate({}[1m])', 'foo', 'bar'), 'foo', 'bar')).toBe('rate({foo="bar"}[1m])'); |
||||
expect(addLabelToQuery(addLabelToQuery('{list="a,b,c"}', 'bar', 'baz'), 'bar', 'baz')).toBe( |
||||
'{bar="baz",list="a,b,c"}' |
||||
expect(addLabelToQuery(addLabelToQuery('rate({}[1m])', 'foo', '=', 'bar'), 'foo', '=', 'bar')).toBe( |
||||
'rate({foo="bar"}[1m])' |
||||
); |
||||
expect(addLabelToQuery(addLabelToQuery('avg({}) + sum({})', 'bar', 'baz'), 'bar', 'baz')).toBe( |
||||
'avg({bar="baz"}) + sum({bar="baz"})' |
||||
expect(addLabelToQuery(addLabelToQuery('{list="a,b,c"}', 'bar', '=', 'baz'), 'bar', '=', 'baz')).toBe( |
||||
'{list="a,b,c", bar="baz"}' |
||||
); |
||||
expect( |
||||
addLabelToQuery( |
||||
addLabelToQuery('avg(rate({bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))', 'bar', '=', 'baz'), |
||||
'bar', |
||||
'=', |
||||
'baz' |
||||
) |
||||
).toBe('avg(rate({bar="baz"} [$__interval]))+ sum(rate({bar="baz"}[5m]))'); |
||||
}); |
||||
|
||||
it('should not remove filters', () => { |
||||
expect(addLabelToQuery('{x="y"} |="yy"', 'bar', 'baz')).toBe('{bar="baz",x="y"} |="yy"'); |
||||
expect(addLabelToQuery('{x="y"} |="yy" !~"xx"', 'bar', 'baz')).toBe('{bar="baz",x="y"} |="yy" !~"xx"'); |
||||
expect(addLabelToQuery('{x="y"} |="yy"', 'bar', '=', 'baz')).toBe('{x="y", bar="baz"} |="yy"'); |
||||
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"' |
||||
expect(addLabelToQuery('{job="grafana"} |= "foo-bar"', 'filename', '=', 'test.txt')).toBe( |
||||
'{job="grafana", filename="test.txt"} |= "foo-bar"' |
||||
); |
||||
}); |
||||
|
||||
it('should add labels to metrics with logical operators', () => { |
||||
expect(addLabelToQuery('{} or {}', 'bar', 'baz')).toBe('{bar="baz"} or {bar="baz"}'); |
||||
expect(addLabelToQuery('{} and {}', 'bar', 'baz')).toBe('{bar="baz"} and {bar="baz"}'); |
||||
expect(addLabelToQuery('{x="y"} or {}', 'bar', '=', 'baz')).toBe('{x="y", bar="baz"} or {bar="baz"}'); |
||||
expect(addLabelToQuery('{x="y"} and {}', 'bar', '=', 'baz')).toBe('{x="y", bar="baz"} and {bar="baz"}'); |
||||
}); |
||||
|
||||
it('should not add ad-hoc filter to template variables', () => { |
||||
expect(addLabelToQuery('sum(rate({job="foo"}[2m])) by (value $variable)', 'bar', 'baz')).toBe( |
||||
'sum(rate({bar="baz",job="foo"}[2m])) by (value $variable)' |
||||
expect(addLabelToQuery('sum(rate({job="foo"}[2m])) by (value $variable)', 'bar', '=', 'baz')).toBe( |
||||
'sum(rate({job="foo", bar="baz"}[2m])) by (value $variable)' |
||||
); |
||||
}); |
||||
|
||||
it('should not add ad-hoc filter to range', () => { |
||||
expect(addLabelToQuery('avg(rate(({job="foo"} > 0)[3h:])) by (label)', 'bar', 'baz')).toBe( |
||||
'avg(rate(({bar="baz",job="foo"} > 0)[3h:])) by (label)' |
||||
expect(addLabelToQuery('avg(rate(({job="foo"} > 0)[3h:])) by (label)', 'bar', '=', 'baz')).toBe( |
||||
'avg(rate(({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', () => { |
||||
expect( |
||||
addLabelToQuery( |
||||
'max by (id, name, type) ({type=~"foo|bar|baz-test"}) * on(id) group_right(id, type, name) sum by (id) ({}) * 1000', |
||||
'max by (id, name, type) ({type=~"foo|bar|baz-test"}) * on(id) group_right(id, type, name) sum by (id) (rate({} [5m])) * 1000', |
||||
'bar', |
||||
'=', |
||||
'baz' |
||||
) |
||||
).toBe( |
||||
'max by (id, name, type) ({bar="baz",type=~"foo|bar|baz-test"}) * on(id) group_right(id, type, name) sum by (id) ({bar="baz"}) * 1000' |
||||
'max by (id, name, type) ({type=~"foo|bar|baz-test", bar="baz"}) * on(id) group_right(id, type, name) sum by (id) (rate({bar="baz"} [5m])) * 1000' |
||||
); |
||||
}); |
||||
it('should not add ad-hoc filter to labels in label list provided with the group modifier', () => { |
||||
expect(addLabelToQuery('rate({}[${__range_s}s])', 'bar', 'baz')).toBe('rate({bar="baz"}[${__range_s}s])'); |
||||
}); |
||||
it('should not add ad-hoc filter to labels to math operations', () => { |
||||
expect(addLabelToQuery('count({job!="foo"} < (5*1024*1024*1024) or vector(0)) - 1', 'bar', 'baz')).toBe( |
||||
'count({bar="baz",job!="foo"} < (5*1024*1024*1024) or vector(0)) - 1' |
||||
expect(addLabelToQuery('rate({x="y"}[${__range_s}s])', 'bar', '=', 'baz')).toBe( |
||||
'rate({x="y", bar="baz"}[${__range_s}s])' |
||||
); |
||||
}); |
||||
it('should not add adhoc filter to line_format expressions', () => { |
||||
expect(addLabelToQuery('{foo="bar"} | logfmt | line_format {{.status}}', 'bar', 'baz')).toBe( |
||||
'{bar="baz",foo="bar"} | logfmt | line_format {{.status}}' |
||||
it('should not add ad-hoc filter to labels to math operations', () => { |
||||
expect(addLabelToQuery('count({job!="foo"} < (5*1024*1024*1024) or vector(0)) - 1', 'bar', '=', 'baz')).toBe( |
||||
'count({job!="foo", bar="baz"} < (5*1024*1024*1024) or vector(0)) - 1' |
||||
); |
||||
}); |
||||
}); |
||||
|
||||
describe('addLabelToSelector()', () => { |
||||
test('should add a label to an empty selector', () => { |
||||
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"}'); |
||||
describe('should add label as label filter is query with parser', () => { |
||||
it('should add label filter after parser', () => { |
||||
expect(addLabelToQuery('{foo="bar"} | logfmt', 'bar', '=', 'baz')).toBe('{foo="bar"} | logfmt | bar=`baz`'); |
||||
}); |
||||
it('should add label filter after multiple parsers', () => { |
||||
expect(addLabelToQuery('{foo="bar"} | logfmt | json', 'bar', '=', 'baz')).toBe( |
||||
'{foo="bar"} | logfmt | bar=`baz` | json | bar=`baz`' |
||||
); |
||||
}); |
||||
it('should add label filter after parser when multiple label filters', () => { |
||||
expect(addLabelToQuery('{foo="bar"} | logfmt | x="y"', 'bar', '=', 'baz')).toBe( |
||||
'{foo="bar"} | logfmt | bar=`baz` | x="y"' |
||||
); |
||||
}); |
||||
it('should add label filter in metric query', () => { |
||||
expect(addLabelToQuery('rate({foo="bar"} | logfmt [5m])', 'bar', '=', 'baz')).toBe( |
||||
'rate({foo="bar"} | logfmt | bar=`baz` [5m])' |
||||
); |
||||
}); |
||||
it('should add label filter in complex metric query', () => { |
||||
expect( |
||||
addLabelToQuery( |
||||
'sum by(host) (rate({foo="bar"} | logfmt | x="y" | line_format "{{.status}}" [5m]))', |
||||
'bar', |
||||
'=', |
||||
'baz' |
||||
) |
||||
).toBe('sum by(host) (rate({foo="bar"} | logfmt | bar=`baz` | x="y" | line_format "{{.status}}" [5m]))'); |
||||
}); |
||||
it('should not add adhoc filter to line_format expressions', () => { |
||||
expect(addLabelToQuery('{foo="bar"} | logfmt | line_format "{{.status}}"', 'bar', '=', 'baz')).toBe( |
||||
'{foo="bar"} | logfmt | bar=`baz` | line_format "{{.status}}"' |
||||
); |
||||
}); |
||||
|
||||
it('should not add adhoc filter to line_format expressions', () => { |
||||
expect(addLabelToQuery('{foo="bar"} | logfmt | line_format "{{status}}"', 'bar', '=', 'baz')).toBe( |
||||
'{foo="bar"} | logfmt | bar=`baz` | line_format "{{status}}"' |
||||
); |
||||
}); |
||||
}); |
||||
}); |
||||
|
||||
@ -1,140 +1,159 @@ |
||||
import { chain, isEqual } from 'lodash'; |
||||
|
||||
import { PROM_KEYWORDS, OPERATORS, LOGICAL_OPERATORS } from 'app/plugins/datasource/prometheus/promql'; |
||||
|
||||
import { LOKI_KEYWORDS } from './syntax'; |
||||
|
||||
const builtInWords = [...PROM_KEYWORDS, ...OPERATORS, ...LOGICAL_OPERATORS, ...LOKI_KEYWORDS]; |
||||
|
||||
// We want to extract all possible metrics and also keywords
|
||||
const metricsAndKeywordsRegexp = /([A-Za-z:][\w:]*)\b(?![\]{=!",])/g; |
||||
|
||||
export function addLabelToQuery( |
||||
query: string, |
||||
key: string, |
||||
value: string | number, |
||||
operator?: string, |
||||
hasNoMetrics?: boolean |
||||
): string { |
||||
import { parser } from '@grafana/lezer-logql'; |
||||
|
||||
import { QueryBuilderLabelFilter } from '../prometheus/querybuilder/shared/types'; |
||||
|
||||
import { LokiQueryModeller } from './querybuilder/LokiQueryModeller'; |
||||
import { buildVisualQueryFromString } from './querybuilder/parsing'; |
||||
import { LokiVisualQuery } from './querybuilder/types'; |
||||
|
||||
/** |
||||
* Adds label filter to existing query. Useful for query modification for example for ad hoc filters. |
||||
* |
||||
* It uses LogQL parser to find instances of labels, alters them and then splices them back into the query. |
||||
* In a case when we have parser, instead of adding new instance of label it adds label filter after the parser. |
||||
* |
||||
* This 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. |
||||
* |
||||
* @param query |
||||
* @param key |
||||
* @param value |
||||
* @param operator |
||||
*/ |
||||
export function addLabelToQuery(query: string, key: string, operator: string, value: string): string { |
||||
if (!key || !value) { |
||||
throw new Error('Need label to add to query.'); |
||||
} |
||||
|
||||
// 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 streamSelectorPositions = getStreamSelectorPositions(query); |
||||
const parserPositions = getParserPositions(query); |
||||
if (!streamSelectorPositions.length) { |
||||
return query; |
||||
} |
||||
|
||||
// Add empty selectors to bare metric names
|
||||
let previousWord: string; |
||||
const filter = toLabelFilter(key, value, operator); |
||||
if (!parserPositions.length) { |
||||
return addFilterToStreamSelector(query, streamSelectorPositions, filter); |
||||
} else { |
||||
return addFilterAsLabelFilter(query, parserPositions, filter); |
||||
} |
||||
} |
||||
|
||||
query = query.replace(metricsAndKeywordsRegexp, (match, word, offset) => { |
||||
const isMetric = isWordMetric(query, word, offset, previousWord, hasNoMetrics); |
||||
previousWord = word; |
||||
type StreamSelectorPosition = { from: number; to: number; query: LokiVisualQuery }; |
||||
type PipelineStagePosition = { from: number; to: number }; |
||||
|
||||
/** |
||||
* Parse the string and get all Selector positions in the query together with parsed representation of the |
||||
* selector. |
||||
* @param query |
||||
*/ |
||||
function getStreamSelectorPositions(query: string): StreamSelectorPosition[] { |
||||
const tree = parser.parse(query); |
||||
const positions: StreamSelectorPosition[] = []; |
||||
tree.iterate({ |
||||
enter: (type, from, to, get): false | void => { |
||||
if (type.name === 'Selector') { |
||||
const visQuery = buildVisualQueryFromString(query.substring(from, to)); |
||||
positions.push({ query: visQuery.query, from, to }); |
||||
return false; |
||||
} |
||||
}, |
||||
}); |
||||
return positions; |
||||
} |
||||
|
||||
return isMetric ? `${word}{}` : word; |
||||
/** |
||||
* Parse the string and get all LabelParser positions in the query. |
||||
* @param query |
||||
*/ |
||||
function getParserPositions(query: string): PipelineStagePosition[] { |
||||
const tree = parser.parse(query); |
||||
const positions: PipelineStagePosition[] = []; |
||||
tree.iterate({ |
||||
enter: (type, from, to, get): false | void => { |
||||
if (type.name === 'LabelParser') { |
||||
positions.push({ from, to }); |
||||
return false; |
||||
} |
||||
}, |
||||
}); |
||||
return positions; |
||||
} |
||||
|
||||
//This is a RegExp for stream selector - e.g. {job="grafana"}
|
||||
const selectorRegexp = /(\$)?{([^{]*)}/g; |
||||
const parts = []; |
||||
let lastIndex = 0; |
||||
let suffix = ''; |
||||
|
||||
let match = selectorRegexp.exec(query); |
||||
/* |
||||
There are 2 possible false positive scenarios:
|
||||
|
||||
1. We match Grafana's variables with ${ syntax - such as${__rate_s}. To filter these out we could use negative lookbehind, |
||||
but Safari browser currently doesn't support it. Therefore we need to hack this by creating 2 matching groups.
|
||||
(\$) is for the Grafana's variables and if we match it, we know this is not a stream selector and we don't want to add label. |
||||
|
||||
2. Log queries can include {{.label}} syntax when line_format is used. We need to filter these out by checking |
||||
if match starts with "{." |
||||
*/ |
||||
while (match) { |
||||
const prefix = query.slice(lastIndex, match.index); |
||||
lastIndex = match.index + match[2].length + 2; |
||||
suffix = query.slice(match.index + match[0].length); |
||||
|
||||
// Filtering our false positives
|
||||
if (match[0].startsWith('{.') || 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); |
||||
} |
||||
function toLabelFilter(key: string, value: string, operator: string): QueryBuilderLabelFilter { |
||||
// We need to make sure that we convert the value back to string because it may be a number
|
||||
return { label: key, op: operator, value }; |
||||
} |
||||
|
||||
match = selectorRegexp.exec(query); |
||||
} |
||||
/** |
||||
* Add filter as to stream selectors |
||||
* @param query |
||||
* @param vectorSelectorPositions |
||||
* @param filter |
||||
*/ |
||||
function addFilterToStreamSelector( |
||||
query: string, |
||||
vectorSelectorPositions: StreamSelectorPosition[], |
||||
filter: QueryBuilderLabelFilter |
||||
): string { |
||||
const modeller = new LokiQueryModeller(); |
||||
let newQuery = ''; |
||||
let prev = 0; |
||||
|
||||
parts.push(suffix); |
||||
return parts.join(''); |
||||
} |
||||
for (let i = 0; i < vectorSelectorPositions.length; i++) { |
||||
// This is basically just doing splice on a string for each matched vector selector.
|
||||
|
||||
const labelRegexp = /(\w+)\s*(=|!=|=~|!~)\s*("[^"]*")/g; |
||||
const match = vectorSelectorPositions[i]; |
||||
const isLast = i === vectorSelectorPositions.length - 1; |
||||
|
||||
export function addLabelToSelector(selector: string, labelKey: string, labelValue: string, labelOperator?: string) { |
||||
const parsedLabels = []; |
||||
const start = query.substring(prev, match.from); |
||||
const end = isLast ? query.substring(match.to) : ''; |
||||
|
||||
// Split selector into labels
|
||||
if (selector) { |
||||
let match = labelRegexp.exec(selector); |
||||
while (match) { |
||||
parsedLabels.push({ key: match[1], operator: match[2], value: match[3] }); |
||||
match = labelRegexp.exec(selector); |
||||
if (!labelExists(match.query.labels, filter)) { |
||||
// We don't want to add duplicate labels.
|
||||
match.query.labels.push(filter); |
||||
} |
||||
const newLabels = modeller.renderQuery(match.query); |
||||
newQuery += start + newLabels + end; |
||||
prev = match.to; |
||||
} |
||||
return newQuery; |
||||
} |
||||
|
||||
// Add new label
|
||||
const operatorForLabelKey = labelOperator || '='; |
||||
parsedLabels.push({ key: labelKey, operator: operatorForLabelKey, value: `"${labelValue}"` }); |
||||
/** |
||||
* Add filter as label filter after the parsers |
||||
* @param query |
||||
* @param parserPositions |
||||
* @param filter |
||||
*/ |
||||
function addFilterAsLabelFilter( |
||||
query: string, |
||||
parserPositions: PipelineStagePosition[], |
||||
filter: QueryBuilderLabelFilter |
||||
): string { |
||||
let newQuery = ''; |
||||
let prev = 0; |
||||
|
||||
// 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(','); |
||||
for (let i = 0; i < parserPositions.length; i++) { |
||||
// This is basically just doing splice on a string for each matched vector selector.
|
||||
const match = parserPositions[i]; |
||||
const isLast = i === parserPositions.length - 1; |
||||
|
||||
return `{${formatted}}`; |
||||
} |
||||
const start = query.substring(prev, match.to); |
||||
const end = isLast ? query.substring(match.to) : ''; |
||||
|
||||
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; |
||||
const labelFilter = ` | ${filter.label}${filter.op}\`${filter.value}\``; |
||||
newQuery += start + labelFilter + end; |
||||
prev = match.to; |
||||
} |
||||
return false; |
||||
return newQuery; |
||||
} |
||||
|
||||
export default addLabelToQuery; |
||||
/** |
||||
* Check if label exists in the list of labels but ignore the operator. |
||||
* @param labels |
||||
* @param filter |
||||
*/ |
||||
function labelExists(labels: QueryBuilderLabelFilter[], filter: QueryBuilderLabelFilter) { |
||||
return labels.find((label) => label.label === filter.label && label.value === filter.value); |
||||
} |
||||
|
||||
Loading…
Reference in new issue