Prometheus: Add a limit for the series resource api in metrics browser (#91555)

* add a limit for the series resource api in metrics browser

* decouple serieslimit from options and only use in metrics browser

* add series limit input to metrics browser

* add warning

* add and fix tests

* add new param to jsdoc

* do not use the limit in other calls outside metrics browser

* update test

* trim limit

* fix tests, remove limit from non labels calls
This commit is contained in:
Brendan O'Handley 2024-08-15 15:39:42 -05:00 committed by GitHub
parent de0e6d0fce
commit f01263803a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 89 additions and 6 deletions

View File

@ -106,6 +106,7 @@ export const Components = {
metricsBrowser: { metricsBrowser: {
openButton: 'data-testid open metrics browser', openButton: 'data-testid open metrics browser',
selectMetric: 'data-testid select a metric', selectMetric: 'data-testid select a metric',
seriesLimit: 'data-testid series limit',
metricList: 'data-testid metric list', metricList: 'data-testid metric list',
labelNamesFilter: 'data-testid label names filter', labelNamesFilter: 'data-testid label names filter',
labelValuesFilter: 'data-testid label values filter', labelValuesFilter: 'data-testid label values filter',

View File

@ -45,8 +45,12 @@ interface BrowserState {
error: string; error: string;
validationStatus: string; validationStatus: string;
valueSearchTerm: string; valueSearchTerm: string;
seriesLimit?: string;
} }
export const DEFAULT_SERIES_LIMIT = '40000';
export const REMOVE_SERIES_LIMIT = 'none';
interface FacettableValue { interface FacettableValue {
name: string; name: string;
selected?: boolean; selected?: boolean;
@ -214,6 +218,10 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
this.setState({ metricSearchTerm: event.target.value }); this.setState({ metricSearchTerm: event.target.value });
}; };
onChangeSeriesLimit = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ seriesLimit: event.target.value.trim() });
};
onChangeValueSearch = (event: ChangeEvent<HTMLInputElement>) => { onChangeValueSearch = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ valueSearchTerm: event.target.value }); this.setState({ valueSearchTerm: event.target.value });
}; };
@ -419,7 +427,7 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
this.updateLabelState(lastFacetted, { loading: true }, `Facetting labels for ${selector}`); this.updateLabelState(lastFacetted, { loading: true }, `Facetting labels for ${selector}`);
} }
try { try {
const possibleLabels = await languageProvider.fetchSeriesLabels(selector, true); const possibleLabels = await languageProvider.fetchSeriesLabels(selector, true, this.state.seriesLimit);
// If selector changed, clear loading state and discard result by returning early // If selector changed, clear loading state and discard result by returning early
if (selector !== buildSelector(this.state.labels)) { if (selector !== buildSelector(this.state.labels)) {
if (lastFacetted) { if (lastFacetted) {
@ -492,7 +500,9 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
<Stack gap={3}> <Stack gap={3}>
<div> <div>
<div className={styles.section}> <div className={styles.section}>
<Label description="Once a metric is selected only possible labels are shown.">1. Select a metric</Label> <Label description="Once a metric is selected only possible labels are shown. Labels are limited by the series limit below.">
1. Select a metric
</Label>
<div> <div>
<Input <Input
onChange={this.onChangeMetricSearch} onChange={this.onChangeMetricSearch}
@ -501,6 +511,17 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric} data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric}
/> />
</div> </div>
<Label description="Set to 'none' to remove limit and show all labels for a selected metric. Removing the limit may cause performance issues.">
Series limit
</Label>
<div>
<Input
onChange={this.onChangeSeriesLimit}
aria-label="Limit results from series endpoint"
value={this.state.seriesLimit ?? DEFAULT_SERIES_LIMIT}
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.seriesLimit}
/>
</div>
<div <div
role="list" role="list"
className={styles.valueListWrapper} className={styles.valueListWrapper}

View File

@ -1,6 +1,7 @@
// Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_provider.test.ts // Core Grafana history https://github.com/grafana/grafana/blob/v11.0.0-preview/public/app/plugins/datasource/prometheus/language_provider.test.ts
import { AbstractLabelOperator, dateTime, TimeRange } from '@grafana/data'; import { AbstractLabelOperator, dateTime, TimeRange } from '@grafana/data';
import { DEFAULT_SERIES_LIMIT } from './components/PrometheusMetricsBrowser';
import { Label } from './components/monaco-query-field/monaco-completion-provider/situation'; 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';
@ -301,6 +302,48 @@ describe('Language completion provider', () => {
end: toPrometheusTimeString, end: toPrometheusTimeString,
'match[]': 'interpolated-metric', 'match[]': 'interpolated-metric',
start: fromPrometheusTimeString, start: fromPrometheusTimeString,
limit: DEFAULT_SERIES_LIMIT,
},
undefined
);
});
it("should not use default limit parameter when 'none' is passed to fetchSeriesLabels", () => {
const languageProvider = new LanguageProvider({
...defaultDatasource,
} as PrometheusDatasource);
const fetchSeriesLabels = languageProvider.fetchSeriesLabels;
const requestSpy = jest.spyOn(languageProvider, 'request');
fetchSeriesLabels('metric-with-limit', undefined, 'none');
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy).toHaveBeenCalledWith(
'/api/v1/series',
[],
{
end: toPrometheusTimeString,
'match[]': 'metric-with-limit',
start: fromPrometheusTimeString,
},
undefined
);
});
it("should not have a limit paranter if 'none' is passed to function", () => {
const languageProvider = new LanguageProvider({
...defaultDatasource,
// interpolateString: (string: string) => string.replace(/\$/, 'interpolated-'),
} as PrometheusDatasource);
const fetchSeriesLabels = languageProvider.fetchSeriesLabels;
const requestSpy = jest.spyOn(languageProvider, 'request');
fetchSeriesLabels('metric-without-limit', false, 'none');
expect(requestSpy).toHaveBeenCalled();
expect(requestSpy).toHaveBeenCalledWith(
'/api/v1/series',
[],
{
end: toPrometheusTimeString,
'match[]': 'metric-without-limit',
start: fromPrometheusTimeString,
}, },
undefined undefined
); );

