Prometheus: Introduce series limit configuration (#107038)

* introduce series limit configuration

* add docs
pull/106661/head
ismail simsek 4 weeks ago committed by GitHub
parent 18757952eb
commit e99e879fe1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      docs/sources/datasources/prometheus/configure/_index.md
  2. 3
      packages/grafana-e2e-selectors/src/selectors/components.ts
  3. 2
      packages/grafana-prometheus/src/components/metrics-browser/MetricSelector.tsx
  4. 5
      packages/grafana-prometheus/src/components/metrics-browser/MetricsBrowserContext.test.tsx
  5. 4
      packages/grafana-prometheus/src/components/metrics-browser/MetricsBrowserContext.tsx
  6. 25
      packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.test.ts
  7. 8
      packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.ts
  8. 45
      packages/grafana-prometheus/src/configuration/PromSettings.tsx
  9. 4
      packages/grafana-prometheus/src/constants.ts
  10. 4
      packages/grafana-prometheus/src/datasource.ts
  11. 1
      packages/grafana-prometheus/src/language_provider.test.ts
  12. 32
      packages/grafana-prometheus/src/language_provider.ts
  13. 4
      packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json
  14. 23
      packages/grafana-prometheus/src/resource_clients.test.ts
  15. 72
      packages/grafana-prometheus/src/resource_clients.ts
  16. 1
      packages/grafana-prometheus/src/types.ts

@ -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,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 };
};

@ -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;

@ -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) => {

@ -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 = {

Loading…
Cancel
Save