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
pull/102556/head
ismail simsek 2 months ago committed by GitHub
parent d7fe097630
commit bf456179e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 2
      packages/grafana-prometheus/src/components/PromQueryField.tsx
  2. 7
      packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.test.tsx
  3. 17
      packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx
  4. 24
      packages/grafana-prometheus/src/components/VariableQueryEditor.tsx
  5. 4
      packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryField.tsx
  6. 3
      packages/grafana-prometheus/src/components/monaco-query-field/MonacoQueryFieldProps.ts
  7. 41
      packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.test.ts
  8. 46
      packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/completions.ts
  9. 7
      packages/grafana-prometheus/src/components/monaco-query-field/monaco-completion-provider/index.ts
  10. 19
      packages/grafana-prometheus/src/datasource.ts
  11. 32
      packages/grafana-prometheus/src/language_provider.test.ts
  12. 103
      packages/grafana-prometheus/src/language_provider.ts
  13. 40
      packages/grafana-prometheus/src/metric_find_query.ts
  14. 25
      packages/grafana-prometheus/src/querybuilder/components/MetricsLabelsSection.tsx
  15. 22
      packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.test.tsx
  16. 9
      packages/grafana-prometheus/src/querybuilder/components/PromQueryBuilder.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<PromQueryFieldProps, PromQueryFi
initialValue={query.expr ?? ''}
placeholder="Enter a PromQL query…"
datasource={datasource}
timeRange={this.props.range ?? getDefaultTimeRange()}
/>
</div>
</div>

@ -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;

@ -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<BrowserPro
}
};
getTimeRange = (): TimeRange => {
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<BrowserPro
this.updateLabelState(lastFacetted, { loading: true }, `Facetting labels for ${selector}`);
}
try {
const possibleLabels = await languageProvider.fetchSeriesLabels(selector, true, this.state.seriesLimit);
const possibleLabels = await languageProvider.fetchSeriesLabels(
this.getTimeRange(),
selector,
true,
this.state.seriesLimit
);
// If selector changed, clear loading state and discard result by returning early
if (selector !== buildSelector(this.state.labels)) {
if (lastFacetted) {
@ -469,7 +478,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
async validateSelector(selector: string) {
const { languageProvider } = this.props;
this.setState({ validationStatus: `Validating selector ${selector}`, error: '' });
const streams = await languageProvider.fetchSeries(selector);
const streams = await languageProvider.fetchSeries(this.getTimeRange(), selector);
this.setState({ validationStatus: `Selector is valid (${streams.length} series found)` });
}

@ -2,7 +2,7 @@
import debounce from 'debounce-promise';
import { FormEvent, useCallback, useEffect, useState } from 'react';
import { QueryEditorProps, SelectableValue, toOption } from '@grafana/data';
import { getDefaultTimeRange, QueryEditorProps, SelectableValue, toOption } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
import { AsyncSelect, InlineField, InlineFieldRow, Input, Select, TextArea } from '@grafana/ui';
@ -111,9 +111,14 @@ export const PromVariableQueryEditor = ({ onChange, query, datasource, range }:
return;
}
const variables = datasource.getVariables().map((variable: string) => ({ 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<string, string[]>) => {
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<string, string[]>) => {
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()}
/>
</>
)}

@ -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<HTMLDivElement>(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

@ -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;
};

@ -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"');
});

@ -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<string[]> {
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<Completion[]> {
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<Completion[]> {
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<Completion[]> {
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<string[]> {
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<Completion[]> {
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<Completion[]> {
export function getCompletions(
situation: Situation,
dataProvider: DataProvider,
timeRange: TimeRange
): Promise<Completion[]> {
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);

@ -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.

@ -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<PromQuery>): Promise<MetricFindValue[]> {
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<string, string[]> = await this.languageProvider.fetchLabelsWithMatch(expr);
let labelsIndex: Record<string, string[]> = 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<PromQuery>) {
async getTagValues(options: DataSourceGetTagValuesOptions<PromQuery>): Promise<MetricFindValue[]> {
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,

@ -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',

@ -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<any>;
@ -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<any[]> => {
this.timeRange = timeRange ?? getDefaultTimeRange();
start = async (timeRange: TimeRange = getDefaultTimeRange()): Promise<any[]> => {
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<Record<string, string[]>> {
async getSeries(timeRange: TimeRange, selector: string, withName?: boolean): Promise<Record<string, string[]>> {
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<string[]> => {
const params = this.datasource.getAdjustedInterval(this.timeRange);
fetchLabelValues = async (range: TimeRange, key: string): Promise<string[]> => {
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<string[]> {
return await this.fetchLabelValues(key);
async getLabelValues(range: TimeRange, key: string): Promise<string[]> {
return await this.fetchLabelValues(range, key);
}
/**
* Fetches all label keys
*/
fetchLabels = async (timeRange?: TimeRange, queries?: PromQuery[]): Promise<string[]> => {
if (timeRange) {
this.timeRange = timeRange;
}
fetchLabels = async (timeRange: TimeRange, queries?: PromQuery[]): Promise<string[]> => {
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<string[]> => {
getSeriesValues = async (timeRange: TimeRange, labelName: string, selector: string): Promise<string[]> => {
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<string[]> => {
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<string[]> => {
getSeriesLabels = async (timeRange: TimeRange, selector: string, otherLabels: Label[]): Promise<string[]> => {
let possibleLabelNames, data: Record<string, string[]>;
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<Record<string, string[]>> => {
fetchLabelsWithMatch = async (
timeRange: TimeRange,
name: string,
withName?: boolean
): Promise<Record<string, string[]>> => {
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<Record<string, string[]>> => {
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<Record<string, string[]>> => {
fetchSeriesLabelsMatch = async (
timeRange: TimeRange,
name: string,
withName?: boolean
): Promise<Record<string, string[]>> => {
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<Array<Record<string, string>>> => {
fetchSeries = async (timeRange: TimeRange, match: string): Promise<Array<Record<string, string>>> => {
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<string[]> => {
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<ScopeSpecFilter[]>((acc, scope) => {

@ -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<MetricFindValue[]> {
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<MetricFindValue[]> {
const start = getPrometheusTime(this.range.from, false);
const end = getPrometheusTime(this.range.to, true);
metricNameAndLabelsQuery(query: string, range: TimeRange): Promise<MetricFindValue[]> {
const start = getPrometheusTime(range.from, false);
const end = getPrometheusTime(range.to, true);
const params = {
'match[]': query,
start: start.toString(),

@ -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<QueryBuilderLabelFilter>): Promise<SelectableValue[]> => {
// 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<string, string[]> = await datasource.languageProvider.fetchLabelsWithMatch(expr);
let labelsIndex: Record<string, string[]> = 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<Array<{ value: string; description?: string }>> {
// 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) => ({

@ -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"}'
)
);

@ -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<PromQueryBuilderProps>((props) => {
return (
<>
<EditorRow>
<MetricsLabelsSection query={query} onChange={onChange} datasource={datasource} />
<MetricsLabelsSection
query={query}
onChange={onChange}
datasource={datasource}
timeRange={data?.timeRange ?? getDefaultTimeRange()}
/>
</EditorRow>
{initHints.length ? (
<div

Loading…
Cancel
Save