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
pull/92010/head
Brendan O'Handley 9 months ago committed by GitHub
parent de0e6d0fce
commit f01263803a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      packages/grafana-e2e-selectors/src/selectors/components.ts
  2. 25
      packages/grafana-prometheus/src/components/PrometheusMetricsBrowser.tsx
  3. 43
      packages/grafana-prometheus/src/language_provider.test.ts
  4. 26
      packages/grafana-prometheus/src/language_provider.ts

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

@ -45,8 +45,12 @@ interface BrowserState {
error: string;
validationStatus: string;
valueSearchTerm: string;
seriesLimit?: string;
}
export const DEFAULT_SERIES_LIMIT = '40000';
export const REMOVE_SERIES_LIMIT = 'none';
interface FacettableValue {
name: string;
selected?: boolean;
@ -214,6 +218,10 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
this.setState({ metricSearchTerm: event.target.value });
};
onChangeSeriesLimit = (event: ChangeEvent<HTMLInputElement>) => {
this.setState({ seriesLimit: event.target.value.trim() });
};
onChangeValueSearch = (event: ChangeEvent<HTMLInputElement>) => {
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}`);
}
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 !== buildSelector(this.state.labels)) {
if (lastFacetted) {
@ -492,7 +500,9 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
<Stack gap={3}>
<div>
<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>
<Input
onChange={this.onChangeMetricSearch}
@ -501,6 +511,17 @@ export class UnthemedPrometheusMetricsBrowser extends React.Component<BrowserPro
data-testid={selectors.components.DataSource.Prometheus.queryEditor.code.metricsBrowser.selectMetric}
/>
</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
role="list"
className={styles.valueListWrapper}

@ -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
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 { PrometheusDatasource } from './datasource';
import LanguageProvider from './language_provider';
@ -301,6 +302,48 @@ describe('Language completion provider', () => {
end: toPrometheusTimeString,
'match[]': 'interpolated-metric',
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
);

@ -12,6 +12,7 @@ import {
} from '@grafana/data';
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 { PrometheusDatasource } from './datasource';
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.
export const SUGGESTIONS_LIMIT = 10000;
type UrlParamsType = {
start?: string;
end?: string;
'match[]'?: string;
limit?: string;
};
const buildCacheHeaders = (durationInSeconds: number) => {
return {
headers: {
@ -181,7 +189,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
if (selector === EMPTY_SELECTOR) {
return await this.fetchDefaultSeries();
} else {
return await this.fetchSeriesLabels(selector, withName);
return await this.fetchSeriesLabels(selector, withName, REMOVE_SERIES_LIMIT);
}
} catch (error) {
// TODO: better error handling
@ -325,7 +333,7 @@ export default class PromQlLanguageProvider extends LanguageProvider {
if (this.datasource.hasLabelsMatchAPISupport()) {
return this.fetchSeriesLabelsMatch(name, withName);
} 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.
* @param name
* @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 range = this.datasource.getAdjustedInterval(this.timeRange);
const urlParams = {
let urlParams: UrlParamsType = {
...range,
'match[]': interpolatedName,
};
if (withLimit !== 'none') {
urlParams = { ...urlParams, limit: withLimit ?? DEFAULT_SERIES_LIMIT };
}
const url = `/api/v1/series`;
const data = await this.request(url, [], urlParams, this.getDefaultCacheHeaders());

Loading…
Cancel
Save