Prometheus: Code editor - upgrade /series API endpoints to use label/values and /labels for supported prometheus clients (#59576)

* Allow prometheus code editor API to use new prometheus API calls for supported data source clients.
Co-authored-by: Ryan McKinley <ryantxu@gmail.com>
This commit is contained in:
Galen Kistler 2023-01-03 12:38:11 -06:00 committed by GitHub
parent 0dffbcbca7
commit 4ed0cc7d18
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 150 additions and 14 deletions

View File

@ -141,8 +141,6 @@ const MonacoQueryField = (props: Props) => {
});
// we construct a DataProvider object
const getSeries = (selector: string) => lpRef.current.getSeries(selector);
const getHistory = () =>
Promise.resolve(historyRef.current.map((h) => h.query.expr).filter((expr) => expr !== undefined));
@ -164,7 +162,17 @@ const MonacoQueryField = (props: Props) => {
const getLabelValues = (labelName: string) => lpRef.current.getLabelValues(labelName);
const dataProvider = { getSeries, getHistory, getAllMetricNames, getAllLabelNames, getLabelValues };
const getSeriesValues = lpRef.current.getSeriesValues;
const getSeriesLabels = lpRef.current.getSeriesLabels;
const dataProvider = {
getHistory,
getAllMetricNames,
getAllLabelNames,
getLabelValues,
getSeriesValues,
getSeriesLabels,
};
const completionProvider = getCompletionProvider(monaco, dataProvider);
// completion-providers in monaco are not registered directly to editor-instances,

View File

@ -27,7 +27,8 @@ export type DataProvider = {
getAllMetricNames: () => Promise<Metric[]>;
getAllLabelNames: () => Promise<string[]>;
getLabelValues: (labelName: string) => Promise<string[]>;
getSeries: (selector: string) => Promise<Record<string, string[]>>;
getSeriesValues: (name: string, match: string) => Promise<string[]>;
getSeriesLabels: (selector: string, otherLabels: Label[]) => Promise<string[]>;
};
// we order items like: history, functions, metrics
@ -109,10 +110,7 @@ async function getLabelNames(
return dataProvider.getAllLabelNames();
} else {
const selector = makeSelector(metric, otherLabels);
const data = await dataProvider.getSeries(selector);
const possibleLabelNames = Object.keys(data); // all names from prometheus
const usedLabelNames = new Set(otherLabels.map((l) => l.name)); // names used in the query
return possibleLabelNames.filter((l) => !usedLabelNames.has(l));
return await dataProvider.getSeriesLabels(selector, otherLabels);
}
}
@ -158,8 +156,7 @@ async function getLabelValues(
return dataProvider.getLabelValues(labelName);
} else {
const selector = makeSelector(metric, otherLabels);
const data = await dataProvider.getSeries(selector);
return data[labelName] ?? [];
return await dataProvider.getSeriesValues(labelName, selector);
}
}

View File

