From e99e879fe1ba571b91b25af51c79d7e1b60bdc1a Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Sat, 21 Jun 2025 15:40:52 +0200 Subject: [PATCH] Prometheus: Introduce series limit configuration (#107038) * introduce series limit configuration * add docs --- .../prometheus/configure/_index.md | 1 + .../src/selectors/components.ts | 3 + .../metrics-browser/MetricSelector.tsx | 2 +- .../MetricsBrowserContext.test.tsx | 5 +- .../metrics-browser/MetricsBrowserContext.tsx | 4 +- .../useMetricsLabelsValues.test.ts | 25 ++++--- .../metrics-browser/useMetricsLabelsValues.ts | 8 +-- .../src/configuration/PromSettings.tsx | 45 ++++++++++++ packages/grafana-prometheus/src/constants.ts | 4 +- packages/grafana-prometheus/src/datasource.ts | 4 +- .../src/language_provider.test.ts | 1 + .../src/language_provider.ts | 32 +++++---- .../src/locales/en-US/grafana-prometheus.json | 4 +- .../src/resource_clients.test.ts | 23 +++--- .../src/resource_clients.ts | 72 ++++++++++--------- packages/grafana-prometheus/src/types.ts | 1 + 16 files changed, 154 insertions(+), 80 deletions(-) diff --git a/docs/sources/datasources/prometheus/configure/_index.md b/docs/sources/datasources/prometheus/configure/_index.md index 33dae86d843..d312245e658 100644 --- a/docs/sources/datasources/prometheus/configure/_index.md +++ b/docs/sources/datasources/prometheus/configure/_index.md @@ -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//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:** diff --git a/packages/grafana-e2e-selectors/src/selectors/components.ts b/packages/grafana-e2e-selectors/src/selectors/components.ts index 952da3cb8e2..760c618475e 100644 --- a/packages/grafana-e2e-selectors/src/selectors/components.ts +++ b/packages/grafana-e2e-selectors/src/selectors/components.ts @@ -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: { diff --git a/packages/grafana-prometheus/src/components/metrics-browser/MetricSelector.tsx b/packages/grafana-prometheus/src/components/metrics-browser/MetricSelector.tsx index 7e2adb10155..86f62480208 100644 --- a/packages/grafana-prometheus/src/components/metrics-browser/MetricSelector.tsx +++ b/packages/grafana-prometheus/src/components/metrics-browser/MetricSelector.tsx @@ -51,7 +51,7 @@ export function MetricSelector() {
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' diff --git a/packages/grafana-prometheus/src/components/metrics-browser/MetricsBrowserContext.test.tsx b/packages/grafana-prometheus/src/components/metrics-browser/MetricsBrowserContext.test.tsx index 720dd73e92a..68d88b3e123 100644 --- a/packages/grafana-prometheus/src/components/metrics-browser/MetricsBrowserContext.test.tsx +++ b/packages/grafana-prometheus/src/components/metrics-browser/MetricsBrowserContext.test.tsx @@ -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 }; }; diff --git a/packages/grafana-prometheus/src/components/metrics-browser/MetricsBrowserContext.tsx b/packages/grafana-prometheus/src/components/metrics-browser/MetricsBrowserContext.tsx index ec822f2978f..0a0cedd9644 100644 --- a/packages/grafana-prometheus/src/components/metrics-browser/MetricsBrowserContext.tsx +++ b/packages/grafana-prometheus/src/components/metrics-browser/MetricsBrowserContext.tsx @@ -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; diff --git a/packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.test.ts b/packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.test.ts index d7493a757db..6cd2ab01869 100644 --- a/packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.test.ts +++ b/packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.test.ts @@ -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) => { diff --git a/packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.ts b/packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.ts index 57364122fb1..08e91411972 100644 --- a/packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.ts +++ b/packages/grafana-prometheus/src/components/metrics-browser/useMetricsLabelsValues.ts @@ -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); - 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>({}); // 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. diff --git a/packages/grafana-prometheus/src/configuration/PromSettings.tsx b/packages/grafana-prometheus/src/configuration/PromSettings.tsx index 54fec49b059..63642bdb592 100644 --- a/packages/grafana-prometheus/src/configuration/PromSettings.tsx +++ b/packages/grafana-prometheus/src/configuration/PromSettings.tsx @@ -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( + optionsWithDefaults.jsonData.seriesLimit?.toString() || `${DEFAULT_SERIES_LIMIT}` + ); + return ( <> {
+ + + 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. + + {docsTip()} + + } + interactive={true} + disabled={optionsWithDefaults.readOnly} + > + <> + { + 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)} + + ; metricNamesAutocompleteSuggestionLimit: number; seriesEndpoint: boolean; + seriesLimit: number; constructor( instanceSettings: DataSourceInstanceSettings, @@ -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; diff --git a/packages/grafana-prometheus/src/language_provider.test.ts b/packages/grafana-prometheus/src/language_provider.test.ts index e6c011141ac..ca4b8a6b9ce 100644 --- a/packages/grafana-prometheus/src/language_provider.test.ts +++ b/packages/grafana-prometheus/src/language_provider.test.ts @@ -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', () => { diff --git a/packages/grafana-prometheus/src/language_provider.ts b/packages/grafana-prometheus/src/language_provider.ts index a5799e40544..99f7d15dbfa 100644 --- a/packages/grafana-prometheus/src/language_provider.ts +++ b/packages/grafana-prometheus/src/language_provider.ts @@ -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; + fetchLabelValues: (range: TimeRange, key: string, limit?: string | number) => Promise; /** * @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; /** * @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>; /** * @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>; /** * @deprecated Use queryLabelKeys() method instead. It'll determine the right endpoint based on the datasource settings */ - fetchSeriesLabelsMatch: (timeRange: TimeRange, name: string, withLimit?: string) => Promise>; + fetchSeriesLabelsMatch: ( + timeRange: TimeRange, + name: string, + withLimit?: string | number + ) => Promise>; /** * @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 => { + fetchLabelValues = async (range: TimeRange, key: string, limit?: string | number): Promise => { 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 => { 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> => { 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> => { 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> => { const interpolatedName = this.datasource.interpolateString(name); const range = this.datasource.getAdjustedInterval(timeRange); @@ -519,8 +523,8 @@ export interface PrometheusLanguageProviderInterface retrieveLabelKeys: () => string[]; queryMetricsMetadata: () => Promise; - queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: string) => Promise; - queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: string) => Promise; + queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: number) => Promise; + queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: number) => Promise; } /** @@ -677,7 +681,7 @@ export class PrometheusLanguageProvider extends PromQlLanguageProvider implement * @param {string} [limit] - Optional maximum number of label keys to return * @returns {Promise} Array of matching label key names, sorted alphabetically */ - public queryLabelKeys = async (timeRange: TimeRange, match?: string, limit?: string): Promise => { + public queryLabelKeys = async (timeRange: TimeRange, match?: string, limit?: number): Promise => { 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 => { return await this.resourceClient.queryLabelValues(timeRange, labelKey, match, limit); }; diff --git a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json index 4eb44134b69..845a8a5a369 100644 --- a/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json +++ b/packages/grafana-prometheus/src/locales/en-US/grafana-prometheus.json @@ -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.", "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": { diff --git a/packages/grafana-prometheus/src/resource_clients.test.ts b/packages/grafana-prometheus/src/resource_clients.test.ts index 98c4dfc4e21..f919615b44e 100644 --- a/packages/grafana-prometheus/src/resource_clients.test.ts +++ b/packages/grafana-prometheus/src/resource_clients.test.ts @@ -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([]); }); diff --git a/packages/grafana-prometheus/src/resource_clients.ts b/packages/grafana-prometheus/src/resource_clients.ts index e97ddffb964..b5f01e93aa4 100644 --- a/packages/grafana-prometheus/src/resource_clients.ts +++ b/packages/grafana-prometheus/src/resource_clients.ts @@ -22,10 +22,10 @@ export interface ResourceApiClient { start: (timeRange: TimeRange) => Promise; queryMetrics: (timeRange: TimeRange) => Promise<{ metrics: string[]; histogramMetrics: string[] }>; - queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: string) => Promise; - queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: string) => Promise; + queryLabelKeys: (timeRange: TimeRange, match?: string, limit?: number) => Promise; + queryLabelValues: (timeRange: TimeRange, labelKey: string, match?: string, limit?: number) => Promise; - querySeries: (timeRange: TimeRange, match: string, limit?: string) => Promise; + querySeries: (timeRange: TimeRange, match: string, limit: number) => Promise; } type RequestFn = ( @@ -35,10 +35,18 @@ type RequestFn = ( ) => Promise; 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} Array of label keys sorted alphabetically */ - public queryLabelKeys = async ( - timeRange: TimeRange, - match?: string, - limit: string = DEFAULT_SERIES_LIMIT - ): Promise => { + public queryLabelKeys = async (timeRange: TimeRange, match?: string, limit?: number): Promise => { 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 => { 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 => { + public queryLabelKeys = async (timeRange: TimeRange, match?: string, limit?: number): Promise => { + 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 => { 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('|'); } diff --git a/packages/grafana-prometheus/src/types.ts b/packages/grafana-prometheus/src/types.ts index 709d81af22a..acd5b803ee2 100644 --- a/packages/grafana-prometheus/src/types.ts +++ b/packages/grafana-prometheus/src/types.ts @@ -56,6 +56,7 @@ export interface PromOptions extends DataSourceJsonData { oauthPassThru?: boolean; codeModeMetricNamesSuggestionLimit?: number; seriesEndpoint?: boolean; + seriesLimit?: number; } export type ExemplarTraceIdDestination = {