The open and composable observability and data visualization platform. Visualize metrics, logs, and traces from multiple sources like Prometheus, Loki, Elasticsearch, InfluxDB, Postgres and many more.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
grafana/public/app/plugins/datasource/loki/queryUtils.test.ts

340 lines
12 KiB

import {
getHighlighterExpressionsFromQuery,
getNormalizedLokiQuery,
isLogsQuery,
isQueryWithLabelFormat,
isQueryWithParser,
isValidQuery,
parseToNodeNamesArray,
Loki Query Editor: Autocompletion and suggestions improvements (unwrap, parser, extracted labels) (#59103) * Chore: refactor test to improve internal categorization of scenarios * feat(loki-monaco-unwrap): add unwrap situation support * Chore: remove redundant path from aggregation situation * feat(loki-monaco-unwrap): add unwrap suggestions * feat(loki-monaco-autocomplete): rename IN_DURATION and add missing tests * feat(loki-monaco-autocomplete): detect parser and line filter * feat(loki-monaco-autocomplete): optionally suggest line filters and parsers * feat(loki-monaco-autocomplete): improve suggestions and update completions tests * feat(loki-monaco-autocomplete): allow for multiple line filters suggestions * Chore: update situations test * Chore: add test case for AFTER_UNWRAP completion * feat(loki-monaco-autocomplete): use logs query instead of labels for data sample * Chore: improve getParser function name and add tests * Chore: update test mock data * Update public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/situation.test.ts Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * feat(loki-monaco-autocomplete): improve after unwrap detection * feat(loki-monaco-autocomplete): remove leftover parser detection * Chore: completely remove parser suggestion exclusion implementation It was correct to have more than one parser, so we don't need to exclude parsers if one is present. Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
3 years ago
getParserFromQuery,
obfuscate,
requestSupportsSplitting,
} from './queryUtils';
import { LokiQuery, LokiQueryType } from './types';
describe('getHighlighterExpressionsFromQuery', () => {
it('returns no expressions for empty query', () => {
expect(getHighlighterExpressionsFromQuery('')).toEqual([]);
});
it('returns no expression for query with empty filter ', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= ``')).toEqual([]);
});
it('returns no expression for query with empty filter and parser', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= `` | json count="counter" | __error__=``')).toEqual([]);
});
it('returns no expression for query with empty filter and chained filter', () => {
expect(
getHighlighterExpressionsFromQuery('{foo="bar"} |= `` |= `highlight` | json count="counter" | __error__=``')
).toEqual(['highlight']);
});
it('returns no expression for query with empty filter, chained and regex filter', () => {
expect(
getHighlighterExpressionsFromQuery(
'{foo="bar"} |= `` |= `highlight` |~ `high.ight` | json count="counter" | __error__=``'
)
).toEqual(['highlight', 'high.ight']);
});
it('returns no expression for query with empty filter, chained and regex quotes filter', () => {
expect(
getHighlighterExpressionsFromQuery(
'{foo="bar"} |= `` |= `highlight` |~ "highlight\\\\d" | json count="counter" | __error__=``'
)
).toEqual(['highlight', 'highlight\\d']);
});
it('returns an expression for query with filter using quotes', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x"')).toEqual(['x']);
});
it('returns an expression for query with filter using backticks', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= `x`')).toEqual(['x']);
});
it('returns expressions for query with filter chain', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x" |~ "y"')).toEqual(['x', 'y']);
});
it('returns expressions for query with filter chain using both backticks and quotes', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x" |~ `y`')).toEqual(['x', 'y']);
});
it('returns expression for query with log parser', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x" | logfmt')).toEqual(['x']);
});
it('returns expressions for query with filter chain followed by log parser', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x" |~ "y" | logfmt')).toEqual(['x', 'y']);
});
it('returns drops expressions for query with negative filter chain using quotes', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x" != "y"')).toEqual(['x']);
});
it('returns expressions for query with filter chain using backticks', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= `x` |~ `y`')).toEqual(['x', 'y']);
});
it('returns expressions for query with filter chain using quotes and backticks', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x" |~ `y`')).toEqual(['x', 'y']);
});
it('returns null if filter term is not wrapped in double quotes', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= x')).toEqual([]);
});
it('escapes filter term if regex filter operator is not used', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |= "x[yz].w"')).toEqual(['x\\[yz\\]\\.w']);
});
it('does not escape filter term if regex filter operator is used', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |~ "x[yz].w" |~ "z.+"')).toEqual(['x[yz].w', 'z.+']);
});
it('removes extra backslash escaping if regex filter operator and quotes are used', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |~ "\\\\w+"')).toEqual(['\\w+']);
});
it('does not remove backslash escaping if regex filter operator and backticks are used', () => {
expect(getHighlighterExpressionsFromQuery('{foo="bar"} |~ `\\w+`')).toEqual(['\\w+']);
});
it.each`
input | expected
${'`"test"`'} | ${'"test"'}
${'"`test`"'} | ${'`test`'}
${'`"test"a`'} | ${'"test"a'}
`('should correctly identify the type of quote used in the term', ({ input, expected }) => {
expect(getHighlighterExpressionsFromQuery(`{foo="bar"} |= ${input}`)).toEqual([expected]);
});
});
describe('getNormalizedLokiQuery', () => {
function expectNormalized(inputProps: Object, outputQueryType: LokiQueryType) {
const input: LokiQuery = { refId: 'A', expr: 'test1', ...inputProps };
const output = getNormalizedLokiQuery(input);
expect(output).toStrictEqual({ refId: 'A', expr: 'test1', queryType: outputQueryType });
}
it('handles no props case', () => {
expectNormalized({}, LokiQueryType.Range);
});
it('handles old-style instant case', () => {
expectNormalized({ instant: true, range: false }, LokiQueryType.Instant);
});
it('handles old-style range case', () => {
expectNormalized({ instant: false, range: true }, LokiQueryType.Range);
});
it('handles new+old style instant', () => {
expectNormalized({ instant: true, range: false, queryType: LokiQueryType.Range }, LokiQueryType.Range);
});
it('handles new+old style range', () => {
expectNormalized({ instant: false, range: true, queryType: LokiQueryType.Instant }, LokiQueryType.Instant);
});
it('handles new<>old conflict (new wins), range', () => {
expectNormalized({ instant: false, range: true, queryType: LokiQueryType.Range }, LokiQueryType.Range);
});
it('handles new<>old conflict (new wins), instant', () => {
expectNormalized({ instant: true, range: false, queryType: LokiQueryType.Instant }, LokiQueryType.Instant);
});
it('handles invalid new, range', () => {
expectNormalized({ queryType: 'invalid' }, LokiQueryType.Range);
});
it('handles invalid new, when old-range exists, use old', () => {
expectNormalized({ instant: false, range: true, queryType: 'invalid' }, LokiQueryType.Range);
});
it('handles invalid new, when old-instant exists, use old', () => {
expectNormalized({ instant: true, range: false, queryType: 'invalid' }, LokiQueryType.Instant);
});
});
describe('isValidQuery', () => {
it('returns false if invalid query', () => {
expect(isValidQuery('{job="grafana')).toBe(false);
});
it('returns true if valid query', () => {
expect(isValidQuery('{job="grafana"}')).toBe(true);
});
});
describe('parseToNodeNamesArray', () => {
it('returns on empty query', () => {
expect(parseToNodeNamesArray('{}')).toEqual(['LogQL', 'Expr', 'LogExpr', 'Selector', '⚠']);
});
it('returns on invalid query', () => {
expect(parseToNodeNamesArray('{job="grafana"')).toEqual([
'LogQL',
'Expr',
'LogExpr',
'Selector',
'Matchers',
'Matcher',
'Identifier',
'Eq',
'String',
'⚠',
]);
});
it('returns on valid query', () => {
expect(parseToNodeNamesArray('{job="grafana"}')).toEqual([
'LogQL',
'Expr',
'LogExpr',
'Selector',
'Matchers',
'Matcher',
'Identifier',
'Eq',
'String',
]);
});
});
describe('obfuscate', () => {
it('obfuscates on invalid query', () => {
expect(obfuscate('{job="grafana"')).toEqual('{Identifier=String');
});
it('obfuscates on valid query', () => {
expect(
obfuscate('sum(sum_over_time({test="test"} |= `` | logfmt | __error__=`` | unwrap test | __error__=`` [10m]))')
).toEqual(
'sum(sum_over_time({Identifier=String} |= String | logfmt | __error__=String | unwrap Identifier | __error__=String [10m]))'
);
});
it('obfuscates on arithmetic operation', () => {
expect(obfuscate('2 + 3')).toEqual('Number + Number');
});
it('obfuscates a comment', () => {
expect(obfuscate('{job="grafana"} # test comment')).toEqual('{Identifier=String} LineComment');
});
it('does not obfuscate interval variables', () => {
expect(
obfuscate(
'sum(quantile_over_time(0.5, {label="$var"} | logfmt | __error__=`` | unwrap latency | __error__=`` [$__interval]))'
)
).toEqual(
'sum(quantile_over_time(Number, {Identifier=String} | logfmt | __error__=String | unwrap Identifier | __error__=String [$__interval]))'
);
});
});
describe('isLogsQuery', () => {
it('returns false if metrics query', () => {
expect(isLogsQuery('rate({job="grafana"}[5m])')).toBe(false);
});
it('returns true if valid query', () => {
expect(isLogsQuery('{job="grafana"}')).toBe(true);
});
});
describe('isQueryWithParser', () => {
it('returns false if query without parser', () => {
expect(isQueryWithParser('rate({job="grafana" |= "error" }[5m])')).toEqual({
parserCount: 0,
queryWithParser: false,
});
});
it('returns true if log query with parser', () => {
expect(isQueryWithParser('{job="grafana"} | json')).toEqual({ parserCount: 1, queryWithParser: true });
});
it('returns true if metric query with parser', () => {
expect(isQueryWithParser('rate({job="grafana"} | json [5m])')).toEqual({ parserCount: 1, queryWithParser: true });
});
it('returns true if query with json parser with expressions', () => {
expect(isQueryWithParser('rate({job="grafana"} | json foo="bar", bar="baz" [5m])')).toEqual({
parserCount: 1,
queryWithParser: true,
});
});
});
describe('isQueryWithLabelFormat', () => {
it('returns true if log query with label format', () => {
expect(isQueryWithLabelFormat('{job="grafana"} | label_format level=lvl')).toBe(true);
});
it('returns true if metrics query with label format', () => {
expect(isQueryWithLabelFormat('rate({job="grafana"} | label_format level=lvl [5m])')).toBe(true);
});
it('returns false if log query without label format', () => {
expect(isQueryWithLabelFormat('{job="grafana"} | json')).toBe(false);
});
it('returns false if metrics query without label format', () => {
expect(isQueryWithLabelFormat('rate({job="grafana"} [5m])')).toBe(false);
});
});
Loki Query Editor: Autocompletion and suggestions improvements (unwrap, parser, extracted labels) (#59103) * Chore: refactor test to improve internal categorization of scenarios * feat(loki-monaco-unwrap): add unwrap situation support * Chore: remove redundant path from aggregation situation * feat(loki-monaco-unwrap): add unwrap suggestions * feat(loki-monaco-autocomplete): rename IN_DURATION and add missing tests * feat(loki-monaco-autocomplete): detect parser and line filter * feat(loki-monaco-autocomplete): optionally suggest line filters and parsers * feat(loki-monaco-autocomplete): improve suggestions and update completions tests * feat(loki-monaco-autocomplete): allow for multiple line filters suggestions * Chore: update situations test * Chore: add test case for AFTER_UNWRAP completion * feat(loki-monaco-autocomplete): use logs query instead of labels for data sample * Chore: improve getParser function name and add tests * Chore: update test mock data * Update public/app/plugins/datasource/loki/components/monaco-query-field/monaco-completion-provider/situation.test.ts Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com> * feat(loki-monaco-autocomplete): improve after unwrap detection * feat(loki-monaco-autocomplete): remove leftover parser detection * Chore: completely remove parser suggestion exclusion implementation It was correct to have more than one parser, so we don't need to exclude parsers if one is present. Co-authored-by: Ivana Huckova <30407135+ivanahuckova@users.noreply.github.com>
3 years ago
describe('getParserFromQuery', () => {
it('returns no parser', () => {
expect(getParserFromQuery('{job="grafana"}')).toBeUndefined();
});
it.each(['json', 'logfmt', 'pattern', 'regexp', 'unpack'])('detects %s parser', (parser: string) => {
expect(getParserFromQuery(`{job="grafana"} | ${parser}`)).toBe(parser);
expect(getParserFromQuery(`sum(count_over_time({place="luna"} | ${parser} | unwrap counter )) by (place)`)).toBe(
parser
);
});
});
Loki Query Splitting: Split queries into sub-queries with smaller time interval (#62767) * Range splitting: range splitting function * Range splitting: experiment with 1 hour splits * Range splitting: reorganize code * Range splitting: improve code readability and meaning * Range splitting: add partition limit to prevent infinite loops * Range splitting: add error handling * Range splitting: disable for logs queries * Range splitting: support any arbitrary time splitting + respect original from/to in the partition * Chore: remove console logs * Chore: delete unused import * Range splitting: actually send requests in sequence * Range splitting: do not split when > 1 query * Range splitting: combine frames * Chore: rename function * split in reverse * polished reversing * keep reference to the right frame in the result * Range splitting: change request state to Streaming * Range splitting: fix moving only 1 unit of time instead of the provided one * Chore: change default parameter to timeShift = 1 * Range splitting: do not split for range queqries * Range splitting: add initial support for log queries * Range splitting: do not use MutableDataFrame It has bad performance and it's not required * Chore: remove unused export * Query Splitting: move to module * loki: split: fix off-by-one error (#62966) loki: split: fix off-by-one loop * Range splitting: disable for logs volume queries * Range splitting: combine any number of fields, not just hardcoded 2 * Range splitting: optimize frame-combining function * Range splitting: further optimize * Range splitting: combine frame length * Range splitting: combine stats * Range splitting: combine stats without assuming the same order * Query splitting: catch and raise errors * Range splitting: create feature flag * Range splitting: implement feature flag * Range splitting: add unit test for datasource query * Range splitting: add basic test for runPartitionedQuery * Range splitting: add unit test for resultLimitReached * Range splitting: test frame merging * Chore: fix unit test --------- Co-authored-by: Sven Grossmann <svennergr@gmail.com> Co-authored-by: Gábor Farkas <gabor.farkas@gmail.com>
2 years ago
describe('requestSupportsSplitting', () => {
it('hidden requests are not partitioned', () => {
const requests: LokiQuery[] = [
{
expr: '{a="b"}',
refId: 'A',
hide: true,
},
];
expect(requestSupportsSplitting(requests)).toBe(false);
});
it('special requests are not partitioned', () => {
const requests: LokiQuery[] = [
{
expr: '{a="b"}',
refId: 'do-not-chunk',
},
];
expect(requestSupportsSplitting(requests)).toBe(false);
});
it('empty requests are not partitioned', () => {
const requests: LokiQuery[] = [
{
expr: '',
refId: 'A',
},
];
expect(requestSupportsSplitting(requests)).toBe(false);
});
it('all other requests are partitioned', () => {
const requests: LokiQuery[] = [
{
expr: '{a="b"}',
refId: 'A',
},
{
expr: 'count_over_time({a="b"}[1h])',
refId: 'B',
},
];
expect(requestSupportsSplitting(requests)).toBe(true);
});
});