mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
0dffbcbca7
commit
4ed0cc7d18
@ -141,8 +141,6 @@ const MonacoQueryField = (props: Props) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// we construct a DataProvider object
|
// we construct a DataProvider object
|
||||||
const getSeries = (selector: string) => lpRef.current.getSeries(selector);
|
|
||||||
|
|
||||||
const getHistory = () =>
|
const getHistory = () =>
|
||||||
Promise.resolve(historyRef.current.map((h) => h.query.expr).filter((expr) => expr !== undefined));
|
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 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);
|
const completionProvider = getCompletionProvider(monaco, dataProvider);
|
||||||
|
|
||||||
// completion-providers in monaco are not registered directly to editor-instances,
|
// completion-providers in monaco are not registered directly to editor-instances,
|
||||||
|
@ -27,7 +27,8 @@ export type DataProvider = {
|
|||||||
getAllMetricNames: () => Promise<Metric[]>;
|
getAllMetricNames: () => Promise<Metric[]>;
|
||||||
getAllLabelNames: () => Promise<string[]>;
|
getAllLabelNames: () => Promise<string[]>;
|
||||||
getLabelValues: (labelName: string) => 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
|
// we order items like: history, functions, metrics
|
||||||
@ -109,10 +110,7 @@ async function getLabelNames(
|
|||||||
return dataProvider.getAllLabelNames();
|
return dataProvider.getAllLabelNames();
|
||||||
} else {
|
} else {
|
||||||
const selector = makeSelector(metric, otherLabels);
|
const selector = makeSelector(metric, otherLabels);
|
||||||
const data = await dataProvider.getSeries(selector);
|
return await dataProvider.getSeriesLabels(selector, otherLabels);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -158,8 +156,7 @@ async function getLabelValues(
|
|||||||
return dataProvider.getLabelValues(labelName);
|
return dataProvider.getLabelValues(labelName);
|
||||||
} else {
|
} else {
|
||||||
const selector = makeSelector(metric, otherLabels);
|
const selector = makeSelector(metric, otherLabels);
|
||||||
const data = await dataProvider.getSeries(selector);
|
return await dataProvider.getSeriesValues(labelName, selector);
|
||||||
return data[labelName] ?? [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -4,6 +4,7 @@ import Plain from 'slate-plain-serializer';
|
|||||||
import { AbstractLabelOperator, HistoryItem } from '@grafana/data';
|
import { AbstractLabelOperator, HistoryItem } from '@grafana/data';
|
||||||
import { SearchFunctionType } from '@grafana/ui';
|
import { SearchFunctionType } from '@grafana/ui';
|
||||||
|
|
||||||
|
import { Label } from './components/monaco-query-field/monaco-completion-provider/situation';
|
||||||
import { PrometheusDatasource } from './datasource';
|
import { PrometheusDatasource } from './datasource';
|
||||||
import LanguageProvider from './language_provider';
|
import LanguageProvider from './language_provider';
|
||||||
import { PromQuery } from './types';
|
import { PromQuery } from './types';
|
||||||
@ -13,6 +14,7 @@ describe('Language completion provider', () => {
|
|||||||
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
metadataRequest: () => ({ data: { data: [] as any[] } }),
|
||||||
getTimeRangeParams: () => ({ start: '0', end: '1' }),
|
getTimeRangeParams: () => ({ start: '0', end: '1' }),
|
||||||
interpolateString: (string: string) => string,
|
interpolateString: (string: string) => string,
|
||||||
|
hasLabelsMatchAPISupport: () => false,
|
||||||
} as unknown as PrometheusDatasource;
|
} as unknown as PrometheusDatasource;
|
||||||
|
|
||||||
describe('cleanText', () => {
|
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', () => {
|
describe('fetchSeries', () => {
|
||||||
it('should use match[] parameter', () => {
|
it('should use match[] parameter', () => {
|
||||||
const languageProvider = new LanguageProvider(datasource);
|
const languageProvider = new LanguageProvider(datasource);
|
||||||
|
@ -14,6 +14,7 @@ import {
|
|||||||
import { BackendSrvRequest } from '@grafana/runtime';
|
import { BackendSrvRequest } from '@grafana/runtime';
|
||||||
import { CompletionItem, CompletionItemGroup, SearchFunctionType, TypeaheadInput, TypeaheadOutput } from '@grafana/ui';
|
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 { PrometheusDatasource } from './datasource';
|
||||||
import {
|
import {
|
||||||
addLimitInfo,
|
addLimitInfo,
|
||||||
@ -98,6 +99,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
* 10 as a max size is totally arbitrary right now.
|
* 10 as a max size is totally arbitrary right now.
|
||||||
*/
|
*/
|
||||||
private labelsCache = new LRU<string, Record<string, string[]>>({ max: 10 });
|
private labelsCache = new LRU<string, Record<string, string[]>>({ max: 10 });
|
||||||
|
private labelValuesCache = new LRU<string, string[]>({ max: 10 });
|
||||||
|
|
||||||
constructor(datasource: PrometheusDatasource, initialValues?: Partial<PromQlLanguageProvider>) {
|
constructor(datasource: PrometheusDatasource, initialValues?: Partial<PromQlLanguageProvider>) {
|
||||||
super();
|
super();
|
||||||
@ -501,19 +503,76 @@ export default class PromQlLanguageProvider extends LanguageProvider {
|
|||||||
return [];
|
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[]
|
* Fetches all values for a label, with optional match[]
|
||||||
* @param name
|
* @param name
|
||||||
* @param match
|
* @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 interpolatedName = name ? this.datasource.interpolateString(name) : null;
|
||||||
const range = this.datasource.getTimeRangeParams();
|
const range = this.datasource.getTimeRangeParams();
|
||||||
const urlParams = {
|
const urlParams = {
|
||||||
...range,
|
...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));
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -157,7 +157,7 @@ export const PromQueryBuilder = React.memo<Props>((props) => {
|
|||||||
if (!forLabel.label) {
|
if (!forLabel.label) {
|
||||||
return Promise.resolve([]);
|
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) => ({
|
return response.map((v) => ({
|
||||||
value: v,
|
value: v,
|
||||||
label: v,
|
label: v,
|
||||||
|
Loading…
Reference in New Issue
Block a user