Compare commits

...

3 Commits

Author SHA1 Message Date
Brendan O'Handley
e55cc33c28 document POC work and comment 2024-11-20 09:24:35 -06:00
Brendan O'Handley
fe9c34225d can debounce monaco when using two debounce functions 2024-11-19 18:10:42 -06:00
Brendan O'Handley
bbc2dbcaff add limit to metric names calls and use regex filtering in the monaco code editor 2024-11-19 16:50:02 -06:00
8 changed files with 121 additions and 29 deletions

View File

@@ -1,7 +1,8 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryField.tsx
import { css } from '@emotion/css';
import { parser } from '@prometheus-io/lezer-promql';
import { debounce } from 'lodash';
import debounce from 'debounce-promise';
import { debounce as debounceLodash } from 'lodash';
import { promLanguageDefinition } from 'monaco-promql';
import { useEffect, useRef } from 'react';
import { useLatest } from 'react-use';
@@ -166,13 +167,16 @@ const MonacoQueryField = (props: Props) => {
// we call the completion-provider.
const filteringCompletionProvider: monacoTypes.languages.CompletionItemProvider = {
...completionProvider,
provideCompletionItems: (model, position, context, token) => {
provideCompletionItems: async (model, position, context, token) => {
// if the model-id does not match, then this call is from a different editor-instance,
// not "our instance", so return nothing
if (editor.getModel()?.id !== model.id) {
return { suggestions: [] };
}
return completionProvider.provideCompletionItems(model, position, context, token);
return await debounce(
() => completionProvider.provideCompletionItems(model, position, context, token),
200
)();
},
};
@@ -208,7 +212,7 @@ const MonacoQueryField = (props: Props) => {
// This can run quite slowly, so we're debouncing this which should accomplish two things
// 1. Should prevent this function from blocking the current call stack by pushing into the web API callback queue
// 2. Should prevent a bunch of duplicates of this function being called as the user is typing
const updateCurrentEditorValue = debounce(() => {
const updateCurrentEditorValue = debounceLodash(() => {
const editorValue = editor.getValue();
onChangeRef.current(editorValue);
}, lpRef.current.datasource.getDebounceTimeInMilliseconds());

View File

@@ -1,5 +1,6 @@
// Core grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/monaco-completion-provider/completions.ts
import UFuzzy from '@leeoniya/ufuzzy';
import debounce from 'debounce-promise';
import { config } from '@grafana/runtime';
@@ -49,8 +50,14 @@ export function filterMetricNames({ metricNames, inputText, limit }: MetricFilte
}
// we order items like: history, functions, metrics
function getAllMetricNamesCompletions(dataProvider: DataProvider): Completion[] {
let metricNames = dataProvider.getAllMetricNames();
async function getAllMetricNamesCompletions(dataProvider: DataProvider): Promise<Completion[]> {
let metricNames: string[] = [];
if (dataProvider.languageProvider.datasource.hasLabelsMatchAPISupport()) {
// use the metric regex with match[] param for partial typing of metric name to filter metric names
metricNames = await debounce(() => dataProvider.getLimitedMetricNames(dataProvider.inputInRange), 50)();
} else {
metricNames = dataProvider.getAllMetricNames();
}
if (
config.featureToggles.prometheusCodeModeMetricNamesSearch &&
@@ -58,7 +65,6 @@ function getAllMetricNamesCompletions(dataProvider: DataProvider): Completion[]
) {
const { monacoSettings } = dataProvider;
monacoSettings.enableAutocompleteSuggestionsUpdate();
if (monacoSettings.inputInRange) {
metricNames = filterMetricNames({
metricNames,
@@ -88,7 +94,7 @@ const FUNCTION_COMPLETIONS: Completion[] = FUNCTIONS.map((f) => ({
}));
async function getAllFunctionsAndMetricNamesCompletions(dataProvider: DataProvider): Promise<Completion[]> {
const metricNames = getAllMetricNamesCompletions(dataProvider);
const metricNames = await getAllMetricNamesCompletions(dataProvider);
return [...FUNCTION_COMPLETIONS, ...metricNames];
}
@@ -212,7 +218,7 @@ async function getLabelValuesForMetricCompletions(
}));
}
export function getCompletions(situation: Situation, dataProvider: DataProvider): Promise<Completion[]> {
export async function getCompletions(situation: Situation, dataProvider: DataProvider): Promise<Completion[]> {
switch (situation.type) {
case 'IN_DURATION':
return Promise.resolve(DURATION_COMPLETIONS);
@@ -222,7 +228,7 @@ export function getCompletions(situation: Situation, dataProvider: DataProvider)
return getAllFunctionsAndMetricNamesCompletions(dataProvider);
}
case 'EMPTY': {
const metricNames = getAllMetricNamesCompletions(dataProvider);
const metricNames = await getAllMetricNamesCompletions(dataProvider);
const historyCompletions = getAllHistoryCompletions(dataProvider);
return Promise.resolve([...historyCompletions, ...FUNCTION_COMPLETIONS, ...metricNames]);
}

View File

@@ -46,8 +46,9 @@ export class DataProvider {
*
* @remarks
* This is useful with fuzzy searching items to provide as Monaco autocomplete suggestions.
* Making dataProvider.inputInRange public because it works better than monacoSettings.inputInRange for debouncing the input because it is updated more regularly
*/
private inputInRange: string;
inputInRange: string;
private suggestionsIncomplete: boolean;
constructor(params: DataProviderParams) {
@@ -70,6 +71,10 @@ export class DataProvider {
return this.languageProvider.metrics;
}
getLimitedMetricNames(m: string): Promise<string[]> {
return this.languageProvider.getLimitedMetrics(m);
}
metricNamesToMetrics(metricNames: string[]): Metric[] {
const { metricsMetadata } = this.languageProvider;
const result: Metric[] = metricNames.map((m) => {

View File

@@ -656,6 +656,7 @@ export class PrometheusDatasource
// By implementing getTagKeys and getTagValues we add ad-hoc filters functionality
async getTagValues(options: DataSourceGetTagValuesOptions<PromQuery>) {
// DO NOT USE FOR METRIC NAMES
const requestId = `[${this.uid}][${options.key}]`;
if (config.featureToggles.promQLScope) {
return (

View File

@@ -12,6 +12,7 @@ import {
Scope,
scopeFilterOperatorMap,
ScopeSpecFilter,
MetricFindValue,
TimeRange,
} from '@grafana/data';
import { BackendSrvRequest } from '@grafana/runtime';
@@ -27,6 +28,7 @@ import {
toPromLikeQuery,
} from './language_utils';
import PromqlSyntax from './promql';
import { formatKeyValueStringsForLabelValuesQuery } from './querybuilder/components/MetricSelect';
import { buildVisualQueryFromString } from './querybuilder/parsing';
import { PrometheusCacheLevel, PromMetricsMetadata, PromQuery } from './types';
@@ -140,6 +142,41 @@ export default class PromQlLanguageProvider extends LanguageProvider {
return Promise.all([this.loadMetricsMetadata(), this.fetchLabels()]);
};
/**
* Only used for getting metrics from a data source that
* supports the match[] parameter
* where we can get a list of metrics based on user input
* that is passed to the label values endpoint
* with the extra param to match on series that contain the input
* as regex for the __name__ label
*
* Also takes a limit param to limit the number of metrics returned
*
* @param metricRegex
* @param limit
* @returns
*/
async getLimitedMetrics(metricRegex: string): Promise<string[]> {
// need time range
// need match params for metric match[]
// need label filters if used in the query builder
// pass in limit into metricFindQuery?
// pass in tome range into metricFindQuery?
// const limit = this.datasource.metricNamesAutocompleteSuggestionLimit.toString();
const metricNames = await this.datasource.metricFindQuery(
formatKeyValueStringsForLabelValuesQuery(metricRegex, [])
);
// metrics should only be strings but MetricFindValue from getTagKeys is a (string|number|undefined)
if (metricNames.every((el: MetricFindValue) => typeof el.value === 'string')) {
return metricNames.map((m: { text: string }) => m.text);
}
return [];
}
async loadMetricsMetadata() {
const headers = buildCacheHeaders(this.datasource.getDaysToCacheMetadata() * secondsInDay);
this.metricsMetadata = fixSummariesMetadata(
@@ -203,18 +240,43 @@ export default class PromQlLanguageProvider extends LanguageProvider {
}
/**
* Used to fetch label values and to fetch metrics with the label key __name__
* Used in
* - languageProvider.start()
* - languageProvider.getLabelValues()
* - languageProvider.fetchDefaultSeries()
*
* @param key
*/
fetchLabelValues = async (key: string): Promise<string[]> => {
const params = this.datasource.getAdjustedInterval(this.timeRange);
fetchLabelValues = async (key: string, fromQueryBuilder?: boolean): Promise<string[]> => {
let params: UrlParamsType = this.datasource.getAdjustedInterval(this.timeRange);
// Do not limit the query builder metric suggestions
// Limit metrics from the code editor and metrics browser
// metric names come from the label __name__
if (!fromQueryBuilder && this.datasource.metricNamesAutocompleteSuggestionLimit && key === '__name__') {
const limit = this.datasource.metricNamesAutocompleteSuggestionLimit.toString();
// add limit to the params
params = { ...params, limit };
}
const interpolatedName = this.datasource.interpolateString(key);
const url = `/api/v1/label/${interpolatedName}/values`;
const value = await this.request(url, [], params, this.getDefaultCacheHeaders());
return value ?? [];
};
async getLabelValues(key: string): Promise<string[]> {
return await this.fetchLabelValues(key);
/**
* This function is used to retrieve label values in completions.ts in Monaco editor
* and used for general label values in the query builder
* Extra: it is also used in the query builder to load initial metrics when there are no label filters
*
* @param key
* @param fromQueryBuilder
* @returns
*/
async getLabelValues(key: string, fromQueryBuilder?: boolean): Promise<string[]> {
return await this.fetchLabelValues(key, fromQueryBuilder);
}
/**
@@ -270,6 +332,11 @@ export default class PromQlLanguageProvider extends LanguageProvider {
/**
* Fetches all values for a label, with optional match[]
* Used in the following locations
* - datasource.getTagValues() for adhoc label values and metrics here languageProvider.getLimitedMetrics which calls getTagValues()
* - languageProvider.getSeriesValues()
* - MetricsLabelsSection.tsx, getLabelValuesFromLabelValuesAPI() for labels
*
* @param name
* @param match
* @param timeRange
@@ -284,10 +351,17 @@ export default class PromQlLanguageProvider extends LanguageProvider {
const interpolatedName = name ? this.datasource.interpolateString(name) : null;
const interpolatedMatch = match ? this.datasource.interpolateString(match) : null;
const range = this.datasource.getAdjustedInterval(timeRange);
const urlParams = {
let urlParams: UrlParamsType = {
...range,
...(interpolatedMatch && { 'match[]': interpolatedMatch }),
};
// only use the limit if calling for metrics and the limit has been set in the config
if (this.datasource.metricNamesAutocompleteSuggestionLimit && interpolatedName === '__name__') {
const limit = this.datasource.metricNamesAutocompleteSuggestionLimit.toString();
urlParams = { ...urlParams, limit };
}
let requestOptions: Partial<BackendSrvRequest> | undefined = {
...this.getDefaultCacheHeaders(),
...(requestId && { requestId }),

View File

@@ -39,6 +39,7 @@ export function MetricCombobox({
*/
const getMetricLabels = useCallback(
async (query: string) => {
// METRIC NAMES
const results = await datasource.metricFindQuery(formatKeyValueStringsForLabelValuesQuery(query, labelsFilters));
const resultsOptions = results.map((result) => {

View File

@@ -124,22 +124,11 @@ export function MetricSelect({
[styles.highlight]
);
/**
* Reformat the query string and label filters to return all valid results for current query editor state
*/
const formatKeyValueStringsForLabelValuesQuery = (
query: string,
labelsFilters?: QueryBuilderLabelFilter[]
): string => {
const queryString = regexifyLabelValuesQueryString(query);
return formatPrometheusLabelFiltersToString(queryString, labelsFilters);
};
/**
* Gets label_values response from prometheus API for current autocomplete query string and any existing labels filters
*/
const getMetricLabels = (query: string) => {
// METRIC NAMES
// Since some customers can have millions of metrics, whenever the user changes the autocomplete text we want to call the backend and request all metrics that match the current query string
const results = datasource.metricFindQuery(formatKeyValueStringsForLabelValuesQuery(query, labelsFilters));
return results.then((results) => {
@@ -424,3 +413,15 @@ export const formatPrometheusLabelFilters = (labelsFilters: QueryBuilderLabelFil
return `,${label.label}="${label.value}"`;
});
};
/**
* Reformat the query string and label filters to return all valid results for current query editor state
*/
export const formatKeyValueStringsForLabelValuesQuery = (
query: string,
labelsFilters?: QueryBuilderLabelFilter[]
): string => {
const queryString = regexifyLabelValuesQueryString(query);
return formatPrometheusLabelFiltersToString(queryString, labelsFilters);
};

View File

@@ -247,7 +247,7 @@ async function getMetrics(
const expr = promQueryModeller.renderLabels(query.labels);
metrics = (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? [];
} else {
metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? [];
metrics = (await datasource.languageProvider.getLabelValues('__name__', true)) ?? [];
}
return metrics.map((m) => ({