@ -4,6 +4,7 @@ import Plain from 'slate-plain-serializer';
import { AbstractLabelOperator, HistoryItem } from '@grafana/data';
import { SearchFunctionType } from '@grafana/ui';
import { Label } from './components/monaco-query-field/monaco-completion-provider/situation';
import { PrometheusDatasource } from './datasource';
import LanguageProvider from './language_provider';
import { PromQuery } from './types';
@ -13,6 +14,7 @@ describe('Language completion provider', () => {
metadataRequest: () => ({ data: { data: [] as any[] } }),
getTimeRangeParams: () => ({ start: '0', end: '1' }),
interpolateString: (string: string) => string,
hasLabelsMatchAPISupport: () => false,
} as unknown as PrometheusDatasource;
describe('cleanText', () => {
@ -66,6 +68,76 @@ describe('Language completion provider', () => {
});
});
describe('getSeriesLabels', () => {
it('should call series endpoint', () => {
const languageProvider = new LanguageProvider({ ...datasource } as PrometheusDatasource);
const getSeriesLabels = languageProvider.getSeriesLabels;
const requestSpy = jest.spyOn(languageProvider, 'request');
const labelName = 'job';
const labelValue = 'grafana';
getSeriesLabels(`{${labelName}="${labelValue}"}`, [{ name: labelName, value: labelValue, op: '=' }] as Label[]);
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy).toHaveBeenCalledWith('/api/v1/series', [], {
end: '1',
'match[]': '{job="grafana"}',
start: '0',
});
});
it('should call labels endpoint', () => {
const languageProvider = new LanguageProvider({
...datasource,
hasLabelsMatchAPISupport: () => true,
} as PrometheusDatasource);
const getSeriesLabels = languageProvider.getSeriesLabels;
const requestSpy = jest.spyOn(languageProvider, 'request');
const labelName = 'job';
const labelValue = 'grafana';
getSeriesLabels(`{${labelName}="${labelValue}"}`, [{ name: labelName, value: labelValue, op: '=' }] as Label[]);
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy).toHaveBeenCalledWith(`/api/v1/labels`, [], {
end: '1',
'match[]': '{job="grafana"}',
start: '0',
});
});
});
describe('getSeriesValues', () => {
it('should call old series endpoint and should use match[] parameter', () => {
const languageProvider = new LanguageProvider(datasource);
const getSeriesValues = languageProvider.getSeriesValues;
const requestSpy = jest.spyOn(languageProvider, 'request');
getSeriesValues('job', '{job="grafana"}');
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy).toHaveBeenCalledWith('/api/v1/series', [], {
end: '1',
'match[]': '{job="grafana"}',
start: '0',
});
});
it('should call new series endpoint and should use match[] parameter', () => {
const languageProvider = new LanguageProvider({
...datasource,
hasLabelsMatchAPISupport: () => true,
} as PrometheusDatasource);
const getSeriesValues = languageProvider.getSeriesValues;
const requestSpy = jest.spyOn(languageProvider, 'request');
const labelName = 'job';
const labelValue = 'grafana';
getSeriesValues(labelName, `{${labelName}="${labelValue}"}`);
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy).toHaveBeenCalledWith(`/api/v1/label/${labelName}/values`, [], {
end: '1',
'match[]': `{${labelName}="${labelValue}"}`,
start: '0',
});
});
});
describe('fetchSeries', () => {
it('should use match[] parameter', () => {
const languageProvider = new LanguageProvider(datasource);

View File

@ -14,6 +14,7 @@ import {
import { BackendSrvRequest } from '@grafana/runtime';
import { CompletionItem, CompletionItemGroup, SearchFunctionType, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
import { Label } from './components/monaco-query-field/monaco-completion-provider/situation';
import { PrometheusDatasource } from './datasource';
import {
addLimitInfo,
@ -98,6 +99,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
* 10 as a max size is totally arbitrary right now.
*/
private labelsCache = new LRU<string, Record<string, string[]>>({ max: 10 });
private labelValuesCache = new LRU<string, string[]>({ max: 10 });
constructor(datasource: PrometheusDatasource, initialValues?: Partial<PromQlLanguageProvider>) {
super();
@ -501,19 +503,76 @@ export default class PromQlLanguageProvider extends LanguageProvider {
return [];
}
/**
* 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
*/
getSeriesValues = async (labelName: string, selector: string): Promise<string[]> => {
if (!this.datasource.hasLabelsMatchAPISupport()) {
const data = await this.getSeries(selector);
return data[labelName] ?? [];
}
return await this.fetchSeriesValuesWithMatch(labelName, selector);
};
/**
* Fetches all values for a label, with optional match[]
* @param name
* @param match
*/
fetchSeriesValues = async (name: string, match?: string): Promise<string[]> => {
fetchSeriesValuesWithMatch = 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 }),
...(match && { 'match[]': match }),
};
return await this.request(`/api/v1/label/${interpolatedName}/values`, [], urlParams);
const cacheParams = new URLSearchParams({
'match[]': interpolatedName ?? '',
start: roundSecToMin(parseInt(range.start, 10)).toString(),
end: roundSecToMin(parseInt(range.end, 10)).toString(),
name: name,
});
const cacheKey = `/api/v1/label/?${cacheParams.toString()}/values`;
let value: string[] | undefined = this.labelValuesCache.get(cacheKey);
if (!value) {
value = await this.request(`/api/v1/label/${interpolatedName}/values`, [], urlParams);
if (value) {
this.labelValuesCache.set(cacheKey, value);
}
}
return value ?? [];
};
/**
* Gets series labels
* Function to replace old getSeries calls in a way that will provide faster endpoints for new prometheus instances,
* while maintaining backward compatability. The old API call got the labels and the values in a single query,
* but with the new query we need two calls, one to get the labels, and another to get the values.
*
* @param selector
* @param otherLabels
*/
getSeriesLabels = async (selector: string, otherLabels: Label[]): Promise<string[]> => {
let possibleLabelNames, data: Record<string, string[]>;
if (!this.datasource.hasLabelsMatchAPISupport()) {
data = await this.getSeries(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);
possibleLabelNames = Object.keys(data);
}
const usedLabelNames = new Set(otherLabels.map((l) => l.name)); // names used in the query
return possibleLabelNames.filter((l) => !usedLabelNames.has(l));
};
/**

View File

@ -157,7 +157,7 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
if (!forLabel.label) {
return Promise.resolve([]);
}
return datasource.languageProvider.fetchSeriesValues(forLabel.label, promQLExpression).then((response) => {
return datasource.languageProvider.fetchSeriesValuesWithMatch(forLabel.label, promQLExpression).then((response) => {
return response.map((v) => ({
value: v,
label: v,