From bf456179e7d1fcc1d9f02c910b929a3d713efffb Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Fri, 21 Mar 2025 10:08:23 +0100 Subject: [PATCH] Prometheus: Use timeRange parameter in each language provider method (#101889) * remove range from class * remove range from language provider * use range parameters in MetricsLabelsSection * use range parameters in metric_find_query * pass range parameter in monaco-query-field * typo * use range in prometheus metrics browser languageProvider calls * fix unit tests * fix unit tests * update unit tests * lint --- .../src/components/PromQueryField.tsx | 2 + .../PrometheusMetricsBrowser.test.tsx | 7 +- .../components/PrometheusMetricsBrowser.tsx | 17 ++- .../src/components/VariableQueryEditor.tsx | 24 ++-- .../monaco-query-field/MonacoQueryField.tsx | 4 +- .../MonacoQueryFieldProps.ts | 3 +- .../completions.test.ts | 41 ++++--- .../monaco-completion-provider/completions.ts | 46 +++++--- .../monaco-completion-provider/index.ts | 7 +- packages/grafana-prometheus/src/datasource.ts | 19 +++- .../src/language_provider.test.ts | 32 +++--- .../src/language_provider.ts | 103 ++++++++---------- .../src/metric_find_query.ts | 40 +++---- .../components/MetricsLabelsSection.tsx | 25 +++-- .../components/PromQueryBuilder.test.tsx | 22 +++- .../components/PromQueryBuilder.tsx | 9 +- 16 files changed, 234 insertions(+), 167 deletions(-) diff --git a/packages/grafana-prometheus/src/components/PromQueryField.tsx b/packages/grafana-prometheus/src/components/PromQueryField.tsx index 4c619f88b7a..05cdda9864c 100644 --- a/packages/grafana-prometheus/src/components/PromQueryField.tsx +++ b/packages/grafana-prometheus/src/components/PromQueryField.tsx @@ -3,6 +3,7 @@ import { css, cx } from '@emotion/css'; import { PureComponent, ReactNode } from 'react'; import { + getDefaultTimeRange, isDataFrame, LocalStorageValueProvider, QueryEditorProps, @@ -249,6 +250,7 @@ class PromQueryFieldClass extends PureComponent diff --git a/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.test.tsx b/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.test.tsx index df14c9e02c8..602aeca8f6a 100644 --- a/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.test.tsx +++ b/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.test.tsx @@ -2,7 +2,7 @@ import { render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; -import { createTheme } from '@grafana/data'; +import { createTheme, getDefaultTimeRange, TimeRange } from '@grafana/data'; import PromQlLanguageProvider from '../language_provider'; @@ -146,7 +146,7 @@ describe('PrometheusMetricsBrowser', () => { const setupProps = (): BrowserProps => { const mockLanguageProvider = { start: () => Promise.resolve(), - getLabelValues: (name: string) => { + getLabelValues: (timeRange: TimeRange, name: string) => { switch (name) { case 'label1': return ['value1-1', 'value1-2']; @@ -163,7 +163,7 @@ describe('PrometheusMetricsBrowser', () => { // The metrics browser expects both label names and label values. // The labels endpoint with match does not supply label values // and so using it breaks the metrics browser. - fetchSeriesLabels: (selector: string) => { + fetchSeriesLabels: (timeRange: TimeRange, selector: string) => { switch (selector) { case '{label1="value1-1"}': return { label1: ['value1-1'], label2: ['value2-1'], label3: ['value3-1'] }; @@ -187,6 +187,7 @@ describe('PrometheusMetricsBrowser', () => { lastUsedLabels: [], storeLastUsedLabels: () => {}, deleteLastUsedLabels: () => {}, + timeRange: getDefaultTimeRange(), }; return defaults; diff --git a/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx b/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx index 1eeb59d35ed..db3ec6c60b4 100644 --- a/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx +++ b/packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx @@ -4,7 +4,7 @@ import { ChangeEvent } from 'react'; import * as React from 'react'; import { FixedSizeList } from 'react-window'; -import { GrafanaTheme2, TimeRange } from '@grafana/data'; +import { getDefaultTimeRange, GrafanaTheme2, TimeRange } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { BrowserLabel as PromLabel, @@ -409,11 +409,15 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component { + return this.props.timeRange ?? getDefaultTimeRange(); + }; + async fetchValues(name: string, selector: string) { const { languageProvider } = this.props; this.updateLabelState(name, { loading: true }, `Fetching values for ${name}`); try { - let rawValues = await languageProvider.getLabelValues(name); + let rawValues = await languageProvider.getLabelValues(this.getTimeRange(), name); // If selector changed, clear loading state and discard result by returning early if (selector !== buildSelector(this.state.labels)) { this.updateLabelState(name, { loading: false }); @@ -444,7 +448,12 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component ({ label: variable, value: variable })); + let timeRange = range; + if (!timeRange) { + timeRange = getDefaultTimeRange(); + } + if (!metric) { // get all the labels - datasource.getTagKeys({ filters: [] }).then((labelNames: Array<{ text: string }>) => { + datasource.getTagKeys({ timeRange, filters: [] }).then((labelNames: Array<{ text: string }>) => { const names = labelNames.map(({ text }) => ({ label: text, value: text })); setLabels(names, variables); }); @@ -122,13 +127,15 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: const labelToConsider = [{ label: '__name__', op: '=', value: metric }]; const expr = promQueryModeller.renderLabels(labelToConsider); - datasource.languageProvider.fetchLabelsWithMatch(expr).then((labelsIndex: Record) => { - const labelNames = Object.keys(labelsIndex); - const names = labelNames.map((value) => ({ label: value, value: value })); - setLabels(names, variables); - }); + datasource.languageProvider + .fetchLabelsWithMatch(timeRange, expr) + .then((labelsIndex: Record) => { + const labelNames = Object.keys(labelsIndex); + const names = labelNames.map((value) => ({ label: value, value: value })); + setLabels(names, variables); + }); } - }, [datasource, qryType, metric]); + }, [datasource, qryType, metric, range]); const onChangeWithVariableString = ( updateVar: { [key: string]: QueryType | string }, @@ -302,6 +309,7 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }: datasource={datasource} onChange={metricsLabelsChange} variableEditor={true} + timeRange={range ?? getDefaultTimeRange()} /> )} diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx index 97e49df416b..d6c86499c25 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx +++ b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx @@ -105,7 +105,7 @@ const MonacoQueryField = (props: Props) => { // we need only one instance of `overrideServices` during the lifetime of the react component const overrideServicesRef = useRef(getOverrideServices()); const containerRef = useRef(null); - const { languageProvider, history, onBlur, onRunQuery, initialValue, placeholder, datasource } = props; + const { languageProvider, history, onBlur, onRunQuery, initialValue, placeholder, datasource, timeRange } = props; const lpRef = useLatest(languageProvider); const historyRef = useLatest(history); @@ -155,7 +155,7 @@ const MonacoQueryField = (props: Props) => { historyProvider: historyRef.current, languageProvider: lpRef.current, }); - const completionProvider = getCompletionProvider(monaco, dataProvider); + const completionProvider = getCompletionProvider(monaco, dataProvider, timeRange); // completion-providers in monaco are not registered directly to editor-instances, // they are registered to languages. this makes it hard for us to have diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts index 4ca3fd894ed..ac2995273e5 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts @@ -1,5 +1,5 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/components/monaco-query-field/MonacoQueryFieldProps.ts -import { HistoryItem } from '@grafana/data'; +import { HistoryItem, TimeRange } from '@grafana/data'; import { PrometheusDatasource } from '../../datasource'; import type PromQlLanguageProvider from '../../language_provider'; @@ -17,4 +17,5 @@ export type Props = { onRunQuery: (value: string) => void; onBlur: (value: string) => void; datasource: PrometheusDatasource; + timeRange: TimeRange; }; diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts index 7a97ba98740..a778cf74300 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts @@ -2,6 +2,7 @@ import { config } from '@grafana/runtime'; import { SUGGESTIONS_LIMIT } from '../../../language_provider'; import { FUNCTIONS } from '../../../promql'; +import { getMockTimeRange } from '../../../test/__mocks__/datasource'; import { filterMetricNames, getCompletions } from './completions'; import { DataProvider, type DataProviderParams } from './data_provider'; @@ -183,6 +184,8 @@ function getSuggestionCountForSituation(situationType: MetricNameSituation, metr } describe.each(metricNameCompletionSituations)('metric name completions in situation %s', (situationType) => { + const timeRange = getMockTimeRange(); + it('should return completions for all metric names when the number of metric names is at or below the limit', async () => { jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValue(metrics.atLimit); const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.atLimit.length); @@ -192,12 +195,12 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat // No text input dataProvider.monacoSettings.setInputInRange(''); - let completions = await getCompletions(situation, dataProvider); + let completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(expectedCompletionsCount); // With text input (use fuzzy search) dataProvider.monacoSettings.setInputInRange('name_1'); - completions = await getCompletions(situation, dataProvider); + completions = await getCompletions(situation, dataProvider, timeRange); expect(completions?.length).toBeLessThanOrEqual(expectedCompletionsCount); }); @@ -210,12 +213,12 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat // Complex query dataProvider.monacoSettings.setInputInRange('metric name one two three four five'); - let completions = await getCompletions(situation, dataProvider); + let completions = await getCompletions(situation, dataProvider, timeRange); expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount); // Simple query with fuzzy match dataProvider.monacoSettings.setInputInRange('metric_name_'); - completions = await getCompletions(situation, dataProvider); + completions = await getCompletions(situation, dataProvider, timeRange); expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount); }); @@ -227,19 +230,19 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat // Do not cross the metrics names threshold jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValueOnce(metrics.atLimit); dataProvider.monacoSettings.setInputInRange('name_1'); - await getCompletions(situation, dataProvider); + await getCompletions(situation, dataProvider, timeRange); expect(dataProvider.monacoSettings.suggestionsIncomplete).toBe(false); // Cross the metric names threshold, without text input jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValueOnce(metrics.beyondLimit); dataProvider.monacoSettings.setInputInRange(''); - await getCompletions(situation, dataProvider); + await getCompletions(situation, dataProvider, timeRange); expect(dataProvider.monacoSettings.suggestionsIncomplete).toBe(true); // Cross the metric names threshold, with text input jest.spyOn(dataProvider, 'getAllMetricNames').mockReturnValueOnce(metrics.beyondLimit); dataProvider.monacoSettings.setInputInRange('name_1'); - await getCompletions(situation, dataProvider); + await getCompletions(situation, dataProvider, timeRange); expect(dataProvider.monacoSettings.suggestionsIncomplete).toBe(true); }); @@ -253,7 +256,7 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat // Test with a complex query (> 4 terms) dataProvider.monacoSettings.setInputInRange('metric name 1 with extra terms more'); - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); const metricCompletions = completions.filter((c) => c.type === 'METRIC_NAME'); expect(metricCompletions.some((c) => c.label === 'metric_name_1_with_extra_terms')).toBe(true); @@ -268,7 +271,7 @@ describe.each(metricNameCompletionSituations)('metric name completions in situat // Test with multiple terms dataProvider.monacoSettings.setInputInRange('metric name 1 2 3 4 5'); - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); const expectedCompletionsCount = getSuggestionCountForSituation(situationType, metrics.beyondLimit.length); expect(completions.length).toBeLessThanOrEqual(expectedCompletionsCount); @@ -308,6 +311,8 @@ describe('Label value completions', () => { }); }); + const timeRange = getMockTimeRange(); + it('should not escape special characters when between quotes', async () => { const situation: Situation = { type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME', @@ -316,7 +321,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(4); expect(completions[0].insertText).toBe('value1'); @@ -333,7 +338,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(4); expect(completions[0].insertText).toBe('"value1"'); @@ -350,6 +355,8 @@ describe('Label value completions', () => { }); }); + const timeRange = getMockTimeRange(); + it('should escape special characters when between quotes', async () => { const situation: Situation = { type: 'IN_LABEL_SELECTOR_WITH_LABEL_NAME', @@ -358,7 +365,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(4); expect(completions[0].insertText).toBe('value1'); @@ -375,7 +382,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(4); expect(completions[0].insertText).toBe('"value1"'); @@ -392,6 +399,8 @@ describe('Label value completions', () => { }); }); + const timeRange = getMockTimeRange(); + it('should handle empty values', async () => { jest.spyOn(dataProvider, 'getLabelValues').mockResolvedValue(['']); @@ -402,7 +411,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(1); expect(completions[0].insertText).toBe('""'); }); @@ -417,7 +426,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(1); expect(completions[0].insertText).toBe('test\\"\\\\value'); }); @@ -432,7 +441,7 @@ describe('Label value completions', () => { otherLabels: [], }; - const completions = await getCompletions(situation, dataProvider); + const completions = await getCompletions(situation, dataProvider, timeRange); expect(completions).toHaveLength(1); expect(completions[0].insertText).toBe('"123"'); }); diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts index ff1f0490560..af0b51bddd4 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts @@ -2,6 +2,7 @@ import UFuzzy from '@leeoniya/ufuzzy'; import { languages } from 'monaco-editor'; +import { TimeRange } from '@grafana/data'; import { config } from '@grafana/runtime'; import { prometheusRegularEscape } from '../../../datasource'; @@ -160,14 +161,15 @@ function makeSelector(metricName: string | undefined, labels: Label[]): string { async function getLabelNames( metric: string | undefined, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { if (metric === undefined && otherLabels.length === 0) { // if there is no filtering, we have to use a special endpoint return Promise.resolve(dataProvider.getAllLabelNames()); } else { const selector = makeSelector(metric, otherLabels); - return await dataProvider.getSeriesLabels(selector, otherLabels); + return await dataProvider.getSeriesLabels(timeRange, selector, otherLabels); } } @@ -176,9 +178,10 @@ async function getLabelNamesForCompletions( suffix: string, triggerOnInsert: boolean, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { - const labelNames = await getLabelNames(metric, otherLabels, dataProvider); + const labelNames = await getLabelNames(metric, otherLabels, dataProvider, timeRange); return labelNames.map((text) => { const isUtf8 = !isValidLegacyName(text); return { @@ -200,31 +203,34 @@ async function getLabelNamesForCompletions( async function getLabelNamesForSelectorCompletions( metric: string | undefined, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { - return getLabelNamesForCompletions(metric, '=', true, otherLabels, dataProvider); + return getLabelNamesForCompletions(metric, '=', true, otherLabels, dataProvider, timeRange); } async function getLabelNamesForByCompletions( metric: string | undefined, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { - return getLabelNamesForCompletions(metric, '', false, otherLabels, dataProvider); + return getLabelNamesForCompletions(metric, '', false, otherLabels, dataProvider, timeRange); } async function getLabelValues( metric: string | undefined, labelName: string, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { if (metric === undefined && otherLabels.length === 0) { // if there is no filtering, we have to use a special endpoint - return dataProvider.getLabelValues(labelName); + return dataProvider.getLabelValues(timeRange, labelName); } else { const selector = makeSelector(metric, otherLabels); - return await dataProvider.getSeriesValues(labelName, selector); + return await dataProvider.getSeriesValues(timeRange, labelName, selector); } } @@ -233,9 +239,10 @@ async function getLabelValuesForMetricCompletions( labelName: string, betweenQuotes: boolean, otherLabels: Label[], - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): Promise { - const values = await getLabelValues(metric, labelName, otherLabels, dataProvider); + const values = await getLabelValues(metric, labelName, otherLabels, dataProvider, timeRange); return values.map((text) => ({ type: 'LABEL_VALUE', label: text, @@ -248,7 +255,11 @@ function formatLabelValueForCompletion(value: string, betweenQuotes: boolean): s return betweenQuotes ? text : `"${text}"`; } -export function getCompletions(situation: Situation, dataProvider: DataProvider): Promise { +export function getCompletions( + situation: Situation, + dataProvider: DataProvider, + timeRange: TimeRange +): Promise { switch (situation.type) { case 'IN_DURATION': return Promise.resolve(DURATION_COMPLETIONS); @@ -263,16 +274,17 @@ export function getCompletions(situation: Situation, dataProvider: DataProvider) return Promise.resolve([...historyCompletions, ...FUNCTION_COMPLETIONS, ...metricNames]); } case 'IN_LABEL_SELECTOR_NO_LABEL_NAME': - return getLabelNamesForSelectorCompletions(situation.metricName, situation.otherLabels, dataProvider); + return getLabelNamesForSelectorCompletions(situation.metricName, situation.otherLabels, dataProvider, timeRange); case 'IN_GROUPING': - return getLabelNamesForByCompletions(situation.metricName, situation.otherLabels, dataProvider); + return getLabelNamesForByCompletions(situation.metricName, situation.otherLabels, dataProvider, timeRange); case 'IN_LABEL_SELECTOR_WITH_LABEL_NAME': return getLabelValuesForMetricCompletions( situation.metricName, situation.labelName, situation.betweenQuotes, situation.otherLabels, - dataProvider + dataProvider, + timeRange ); default: throw new NeverCaseError(situation); diff --git a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts index 5108401b2fd..def57a02621 100644 --- a/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts +++ b/packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts @@ -1,4 +1,5 @@ // 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/index.ts +import { TimeRange } from '@grafana/data'; import type { Monaco, monacoTypes } from '@grafana/ui'; import { CompletionType, getCompletions } from './completions'; @@ -48,7 +49,8 @@ function getMonacoCompletionItemKind(type: CompletionType, monaco: Monaco): mona export function getCompletionProvider( monaco: Monaco, - dataProvider: DataProvider + dataProvider: DataProvider, + timeRange: TimeRange ): monacoTypes.languages.CompletionItemProvider { const provideCompletionItems = ( model: monacoTypes.editor.ITextModel, @@ -84,7 +86,8 @@ export function getCompletionProvider( const offset = model.getOffsetAt(positionClone); const situation = getSituation(model.getValue(), offset); - const completionsPromise = situation != null ? getCompletions(situation, dataProvider) : Promise.resolve([]); + const completionsPromise = + situation != null ? getCompletions(situation, dataProvider, timeRange) : Promise.resolve([]); return completionsPromise.then((items) => { // monaco by-default alphabetically orders the items. diff --git a/packages/grafana-prometheus/src/datasource.ts b/packages/grafana-prometheus/src/datasource.ts index 44e93ebb7d1..082e391ad9b 100644 --- a/packages/grafana-prometheus/src/datasource.ts +++ b/packages/grafana-prometheus/src/datasource.ts @@ -483,7 +483,7 @@ export class PrometheusDatasource return Promise.resolve([]); } - const timeRange = options?.range ?? this.languageProvider.timeRange ?? getDefaultTimeRange(); + const timeRange = options?.range ?? getDefaultTimeRange(); const scopedVars = { ...this.getIntervalVars(), @@ -654,6 +654,10 @@ export class PrometheusDatasource // it is used in metric_find_query.ts // and in Tempo here grafana/public/app/plugins/datasource/tempo/QueryEditor/ServiceGraphSection.tsx async getTagKeys(options: DataSourceGetTagKeysOptions): Promise { + if (!options.timeRange) { + options.timeRange = getDefaultTimeRange(); + } + if (config.featureToggles.promQLScope && (options?.scopes?.length ?? 0) > 0) { const suggestions = await this.languageProvider.fetchSuggestions( options.timeRange, @@ -680,7 +684,10 @@ export class PrometheusDatasource })); const expr = promQueryModeller.renderLabels(labelFilters); - let labelsIndex: Record = await this.languageProvider.fetchLabelsWithMatch(expr); + let labelsIndex: Record = await this.languageProvider.fetchLabelsWithMatch( + options.timeRange, + expr + ); // filter out already used labels return Object.keys(labelsIndex) @@ -689,7 +696,11 @@ export class PrometheusDatasource } // By implementing getTagKeys and getTagValues we add ad-hoc filters functionality - async getTagValues(options: DataSourceGetTagValuesOptions) { + async getTagValues(options: DataSourceGetTagValuesOptions): Promise { + if (!options.timeRange) { + options.timeRange = getDefaultTimeRange(); + } + const requestId = `[${this.uid}][${options.key}]`; if (config.featureToggles.promQLScope && (options?.scopes?.length ?? 0) > 0) { return ( @@ -715,7 +726,7 @@ export class PrometheusDatasource if (this.hasLabelsMatchAPISupport()) { return ( - await this.languageProvider.fetchSeriesValuesWithMatch(options.key, expr, requestId, options.timeRange) + await this.languageProvider.fetchSeriesValuesWithMatch(options.timeRange, options.key, expr, requestId) ).map((v) => ({ value: v, text: v, diff --git a/packages/grafana-prometheus/src/language_provider.test.ts b/packages/grafana-prometheus/src/language_provider.test.ts index 2b0c7edcaf3..e91845d1f00 100644 --- a/packages/grafana-prometheus/src/language_provider.test.ts +++ b/packages/grafana-prometheus/src/language_provider.test.ts @@ -110,6 +110,8 @@ describe('Language completion provider', () => { }); describe('getSeriesLabels', () => { + const timeRange = getMockTimeRange(); + it('should call labels endpoint', () => { const languageProvider = new LanguageProvider({ ...defaultDatasource, @@ -120,7 +122,7 @@ describe('Language completion provider', () => { const labelName = 'job'; const labelValue = 'grafana'; - getSeriesLabels(`{${labelName}="${labelValue}"}`, [ + getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [ { name: labelName, value: labelValue, @@ -151,7 +153,7 @@ describe('Language completion provider', () => { const labelName = 'job'; const labelValue = 'grafana'; - getSeriesLabels(`{${labelName}="${labelValue}"}`, [ + getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [ { name: labelName, value: labelValue, @@ -186,7 +188,7 @@ describe('Language completion provider', () => { const labelName = 'job'; const labelValue = 'grafana'; - getSeriesLabels(`{${labelName}="${labelValue}"}`, [ + getSeriesLabels(timeRange, `{${labelName}="${labelValue}"}`, [ { name: labelName, value: labelValue, @@ -217,13 +219,15 @@ describe('Language completion provider', () => { }); describe('getSeriesValues', () => { + const timeRange = getMockTimeRange(); + it('should call old series endpoint and should use match[] parameter', () => { const languageProvider = new LanguageProvider({ ...defaultDatasource, } as PrometheusDatasource); const getSeriesValues = languageProvider.getSeriesValues; const requestSpy = jest.spyOn(languageProvider, 'request'); - getSeriesValues('job', '{job="grafana"}'); + getSeriesValues(timeRange, 'job', '{job="grafana"}'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -246,7 +250,7 @@ describe('Language completion provider', () => { const requestSpy = jest.spyOn(languageProvider, 'request'); const labelName = 'job'; const labelValue = 'grafana'; - getSeriesValues(labelName, `{${labelName}="${labelValue}"}`); + getSeriesValues(timeRange, labelName, `{${labelName}="${labelValue}"}`); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( `/api/v1/label/${labelName}/values`, @@ -267,7 +271,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const getSeriesValues = languageProvider.getSeriesValues; const requestSpy = jest.spyOn(languageProvider, 'request'); - getSeriesValues('job', '{instance="$instance", job="grafana"}'); + getSeriesValues(timeRange, 'job', '{instance="$instance", job="grafana"}'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -288,7 +292,7 @@ describe('Language completion provider', () => { const timeRange = getMockTimeRange(); await languageProvider.start(timeRange); const requestSpy = jest.spyOn(languageProvider, 'request'); - await languageProvider.fetchSeries('{job="grafana"}'); + await languageProvider.fetchSeries(timeRange, '{job="grafana"}'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -311,7 +315,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchSeriesLabels = languageProvider.fetchSeriesLabels; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchSeriesLabels('$metric'); + fetchSeriesLabels(getMockTimeRange(), '$metric'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -332,7 +336,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchSeriesLabels = languageProvider.fetchSeriesLabels; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchSeriesLabels('metric-with-limit', undefined, 'none'); + fetchSeriesLabels(getMockTimeRange(), 'metric-with-limit', undefined, 'none'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -353,7 +357,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchSeriesLabels = languageProvider.fetchSeriesLabels; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchSeriesLabels('metric-without-limit', false, 'none'); + fetchSeriesLabels(getMockTimeRange(), 'metric-without-limit', false, 'none'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/series', @@ -619,7 +623,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchLabelValues = languageProvider.fetchLabelValues; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchLabelValues('$job'); + fetchLabelValues(getMockTimeRange(), '$job'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/label/interpolated_job/values', @@ -639,7 +643,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchLabelValues = languageProvider.fetchLabelValues; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchLabelValues('"http.status:sum"'); + fetchLabelValues(getMockTimeRange(), '"http.status:sum"'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/label/U__http_2e_status:sum/values', @@ -661,7 +665,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchSeriesValuesWithMatch = languageProvider.fetchSeriesValuesWithMatch; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchSeriesValuesWithMatch('"http.status:sum"', '{__name__="a_utf8_http_requests_total"}'); + fetchSeriesValuesWithMatch(getMockTimeRange(), '"http.status:sum"', '{__name__="a_utf8_http_requests_total"}'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/label/U__http_2e_status:sum/values', @@ -681,7 +685,7 @@ describe('Language completion provider', () => { } as PrometheusDatasource); const fetchSeriesValuesWithMatch = languageProvider.fetchSeriesValuesWithMatch; const requestSpy = jest.spyOn(languageProvider, 'request'); - fetchSeriesValuesWithMatch('"http_status_sum"', '{__name__="a_utf8_http_requests_total"}'); + fetchSeriesValuesWithMatch(getMockTimeRange(), '"http_status_sum"', '{__name__="a_utf8_http_requests_total"}'); expect(requestSpy).toHaveBeenCalled(); expect(requestSpy).toHaveBeenCalledWith( '/api/v1/label/http_status_sum/values', diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index 22c427c979b..04a9f7e6e7b 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -79,7 +79,6 @@ const PREFIX_DELIMITER_REGEX = const secondsInDay = 86400; export default class PromQlLanguageProvider extends LanguageProvider { histogramMetrics: string[]; - timeRange: TimeRange; metrics: string[]; metricsMetadata?: PromMetricsMetadata; declare startTask: Promise; @@ -92,7 +91,6 @@ export default class PromQlLanguageProvider extends LanguageProvider { this.datasource = datasource; this.histogramMetrics = []; - this.timeRange = getDefaultTimeRange(); this.metrics = []; Object.assign(this, initialValues); @@ -109,7 +107,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { cleanText(s: string) { const parts = s.split(PREFIX_DELIMITER_REGEX); const last = parts.pop()!; - return last.trimLeft().replace(/"$/, '').replace(/^"/, ''); + return last.trimStart().replace(/"$/, '').replace(/^"/, ''); } get syntax() { @@ -129,16 +127,14 @@ export default class PromQlLanguageProvider extends LanguageProvider { return defaultValue; }; - start = async (timeRange?: TimeRange): Promise => { - this.timeRange = timeRange ?? getDefaultTimeRange(); - + start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise => { if (this.datasource.lookupsDisabled) { return []; } - this.metrics = (await this.fetchLabelValues('__name__')) || []; + this.metrics = (await this.fetchLabelValues(timeRange, '__name__')) || []; this.histogramMetrics = processHistogramMetrics(this.metrics).sort(); - return Promise.all([this.loadMetricsMetadata(), this.fetchLabels()]); + return Promise.all([this.loadMetricsMetadata(), this.fetchLabels(timeRange)]); }; async loadMetricsMetadata() { @@ -186,15 +182,15 @@ export default class PromQlLanguageProvider extends LanguageProvider { }; } - async getSeries(selector: string, withName?: boolean): Promise> { + async getSeries(timeRange: TimeRange, selector: string, withName?: boolean): Promise> { if (this.datasource.lookupsDisabled) { return {}; } try { if (selector === EMPTY_SELECTOR) { - return await this.fetchDefaultSeries(); + return await this.fetchDefaultSeries(timeRange); } else { - return await this.fetchSeriesLabels(selector, withName, REMOVE_SERIES_LIMIT); + return await this.fetchSeriesLabels(timeRange, selector, withName, REMOVE_SERIES_LIMIT); } } catch (error) { // TODO: better error handling @@ -203,11 +199,8 @@ export default class PromQlLanguageProvider extends LanguageProvider { } } - /** - * @param key - */ - fetchLabelValues = async (key: string): Promise => { - const params = this.datasource.getAdjustedInterval(this.timeRange); + fetchLabelValues = async (range: TimeRange, key: string): Promise => { + const params = this.datasource.getAdjustedInterval(range); const interpolatedName = this.datasource.interpolateString(key); const interpolatedAndEscapedName = escapeForUtf8Support(removeQuotesIfExist(interpolatedName)); const url = `/api/v1/label/${interpolatedAndEscapedName}/values`; @@ -215,19 +208,16 @@ export default class PromQlLanguageProvider extends LanguageProvider { return value ?? []; }; - async getLabelValues(key: string): Promise { - return await this.fetchLabelValues(key); + async getLabelValues(range: TimeRange, key: string): Promise { + return await this.fetchLabelValues(range, key); } /** * Fetches all label keys */ - fetchLabels = async (timeRange?: TimeRange, queries?: PromQuery[]): Promise => { - if (timeRange) { - this.timeRange = timeRange; - } + fetchLabels = async (timeRange: TimeRange, queries?: PromQuery[]): Promise => { let url = '/api/v1/labels'; - const timeParams = this.datasource.getAdjustedInterval(this.timeRange); + const timeParams = this.datasource.getAdjustedInterval(timeRange); this.labelFetchTs = Date.now().valueOf(); const searchParams = new URLSearchParams({ ...timeParams }); @@ -259,17 +249,15 @@ export default class PromQlLanguageProvider extends LanguageProvider { /** * Gets series values - * Function to replace old getSeries calls in a way that will provide faster endpoints for new prometheus instances, - * while maintaining backward compatability - * @param labelName - * @param selector + * Function to replace old getSeries calls in a way that will provide faster endpoints + * for new prometheus instances, while maintaining backward compatability */ - getSeriesValues = async (labelName: string, selector: string): Promise => { + getSeriesValues = async (timeRange: TimeRange, labelName: string, selector: string): Promise => { if (!this.datasource.hasLabelsMatchAPISupport()) { - const data = await this.getSeries(selector); + const data = await this.getSeries(timeRange, selector); return data[removeQuotesIfExist(labelName)] ?? []; } - return await this.fetchSeriesValuesWithMatch(labelName, selector); + return await this.fetchSeriesValuesWithMatch(timeRange, labelName, selector); }; /** @@ -280,10 +268,10 @@ export default class PromQlLanguageProvider extends LanguageProvider { * @param requestId */ fetchSeriesValuesWithMatch = async ( + timeRange: TimeRange, name: string, match: string, - requestId?: string, - timeRange: TimeRange = this.timeRange + requestId?: string ): Promise => { const interpolatedName = name ? this.datasource.interpolateString(name) : null; const interpolatedMatch = match ? this.datasource.interpolateString(match) : null; @@ -321,16 +309,16 @@ export default class PromQlLanguageProvider extends LanguageProvider { * @param selector * @param otherLabels */ - getSeriesLabels = async (selector: string, otherLabels: Label[]): Promise => { + getSeriesLabels = async (timeRange: TimeRange, selector: string, otherLabels: Label[]): Promise => { let possibleLabelNames, data: Record; if (!this.datasource.hasLabelsMatchAPISupport()) { - data = await this.getSeries(selector); + data = await this.getSeries(timeRange, selector); possibleLabelNames = Object.keys(data); // all names from prometheus } else { // Exclude __name__ from output otherLabels.push({ name: '__name__', value: '', op: '!=' }); - data = await this.fetchSeriesLabelsMatch(selector); + data = await this.fetchSeriesLabelsMatch(timeRange, selector); possibleLabelNames = Object.keys(data); } @@ -341,31 +329,31 @@ export default class PromQlLanguageProvider extends LanguageProvider { /** * Fetch labels using the best endpoint that datasource supports. * This is cached by its args but also by the global timeRange currently selected as they can change over requested time. - * @param name - * @param withName */ - fetchLabelsWithMatch = async (name: string, withName?: boolean): Promise> => { + fetchLabelsWithMatch = async ( + timeRange: TimeRange, + name: string, + withName?: boolean + ): Promise> => { if (this.datasource.hasLabelsMatchAPISupport()) { - return this.fetchSeriesLabelsMatch(name, withName); + return this.fetchSeriesLabelsMatch(timeRange, name, withName); } else { - return this.fetchSeriesLabels(name, withName, REMOVE_SERIES_LIMIT); + return this.fetchSeriesLabels(timeRange, name, withName, REMOVE_SERIES_LIMIT); } }; /** * Fetch labels for a series using /series endpoint. This is cached by its args but also by the global timeRange currently selected as * they can change over requested time. - * @param name - * @param withName - * @param withLimit */ fetchSeriesLabels = async ( + timeRange: TimeRange, name: string, withName?: boolean, withLimit?: string ): Promise> => { const interpolatedName = this.datasource.interpolateString(name); - const range = this.datasource.getAdjustedInterval(this.timeRange); + const range = this.datasource.getAdjustedInterval(timeRange); let urlParams: UrlParamsType = { ...range, 'match[]': interpolatedName, @@ -385,12 +373,14 @@ export default class PromQlLanguageProvider extends LanguageProvider { /** * Fetch labels for a series using /labels endpoint. This is cached by its args but also by the global timeRange currently selected as * they can change over requested time. - * @param name - * @param withName */ - fetchSeriesLabelsMatch = async (name: string, withName?: boolean): Promise> => { + fetchSeriesLabelsMatch = async ( + timeRange: TimeRange, + name: string, + withName?: boolean + ): Promise> => { const interpolatedName = this.datasource.interpolateString(name); - const range = this.datasource.getAdjustedInterval(this.timeRange); + const range = this.datasource.getAdjustedInterval(timeRange); const urlParams = { ...range, 'match[]': interpolatedName, @@ -404,11 +394,10 @@ export default class PromQlLanguageProvider extends LanguageProvider { /** * Fetch series for a selector. Use this for raw results. Use fetchSeriesLabels() to get labels. - * @param match */ - fetchSeries = async (match: string): Promise>> => { + fetchSeries = async (timeRange: TimeRange, match: string): Promise>> => { const url = '/api/v1/series'; - const range = this.datasource.getTimeRangeParams(this.timeRange); + const range = this.datasource.getTimeRangeParams(timeRange); const params = { ...range, 'match[]': match }; return await this.request(url, {}, params, this.getDefaultCacheHeaders()); }; @@ -418,8 +407,8 @@ export default class PromQlLanguageProvider extends LanguageProvider { * because we can cache more aggressively here and also we do not want to invalidate this cache the same way as in * fetchSeriesLabels. */ - fetchDefaultSeries = once(async () => { - const values = await Promise.all(DEFAULT_KEYS.map((key) => this.fetchLabelValues(key))); + fetchDefaultSeries = once(async (timeRange: TimeRange) => { + const values = await Promise.all(DEFAULT_KEYS.map((key) => this.fetchLabelValues(timeRange, key))); return DEFAULT_KEYS.reduce((acc, key, i) => ({ ...acc, [key]: values[i] }), {}); }); @@ -442,12 +431,12 @@ export default class PromQlLanguageProvider extends LanguageProvider { limit?: number, requestId?: string ): Promise => { - if (timeRange) { - this.timeRange = timeRange; + if (!timeRange) { + timeRange = getDefaultTimeRange(); } const url = '/suggestions'; - const timeParams = this.datasource.getAdjustedInterval(this.timeRange); + const timeParams = this.datasource.getAdjustedInterval(timeRange); const value = await this.request( url, [], @@ -456,7 +445,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { queries: queries?.map((q) => this.datasource.interpolateString(q.expr, { ...this.datasource.getIntervalVars(), - ...this.datasource.getRangeScopedVars(this.timeRange), + ...this.datasource.getRangeScopedVars(timeRange), }) ), scopes: scopes?.reduce((acc, scope) => { diff --git a/packages/grafana-prometheus/src/metric_find_query.ts b/packages/grafana-prometheus/src/metric_find_query.ts index 69494f0f7fc..12d17e7126f 100644 --- a/packages/grafana-prometheus/src/metric_find_query.ts +++ b/packages/grafana-prometheus/src/metric_find_query.ts @@ -1,7 +1,7 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/metric_find_query.ts import { chain, map as _map, uniq } from 'lodash'; -import { getDefaultTimeRange, MetricFindValue, TimeRange } from '@grafana/data'; +import { MetricFindValue, TimeRange } from '@grafana/data'; import { PrometheusDatasource } from './datasource'; import { getPrometheusTime } from './language_utils'; @@ -15,19 +15,15 @@ import { import { escapeForUtf8Support, isValidLegacyName } from './utf8_support'; export class PrometheusMetricFindQuery { - range: TimeRange; - constructor( private datasource: PrometheusDatasource, private query: string ) { this.datasource = datasource; this.query = query; - this.range = getDefaultTimeRange(); } process(timeRange: TimeRange): Promise { - this.range = timeRange; const labelNamesRegex = PrometheusLabelNamesRegex; const labelNamesRegexWithMatch = PrometheusLabelNamesRegexWithMatch; const labelValuesRegex = PrometheusLabelValuesRegex; @@ -38,7 +34,7 @@ export class PrometheusMetricFindQuery { if (labelNamesMatchQuery) { const selector = `{__name__=~".*${labelNamesMatchQuery[1]}.*"}`; - return this.datasource.languageProvider.getSeriesLabels(selector, []).then((results) => + return this.datasource.languageProvider.getSeriesLabels(timeRange, selector, []).then((results) => results.map((result) => ({ text: result, })) @@ -54,35 +50,35 @@ export class PrometheusMetricFindQuery { const filter = labelValuesQuery[1]; const label = labelValuesQuery[2]; if (isFilterDefined(filter)) { - return this.labelValuesQuery(label, filter); + return this.labelValuesQuery(label, timeRange, filter); } else { // Exclude the filter part of the expression because it is blank or empty - return this.labelValuesQuery(label); + return this.labelValuesQuery(label, timeRange); } } const metricNamesQuery = this.query.match(metricNamesRegex); if (metricNamesQuery) { - return this.metricNameQuery(metricNamesQuery[1]); + return this.metricNameQuery(metricNamesQuery[1], timeRange); } const queryResultQuery = this.query.match(queryResultRegex); if (queryResultQuery) { - return this.queryResultQuery(queryResultQuery[1]); + return this.queryResultQuery(queryResultQuery[1], timeRange); } // if query contains full metric name, return metric name and label list const expressions = ['label_values()', 'metrics()', 'query_result()']; if (!expressions.includes(this.query)) { - return this.metricNameAndLabelsQuery(this.query); + return this.metricNameAndLabelsQuery(this.query, timeRange); } return Promise.resolve([]); } - labelValuesQuery(label: string, metric?: string) { - const start = getPrometheusTime(this.range.from, false); - const end = getPrometheusTime(this.range.to, true); + labelValuesQuery(label: string, range: TimeRange, metric?: string) { + const start = getPrometheusTime(range.from, false); + const end = getPrometheusTime(range.to, true); const params = { ...(metric && { 'match[]': metric }), start: start.toString(), end: end.toString() }; let escapedLabel = label; @@ -118,9 +114,9 @@ export class PrometheusMetricFindQuery { } } - metricNameQuery(metricFilterPattern: string) { - const start = getPrometheusTime(this.range.from, false); - const end = getPrometheusTime(this.range.to, true); + metricNameQuery(metricFilterPattern: string, range: TimeRange) { + const start = getPrometheusTime(range.from, false); + const end = getPrometheusTime(range.to, true); const params = { start: start.toString(), end: end.toString(), @@ -143,11 +139,11 @@ export class PrometheusMetricFindQuery { }); } - queryResultQuery(query: string) { + queryResultQuery(query: string, range: TimeRange) { const url = '/api/v1/query'; const params = { query, - time: getPrometheusTime(this.range.to, true).toString(), + time: getPrometheusTime(range.to, true).toString(), }; return this.datasource.metadataRequest(url, params).then((result) => { switch (result.data.data.resultType) { @@ -182,9 +178,9 @@ export class PrometheusMetricFindQuery { }); } - metricNameAndLabelsQuery(query: string): Promise { - const start = getPrometheusTime(this.range.from, false); - const end = getPrometheusTime(this.range.to, true); + metricNameAndLabelsQuery(query: string, range: TimeRange): Promise { + const start = getPrometheusTime(range.from, false); + const end = getPrometheusTime(range.to, true); const params = { 'match[]': query, start: start.toString(), diff --git a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx index ae6353e8b1a..f6f62ffb54b 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx @@ -1,7 +1,7 @@ // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/querybuilder/components/MetricsLabelsSection.tsx import { useCallback } from 'react'; -import { SelectableValue } from '@grafana/data'; +import { SelectableValue, TimeRange } from '@grafana/data'; import { config } from '@grafana/runtime'; import { PrometheusDatasource } from '../../datasource'; @@ -22,6 +22,7 @@ export interface MetricsLabelsSectionProps { onChange: (update: PromVisualQuery) => void; variableEditor?: boolean; onBlur?: () => void; + timeRange: TimeRange; } export function MetricsLabelsSection({ @@ -30,6 +31,7 @@ export function MetricsLabelsSection({ onChange, onBlur, variableEditor, + timeRange, }: MetricsLabelsSectionProps) { // fixing the use of 'as' from refactoring // @ts-ignore @@ -63,7 +65,7 @@ export function MetricsLabelsSection({ const onGetLabelNames = async (forLabel: Partial): Promise => { // If no metric we need to use a different method if (!query.metric) { - await datasource.languageProvider.fetchLabels(); + await datasource.languageProvider.fetchLabels(timeRange); return datasource.languageProvider.getLabelKeys().map((k) => ({ value: k })); } @@ -71,7 +73,7 @@ export function MetricsLabelsSection({ labelsToConsider.push({ label: '__name__', op: '=', value: query.metric }); const expr = promQueryModeller.renderLabels(labelsToConsider); - let labelsIndex: Record = await datasource.languageProvider.fetchLabelsWithMatch(expr); + let labelsIndex: Record = await datasource.languageProvider.fetchLabelsWithMatch(timeRange, expr); // filter out already used labels return Object.keys(labelsIndex) @@ -124,7 +126,7 @@ export function MetricsLabelsSection({ if (!forLabel.label) { return Promise.resolve([]); } - const result = datasource.languageProvider.fetchSeries(promQLExpression); + const result = datasource.languageProvider.fetchSeries(timeRange, promQLExpression); const forLabelInterpolated = datasource.interpolateString(forLabel.label); return result.then((result) => { // This query returns duplicate values, scrub them out @@ -154,7 +156,7 @@ export function MetricsLabelsSection({ const requestId = `[${datasource.uid}][${query.metric}][${forLabel.label}][${forLabel.op}]`; return datasource.languageProvider - .fetchSeriesValuesWithMatch(forLabel.label, promQLExpression, requestId) + .fetchSeriesValuesWithMatch(timeRange, forLabel.label, promQLExpression, requestId) .then((response) => response.map((v) => ({ value: v, label: v }))); }; @@ -169,7 +171,7 @@ export function MetricsLabelsSection({ } // If no metric is selected, we can get the raw list of labels if (!query.metric) { - return (await datasource.languageProvider.getLabelValues(forLabel.label)).map((v) => ({ value: v })); + return (await datasource.languageProvider.getLabelValues(timeRange, forLabel.label)).map((v) => ({ value: v })); } const labelsToConsider = query.labels.filter((x) => x !== forLabel); @@ -191,8 +193,8 @@ export function MetricsLabelsSection({ }; const onGetMetrics = useCallback(() => { - return withTemplateVariableOptions(getMetrics(datasource, query)); - }, [datasource, query, withTemplateVariableOptions]); + return withTemplateVariableOptions(getMetrics(datasource, query, timeRange)); + }, [datasource, query, timeRange, withTemplateVariableOptions]); const MetricSelectComponent = config.featureToggles.prometheusUsesCombobox ? MetricCombobox : MetricSelect; @@ -229,7 +231,8 @@ export function MetricsLabelsSection({ */ async function getMetrics( datasource: PrometheusDatasource, - query: PromVisualQuery + query: PromVisualQuery, + timeRange: TimeRange ): Promise> { // Makes sure we loaded the metadata for metrics. Usually this is done in the start() method of the provider but we // don't use it with the visual builder and there is no need to run all the start() setup anyway. @@ -245,9 +248,9 @@ async function getMetrics( let metrics: string[]; if (query.labels.length > 0) { const expr = promQueryModeller.renderLabels(query.labels); - metrics = (await datasource.languageProvider.getSeries(expr, true))['__name__'] ?? []; + metrics = (await datasource.languageProvider.getSeries(timeRange, expr, true))['__name__'] ?? []; } else { - metrics = (await datasource.languageProvider.getLabelValues('__name__')) ?? []; + metrics = (await datasource.languageProvider.getLabelValues(timeRange, '__name__')) ?? []; } return metrics.map((m) => ({ diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx index 8e58f495963..372873eebb1 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx @@ -89,7 +89,7 @@ describe('PromQueryBuilder', () => { it('tries to load metrics without labels', async () => { const { languageProvider, container } = setup(); await openMetricSelect(container); - await waitFor(() => expect(languageProvider.getLabelValues).toHaveBeenCalledWith('__name__')); + await waitFor(() => expect(languageProvider.getLabelValues).toHaveBeenCalledWith(expect.anything(), '__name__')); }); it('tries to load metrics with labels', async () => { @@ -98,7 +98,13 @@ describe('PromQueryBuilder', () => { labels: [{ label: 'label_name', op: '=', value: 'label_value' }], }); await openMetricSelect(container); - await waitFor(() => expect(languageProvider.getSeries).toHaveBeenCalledWith('{label_name="label_value"}', true)); + await waitFor(() => + expect(languageProvider.getSeries).toHaveBeenCalledWith( + expect.anything(), + '{label_name="label_value"}', + expect.anything() + ) + ); }); it('tries to load variables in metric field', async () => { @@ -113,7 +119,10 @@ describe('PromQueryBuilder', () => { const { languageProvider } = setup(); await openLabelNameSelect(); await waitFor(() => - expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith('{__name__="random_metric"}') + expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith( + expect.anything(), + '{__name__="random_metric"}' + ) ); }); @@ -135,6 +144,7 @@ describe('PromQueryBuilder', () => { await openLabelNameSelect(1); await waitFor(() => expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith( + expect.anything(), '{label_name="label_value", __name__="random_metric"}' ) ); @@ -273,7 +283,10 @@ describe('PromQueryBuilder', () => { }); await openLabelNameSelect(); await waitFor(() => - expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith('{__name__="random_metric"}') + expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith( + expect.anything(), + '{__name__="random_metric"}' + ) ); }); @@ -301,6 +314,7 @@ describe('PromQueryBuilder', () => { await openLabelNameSelect(1); await waitFor(() => expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith( + expect.anything(), '{label_name="label_value", __name__="random_metric"}' ) ); diff --git a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx index 11c79f5932c..a7e0841f634 100644 --- a/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx +++ b/packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.tsx @@ -2,7 +2,7 @@ import { css } from '@emotion/css'; import { memo, useState } from 'react'; -import { DataSourceApi, PanelData } from '@grafana/data'; +import { DataSourceApi, getDefaultTimeRange, PanelData } from '@grafana/data'; import { selectors } from '@grafana/e2e-selectors'; import { EditorRow } from '@grafana/plugin-ui'; @@ -43,7 +43,12 @@ export const PromQueryBuilder = memo((props) => { return ( <> - + {initHints.length ? (