Skip to content

Commit

Permalink
Prometheus: remove /series endpoint calls in query builder label name…
Browse files Browse the repository at this point in the history
…s and values for supported clients (grafana#58087)

* add other filter variables to match param for label values query against filter values, in order to resolve bug in which filter value options would display that aren't relevant in the current query editor context, i.e. options would display that upon select would display no data

* expanding current unit test coverage to cover calls to new API

* interpolate the label name string instead of the match promql expression
  • Loading branch information
gtk-grafana authored Nov 8, 2022
1 parent 904c6f1 commit 9281746
Show file tree
Hide file tree
Showing 6 changed files with 245 additions and 22 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export class EmptyLanguageProviderMock {
getSeries = jest.fn().mockReturnValue({ __name__: [] });
fetchSeries = jest.fn().mockReturnValue([]);
fetchSeriesLabels = jest.fn().mockReturnValue([]);
fetchSeriesLabelsMatch = jest.fn().mockReturnValue([]);
fetchLabels = jest.fn();
loadMetricsMetadata = jest.fn();
}
59 changes: 57 additions & 2 deletions public/app/plugins/datasource/prometheus/language_provider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { once, chain, difference } from 'lodash';
import { chain, difference, once } from 'lodash';
import LRU from 'lru-cache';
import Prism from 'prismjs';
import { Value } from 'slate';
Expand Down Expand Up @@ -471,6 +471,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
}
}

