Skip to content

Commit

Permalink
feat: add support limit param for metrics discovery (#91)
Browse files Browse the repository at this point in the history
Co-authored-by: Roman Khavronenko <[email protected]>
  • Loading branch information
Loori-R and hagen1778 authored Sep 7, 2023
1 parent a5f46d9 commit 3d45f97
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 9 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Changelog

## tip
* FEATURE: add datasource settings for limiting the number of metrics during discovery. The proper limits should protect users from slowing down the browser when datasource returns big amounts of discovered metrics in response. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/82).

* BUGFIX: correctly handle custom query parameters in annotation queries. See [this issue](https://github.com/VictoriaMetrics/grafana-datasource/issues/95)

Expand Down
3 changes: 3 additions & 0 deletions src/configuration/ConfigEditor.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { PromOptions } from '../types';
import { AzureAuthSettings } from './AzureAuthSettings';
import { hasCredentials, setDefaultCredentials, resetCredentials } from './AzureCredentialsConfig';
import { HelpfulLinks } from "./HelpfulLinks";
import { LimitsSettings } from "./LimitsSettings";
import { PromSettings } from './PromSettings';

export enum DataSourceType {
Expand Down Expand Up @@ -66,6 +67,8 @@ export const ConfigEditor = (props: Props) => {
<AlertingSettings<PromOptions> alertmanagerDataSources={alertmanagers}{...props}/>

<PromSettings {...props}/>

<LimitsSettings {...props}/>
</>
);
};
113 changes: 113 additions & 0 deletions src/configuration/LimitsSettings.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import React, { SyntheticEvent } from 'react';

import {
DataSourcePluginOptionsEditorProps,
SelectableValue,
} from '@grafana/data';
import {
Button,
EventsWithValidation,
InlineField,
LegacyForms,
regexValidation
} from '@grafana/ui';

import { LimitMetrics, PromOptions } from '../types';

import { getValueFromEventItem } from "./PromSettings";

const { Input } = LegacyForms;

const limitsSettingsValidationEvents = {
[EventsWithValidation.onBlur]: [
regexValidation(
/^$|^\d+$/,
'Value is not valid, you can use number'
),
],
};

const limitFields = [
{
label: "Max series",
tooltip: <><code>-search.maxSeries</code> limits the number of time series, which may be returned from <a href="https://prometheus.io/docs/prometheus/latest/querying/api/#finding-series-by-label-matchers" target="_blank" rel="noreferrer">/api/v1/series</a>. This endpoint is used mostly by Grafana for auto-completion of metric names, label names and label values. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of <a href="https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate" target="_blank" rel="noreferrer">high churn rate</a>. In this case it might be useful to set the <code>Max series</code> to quite low value in order limit CPU and memory usage of the datasource.</>,
placeholder: "",
key: "maxSeries" as keyof LimitMetrics
},
{
label: "Max tag values",
tooltip: <><code>-search.maxTagValues</code> limits the number of items, which may be returned from <a href="https://prometheus.io/docs/prometheus/latest/querying/api/#querying-label-values" target="_blank" rel="noreferrer">/api/v1/label/.../values</a>. This endpoint is used mostly by Grafana for auto-completion of label values. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of <a href="https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate" target="_blank" rel="noreferrer">high churn rate</a>. In this case, it might be useful to set the <code>Max tag values</code> to quite low value in order to limit CPU and memory usage of the datasource.</>,
placeholder: "",
key: "maxTagValues" as keyof LimitMetrics
},
{
label: "Max tag keys",
tooltip: <><code>-search.maxTagKeys</code> limits the number of items, which may be returned from <a href="https://prometheus.io/docs/prometheus/latest/querying/api/#getting-label-names" target="_blank" rel="noreferrer">/api/v1/labels</a>. This endpoint is used mostly by Grafana for auto-completion of label names. Queries to this endpoint may take big amounts of CPU time and memory when the database contains big number of unique time series because of <a href="https://docs.victoriametrics.com/FAQ.html#what-is-high-churn-rate" target="_blank" rel="noreferrer">high churn rate</a>. In this case it might be useful to set the <code>Max tag keys</code> to quite low value in order to limit CPU and memory usage of the datasource.</>,
placeholder: "",
key: "maxTagKeys" as keyof LimitMetrics
}
]

type Props = Pick<DataSourcePluginOptionsEditorProps<PromOptions>, 'options' | 'onOptionsChange'>;

export const LimitsSettings = (props: Props) => {
const { options, onOptionsChange } = props;

return (
<>
<h3 className="page-heading">Limits</h3>
<p className="text-help">Leave the field blank or set the value to <code>0</code> to remove the limit</p>
<div className="gf-form-group">
{limitFields.map((field) => (
<div className="gf-form" key={field.key}>
<InlineField
label={field.label}
labelWidth={28}
tooltip={field.tooltip}
interactive={true}
>
<Input
className="width-6"
value={`${options.jsonData?.limitMetrics?.[field.key] || ''}`}
onChange={onChangeHandler(field.key, options, onOptionsChange)}
spellCheck={false}
placeholder={field.placeholder}
validationEvents={limitsSettingsValidationEvents}
/>
</InlineField>
</div>
))}
<a
className="text-link"
target="_blank"
href={"https://docs.victoriametrics.com/#prometheus-querying-api-enhancements"}
rel="noreferrer"
>
<Button
variant={'secondary'}
fill={"text"}
icon={"book"}
size={"sm"}
>
API Limits Docs
</Button>
</a>
</div>
</>
)
};

const onChangeHandler =
(key: keyof LimitMetrics, options: Props['options'], onOptionsChange: Props['onOptionsChange']) =>
(eventItem: SyntheticEvent<HTMLInputElement> | SelectableValue<string>) => {
onOptionsChange({
...options,
jsonData: {
...options.jsonData,
limitMetrics: {
...options.jsonData.limitMetrics,
[key]: getValueFromEventItem(eventItem),
}
},
});
};
13 changes: 11 additions & 2 deletions src/datasource.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ import { getOriginalMetricName, transform, transformV2 } from './result_transfor
import { getTimeSrv, TimeSrv } from './services/TimeSrv';
import {
ExemplarTraceIdDestination,
LimitMetrics,
PromDataErrorResponse,
PromDataSuccessResponse,
PromMatrixData,
Expand Down Expand Up @@ -111,6 +112,7 @@ export class PrometheusDatasource
subType: PromApplication;
rulerEnabled: boolean;
withTemplates: WithTemplate[];
limitMetrics: LimitMetrics;

constructor(
instanceSettings: DataSourceInstanceSettings<PromOptions>,
Expand Down Expand Up @@ -142,6 +144,7 @@ export class PrometheusDatasource
this.variables = new PrometheusVariableSupport(this, this.templateSrv, this.timeSrv);
this.exemplarsAvailable = false;
this.withTemplates = instanceSettings.jsonData.withTemplates ?? [];
this.limitMetrics = instanceSettings.jsonData.limitMetrics ?? {};
}

init = async () => {
Expand Down Expand Up @@ -801,13 +804,15 @@ export class PrometheusDatasource
return uniqueLabels.map((value: any) => ({ text: value }));
} else {
// Get all tags
const result = await this.metadataRequest('/api/v1/labels');
const limit = this.getLimitMetrics('maxTagKeys');
const result = await this.metadataRequest('/api/v1/labels', { limit });
return result?.data?.data?.map((value: any) => ({ text: value })) ?? [];
}
}

async getTagValues(options: { key?: string } = {}) {
const result = await this.metadataRequest(`/api/v1/label/${options.key}/values`);
const limit = this.getLimitMetrics('maxTagValues');
const result = await this.metadataRequest(`/api/v1/label/${options.key}/values`, { limit });
return result?.data?.data?.map((value: any) => ({ text: value })) ?? [];
}

Expand Down Expand Up @@ -938,6 +943,10 @@ export class PrometheusDatasource
};
}

