Prometheus: Use fuzzy string matching to autocomplete metric names and label (#32207)

* Fuzzy search prototype

* Aggregate filter and sorting functions for auto-complete suggestions

* Add a test for fuzzy search

* Simplify setting fuzzy search information

* Rename SimpleHighlighter

* Test PartialHighlighter

* Add PartialHighlighter snapshot

* Simplify PartialHighlighter

* Revert env change

* Clean up the code

* Add fuzzy search for labels

* Bring back backwards compatiblity

* Expose search function type only

* Update docs

* Covert snapshot test to assertions

* Fix docs

* Fix language provider test

* Add a test for autocomplete logic

* Clean up

* Mock Editor functions

* Add fuzzy search to Prometheus labels

* Add docs about backwards compatibility

* Simplify main fuzzy search loop
This commit is contained in:
Piotr Jamróz
2021-04-16 17:06:33 +02:00
committed by GitHub
parent 2c862678ab
commit dd095642e2
15 changed files with 614 additions and 56 deletions

View File

@@ -18,7 +18,7 @@ import { CloudWatchQuery, TSDBResponse } from './types';
import { AbsoluteTimeRange, HistoryItem, LanguageProvider } from '@grafana/data';
import { CloudWatchDatasource } from './datasource';
import { Token, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import { CompletionItemGroup, SearchFunctionType, Token, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import Prism, { Grammar } from 'prismjs';
export type CloudWatchHistoryItem = HistoryItem<CloudWatchQuery>;
@@ -167,8 +167,12 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
private handleKeyword = async (context?: TypeaheadContext): Promise<TypeaheadOutput> => {
const suggs = await this.getFieldCompletionItems(context?.logGroupNames ?? []);
const functionSuggestions = [
{ prefixMatch: true, label: 'Functions', items: STRING_FUNCTIONS.concat(DATETIME_FUNCTIONS, IP_FUNCTIONS) },
const functionSuggestions: CompletionItemGroup[] = [
{
searchFunctionType: SearchFunctionType.Prefix,
label: 'Functions',
items: STRING_FUNCTIONS.concat(DATETIME_FUNCTIONS, IP_FUNCTIONS),
},
];
suggs.suggestions.push(...functionSuggestions);
@@ -244,7 +248,7 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
return {
suggestions: [
{
prefixMatch: true,
searchFunctionType: SearchFunctionType.Prefix,
label: 'Sort Order',
items: [
{
@@ -268,22 +272,32 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
};
private getCommandCompletionItems = (): TypeaheadOutput => {
return { suggestions: [{ prefixMatch: true, label: 'Commands', items: QUERY_COMMANDS }] };
return {
suggestions: [{ searchFunctionType: SearchFunctionType.Prefix, label: 'Commands', items: QUERY_COMMANDS }],
};
};
private getFieldAndFilterFunctionCompletionItems = (): TypeaheadOutput => {
return { suggestions: [{ prefixMatch: true, label: 'Functions', items: FIELD_AND_FILTER_FUNCTIONS }] };
return {
suggestions: [
{ searchFunctionType: SearchFunctionType.Prefix, label: 'Functions', items: FIELD_AND_FILTER_FUNCTIONS },
],
};
};
private getStatsAggCompletionItems = (): TypeaheadOutput => {
return { suggestions: [{ prefixMatch: true, label: 'Functions', items: AGGREGATION_FUNCTIONS_STATS }] };
return {
suggestions: [
{ searchFunctionType: SearchFunctionType.Prefix, label: 'Functions', items: AGGREGATION_FUNCTIONS_STATS },
],
};
};
private getBoolFuncCompletionItems = (): TypeaheadOutput => {
return {
suggestions: [
{
prefixMatch: true,
searchFunctionType: SearchFunctionType.Prefix,
label: 'Functions',
items: BOOLEAN_FUNCTIONS,
},
@@ -295,7 +309,7 @@ export class CloudWatchLanguageProvider extends LanguageProvider {
return {
suggestions: [
{
prefixMatch: true,
searchFunctionType: SearchFunctionType.Prefix,
label: 'Functions',
items: NUMERIC_OPERATORS.concat(BOOLEAN_FUNCTIONS),
},

View File

@@ -29,13 +29,13 @@ const NS_IN_MS = 1000000;
// When changing RATE_RANGES, check if Prometheus/PromQL ranges should be changed too
// @see public/app/plugins/datasource/prometheus/promql.ts
const RATE_RANGES: CompletionItem[] = [
{ label: '$__interval', sortText: '$__interval' },
{ label: '1m', sortText: '00:01:00' },
{ label: '5m', sortText: '00:05:00' },
{ label: '10m', sortText: '00:10:00' },
{ label: '30m', sortText: '00:30:00' },
{ label: '1h', sortText: '01:00:00' },
{ label: '1d', sortText: '24:00:00' },
{ label: '$__interval', sortValue: '$__interval' },
{ label: '1m', sortValue: '00:01:00' },
{ label: '5m', sortValue: '00:05:00' },
{ label: '10m', sortValue: '00:10:00' },
{ label: '30m', sortValue: '00:30:00' },
{ label: '1h', sortValue: '01:00:00' },
{ label: '1d', sortValue: '24:00:00' },
];
export const LABEL_REFRESH_INTERVAL = 1000 * 30; // 30sec

View File

@@ -5,6 +5,7 @@ import { PrometheusDatasource } from './datasource';
import { HistoryItem } from '@grafana/data';
import { PromQuery } from './types';
import Mock = jest.Mock;
import { SearchFunctionType } from '@grafana/ui';
describe('Language completion provider', () => {
const datasource: PrometheusDatasource = ({
@@ -123,14 +124,14 @@ describe('Language completion provider', () => {
expect(result.suggestions).toMatchObject([
{
items: [
{ label: '$__interval', sortText: '$__interval' }, // TODO: figure out why this row and sortText is needed
{ label: '$__rate_interval', sortText: '$__rate_interval' },
{ label: '1m', sortText: '00:01:00' },
{ label: '5m', sortText: '00:05:00' },
{ label: '10m', sortText: '00:10:00' },
{ label: '30m', sortText: '00:30:00' },
{ label: '1h', sortText: '01:00:00' },
{ label: '1d', sortText: '24:00:00' },
{ label: '$__interval', sortValue: '$__interval' }, // TODO: figure out why this row and sortValue is needed
{ label: '$__rate_interval', sortValue: '$__rate_interval' },
{ label: '1m', sortValue: '00:01:00' },
{ label: '5m', sortValue: '00:05:00' },
{ label: '10m', sortValue: '00:10:00' },
{ label: '30m', sortValue: '00:30:00' },
{ label: '1h', sortValue: '01:00:00' },
{ label: '1d', sortValue: '24:00:00' },
],
label: 'Range vector',
},
@@ -236,7 +237,13 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'instance' }], label: 'Labels' }]);
expect(result.suggestions).toEqual([
{
items: [{ label: 'job' }, { label: 'instance' }],
label: 'Labels',
searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});
it('returns label suggestions on label context and metric', async () => {
@@ -255,7 +262,9 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
expect(result.suggestions).toEqual([
{ items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
]);
});
it('returns label suggestions on label context but leaves out labels that already exist', async () => {
@@ -286,7 +295,9 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-labels');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
expect(result.suggestions).toEqual([
{ items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
]);
});
it('returns label value suggestions inside a label value context after a negated matching operator', async () => {
@@ -311,6 +322,7 @@ describe('Language completion provider', () => {
{
items: [{ label: 'value1' }, { label: 'value2' }],
label: 'Label values for "job"',
searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});
@@ -346,7 +358,9 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-label-values');
expect(result.suggestions).toEqual([{ items: [{ label: 'baz' }], label: 'Label values for "bar"' }]);
expect(result.suggestions).toEqual([
{ items: [{ label: 'baz' }], label: 'Label values for "bar"', searchFunctionType: SearchFunctionType.Fuzzy },
]);
});
it('returns label suggestions on aggregation context and metric w/ selector', async () => {
@@ -364,7 +378,9 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
expect(result.suggestions).toEqual([
{ items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
]);
});
it('returns label suggestions on aggregation context and metric w/o selector', async () => {
@@ -382,7 +398,9 @@ describe('Language completion provider', () => {
value: valueWithSelection,
});
expect(result.context).toBe('context-aggregation');
expect(result.suggestions).toEqual([{ items: [{ label: 'bar' }], label: 'Labels' }]);
expect(result.suggestions).toEqual([
{ items: [{ label: 'bar' }], label: 'Labels', searchFunctionType: SearchFunctionType.Fuzzy },
]);
});
it('returns label suggestions inside a multi-line aggregation context', async () => {
@@ -406,6 +424,7 @@ describe('Language completion provider', () => {
{
items: [{ label: 'bar' }],
label: 'Labels',
searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});
@@ -429,6 +448,7 @@ describe('Language completion provider', () => {
{
items: [{ label: 'bar' }],
label: 'Labels',
searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});
@@ -452,6 +472,7 @@ describe('Language completion provider', () => {
{
items: [{ label: 'bar' }],
label: 'Labels',
searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});
@@ -490,6 +511,7 @@ describe('Language completion provider', () => {
{
items: [{ label: 'bar' }],
label: 'Labels',
searchFunctionType: SearchFunctionType.Fuzzy,
},
]);
});

View File

@@ -3,16 +3,16 @@ import LRU from 'lru-cache';
import { Value } from 'slate';
import { dateTime, HistoryItem, LanguageProvider } from '@grafana/data';
import { CompletionItem, CompletionItemGroup, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import { CompletionItem, CompletionItemGroup, SearchFunctionType, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import {
addLimitInfo,
fixSummariesMetadata,
limitSuggestions,
parseSelector,
processHistogramLabels,
processLabels,
roundSecToMin,
addLimitInfo,
limitSuggestions,
} from './language_utils';
import PromqlSyntax, { FUNCTIONS, RATE_RANGES } from './promql';
@@ -201,7 +201,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
getEmptyCompletionItems = (context: { history: Array<HistoryItem<PromQuery>> }): TypeaheadOutput => {
const { history } = context;
const suggestions = [];
const suggestions: CompletionItemGroup[] = [];
if (history && history.length) {
const historyItems = _.chain(history)
@@ -214,7 +214,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
.value();
suggestions.push({
prefixMatch: true,
searchFunctionType: SearchFunctionType.Prefix,
skipSort: true,
label: 'History',
items: historyItems,
@@ -226,10 +226,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
getTermCompletionItems = (): TypeaheadOutput => {
const { metrics, metricsMetadata } = this;
const suggestions = [];
const suggestions: CompletionItemGroup[] = [];
suggestions.push({
prefixMatch: true,
searchFunctionType: SearchFunctionType.Prefix,
label: 'Functions',
items: FUNCTIONS.map(setFunctionKind),
});
@@ -239,6 +239,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
suggestions.push({
label: `Metrics${limitInfo}`,
items: limitSuggestions(metrics).map((m) => addMetricsMetadata(m, metricsMetadata)),
searchFunctionType: SearchFunctionType.Fuzzy,
});
}
@@ -313,6 +314,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
suggestions.push({
label: `Labels${limitInfo}`,
items: Object.keys(labelValues).map(wrapLabel),
searchFunctionType: SearchFunctionType.Fuzzy,
});
}
return result;
@@ -379,6 +381,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
suggestions.push({
label: `Label values for "${labelKey}"${limitInfo}`,
items: labelValues[labelKey].map(wrapLabel),
searchFunctionType: SearchFunctionType.Fuzzy,
});
}
} else {
@@ -391,7 +394,11 @@ export default class PromQlLanguageProvider extends LanguageProvider {
context = 'context-labels';
const newItems = possibleKeys.map((key) => ({ label: key }));
const limitInfo = addLimitInfo(newItems);
const newSuggestion: CompletionItemGroup = { label: `Labels${limitInfo}`, items: newItems };
const newSuggestion: CompletionItemGroup = {
label: `Labels${limitInfo}`,
items: newItems,
searchFunctionType: SearchFunctionType.Fuzzy,
};
suggestions.push(newSuggestion);
}
}

View File

@@ -4,14 +4,14 @@ import { CompletionItem } from '@grafana/ui';
// When changing RATE_RANGES, check if Loki/LogQL ranges should be changed too
// @see public/app/plugins/datasource/loki/language_provider.ts
export const RATE_RANGES: CompletionItem[] = [
{ label: '$__interval', sortText: '$__interval' },
{ label: '$__rate_interval', sortText: '$__rate_interval' },
{ label: '1m', sortText: '00:01:00' },
{ label: '5m', sortText: '00:05:00' },
{ label: '10m', sortText: '00:10:00' },
{ label: '30m', sortText: '00:30:00' },
{ label: '1h', sortText: '01:00:00' },
{ label: '1d', sortText: '24:00:00' },
{ label: '$__interval', sortValue: '$__interval' },
{ label: '$__rate_interval', sortValue: '$__rate_interval' },
{ label: '1m', sortValue: '00:01:00' },
{ label: '5m', sortValue: '00:05:00' },
{ label: '10m', sortValue: '00:10:00' },
{ label: '30m', sortValue: '00:30:00' },
{ label: '1h', sortValue: '01:00:00' },
{ label: '1d', sortValue: '24:00:00' },
];
export const OPERATORS = ['by', 'group_left', 'group_right', 'ignoring', 'on', 'offset', 'without'];