View File

@ -12,6 +12,7 @@ import {
} from '@grafana/data'; } from '@grafana/data';
import { BackendSrvRequest } from '@grafana/runtime'; import { BackendSrvRequest } from '@grafana/runtime';
import { DEFAULT_SERIES_LIMIT, REMOVE_SERIES_LIMIT } from './components/PrometheusMetricsBrowser';
import { Label } from './components/monaco-query-field/monaco-completion-provider/situation'; import { Label } from './components/monaco-query-field/monaco-completion-provider/situation';
import { PrometheusDatasource } from './datasource'; import { PrometheusDatasource } from './datasource';
import { import {
@ -30,6 +31,13 @@ const EMPTY_SELECTOR = '{}';
// Max number of items (metrics, labels, values) that we display as suggestions. Prevents from running out of memory. // Max number of items (metrics, labels, values) that we display as suggestions. Prevents from running out of memory.
export const SUGGESTIONS_LIMIT = 10000; export const SUGGESTIONS_LIMIT = 10000;
type UrlParamsType = {
start?: string;
end?: string;
'match[]'?: string;
limit?: string;
};
const buildCacheHeaders = (durationInSeconds: number) => { const buildCacheHeaders = (durationInSeconds: number) => {
return { return {
headers: { headers: {
@ -181,7 +189,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
if (selector === EMPTY_SELECTOR) { if (selector === EMPTY_SELECTOR) {
return await this.fetchDefaultSeries(); return await this.fetchDefaultSeries();
} else { } else {
return await this.fetchSeriesLabels(selector, withName); return await this.fetchSeriesLabels(selector, withName, REMOVE_SERIES_LIMIT);
} }
} catch (error) { } catch (error) {
// TODO: better error handling // TODO: better error handling
@ -325,7 +333,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
if (this.datasource.hasLabelsMatchAPISupport()) { if (this.datasource.hasLabelsMatchAPISupport()) {
return this.fetchSeriesLabelsMatch(name, withName); return this.fetchSeriesLabelsMatch(name, withName);
} else { } else {
return this.fetchSeriesLabels(name, withName); return this.fetchSeriesLabels(name, withName, REMOVE_SERIES_LIMIT);
} }
}; };
@ -334,14 +342,24 @@ export default class PromQlLanguageProvider extends LanguageProvider {
* they can change over requested time. * they can change over requested time.
* @param name * @param name
* @param withName * @param withName
* @param withLimit
*/ */
fetchSeriesLabels = async (name: string, withName?: boolean): Promise<Record<string, string[]>> => { fetchSeriesLabels = async (
name: string,
withName?: boolean,
withLimit?: string
): Promise<Record<string, string[]>> => {
const interpolatedName = this.datasource.interpolateString(name); const interpolatedName = this.datasource.interpolateString(name);
const range = this.datasource.getAdjustedInterval(this.timeRange); const range = this.datasource.getAdjustedInterval(this.timeRange);
const urlParams = { let urlParams: UrlParamsType = {
...range, ...range,
'match[]': interpolatedName, 'match[]': interpolatedName,
}; };
if (withLimit !== 'none') {
urlParams = { ...urlParams, limit: withLimit ?? DEFAULT_SERIES_LIMIT };
}
const url = `/api/v1/series`; const url = `/api/v1/series`;
const data = await this.request(url, [], urlParams, this.getDefaultCacheHeaders()); const data = await this.request(url, [], urlParams, this.getDefaultCacheHeaders());