getLimitMetrics(key: keyof LimitMetrics): number {
return this.limitMetrics[key] || 0;
}

getOriginalMetricName(labelData: { [key: string]: string }) {
return getOriginalMetricName(labelData);
}
Expand Down
14 changes: 11 additions & 3 deletions src/language_provider.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {Block, Editor, Editor as SlateEditor, EditorProperties} from 'slate';
import { Block, Editor, Editor as SlateEditor, EditorProperties } from 'slate';
import Plain from 'slate-plain-serializer';

import { AbstractLabelOperator, HistoryItem } from '@grafana/data';
Expand All @@ -14,6 +14,7 @@ describe('Language completion provider', () => {
getTimeRangeParams: () => ({ start: '0', end: '1' }),
interpolateString: (string: string) => string,
hasLabelsMatchAPISupport: () => false,
getLimitMetrics: () => 0,
} as unknown as PrometheusDatasource;

describe('fetchSeries', () => {
Expand All @@ -26,7 +27,7 @@ describe('Language completion provider', () => {
expect(requestSpy).toHaveBeenCalledWith(
'/api/v1/series',
{},
{ end: '1', 'match[]': '{job="grafana"}', start: '0' }
{ end: '1', 'match[]': '{job="grafana"}', start: '0', limit: 0 }
);
});
});
Expand All @@ -45,6 +46,7 @@ describe('Language completion provider', () => {
end: '1',
'match[]': 'interpolated-metric',
start: '0',
limit: 0,
});
});
});
Expand All @@ -62,6 +64,7 @@ describe('Language completion provider', () => {
expect(requestSpy).toHaveBeenCalledWith('/api/v1/label/interpolated-job/values', [], {
end: '1',
start: '0',
limit: 0,
});
});
});
Expand Down Expand Up @@ -97,7 +100,7 @@ describe('Language completion provider', () => {
];
const items = { text: '', prefix: '', value, wrapperClasses: [] } as TypeaheadInput;
const result = await instance.provideCompletionItems(
items,
items,
{ history }
);
expect(result.context).toBeUndefined();
Expand Down Expand Up @@ -260,6 +263,7 @@ describe('Language completion provider', () => {
metadataRequest: () => ({ data: { data: [{ __name__: 'metric', bar: 'bazinga' }] } }),
getTimeRangeParams: () => ({ start: '0', end: '1' }),
interpolateString: (string: string) => string,
getLimitMetrics: () => 0,
} as unknown as PrometheusDatasource;
const instance = new LanguageProvider(datasources);
const value = Plain.deserialize('metric{}');
Expand Down Expand Up @@ -295,6 +299,7 @@ describe('Language completion provider', () => {
}),
getTimeRangeParams: () => ({ start: '0', end: '1' }),
interpolateString: (string: string) => string,
getLimitMetrics: () => 0,
} as unknown as PrometheusDatasource;
const instance = new LanguageProvider(datasource);
const value = Plain.deserialize('{job1="foo",job2!="foo",job3=~"foo",__name__="metric",}');
Expand Down Expand Up @@ -546,6 +551,7 @@ describe('Language completion provider', () => {
metadataRequest: jest.fn(() => ({ data: { data: [] } })),
getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })),
interpolateString: (string: string) => string,
getLimitMetrics: () => 0,
} as unknown as PrometheusDatasource;

const mockedMetadataRequest = jest.mocked(datasource.metadataRequest);
Expand Down Expand Up @@ -577,6 +583,7 @@ describe('Language completion provider', () => {
metadataRequest: jest.fn(() => ({ data: { data: ['foo', 'bar'] as string[] } })),
getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })),
lookupsDisabled: true,
getLimitMetrics: () => 0,
} as unknown as PrometheusDatasource;
const mockedMetadataRequest = jest.mocked(datasource.metadataRequest);
const instance = new LanguageProvider(datasource);
Expand Down Expand Up @@ -604,6 +611,7 @@ describe('Language completion provider', () => {
getTimeRangeParams: jest.fn(() => ({ start: '0', end: '1' })),
lookupsDisabled: false,
interpolateString: (string: string) => string,
getLimitMetrics: () => 0,
} as unknown as PrometheusDatasource;
const mockedMetadataRequest = jest.mocked(datasource.metadataRequest);
const instance = new LanguageProvider(datasource);
Expand Down
11 changes: 8 additions & 3 deletions src/language_provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -485,8 +485,9 @@ export default class PromQlLanguageProvider extends LanguageProvider {

fetchLabelValues = async (key: string): Promise<string[]> => {
const params = this.datasource.getTimeRangeParams();
const limit = this.datasource.getLimitMetrics('maxTagValues');
const url = `/api/v1/label/${this.datasource.interpolateString(key)}/values`;
return await this.request(url, [], params);
return await this.request(url, [], { ...params, limit });
};

async getLabelValues(key: string): Promise<string[]> {
Expand All @@ -499,9 +500,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
async fetchLabels(): Promise<string[]> {
const url = '/api/v1/labels';
const params = this.datasource.getTimeRangeParams();
const limit = this.datasource.getLimitMetrics('maxTagKeys');
this.labelFetchTs = Date.now().valueOf();

const res = await this.request(url, [], params);
const res = await this.request(url, [], { ...params, limit });
if (Array.isArray(res)) {
this.labelKeys = res.slice().sort();
}
Expand All @@ -518,8 +520,10 @@ export default class PromQlLanguageProvider extends LanguageProvider {
fetchSeriesLabels = async (name: string, withName?: boolean): Promise<Record<string, string[]>> => {
const interpolatedName = this.datasource.interpolateString(name);
const range = this.datasource.getTimeRangeParams();
const limit = this.datasource.getLimitMetrics('maxSeries');
const urlParams = {
...range,
limit,
'match[]': interpolatedName,
};
const url = `/api/v1/series`;
Expand Down Expand Up @@ -552,7 +556,8 @@ export default class PromQlLanguageProvider extends LanguageProvider {
fetchSeries = async (match: string): Promise<Array<Record<string, string>>> => {
const url = '/api/v1/series';
const range = this.datasource.getTimeRangeParams();
const params = { ...range, 'match[]': match };
const limit = this.datasource.getLimitMetrics('maxSeries');
const params = { ...range, 'match[]': match, limit };
return await this.request(url, {}, params);
};

Expand Down
10 changes: 9 additions & 1 deletion src/metric_find_query.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,11 @@ export default class PrometheusMetricFindQuery {
labelNamesQuery() {
const start = this.datasource.getPrometheusTime(this.range.from, false);
const end = this.datasource.getPrometheusTime(this.range.to, true);
const limit = this.datasource.getLimitMetrics('maxTagKeys');
const params = {
start: start.toString(),
end: end.toString(),
limit,
};

const url = `/api/v1/labels`;
Expand All @@ -88,10 +90,12 @@ export default class PrometheusMetricFindQuery {
labelValuesQuery(label: string, metric?: string) {
const start = this.datasource.getPrometheusTime(this.range.from, false);
const end = this.datasource.getPrometheusTime(this.range.to, true);
const limit = this.datasource.getLimitMetrics(!metric ? 'maxTagValues' : 'maxSeries')
const params = {
...(metric && { 'match[]': metric }),
start: start.toString(),
end: end.toString()
end: end.toString(),
limit
};

if (!metric) {
Expand Down Expand Up @@ -125,9 +129,11 @@ export default class PrometheusMetricFindQuery {
metricNameQuery(metricFilterPattern: string) {
const start = this.datasource.getPrometheusTime(this.range.from, false);
const end = this.datasource.getPrometheusTime(this.range.to, true);
const limit = this.datasource.getLimitMetrics('maxTagValues');
const params = {
start: start.toString(),
end: end.toString(),
limit,
};
const url = `/api/v1/label/__name__/values`;

Expand Down Expand Up @@ -188,10 +194,12 @@ export default class PrometheusMetricFindQuery {
metricNameAndLabelsQuery(query: string): Promise<MetricFindValue[]> {
const start = this.datasource.getPrometheusTime(this.range.from, false);
const end = this.datasource.getPrometheusTime(this.range.to, true);
const limit = this.datasource.getLimitMetrics('maxSeries')
const params = {
'match[]': query,
start: start.toString(),
end: end.toString(),
limit,
};

const url = `/api/v1/series`;
Expand Down
7 changes: 7 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,12 +52,19 @@ export interface PromOptions extends DataSourceJsonData {
disableMetricsLookup?: boolean;
exemplarTraceIdDestinations?: ExemplarTraceIdDestination[];
withTemplates?: WithTemplate[];
limitMetrics?: LimitMetrics;
}

export enum PromQueryType {
timeSeriesQuery = 'timeSeriesQuery',
}

export type LimitMetrics = {
maxTagKeys?: number;
maxTagValues?: number;
maxSeries?: number;
};

export type ExemplarTraceIdDestination = {
name: string;
url?: string;
Expand Down

0 comments on commit 3d45f97

Please sign in to comment.