/**
* @todo cache
* @param key
*/
fetchLabelValues = async (key: string): Promise<string[]> => {
const params = this.datasource.getTimeRangeParams();
const url = `/api/v1/label/${this.datasource.interpolateString(key)}/values`;
Expand Down Expand Up @@ -498,7 +502,22 @@ export default class PromQlLanguageProvider extends LanguageProvider {
}

/**
* Fetch labels for a series. This is cached by its args but also by the global timeRange currently selected as
* Fetches all values for a label, with optional match[]
* @param name
* @param match
*/
fetchSeriesValues = async (name: string, match?: string): Promise<string[]> => {
const interpolatedName = name ? this.datasource.interpolateString(name) : null;
const range = this.datasource.getTimeRangeParams();
const urlParams = {
...range,
...(interpolatedName && { 'match[]': match }),
};
return await this.request(`/api/v1/label/${interpolatedName}/values`, [], urlParams);
};

/**
* 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
Expand Down Expand Up @@ -533,6 +552,42 @@ export default class PromQlLanguageProvider extends LanguageProvider {
return value;
};

/**
* 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[]>> => {
const interpolatedName = this.datasource.interpolateString(name);
const range = this.datasource.getTimeRangeParams();
const urlParams = {
...range,
'match[]': interpolatedName,
};
const url = `/api/v1/labels`;
// Cache key is a bit different here. We add the `withName` param and also round up to a minute the intervals.
// The rounding may seem strange but makes relative intervals like now-1h less prone to need separate request every
// millisecond while still actually getting all the keys for the correct interval. This still can create problems
// when user does not the newest values for a minute if already cached.
const cacheParams = new URLSearchParams({
'match[]': interpolatedName,
start: roundSecToMin(parseInt(range.start, 10)).toString(),
end: roundSecToMin(parseInt(range.end, 10)).toString(),
withName: withName ? 'true' : 'false',
});

const cacheKey = `${url}?${cacheParams.toString()}`;
let value = this.labelsCache.get(cacheKey);
if (!value) {
const data: string[] = await this.request(url, [], urlParams);
// Convert string array to Record<string , []>
value = data.reduce((ac, a) => ({ ...ac, [a]: '' }), {});
this.labelsCache.set(cacheKey, value);
}
return value;
};

/**
* Fetch series for a selector. Use this for raw results. Use fetchSeriesLabels() to get labels.
* @param match
Expand Down
80 changes: 75 additions & 5 deletions public/app/plugins/datasource/prometheus/metric_find_query.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import { DataSourceInstanceSettings, toUtc } from '@grafana/data';
import { FetchResponse } from '@grafana/runtime';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__

import { PromApplication } from '../../../types/unified-alerting-dto';

import { PrometheusDatasource } from './datasource';
import PrometheusMetricFindQuery from './metric_find_query';
import { PromOptions } from './types';
Expand All @@ -23,7 +25,7 @@ const instanceSettings = {
user: 'test',
password: 'mupp',
jsonData: { httpMethod: 'GET' },
} as unknown as DataSourceInstanceSettings<PromOptions>;
} as Partial<DataSourceInstanceSettings<PromOptions>> as DataSourceInstanceSettings<PromOptions>;
const raw = {
from: toUtc('2018-04-25 10:00'),
to: toUtc('2018-04-25 11:00'),
Expand Down Expand Up @@ -52,14 +54,22 @@ beforeEach(() => {
});

describe('PrometheusMetricFindQuery', () => {
let ds: PrometheusDatasource;
let legacyPrometheusDatasource: PrometheusDatasource;
let prometheusDatasource: PrometheusDatasource;
beforeEach(() => {
ds = new PrometheusDatasource(instanceSettings, templateSrvStub);
legacyPrometheusDatasource = new PrometheusDatasource(instanceSettings, templateSrvStub);
prometheusDatasource = new PrometheusDatasource(
{
...instanceSettings,
jsonData: { ...instanceSettings.jsonData, prometheusVersion: '2.2.0', prometheusType: PromApplication.Mimir },
},
templateSrvStub
);
});

const setupMetricFindQuery = (data: any) => {
const setupMetricFindQuery = (data: any, datasource?: PrometheusDatasource) => {
fetchMock.mockImplementation(() => of({ status: 'success', data: data.response } as unknown as FetchResponse));
return new PrometheusMetricFindQuery(ds, data.query);
return new PrometheusMetricFindQuery(datasource ?? legacyPrometheusDatasource, data.query);
};

describe('When performing metricFindQuery', () => {
Expand Down Expand Up @@ -102,6 +112,7 @@ describe('PrometheusMetricFindQuery', () => {
});
});

// <LegacyPrometheus>
it('label_values(metric, resource) should generate series query with correct time', async () => {
const query = setupMetricFindQuery({
query: 'label_values(metric, resource)',
Expand Down Expand Up @@ -179,6 +190,7 @@ describe('PrometheusMetricFindQuery', () => {
headers: {},
});
});
// </LegacyPrometheus>

it('metrics(metric.*) should generate metric name query', async () => {
const query = setupMetricFindQuery({
Expand Down Expand Up @@ -277,5 +289,63 @@ describe('PrometheusMetricFindQuery', () => {
headers: {},
});
});

// <ModernPrometheus>
it('label_values(metric, resource) should generate label values query with correct time', async () => {
const metricName = 'metricName';
const resourceName = 'resourceName';
const query = setupMetricFindQuery(
{
query: `label_values(${metricName}, ${resourceName})`,
response: {
data: [
{ __name__: `${metricName}`, resourceName: 'value1' },
{ __name__: `${metricName}`, resourceName: 'value2' },
{ __name__: `${metricName}`, resourceName: 'value3' },
],
},
},
prometheusDatasource
);
const results = await query.process();

expect(results).toHaveLength(3);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith({
method: 'GET',
url: `/api/datasources/1/resources/api/v1/label/${resourceName}/values?match${encodeURIComponent(
'[]'
)}=${metricName}&start=${raw.from.unix()}&end=${raw.to.unix()}`,
hideFromInspector: true,
headers: {},
});
});

it('label_values(metric{label1="foo", label2="bar", label3="baz"}, resource) should generate label values query with correct time', async () => {
const metricName = 'metricName';
const resourceName = 'resourceName';
const label1Name = 'label1';
const label1Value = 'label1Value';
const query = setupMetricFindQuery(
{
query: `label_values(${metricName}{${label1Name}="${label1Value}"}, ${resourceName})`,
response: {
data: [{ __name__: metricName, resourceName: label1Value }],
},
},
prometheusDatasource
);
const results = await query.process();

expect(results).toHaveLength(1);
expect(fetchMock).toHaveBeenCalledTimes(1);
expect(fetchMock).toHaveBeenCalledWith({
method: 'GET',
url: `/api/datasources/1/resources/api/v1/label/${resourceName}/values?match%5B%5D=${metricName}%7B${label1Name}%3D%22${label1Value}%22%7D&start=1524650400&end=1524654000`,
hideFromInspector: true,
headers: {},
});
});
// </ ModernPrometheus>
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface Props {
labelsFilters: QueryBuilderLabelFilter[];
}

const MAX_NUMBER_OF_RESULTS = 1000;
export const PROMETHEUS_QUERY_BUILDER_MAX_RESULTS = 1000;

export function MetricSelect({ datasource, query, onChange, onGetMetrics, labelsFilters }: Props) {
const styles = useStyles2(getStyles);
Expand Down Expand Up @@ -109,8 +109,8 @@ export function MetricSelect({ datasource, query, onChange, onGetMetrics, labels
// Since some customers can have millions of metrics, whenever the user changes the autocomplete text we want to call the backend and request all metrics that match the current query string
const results = datasource.metricFindQuery(formatKeyValueStringsForLabelValuesQuery(query, labelsFilters));
return results.then((results) => {
if (results.length > MAX_NUMBER_OF_RESULTS) {
results.splice(0, results.length - MAX_NUMBER_OF_RESULTS);
if (results.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
results.splice(0, results.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
}
return results.map((result) => {
return {
Expand All @@ -137,8 +137,8 @@ export function MetricSelect({ datasource, query, onChange, onGetMetrics, labels
onOpenMenu={async () => {
setState({ isLoading: true });
const metrics = await onGetMetrics();
if (metrics.length > MAX_NUMBER_OF_RESULTS) {
metrics.splice(0, metrics.length - MAX_NUMBER_OF_RESULTS);
if (metrics.length > PROMETHEUS_QUERY_BUILDER_MAX_RESULTS) {
metrics.splice(0, metrics.length - PROMETHEUS_QUERY_BUILDER_MAX_RESULTS);
}
setState({ metrics, isLoading: undefined });
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,11 @@ import {
TimeRange,
} from '@grafana/data';

import { PromApplication } from '../../../../../types/unified-alerting-dto';
import { PrometheusDatasource } from '../../datasource';
import PromQlLanguageProvider from '../../language_provider';
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
import { PromOptions } from '../../types';
import { getLabelSelects } from '../testUtils';
import { PromVisualQuery } from '../types';

Expand Down Expand Up @@ -101,6 +103,7 @@ describe('PromQueryBuilder', () => {
await waitFor(() => expect(datasource.getVariables).toBeCalled());
});

// <LegacyPrometheus>
it('tries to load labels when metric selected', async () => {
const { languageProvider } = setup();
await openLabelNameSelect();
Expand All @@ -127,6 +130,7 @@ describe('PromQueryBuilder', () => {
expect(languageProvider.fetchSeriesLabels).toBeCalledWith('{label_name="label_value", __name__="random_metric"}')
);
});
//</LegacyPrometheus>

it('tries to load labels when metric is not selected', async () => {
const { languageProvider } = setup({
Expand Down Expand Up @@ -224,16 +228,56 @@ describe('PromQueryBuilder', () => {
);
expect(await screen.queryByText(EXPLAIN_LABEL_FILTER_CONTENT)).not.toBeInTheDocument();
});

// <ModernPrometheus>
it('tries to load labels when metric selected modern prom', async () => {
const { languageProvider } = setup(undefined, undefined, {
jsonData: { prometheusVersion: '2.38.1', prometheusType: PromApplication.Prometheus },
});
await openLabelNameSelect();
await waitFor(() => expect(languageProvider.fetchSeriesLabelsMatch).toBeCalledWith('{__name__="random_metric"}'));
});

it('tries to load variables in label field modern prom', async () => {
const { datasource } = setup(undefined, undefined, {
jsonData: { prometheusVersion: '2.38.1', prometheusType: PromApplication.Prometheus },
});
datasource.getVariables = jest.fn().mockReturnValue([]);
await openLabelNameSelect();
await waitFor(() => expect(datasource.getVariables).toBeCalled());
});

it('tries to load labels when metric selected and other labels are already present modern prom', async () => {
const { languageProvider } = setup(
{
...defaultQuery,
labels: [
{ label: 'label_name', op: '=', value: 'label_value' },
{ label: 'foo', op: '=', value: 'bar' },
],
},
undefined,
{ jsonData: { prometheusVersion: '2.38.1', prometheusType: PromApplication.Prometheus } }
);
await openLabelNameSelect(1);
await waitFor(() =>
expect(languageProvider.fetchSeriesLabelsMatch).toBeCalledWith(
'{label_name="label_value", __name__="random_metric"}'
)
);
});
//</ModernPrometheus>
});

function createDatasource() {
function createDatasource(options?: Partial<DataSourceInstanceSettings<PromOptions>>) {
const languageProvider = new EmptyLanguageProviderMock() as unknown as PromQlLanguageProvider;
const datasource = new PrometheusDatasource(
{
url: '',
jsonData: {},
meta: {} as DataSourcePluginMeta,
} as DataSourceInstanceSettings,
...options,
} as DataSourceInstanceSettings<PromOptions>,
undefined,
undefined,
languageProvider
Expand All @@ -251,8 +295,12 @@ function createProps(datasource: PrometheusDatasource, data?: PanelData) {
};
}

function setup(query: PromVisualQuery = defaultQuery, data?: PanelData) {
const { datasource, languageProvider } = createDatasource();
function setup(
query: PromVisualQuery = defaultQuery,
data?: PanelData,
datasourceOptionsOverride?: Partial<DataSourceInstanceSettings<PromOptions>>
) {
const { datasource, languageProvider } = createDatasource(datasourceOptionsOverride);
const props = createProps(datasource, data);
const { container } = render(<PromQueryBuilder {...props} query={query} />);
return { languageProvider, datasource, container };
Expand Down
Loading

0 comments on commit 9281746

Please sign in to comment.