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
This commit is contained in:
@@ -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
|
||||
|
||||
+2
-1
@@ -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;
|
||||
};
|
||||
|
||||
+25
-16
@@ -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"');
|
||||
});
|
||||
|
||||
+29
-17
@@ -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);
|
||||
|
||||
+5
-2
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user