Commit a7f4e4c5 by Andrej Ocenas Committed by GitHub

Prometheus: Refactor labels caching (#20898)

parent 58cffde0
......@@ -31,6 +31,7 @@
"@types/jest": "24.0.13",
"@types/jquery": "1.10.35",
"@types/lodash": "4.14.123",
"@types/lru-cache": "^5.1.0",
"@types/marked": "0.6.5",
"@types/mousetrap": "1.6.3",
"@types/node": "11.13.4",
......@@ -223,6 +224,7 @@
"is-hotkey": "0.1.4",
"jquery": "3.4.1",
"lodash": "4.17.15",
"lru-cache": "^5.1.1",
"marked": "0.6.2",
"memoize-one": "5.1.1",
"moment": "2.24.0",
......
......@@ -10,7 +10,7 @@ import { Typeahead } from '../components/Typeahead/Typeahead';
import { CompletionItem, TypeaheadOutput, TypeaheadInput, SuggestionsState } from '../types/completion';
import { makeFragment } from '../utils/slate';
export const TYPEAHEAD_DEBOUNCE = 100;
export const TYPEAHEAD_DEBOUNCE = 250;
// Commands added to the editor by this plugin.
interface SuggestionsPluginCommands {
......
import _ from 'lodash';
import LRU from 'lru-cache';
import { dateTime, LanguageProvider, HistoryItem } from '@grafana/data';
import { CompletionItem, TypeaheadInput, TypeaheadOutput, CompletionItemGroup } from '@grafana/ui';
......@@ -42,23 +43,24 @@ export function addHistoryMetadata(item: CompletionItem, history: any[]): Comple
export default class PromQlLanguageProvider extends LanguageProvider {
histogramMetrics?: string[];
timeRange?: { start: number; end: number };
labelKeys?: { [index: string]: string[] }; // metric -> [labelKey,...]
labelValues?: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...]
metrics?: string[];
startTask: Promise<any>;
datasource: PrometheusDatasource;
constructor(datasource: PrometheusDatasource, initialValues?: any) {
/**
* Cache for labels of series. This is bit simplistic in the sense that it just counts responses each as a 1 and does
* not account for different size of a response. If that is needed a `length` function can be added in the options.
* 10 as a max size is totally arbitrary right now.
*/
private labelsCache = new LRU<string, Record<string, string[]>>(10);
constructor(datasource: PrometheusDatasource) {
super();
this.datasource = datasource;
this.histogramMetrics = [];
this.timeRange = { start: 0, end: 0 };
this.labelKeys = {};
this.labelValues = {};
this.metrics = [];
Object.assign(this, initialValues);
}
// Strip syntax chars
......@@ -216,20 +218,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
};
}
roundToMinutes(seconds: number): number {
return Math.floor(seconds / 60);
}
timeRangeChanged(): boolean {
const dsRange = this.datasource.getTimeRange();
return (
this.roundToMinutes(dsRange.end) !== this.roundToMinutes(this.timeRange.end) ||
this.roundToMinutes(dsRange.start) !== this.roundToMinutes(this.timeRange.start)
);
}
getAggregationCompletionItems = ({ value }: TypeaheadInput): TypeaheadOutput => {
const refresher: Promise<any> = null;
getAggregationCompletionItems = async ({ value }: TypeaheadInput): Promise<TypeaheadOutput> => {
const suggestions: CompletionItemGroup[] = [];
// Stitch all query lines together to support multi-line queries
......@@ -258,7 +247,6 @@ export default class PromQlLanguageProvider extends LanguageProvider {
}
const result = {
refresher,
suggestions,
context: 'context-aggregation',
};
......@@ -275,13 +263,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
const selector = parseSelector(selectorString, selectorString.length - 2).selector;
const labelKeys = this.labelKeys[selector];
if (labelKeys && !this.timeRangeChanged()) {
suggestions.push({ label: 'Labels', items: labelKeys.map(wrapLabel) });
} else {
result.refresher = this.fetchSeriesLabels(selector);
const labelValues = await this.getLabelValues(selector);
if (labelValues) {
suggestions.push({ label: 'Labels', items: Object.keys(labelValues).map(wrapLabel) });
}
return result;
};
......@@ -307,36 +292,31 @@ export default class PromQlLanguageProvider extends LanguageProvider {
const containsMetric = selector.includes('__name__=');
const existingKeys = parsedSelector ? parsedSelector.labelKeys : [];
const suggestions: CompletionItemGroup[] = [];
let labelValues;
// Query labels for selector
if (selector) {
if (selector === EMPTY_SELECTOR) {
// For empty selector we do not need to check range
if (!this.labelValues[selector]) {
// Query label values for default labels
await Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key)));
}
} else {
if (!this.labelValues[selector] || this.timeRangeChanged()) {
await this.fetchSeriesLabels(selector, !containsMetric);
}
}
labelValues = await this.getLabelValues(selector, !containsMetric);
}
if (!labelValues) {
console.warn(`Server did not return any values for selector = ${selector}`);
return { suggestions };
}
const suggestions = [];
let context: string;
if ((text && text.match(/^!?=~?/)) || wrapperClasses.includes('attr-value')) {
// Label values
if (labelKey && this.labelValues[selector] && this.labelValues[selector][labelKey]) {
const labelValues = this.labelValues[selector][labelKey];
if (labelKey && labelValues[labelKey]) {
context = 'context-label-values';
suggestions.push({
label: `Label values for "${labelKey}"`,
items: labelValues.map(wrapLabel),
items: labelValues[labelKey].map(wrapLabel),
});
}
} else {
// Label keys
const labelKeys = this.labelKeys[selector] || (containsMetric ? null : DEFAULT_KEYS);
const labelKeys = labelValues ? Object.keys(labelValues) : containsMetric ? null : DEFAULT_KEYS;
if (labelKeys) {
const possibleKeys = _.difference(labelKeys, existingKeys);
......@@ -352,30 +332,62 @@ export default class PromQlLanguageProvider extends LanguageProvider {
return { context, suggestions };
};
fetchLabelValues = async (key: string) => {
async getLabelValues(selector: string, withName?: boolean) {
try {
const data = await this.request(`/api/v1/label/${key}/values`);
const existingValues = this.labelValues[EMPTY_SELECTOR];
const values = {
...existingValues,
[key]: data,
};
this.labelValues[EMPTY_SELECTOR] = values;
} catch (e) {
console.error(e);
if (selector === EMPTY_SELECTOR) {
return await this.fetchDefaultLabels();
} else {
return await this.fetchSeriesLabels(selector, withName);
}
} catch (error) {
// TODO: better error handling
console.error(error);
return undefined;
}
}
fetchLabelValues = async (key: string): Promise<Record<string, string[]>> => {
const data = await this.request(`/api/v1/label/${key}/values`);
return { [key]: data };
};
fetchSeriesLabels = async (name: string, withName?: boolean) => {
try {
const tRange = this.datasource.getTimeRange();
const data = await this.request(`/api/v1/series?match[]=${name}&start=${tRange['start']}&end=${tRange['end']}`);
const { keys, values } = processLabels(data, withName);
this.labelKeys[name] = keys;
this.labelValues[name] = values;
this.timeRange = tRange;
} catch (e) {
console.error(e);
roundToMinutes(seconds: number): number {
return Math.floor(seconds / 60);
}
/**
* Fetch labels for a series. This is cached by it's args but also by the global timeRange currently selected as
* they can change over requested time.
* @param name
* @param withName
*/
fetchSeriesLabels = async (name: string, withName?: boolean): Promise<Record<string, string[]>> => {
const tRange = this.datasource.getTimeRange();
const url = `/api/v1/series?match[]=${name}&start=${tRange['start']}&end=${tRange['end']}`;
// 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 cacheKey = `/api/v1/series?match[]=${name}&start=${this.roundToMinutes(
tRange['start']
)}&end=${this.roundToMinutes(tRange['end'])}&withName=${!!withName}`;
let value = this.labelsCache.get(cacheKey);
if (!value) {
const data = await this.request(url);
const { values } = processLabels(data, withName);
value = values;
this.labelsCache.set(cacheKey, value);
}
return value;
};
/**
* Fetch this only one as we assume this won't change over time. This is cached differently from fetchSeriesLabels
* because we can cache more aggressively here and also we do not want to invalidate this cache the same way as in
* fetchSeriesLabels.
*/
fetchDefaultLabels = _.once(async () => {
const values = await Promise.all(DEFAULT_KEYS.map(key => this.fetchLabelValues(key)));
return values.reduce((acc, value) => ({ ...acc, ...value }), {});
});
}
......@@ -4071,6 +4071,11 @@
resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.123.tgz#39be5d211478c8dd3bdae98ee75bb7efe4abfe4d"
integrity sha512-pQvPkc4Nltyx7G1Ww45OjVqUsJP4UsZm+GWJpigXgkikZqJgRm4c48g027o6tdgubWHwFRF15iFd+Y4Pmqv6+Q==
"@types/lru-cache@^5.1.0":
version "5.1.0"
resolved "https://registry.yarnpkg.com/@types/lru-cache/-/lru-cache-5.1.0.tgz#57f228f2b80c046b4a1bd5cac031f81f207f4f03"
integrity sha512-RaE0B+14ToE4l6UqdarKPnXwVDuigfFv+5j9Dze/Nqr23yyuqdNvzcZi3xB+3Agvi5R4EOgAksfv3lXX4vBt9w==
"@types/marked@0.6.5":
version "0.6.5"
resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.6.5.tgz#3cf2a56ef615dad24aaf99784ef90a9eba4e29d8"
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment