Prometheus: Introduce series limit configuration (#107038)
* introduce series limit configuration * add docs
This commit is contained in:
@@ -214,6 +214,7 @@ Following are optional configuration settings you can configure for more control
|
||||
- **Custom query parameters** - Add custom parameters to the Prometheus query URL, which allow for more control over how queries are executed. Examples: `timeout`, `partial_response`, `dedup`, or `max_source_resolution`. Multiple parameters should be joined using `&`.
|
||||
- **HTTP method** - Select either the `POST` or `GET` HTTP method to query your data source. `POST`is recommended and selected by default, as it supports larger queries. Select `GET` if you're using Prometheus version 2.1 or older, or if your network restricts `POST` requests.
|
||||
Toggle on
|
||||
- **Series limit** - Number of maximum returned series. The limit applies to all resources (metrics, labels, and values) for both endpoints (series and labels). Leave the field empty to use the default limit (40000). Set to 0 to disable the limit and fetch everything — this may cause performance issues. Default limit is 40000.
|
||||
- **Use series endpoint** - Enabling this option makes Grafana use the series endpoint (/api/v1/series) with the match[] parameter instead of the label values endpoint (/api/v1/label/<label_name>/values). While the label values endpoint is generally more performant, some users may prefer the series endpoint because it supports the `POST` method, whereas the label values endpoint only allows `GET` requests.
|
||||
|
||||
**Exemplars:**
|
||||
|
||||
@@ -229,6 +229,9 @@ export const versionedComponents = {
|
||||
codeModeMetricNamesSuggestionLimit: {
|
||||
'11.1.0': 'data-testid code mode metric names suggestion limit',
|
||||
},
|
||||
seriesLimit: {
|
||||
'12.0.2': 'data-testid maximum series limit',
|
||||
},
|
||||
},
|
||||
queryEditor: {
|
||||
explain: {
|
||||
|
||||
@@ -51,7 +51,7 @@ export function MetricSelector() {
|
||||
</Label>
|
||||
<div>
|
||||
<Input
|
||||
onChange={(e) => setSeriesLimit(e.currentTarget.value.trim())}
|
||||
onChange={(e) => setSeriesLimit(parseInt(e.currentTarget.value.trim(), 10))}
|
||||
aria-label={t(
|
||||
'grafana-prometheus.components.metric-selector.aria-label-limit-results-from-series-endpoint',
|
||||
'Limit results from series endpoint'
|
||||
|
||||
+4
-1
@@ -4,7 +4,8 @@ import { ReactNode } from 'react';
|
||||
|
||||
import { TimeRange } from '@grafana/data';
|
||||
|
||||
import { LAST_USED_LABELS_KEY, METRIC_LABEL } from '../../constants';
|
||||
import { DEFAULT_SERIES_LIMIT, LAST_USED_LABELS_KEY, METRIC_LABEL } from '../../constants';
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { PrometheusLanguageProviderInterface } from '../../language_provider';
|
||||
import { getMockTimeRange } from '../../test/__mocks__/datasource';
|
||||
|
||||
@@ -53,6 +54,8 @@ const setupLanguageProviderMock = () => {
|
||||
}),
|
||||
} as unknown as PrometheusLanguageProviderInterface;
|
||||
|
||||
mockLanguageProvider.datasource = { seriesLimit: DEFAULT_SERIES_LIMIT } as unknown as PrometheusDatasource;
|
||||
|
||||
return { mockTimeRange, mockLanguageProvider };
|
||||
};
|
||||
|
||||
|
||||
+2
-2
@@ -24,8 +24,8 @@ interface MetricsBrowserContextType {
|
||||
setStatus: (status: string) => void;
|
||||
|
||||
// Series limit settings
|
||||
seriesLimit: string;
|
||||
setSeriesLimit: (limit: string) => void;
|
||||
seriesLimit: number;
|
||||
setSeriesLimit: (limit: number) => void;
|
||||
|
||||
// Callback when selector changes
|
||||
onChange: (selector: string) => void;
|
||||
|
||||
+14
-11
@@ -3,7 +3,8 @@ import { act, renderHook, waitFor } from '@testing-library/react';
|
||||
import { TimeRange } from '@grafana/data';
|
||||
|
||||
import { DEFAULT_SERIES_LIMIT, EMPTY_SELECTOR, LAST_USED_LABELS_KEY, METRIC_LABEL } from '../../constants';
|
||||
import { PrometheusLanguageProviderInterface } from '../../language_provider';
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { PrometheusLanguageProvider, PrometheusLanguageProviderInterface } from '../../language_provider';
|
||||
import { getMockTimeRange } from '../../test/__mocks__/datasource';
|
||||
|
||||
import * as selectorBuilderModule from './selectorBuilder';
|
||||
@@ -30,16 +31,18 @@ const setupMocks = () => {
|
||||
Object.defineProperty(window, 'localStorage', { value: localStorageMock });
|
||||
|
||||
// Mock language provider
|
||||
const mockLanguageProvider: PrometheusLanguageProviderInterface = {
|
||||
retrieveMetrics: jest.fn().mockReturnValue(['metric1', 'metric2', 'metric3']),
|
||||
retrieveLabelKeys: jest.fn().mockReturnValue(['__name__', 'instance', 'job', 'service']),
|
||||
retrieveMetricsMetadata: jest.fn().mockReturnValue({
|
||||
metric1: { type: 'counter', help: 'Test metric 1' },
|
||||
metric2: { type: 'gauge', help: 'Test metric 2' },
|
||||
}),
|
||||
queryLabelValues: jest.fn(),
|
||||
queryLabelKeys: jest.fn(),
|
||||
} as unknown as PrometheusLanguageProviderInterface;
|
||||
const mockLanguageProvider: PrometheusLanguageProviderInterface = new PrometheusLanguageProvider({
|
||||
seriesLimit: DEFAULT_SERIES_LIMIT,
|
||||
} as unknown as PrometheusDatasource);
|
||||
|
||||
mockLanguageProvider.retrieveMetrics = jest.fn().mockReturnValue(['metric1', 'metric2', 'metric3']);
|
||||
mockLanguageProvider.retrieveLabelKeys = jest.fn().mockReturnValue(['__name__', 'instance', 'job', 'service']);
|
||||
mockLanguageProvider.retrieveMetricsMetadata = jest.fn().mockReturnValue({
|
||||
metric1: { type: 'counter', help: 'Test metric 1' },
|
||||
metric2: { type: 'gauge', help: 'Test metric 2' },
|
||||
});
|
||||
mockLanguageProvider.queryLabelValues = jest.fn();
|
||||
mockLanguageProvider.queryLabelKeys = jest.fn();
|
||||
|
||||
// Mock standard responses
|
||||
(mockLanguageProvider.queryLabelValues as jest.Mock).mockImplementation((_timeRange: TimeRange, label: string) => {
|
||||
|
||||
+4
-4
@@ -3,7 +3,7 @@ import { useDebounce } from 'react-use';
|
||||
|
||||
import { TimeRange } from '@grafana/data';
|
||||
|
||||
import { DEFAULT_SERIES_LIMIT, EMPTY_SELECTOR, LAST_USED_LABELS_KEY, METRIC_LABEL } from '../../constants';
|
||||
import { EMPTY_SELECTOR, LAST_USED_LABELS_KEY, METRIC_LABEL } from '../../constants';
|
||||
import { PrometheusLanguageProviderInterface } from '../../language_provider';
|
||||
|
||||
import { Metric } from './MetricsBrowserContext';
|
||||
@@ -11,10 +11,10 @@ import { buildSelector } from './selectorBuilder';
|
||||
|
||||
export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: PrometheusLanguageProviderInterface) => {
|
||||
const timeRangeRef = useRef<TimeRange>(timeRange);
|
||||
const lastSeriesLimitRef = useRef(DEFAULT_SERIES_LIMIT);
|
||||
const lastSeriesLimitRef = useRef(languageProvider.datasource.seriesLimit);
|
||||
const isInitializedRef = useRef(false);
|
||||
|
||||
const [seriesLimit, setSeriesLimit] = useState(DEFAULT_SERIES_LIMIT);
|
||||
const [seriesLimit, setSeriesLimit] = useState(languageProvider.datasource.seriesLimit);
|
||||
const [err, setErr] = useState('');
|
||||
const [status, setStatus] = useState('Ready');
|
||||
const [validationStatus, setValidationStatus] = useState('');
|
||||
@@ -28,7 +28,7 @@ export const useMetricsLabelsValues = (timeRange: TimeRange, languageProvider: P
|
||||
const [selectedLabelValues, setSelectedLabelValues] = useState<Record<string, string[]>>({});
|
||||
|
||||
// Memoize the effective series limit to use the default when seriesLimit is empty
|
||||
const effectiveLimit = useMemo(() => seriesLimit || DEFAULT_SERIES_LIMIT, [seriesLimit]);
|
||||
const effectiveLimit = useMemo(() => seriesLimit, [seriesLimit]);
|
||||
|
||||
// We don't want to trigger fetching for small amount of time changes.
|
||||
// When MetricsBrowser re-renders for any reason we might receive a new timerange.
|
||||
|
||||
@@ -16,11 +16,13 @@ import { InlineField, Input, Select, Switch, TextLink, useTheme2 } from '@grafan
|
||||
|
||||
import {
|
||||
countError,
|
||||
DEFAULT_SERIES_LIMIT,
|
||||
DURATION_REGEX,
|
||||
durationError,
|
||||
MULTIPLE_DURATION_REGEX,
|
||||
NON_NEGATIVE_INTEGER_REGEX,
|
||||
PROM_CONFIG_LABEL_WIDTH,
|
||||
seriesLimitError,
|
||||
SUGGESTIONS_LIMIT,
|
||||
} from '../constants';
|
||||
import { QueryEditorMode } from '../querybuilder/shared/types';
|
||||
@@ -95,6 +97,10 @@ export const PromSettings = (props: Props) => {
|
||||
codeModeMetricNamesSuggestionLimit: '',
|
||||
});
|
||||
|
||||
const [seriesLimit, setSeriesLimit] = useState<string>(
|
||||
optionsWithDefaults.jsonData.seriesLimit?.toString() || `${DEFAULT_SERIES_LIMIT}`
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConfigSubSection
|
||||
@@ -633,6 +639,45 @@ export const PromSettings = (props: Props) => {
|
||||
</InlineField>
|
||||
</div>
|
||||
</div>
|
||||
<InlineField
|
||||
labelWidth={PROM_CONFIG_LABEL_WIDTH}
|
||||
label={t('grafana-prometheus.configuration.prom-settings.label-series-limit', 'Series limit')}
|
||||
tooltip={
|
||||
<>
|
||||
<Trans i18nKey="grafana-prometheus.configuration.prom-settings.tooltip-series-limit">
|
||||
The limit applies to all resources (metrics, labels, and values) for both endpoints (series and
|
||||
labels). Leave the field empty to use the default limit (40000). Set to 0 to disable the limit and
|
||||
fetch everything — this may cause performance issues. Default limit is 40000.
|
||||
</Trans>
|
||||
{docsTip()}
|
||||
</>
|
||||
}
|
||||
interactive={true}
|
||||
disabled={optionsWithDefaults.readOnly}
|
||||
>
|
||||
<>
|
||||
<Input
|
||||
className="width-20"
|
||||
value={seriesLimit}
|
||||
spellCheck={false}
|
||||
// eslint-disable-next-line @grafana/i18n/no-untranslated-strings
|
||||
placeholder="40000"
|
||||
onChange={(event: { currentTarget: { value: string } }) => {
|
||||
setSeriesLimit(event.currentTarget.value);
|
||||
onOptionsChange({
|
||||
...optionsWithDefaults,
|
||||
jsonData: {
|
||||
...optionsWithDefaults.jsonData,
|
||||
seriesLimit: parseInt(event.currentTarget.value, 10),
|
||||
},
|
||||
});
|
||||
}}
|
||||
onBlur={(e) => validateInput(e.currentTarget.value, NON_NEGATIVE_INTEGER_REGEX, seriesLimitError)}
|
||||
data-testid={selectors.components.DataSource.Prometheus.configPage.seriesLimit}
|
||||
/>
|
||||
{validateInput(seriesLimit, NON_NEGATIVE_INTEGER_REGEX, seriesLimitError)}
|
||||
</>
|
||||
</InlineField>
|
||||
<InlineField
|
||||
labelWidth={PROM_CONFIG_LABEL_WIDTH}
|
||||
label={t('grafana-prometheus.configuration.prom-settings.label-use-series-endpoint', 'Use series endpoint')}
|
||||
|
||||
@@ -17,7 +17,7 @@ export const MULTIPLE_DURATION_REGEX = /(\d+)(.+)/;
|
||||
export const NON_NEGATIVE_INTEGER_REGEX = /^(0|[1-9]\d*)(\.\d+)?(e\+?\d+)?$/; // non-negative integers, including scientific notation
|
||||
|
||||
export const EMPTY_SELECTOR = '{}';
|
||||
export const DEFAULT_SERIES_LIMIT = '40000';
|
||||
export const DEFAULT_SERIES_LIMIT = 40000;
|
||||
export const MATCH_ALL_LABELS_STR = '__name__!=""';
|
||||
export const MATCH_ALL_LABELS = '{__name__!=""}';
|
||||
export const METRIC_LABEL = '__name__';
|
||||
@@ -29,3 +29,5 @@ export const REMOVE_SERIES_LIMIT = 'none';
|
||||
|
||||
export const durationError = 'Value is not valid, you can use number with time unit specifier: y, M, w, d, h, m, s';
|
||||
export const countError = 'Value is not valid, you can use non-negative integers, including scientific notation';
|
||||
export const seriesLimitError =
|
||||
'Value is not valid, you can use only numbers or leave it empty to use default limit or set 0 to have no limit.';
|
||||
|
||||
@@ -42,7 +42,7 @@ import {
|
||||
|
||||
import { addLabelToQuery } from './add_label_to_query';
|
||||
import { PrometheusAnnotationSupport } from './annotations';
|
||||
import { SUGGESTIONS_LIMIT } from './constants';
|
||||
import { DEFAULT_SERIES_LIMIT, SUGGESTIONS_LIMIT } from './constants';
|
||||
import { prometheusRegularEscape, prometheusSpecialRegexEscape } from './escaping';
|
||||
import {
|
||||
exportToAbstractQuery,
|
||||
@@ -109,6 +109,7 @@ export class PrometheusDatasource
|
||||
cache: QueryCache<PromQuery>;
|
||||
metricNamesAutocompleteSuggestionLimit: number;
|
||||
seriesEndpoint: boolean;
|
||||
seriesLimit: number;
|
||||
|
||||
constructor(
|
||||
instanceSettings: DataSourceInstanceSettings<PromOptions>,
|
||||
@@ -132,6 +133,7 @@ export class PrometheusDatasource
|
||||
this.customQueryParameters = new URLSearchParams(instanceSettings.jsonData.customQueryParameters);
|
||||
this.datasourceConfigurationPrometheusFlavor = instanceSettings.jsonData.prometheusType;
|
||||
this.datasourceConfigurationPrometheusVersion = instanceSettings.jsonData.prometheusVersion;
|
||||
this.seriesLimit = instanceSettings.jsonData.seriesLimit ?? DEFAULT_SERIES_LIMIT;
|
||||
this.seriesEndpoint = instanceSettings.jsonData.seriesEndpoint ?? false;
|
||||
this.defaultEditor = instanceSettings.jsonData.defaultEditor;
|
||||
this.disableRecordingRules = instanceSettings.jsonData.disableRecordingRules ?? false;
|
||||
|
||||
@@ -81,6 +81,7 @@ describe('Prometheus Language Provider', () => {
|
||||
cacheLevel: PrometheusCacheLevel.None,
|
||||
getIntervalVars: () => ({}),
|
||||
getRangeScopedVars: () => ({}),
|
||||
seriesLimit: DEFAULT_SERIES_LIMIT,
|
||||
} as unknown as PrometheusDatasource;
|
||||
|
||||
describe('Series and label fetching', () => {
|
||||
|
||||
@@ -103,7 +103,7 @@ export interface PrometheusLegacyLanguageProvider {
|
||||
/**
|
||||
* @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
fetchLabelValues: (range: TimeRange, key: string, limit?: string) => Promise<string[]>;
|
||||
fetchLabelValues: (range: TimeRange, key: string, limit?: string | number) => Promise<string[]>;
|
||||
/**
|
||||
* @deprecated Use queryLabelValues() method insteadIt'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
@@ -124,7 +124,7 @@ export interface PrometheusLegacyLanguageProvider {
|
||||
name: string,
|
||||
match?: string,
|
||||
requestId?: string,
|
||||
withLimit?: string
|
||||
withLimit?: string | number
|
||||
) => Promise<string[]>;
|
||||
/**
|
||||
* @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings
|
||||
@@ -137,7 +137,7 @@ export interface PrometheusLegacyLanguageProvider {
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withName?: boolean,
|
||||
withLimit?: string
|
||||
withLimit?: string | number
|
||||
) => Promise<Record<string, string[]>>;
|
||||
/**
|
||||
* @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings
|
||||
@@ -146,12 +146,16 @@ export interface PrometheusLegacyLanguageProvider {
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withName?: boolean,
|
||||
withLimit?: string
|
||||
withLimit?: string | number
|
||||
) => Promise<Record<string, string[]>>;
|
||||
/**
|
||||
* @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings
|
||||
*/
|
||||
fetchSeriesLabelsMatch: (timeRange: TimeRange, name: string, withLimit?: string) => Promise<Record<string, string[]>>;
|
||||
fetchSeriesLabelsMatch: (
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withLimit?: string | number
|
||||
) => Promise<Record<string, string[]>>;
|
||||
/**
|
||||
* @deprecated If you need labelKeys or labelValues please use queryLabelKeys() or queryLabelValues() functions
|
||||
*/
|
||||
@@ -249,7 +253,7 @@ export default class PromQlLanguageProvider extends LanguageProvider implements
|
||||
}
|
||||
}
|
||||
|
||||
fetchLabelValues = async (range: TimeRange, key: string, limit?: string): Promise<string[]> => {
|
||||
fetchLabelValues = async (range: TimeRange, key: string, limit?: string | number): Promise<string[]> => {
|
||||
const params = { ...this.datasource.getAdjustedInterval(range), ...(limit ? { limit } : {}) };
|
||||
const interpolatedName = this.datasource.interpolateString(key);
|
||||
const interpolatedAndEscapedName = escapeForUtf8Support(removeQuotesIfExist(interpolatedName));
|
||||
@@ -321,7 +325,7 @@ export default class PromQlLanguageProvider extends LanguageProvider implements
|
||||
name: string,
|
||||
match?: string,
|
||||
requestId?: string,
|
||||
withLimit?: string
|
||||
withLimit?: string | number
|
||||
): Promise<string[]> => {
|
||||
const interpolatedName = name ? this.datasource.interpolateString(name) : null;
|
||||
const interpolatedMatch = match ? this.datasource.interpolateString(match) : null;
|
||||
@@ -377,7 +381,7 @@ export default class PromQlLanguageProvider extends LanguageProvider implements
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withName?: boolean,
|
||||
withLimit?: string
|
||||
withLimit?: string | number
|
||||
): Promise<Record<string, string[]>> => {
|
||||
if (this.datasource.hasLabelsMatchAPISupport()) {
|
||||
return this.fetchSeriesLabelsMatch(timeRange, name, withLimit);
|
||||
@@ -394,7 +398,7 @@ export default class PromQlLanguageProvider extends LanguageProvider implements
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withName?: boolean,
|
||||
withLimit?: string
|
||||
withLimit?: string | number
|
||||
): Promise<Record<string, string[]>> => {
|
||||
const interpolatedName = this.datasource.interpolateString(name);
|
||||
const range = this.datasource.getAdjustedInterval(timeRange);
|
||||
@@ -416,7 +420,7 @@ export default class PromQlLanguageProvider extends LanguageProvider implements
|
||||
fetchSeriesLabelsMatch = async (
|
||||
timeRange: TimeRange,
|
||||
name: string,
|
||||
withLimit?: string
|
||||
withLimit?: string | number
|
||||
): Promise<Record<string, string[]>> => {
|
||||
const interpolatedName = this.datasource.interpolateString(name);
|
||||
const range = this.datasource.getAdjustedInterval(timeRange);
|
||||
@@ -519,8 +523,8 @@ export interface PrometheusLanguageProviderInterface
|
||||
retrieveLabelKeys: () => string[];
|
||||
|
||||
queryMetricsMetadata: () => Promise<PromMetricsMetadata>;
|
||||
queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: string) => Promise<string[]>;
|
||||
queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: string) => Promise<string[]>;
|
||||
queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: number) => Promise<string[]>;
|
||||
queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: number) => Promise<string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -677,7 +681,7 @@ export class PrometheusLanguageProvider extends PromQlLanguageProvider implement
|
||||
* @param {string} [limit] - Optional maximum number of label keys to return
|
||||
* @returns {Promise<string[]>} Array of matching label key names, sorted alphabetically
|
||||
*/
|
||||
public queryLabelKeys = async (timeRange: TimeRange, match?: string, limit?: string): Promise<string[]> => {
|
||||
public queryLabelKeys = async (timeRange: TimeRange, match?: string, limit?: number): Promise<string[]> => {
|
||||
return await this.resourceClient.queryLabelKeys(timeRange, match, limit);
|
||||
};
|
||||
|
||||
@@ -710,7 +714,7 @@ export class PrometheusLanguageProvider extends PromQlLanguageProvider implement
|
||||
timeRange: TimeRange,
|
||||
labelKey: string,
|
||||
match?: string,
|
||||
limit?: string
|
||||
limit?: number
|
||||
): Promise<string[]> => {
|
||||
return await this.resourceClient.queryLabelValues(timeRange, labelKey, match, limit);
|
||||
};
|
||||
|
||||
@@ -171,6 +171,7 @@
|
||||
"label-query-timeout": "Query timeout",
|
||||
"label-scrape-interval": "Scrape interval",
|
||||
"label-use-series-endpoint": "Use series endpoint",
|
||||
"label-series-limit": "Series limit",
|
||||
"more-info": "For more information on configuring prometheus type and version in data sources, see the <2>provisioning documentation</2>.",
|
||||
"placeholder-example-maxsourceresolutionmtimeout": "Example: {{example}}",
|
||||
"title-interval-behaviour": "Interval behaviour",
|
||||
@@ -190,7 +191,8 @@
|
||||
"tooltip-query-overlap-window": "Set a duration like {{example1}} or {{example2}} or {{example3}}. Default of {{default}}. This duration will be added to the duration of each incremental request.",
|
||||
"tooltip-query-timeout": "Set the Prometheus query timeout.",
|
||||
"tooltip-scrape-interval": "This interval is how frequently Prometheus scrapes targets. Set this to the typical scrape and evaluation interval configured in your Prometheus config file. If you set this to a greater value than your Prometheus config file interval, Grafana will evaluate the data according to this interval and you will see less data points. Defaults to {{default}}.",
|
||||
"tooltip-use-series-endpoint": "Checking this option will favor the series endpoint with {{exampleParameter}} parameter over the label values endpoint with {{exampleParameter}} parameter. While the label values endpoint is considered more performant, some users may prefer the series because it has a POST method while the label values endpoint only has a GET method."
|
||||
"tooltip-use-series-endpoint": "Checking this option will favor the series endpoint with {{exampleParameter}} parameter over the label values endpoint with {{exampleParameter}} parameter. While the label values endpoint is considered more performant, some users may prefer the series because it has a POST method while the label values endpoint only has a GET method.",
|
||||
"tooltip-series-limit": "The limit applies to all resources (metrics, labels, and values) for both endpoints (series and labels). Leave the field empty to use the default limit (40000). Set to 0 to disable the limit and fetch everything — this may cause performance issues. Default limit is 40000."
|
||||
}
|
||||
},
|
||||
"querybuilder": {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { dateTime, TimeRange } from '@grafana/data';
|
||||
|
||||
import { DEFAULT_SERIES_LIMIT } from './constants';
|
||||
import { PrometheusDatasource } from './datasource';
|
||||
import { BaseResourceClient, LabelsApiClient, processSeries, SeriesApiClient } from './resource_clients';
|
||||
import { PrometheusCacheLevel } from './types';
|
||||
@@ -32,6 +33,7 @@ describe('LabelsApiClient', () => {
|
||||
jest.clearAllMocks();
|
||||
client = new LabelsApiClient(mockRequest, {
|
||||
cacheLevel: PrometheusCacheLevel.Low,
|
||||
seriesLimit: DEFAULT_SERIES_LIMIT,
|
||||
getAdjustedInterval: mockGetAdjustedInterval,
|
||||
getTimeRangeParams: mockGetTimeRangeParams,
|
||||
interpolateString: mockInterpolateString,
|
||||
@@ -70,7 +72,7 @@ describe('LabelsApiClient', () => {
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
'/api/v1/labels',
|
||||
{
|
||||
limit: '40000',
|
||||
limit: 40000,
|
||||
start: expect.any(String),
|
||||
end: expect.any(String),
|
||||
},
|
||||
@@ -87,7 +89,7 @@ describe('LabelsApiClient', () => {
|
||||
'/api/v1/labels',
|
||||
{
|
||||
'match[]': '{job="grafana"}',
|
||||
limit: '40000',
|
||||
limit: 40000,
|
||||
start: expect.any(String),
|
||||
end: expect.any(String),
|
||||
},
|
||||
@@ -109,7 +111,7 @@ describe('LabelsApiClient', () => {
|
||||
{
|
||||
start: expect.any(String),
|
||||
end: expect.any(String),
|
||||
limit: '40000',
|
||||
limit: 40000,
|
||||
},
|
||||
defaultCacheHeaders
|
||||
);
|
||||
@@ -126,7 +128,7 @@ describe('LabelsApiClient', () => {
|
||||
{
|
||||
start: expect.any(String),
|
||||
end: expect.any(String),
|
||||
limit: '40000',
|
||||
limit: 40000,
|
||||
},
|
||||
defaultCacheHeaders
|
||||
);
|
||||
@@ -826,6 +828,7 @@ describe('BaseResourceClient', () => {
|
||||
const mockGetTimeRangeParams = jest.fn();
|
||||
const mockDatasource = {
|
||||
cacheLevel: PrometheusCacheLevel.Low,
|
||||
seriesLimit: DEFAULT_SERIES_LIMIT,
|
||||
getTimeRangeParams: mockGetTimeRangeParams,
|
||||
} as unknown as PrometheusDatasource;
|
||||
|
||||
@@ -859,7 +862,7 @@ describe('BaseResourceClient', () => {
|
||||
it('should make request with correct parameters', async () => {
|
||||
mockRequest.mockResolvedValueOnce([{ __name__: 'metric1' }]);
|
||||
|
||||
const result = await client.querySeries(mockTimeRange, '{job="grafana"}');
|
||||
const result = await client.querySeries(mockTimeRange, '{job="grafana"}', DEFAULT_SERIES_LIMIT);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
'/api/v1/series',
|
||||
@@ -867,7 +870,7 @@ describe('BaseResourceClient', () => {
|
||||
start: '1681300260',
|
||||
end: '1681300320',
|
||||
'match[]': '{job="grafana"}',
|
||||
limit: '40000',
|
||||
limit: 40000,
|
||||
},
|
||||
{ headers: { 'X-Grafana-Cache': 'private, max-age=60' } }
|
||||
);
|
||||
@@ -877,7 +880,7 @@ describe('BaseResourceClient', () => {
|
||||
it('should use custom limit when provided', async () => {
|
||||
mockRequest.mockResolvedValueOnce([]);
|
||||
|
||||
await client.querySeries(mockTimeRange, '{job="grafana"}', '1000');
|
||||
await client.querySeries(mockTimeRange, '{job="grafana"}', 1000);
|
||||
|
||||
expect(mockRequest).toHaveBeenCalledWith(
|
||||
'/api/v1/series',
|
||||
@@ -885,7 +888,7 @@ describe('BaseResourceClient', () => {
|
||||
start: '1681300260',
|
||||
end: '1681300320',
|
||||
'match[]': '{job="grafana"}',
|
||||
limit: '1000',
|
||||
limit: 1000,
|
||||
},
|
||||
{ headers: { 'X-Grafana-Cache': 'private, max-age=60' } }
|
||||
);
|
||||
@@ -894,7 +897,7 @@ describe('BaseResourceClient', () => {
|
||||
it('should handle empty response', async () => {
|
||||
mockRequest.mockResolvedValueOnce(null);
|
||||
|
||||
const result = await client.querySeries(mockTimeRange, '{job="grafana"}');
|
||||
const result = await client.querySeries(mockTimeRange, '{job="grafana"}', DEFAULT_SERIES_LIMIT);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
@@ -902,7 +905,7 @@ describe('BaseResourceClient', () => {
|
||||
it('should handle non-array response', async () => {
|
||||
mockRequest.mockResolvedValueOnce({ error: 'invalid response' });
|
||||
|
||||
const result = await client.querySeries(mockTimeRange, '{job="grafana"}');
|
||||
const result = await client.querySeries(mockTimeRange, '{job="grafana"}', DEFAULT_SERIES_LIMIT);
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
@@ -22,10 +22,10 @@ export interface ResourceApiClient {
|
||||
start: (timeRange: TimeRange) => Promise<void>;
|
||||
|
||||
queryMetrics: (timeRange: TimeRange) => Promise<{ metrics: string[]; histogramMetrics: string[] }>;
|
||||
queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: string) => Promise<string[]>;
|
||||
queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: string) => Promise<string[]>;
|
||||
queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: number) => Promise<string[]>;
|
||||
queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: number) => Promise<string[]>;
|
||||
|
||||
querySeries: (timeRange: TimeRange, match: string, limit?: string) => Promise<PrometheusSeriesResponse>;
|
||||
querySeries: (timeRange: TimeRange, match: string, limit: number) => Promise<PrometheusSeriesResponse>;
|
||||
}
|
||||
|
||||
type RequestFn = (
|
||||
@@ -35,10 +35,18 @@ type RequestFn = (
|
||||
) => Promise<unknown>;
|
||||
|
||||
export abstract class BaseResourceClient {
|
||||
private seriesLimit: number;
|
||||
|
||||
constructor(
|
||||
protected readonly request: RequestFn,
|
||||
protected readonly datasource: PrometheusDatasource
|
||||
) {}
|
||||
) {
|
||||
this.seriesLimit = this.datasource.seriesLimit;
|
||||
}
|
||||
|
||||
protected getEffectiveLimit(limit?: number): number {
|
||||
return limit || this.seriesLimit;
|
||||
}
|
||||
|
||||
protected async requestLabels(
|
||||
url: string,
|
||||
@@ -65,7 +73,7 @@ export abstract class BaseResourceClient {
|
||||
* @param {string} match - Label matcher to filter time series
|
||||
* @param {string} limit - Maximum number of series to return
|
||||
*/
|
||||
public querySeries = async (timeRange: TimeRange, match: string, limit: string = DEFAULT_SERIES_LIMIT) => {
|
||||
public querySeries = async (timeRange: TimeRange, match: string, limit: number) => {
|
||||
const effectiveMatch = !match || match === EMPTY_SELECTOR ? MATCH_ALL_LABELS : match;
|
||||
const timeParams = this.datasource.getTimeRangeParams(timeRange);
|
||||
const searchParams = { ...timeParams, 'match[]': effectiveMatch, limit };
|
||||
@@ -102,16 +110,13 @@ export class LabelsApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
* @param {string} limit - Maximum number of results to return
|
||||
* @returns {Promise<string[]>} Array of label keys sorted alphabetically
|
||||
*/
|
||||
public queryLabelKeys = async (
|
||||
timeRange: TimeRange,
|
||||
match?: string,
|
||||
limit: string = DEFAULT_SERIES_LIMIT
|
||||
): Promise<string[]> => {
|
||||
public queryLabelKeys = async (timeRange: TimeRange, match?: string, limit?: number): Promise<string[]> => {
|
||||
let url = '/api/v1/labels';
|
||||
const timeParams = getRangeSnapInterval(this.datasource.cacheLevel, timeRange);
|
||||
const searchParams = { limit, ...timeParams, ...(match ? { 'match[]': match } : {}) };
|
||||
const effectiveLimit = this.getEffectiveLimit(limit);
|
||||
const searchParams = { limit: effectiveLimit, ...timeParams, ...(match ? { 'match[]': match } : {}) };
|
||||
const effectiveMatch = match ?? '';
|
||||
const maybeCachedKeys = this._cache.getLabelKeys(timeRange, effectiveMatch, limit);
|
||||
const maybeCachedKeys = this._cache.getLabelKeys(timeRange, effectiveMatch, effectiveLimit);
|
||||
if (maybeCachedKeys) {
|
||||
return maybeCachedKeys;
|
||||
}
|
||||
@@ -119,7 +124,7 @@ export class LabelsApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
const res = await this.requestLabels(url, searchParams, getDefaultCacheHeaders(this.datasource.cacheLevel));
|
||||
if (Array.isArray(res)) {
|
||||
this.labelKeys = res.slice().sort();
|
||||
this._cache.setLabelKeys(timeRange, effectiveMatch, limit, this.labelKeys);
|
||||
this._cache.setLabelKeys(timeRange, effectiveMatch, effectiveLimit, this.labelKeys);
|
||||
return this.labelKeys.slice();
|
||||
}
|
||||
|
||||
@@ -139,21 +144,22 @@ export class LabelsApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
timeRange: TimeRange,
|
||||
labelKey: string,
|
||||
match?: string,
|
||||
limit: string = DEFAULT_SERIES_LIMIT
|
||||
limit?: number
|
||||
): Promise<string[]> => {
|
||||
const timeParams = this.datasource.getAdjustedInterval(timeRange);
|
||||
const searchParams = { limit, ...timeParams, ...(match ? { 'match[]': match } : {}) };
|
||||
const effectiveLimit = this.getEffectiveLimit(limit);
|
||||
const searchParams = { limit: effectiveLimit, ...timeParams, ...(match ? { 'match[]': match } : {}) };
|
||||
const interpolatedName = this.datasource.interpolateString(labelKey);
|
||||
const interpolatedAndEscapedName = escapeForUtf8Support(removeQuotesIfExist(interpolatedName));
|
||||
const effectiveMatch = `${match ?? ''}-${interpolatedAndEscapedName}`;
|
||||
const maybeCachedValues = this._cache.getLabelValues(timeRange, effectiveMatch, limit);
|
||||
const maybeCachedValues = this._cache.getLabelValues(timeRange, effectiveMatch, effectiveLimit);
|
||||
if (maybeCachedValues) {
|
||||
return maybeCachedValues;
|
||||
}
|
||||
|
||||
const url = `/api/v1/label/${interpolatedAndEscapedName}/values`;
|
||||
const value = await this.requestLabels(url, searchParams, getDefaultCacheHeaders(this.datasource.cacheLevel));
|
||||
this._cache.setLabelValues(timeRange, effectiveMatch, limit, value ?? []);
|
||||
this._cache.setLabelValues(timeRange, effectiveMatch, effectiveLimit, value ?? []);
|
||||
return value ?? [];
|
||||
};
|
||||
}
|
||||
@@ -181,20 +187,17 @@ export class SeriesApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
return { metrics: this.metrics, histogramMetrics: this.histogramMetrics };
|
||||
};
|
||||
|
||||
public queryLabelKeys = async (
|
||||
timeRange: TimeRange,
|
||||
match?: string,
|
||||
limit: string = DEFAULT_SERIES_LIMIT
|
||||
): Promise<string[]> => {
|
||||
public queryLabelKeys = async (timeRange: TimeRange, match?: string, limit?: number): Promise<string[]> => {
|
||||
const effectiveLimit = this.getEffectiveLimit(limit);
|
||||
const effectiveMatch = !match || match === EMPTY_SELECTOR ? MATCH_ALL_LABELS : match;
|
||||
const maybeCachedKeys = this._cache.getLabelKeys(timeRange, effectiveMatch, limit);
|
||||
const maybeCachedKeys = this._cache.getLabelKeys(timeRange, effectiveMatch, effectiveLimit);
|
||||
if (maybeCachedKeys) {
|
||||
return maybeCachedKeys;
|
||||
}
|
||||
|
||||
const series = await this.querySeries(timeRange, effectiveMatch, limit);
|
||||
const series = await this.querySeries(timeRange, effectiveMatch, effectiveLimit);
|
||||
const { labelKeys } = processSeries(series);
|
||||
this._cache.setLabelKeys(timeRange, effectiveMatch, limit, labelKeys);
|
||||
this._cache.setLabelKeys(timeRange, effectiveMatch, effectiveLimit, labelKeys);
|
||||
return labelKeys;
|
||||
};
|
||||
|
||||
@@ -202,7 +205,7 @@ export class SeriesApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
timeRange: TimeRange,
|
||||
labelKey: string,
|
||||
match?: string,
|
||||
limit: string = DEFAULT_SERIES_LIMIT
|
||||
limit?: number
|
||||
): Promise<string[]> => {
|
||||
let effectiveMatch = '';
|
||||
if (!match || match === EMPTY_SELECTOR) {
|
||||
@@ -222,14 +225,15 @@ export class SeriesApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
effectiveMatch = `{${metricFilter}${labelFilters}}`;
|
||||
}
|
||||
|
||||
const maybeCachedValues = this._cache.getLabelValues(timeRange, effectiveMatch, limit);
|
||||
const effectiveLimit = this.getEffectiveLimit(limit);
|
||||
const maybeCachedValues = this._cache.getLabelValues(timeRange, effectiveMatch, effectiveLimit);
|
||||
if (maybeCachedValues) {
|
||||
return maybeCachedValues;
|
||||
}
|
||||
|
||||
const series = await this.querySeries(timeRange, effectiveMatch, limit);
|
||||
const series = await this.querySeries(timeRange, effectiveMatch, effectiveLimit);
|
||||
const { labelValues } = processSeries(series, removeQuotesIfExist(labelKey));
|
||||
this._cache.setLabelValues(timeRange, effectiveMatch, limit, labelValues);
|
||||
this._cache.setLabelValues(timeRange, effectiveMatch, effectiveLimit, labelValues);
|
||||
return labelValues;
|
||||
};
|
||||
}
|
||||
@@ -243,7 +247,7 @@ class ResourceClientsCache {
|
||||
|
||||
constructor(private cacheLevel: PrometheusCacheLevel = PrometheusCacheLevel.High) {}
|
||||
|
||||
public setLabelKeys(timeRange: TimeRange, match: string, limit: string, keys: string[]) {
|
||||
public setLabelKeys(timeRange: TimeRange, match: string, limit: number, keys: string[]) {
|
||||
if (keys.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -254,7 +258,7 @@ class ResourceClientsCache {
|
||||
this._accessTimestamps[cacheKey] = Date.now();
|
||||
}
|
||||
|
||||
public getLabelKeys(timeRange: TimeRange, match: string, limit: string): string[] | undefined {
|
||||
public getLabelKeys(timeRange: TimeRange, match: string, limit: number): string[] | undefined {
|
||||
const cacheKey = this.getCacheKey(timeRange, match, limit, 'key');
|
||||
const result = this._cache[cacheKey];
|
||||
if (result) {
|
||||
@@ -264,7 +268,7 @@ class ResourceClientsCache {
|
||||
return result;
|
||||
}
|
||||
|
||||
public setLabelValues(timeRange: TimeRange, match: string, limit: string, values: string[]) {
|
||||
public setLabelValues(timeRange: TimeRange, match: string, limit: number, values: string[]) {
|
||||
if (values.length === 0) {
|
||||
return;
|
||||
}
|
||||
@@ -275,7 +279,7 @@ class ResourceClientsCache {
|
||||
this._accessTimestamps[cacheKey] = Date.now();
|
||||
}
|
||||
|
||||
public getLabelValues(timeRange: TimeRange, match: string, limit: string): string[] | undefined {
|
||||
public getLabelValues(timeRange: TimeRange, match: string, limit: number): string[] | undefined {
|
||||
const cacheKey = this.getCacheKey(timeRange, match, limit, 'value');
|
||||
const result = this._cache[cacheKey];
|
||||
if (result) {
|
||||
@@ -285,7 +289,7 @@ class ResourceClientsCache {
|
||||
return result;
|
||||
}
|
||||
|
||||
private getCacheKey(timeRange: TimeRange, match: string, limit: string, type: 'key' | 'value') {
|
||||
private getCacheKey(timeRange: TimeRange, match: string, limit: number, type: 'key' | 'value') {
|
||||
const snappedTimeRange = getRangeSnapInterval(this.cacheLevel, timeRange);
|
||||
return [snappedTimeRange.start, snappedTimeRange.end, limit, match, type].join('|');
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ export interface PromOptions extends DataSourceJsonData {
|
||||
oauthPassThru?: boolean;
|
||||
codeModeMetricNamesSuggestionLimit?: number;
|
||||
seriesEndpoint?: boolean;
|
||||
seriesLimit?: number;
|
||||
}
|
||||
|
||||
export type ExemplarTraceIdDestination = {
|
||||
|
||||
Reference in New Issue
Block a user