Prometheus: Use new language provider methods in visual query builder (#106204)
* refactor language provider * update tests * more tests * betterer and api endpoints * copilot updates * betterer * remove default value * prettier * introduce new methods * provide unit tests for labelValues * update metadata fetch * move all cache related stuff in caching.ts * provide interface * provide deprecation messages * unit tests for new interface * separation of concerns * update tests * fix unit test * fix some types * Revert "fix some types" This reverts commit7e64b93b5f. * revert interface usage * betterer * use PrometheusLanguageProviderInterface in everywhere * introduce resource clients * unit tests * act accordingly with the feature toggle * some more unit tests * add feature toggle * Revert "add feature toggle" This reverts commit5c93ac324f. * remove feature toggle * update tests * backward compatibility * fix scope issues * comment update * stronger types * prettier * betterer * use new methods in metrics browser and query field * always return data * Revert "always return data" This reverts commit38e493c189. * Revert "Revert "always return data"" This reverts commitb5d3b5d2b0. * handle error * lint * use new method in query builder * fix metrics modal tests too * use labelValues method while searching in combobox * update metrics modal regex search * lint * fix unit test * introduce resource clients and better refactoring * prettier * type fixes * betterer * no empty matcher for series calls * better matchers * add additional tests * proper match string for series * introduce series cache * introduce series cache for series label values * lint * cache values too * utf8 safe label values query with series endpoint * fix unit tests * caching for labels api client * betterer * fix errors
This commit is contained in:
+5
-1
@@ -480,7 +480,11 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "1"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "2"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"]
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "3"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "4"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "5"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "6"],
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "7"]
|
||||
],
|
||||
"packages/grafana-prometheus/src/types.ts:5381": [
|
||||
[0, 0, 0, "Unexpected any. Specify a different type.", "0"],
|
||||
|
||||
@@ -143,6 +143,7 @@ describe('PromVariableQueryEditor', () => {
|
||||
metricsMetadata: {},
|
||||
getLabelValues: jest.fn().mockImplementation(() => ['that']),
|
||||
fetchLabelsWithMatch: jest.fn().mockImplementation(() => Promise.resolve({ those: 'those' })),
|
||||
queryLabelValues: jest.fn().mockResolvedValue([]),
|
||||
} as Partial<PrometheusLanguageProviderInterface>,
|
||||
getTagKeys: jest
|
||||
.fn()
|
||||
|
||||
@@ -21,4 +21,9 @@ export class EmptyLanguageProviderMock {
|
||||
fetchLabels = jest.fn();
|
||||
loadMetricsMetadata = jest.fn();
|
||||
retrieveMetrics = jest.fn().mockReturnValue(['metric']);
|
||||
queryLabelKeys = jest.fn().mockResolvedValue([]);
|
||||
queryLabelValues = jest.fn().mockResolvedValue([]);
|
||||
retrieveLabelKeys = jest.fn().mockReturnValue([]);
|
||||
retrieveMetricsMetadata = jest.fn().mockReturnValue({});
|
||||
queryMetricsMetadata = jest.fn().mockResolvedValue({});
|
||||
}
|
||||
|
||||
@@ -71,7 +71,7 @@ async function loadGroupByLabels(
|
||||
}
|
||||
|
||||
const expr = modeller.renderLabels(labels);
|
||||
const result = await datasource.languageProvider.fetchLabelsWithMatch(timeRange, expr);
|
||||
const result = await datasource.languageProvider.queryLabelKeys(timeRange, expr);
|
||||
|
||||
return Object.keys(result).map((x) => ({
|
||||
label: x,
|
||||
|
||||
@@ -3,9 +3,12 @@ import userEvent from '@testing-library/user-event';
|
||||
|
||||
import '@testing-library/jest-dom';
|
||||
|
||||
import { DataSourceInstanceSettings, MetricFindValue } from '@grafana/data';
|
||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { PrometheusLanguageProviderInterface } from '../../language_provider';
|
||||
import { EmptyLanguageProviderMock } from '../../language_provider.mock';
|
||||
import { getMockTimeRange } from '../../test/__mocks__/datasource';
|
||||
import { PromOptions } from '../../types';
|
||||
|
||||
import { MetricCombobox, MetricComboboxProps } from './MetricCombobox';
|
||||
@@ -34,36 +37,13 @@ describe('MetricCombobox', () => {
|
||||
jsonData: { httpMethod: 'GET' },
|
||||
} as unknown as DataSourceInstanceSettings<PromOptions>;
|
||||
|
||||
const mockDatasource = new PrometheusDatasource(instanceSettings);
|
||||
const mockLanguageProvider = new EmptyLanguageProviderMock() as unknown as PrometheusLanguageProviderInterface;
|
||||
const mockDatasource = new PrometheusDatasource(instanceSettings, undefined, mockLanguageProvider);
|
||||
|
||||
// Options returned when user first opens the combobox - returned by onGetMetrics
|
||||
const initialMockValues = [{ label: 'top_metric_one' }, { label: 'top_metric_two' }, { label: 'top_metric_three' }];
|
||||
const mockOnGetMetrics = jest.fn(() => Promise.resolve(initialMockValues.map((v) => ({ value: v.label }))));
|
||||
|
||||
// Options returned when user searches for a metric
|
||||
const mockValues = [{ label: 'random_metric' }, { label: 'unique_metric' }, { label: 'more_unique_metric' }];
|
||||
mockDatasource.metricFindQuery = jest.fn((query: string) => {
|
||||
// return Promise.resolve([]);
|
||||
// Use the label values regex to get the values inside the label_values function call
|
||||
const labelValuesRegex = /^label_values\((?:(.+),\s*)?([a-zA-Z_][a-zA-Z0-9_]*)\)\s*$/;
|
||||
const queryValueArray = query.match(labelValuesRegex) as RegExpMatchArray;
|
||||
const queryValueRaw = queryValueArray[1];
|
||||
|
||||
// Remove the wrapping regex
|
||||
const queryValue = queryValueRaw.substring(queryValueRaw.indexOf('".*') + 3, queryValueRaw.indexOf('.*"'));
|
||||
|
||||
// Run the regex that we'd pass into prometheus API against the strings in the test
|
||||
return Promise.resolve(
|
||||
mockValues
|
||||
.filter((value) => value.label.match(queryValue))
|
||||
.map((result) => {
|
||||
return {
|
||||
text: result.label,
|
||||
};
|
||||
}) as MetricFindValue[]
|
||||
);
|
||||
});
|
||||
|
||||
const mockOnChange = jest.fn();
|
||||
|
||||
const defaultProps: MetricComboboxProps = {
|
||||
@@ -78,6 +58,7 @@ describe('MetricCombobox', () => {
|
||||
datasource: mockDatasource,
|
||||
labelsFilters: [],
|
||||
variableEditor: false,
|
||||
timeRange: getMockTimeRange(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -103,6 +84,9 @@ describe('MetricCombobox', () => {
|
||||
});
|
||||
|
||||
it('fetches metrics for the users query', async () => {
|
||||
// Mock the queryLabelValues to return the expected metric
|
||||
mockDatasource.languageProvider.queryLabelValues = jest.fn().mockResolvedValue(['unique_metric']);
|
||||
|
||||
render(<MetricCombobox {...defaultProps} />);
|
||||
|
||||
const combobox = screen.getByPlaceholderText('Select metric');
|
||||
@@ -113,8 +97,12 @@ describe('MetricCombobox', () => {
|
||||
expect(item).toBeInTheDocument();
|
||||
|
||||
// This should be asserted by the above check, but double check anyway
|
||||
// This is the actual argument, created by formatKeyValueStringsForLabelValuesQuery()
|
||||
expect(mockDatasource.metricFindQuery).toHaveBeenCalledWith('label_values({__name__=~".*unique.*"},__name__)');
|
||||
// This is the actual argument, created by formatKeyValueStrings()
|
||||
expect(mockDatasource.languageProvider.queryLabelValues).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'__name__',
|
||||
'{__name__=~".*unique.*"}'
|
||||
);
|
||||
});
|
||||
|
||||
it('calls onChange with the correct value when a metric is selected', async () => {
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import { SelectableValue } from '@grafana/data';
|
||||
import { SelectableValue, TimeRange } from '@grafana/data';
|
||||
import { selectors } from '@grafana/e2e-selectors';
|
||||
import { Trans, t } from '@grafana/i18n';
|
||||
import { EditorField, EditorFieldGroup, InputGroup } from '@grafana/plugin-ui';
|
||||
import { Button, InlineField, InlineFieldRow, Combobox, ComboboxOption } from '@grafana/ui';
|
||||
|
||||
import { METRIC_LABEL } from '../../components/metrics-browser/types';
|
||||
import { PrometheusDatasource } from '../../datasource';
|
||||
import { regexifyLabelValuesQueryString } from '../parsingUtils';
|
||||
import { QueryBuilderLabelFilter } from '../shared/types';
|
||||
@@ -23,6 +24,7 @@ export interface MetricComboboxProps {
|
||||
labelsFilters: QueryBuilderLabelFilter[];
|
||||
onBlur?: () => void;
|
||||
variableEditor?: boolean;
|
||||
timeRange: TimeRange;
|
||||
}
|
||||
|
||||
export function MetricCombobox({
|
||||
@@ -32,6 +34,7 @@ export function MetricCombobox({
|
||||
onGetMetrics,
|
||||
labelsFilters,
|
||||
variableEditor,
|
||||
timeRange,
|
||||
}: Readonly<MetricComboboxProps>) {
|
||||
const [metricsModalOpen, setMetricsModalOpen] = useState(false);
|
||||
|
||||
@@ -40,17 +43,18 @@ export function MetricCombobox({
|
||||
*/
|
||||
const getMetricLabels = useCallback(
|
||||
async (query: string) => {
|
||||
const results = await datasource.metricFindQuery(formatKeyValueStringsForLabelValuesQuery(query, labelsFilters));
|
||||
const match = formatKeyValueStrings(query, labelsFilters);
|
||||
const results = await datasource.languageProvider.queryLabelValues(timeRange, METRIC_LABEL, match);
|
||||
|
||||
const resultsOptions = results.map((result) => {
|
||||
return {
|
||||
label: result.text,
|
||||
value: result.text,
|
||||
label: result,
|
||||
value: result,
|
||||
};
|
||||
});
|
||||
return resultsOptions;
|
||||
},
|
||||
[datasource, labelsFilters]
|
||||
[datasource.languageProvider, labelsFilters, timeRange]
|
||||
);
|
||||
|
||||
const onComboboxChange = useCallback(
|
||||
@@ -130,6 +134,7 @@ export function MetricCombobox({
|
||||
query={query}
|
||||
onChange={onChange}
|
||||
initialMetrics={loadMetricsExplorerMetrics}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
)}
|
||||
{variableEditor ? (
|
||||
@@ -165,7 +170,7 @@ export const formatPrometheusLabelFiltersToString = (
|
||||
): string => {
|
||||
const filterArray = labelsFilters ? formatPrometheusLabelFilters(labelsFilters) : [];
|
||||
|
||||
return `label_values({__name__=~".*${queryString}"${filterArray ? filterArray.join('') : ''}},__name__)`;
|
||||
return `{__name__=~".*${queryString}"${filterArray ? filterArray.join('') : ''}}`;
|
||||
};
|
||||
|
||||
export const formatPrometheusLabelFilters = (labelsFilters: QueryBuilderLabelFilter[]): string[] => {
|
||||
@@ -177,7 +182,7 @@ export const formatPrometheusLabelFilters = (labelsFilters: QueryBuilderLabelFil
|
||||
/**
|
||||
* Reformat the query string and label filters to return all valid results for current query editor state
|
||||
*/
|
||||
const formatKeyValueStringsForLabelValuesQuery = (query: string, labelsFilters?: QueryBuilderLabelFilter[]): string => {
|
||||
const formatKeyValueStrings = (query: string, labelsFilters?: QueryBuilderLabelFilter[]): string => {
|
||||
const queryString = regexifyLabelValuesQueryString(query);
|
||||
|
||||
return formatPrometheusLabelFiltersToString(queryString, labelsFilters);
|
||||
|
||||
+12
-18
@@ -25,16 +25,11 @@ const createMockDatasource = () => {
|
||||
hasLabelsMatchAPISupport: jest.fn().mockReturnValue(true),
|
||||
lookupsDisabled: false,
|
||||
languageProvider: {
|
||||
fetchLabels: jest.fn().mockResolvedValue({}),
|
||||
getLabelKeys: jest.fn().mockReturnValue(['label1', 'label2']),
|
||||
fetchLabelsWithMatch: jest.fn().mockResolvedValue({ label1: [], label2: [] }),
|
||||
fetchSeries: jest.fn().mockResolvedValue([{ label1: 'value1' }]),
|
||||
fetchSeriesValuesWithMatch: jest.fn().mockResolvedValue(['value1', 'value2']),
|
||||
getLabelValues: jest.fn().mockResolvedValue(['value1', 'value2']),
|
||||
getSeries: jest.fn().mockResolvedValue({ __name__: ['metric1', 'metric2'] }),
|
||||
getSeriesValues: jest.fn().mockResolvedValue(['metric1', 'metric2']),
|
||||
loadMetricsMetadata: jest.fn().mockResolvedValue({}),
|
||||
metricsMetadata: { metric1: { type: 'counter', help: 'help text' } },
|
||||
queryLabelKeys: jest.fn().mockResolvedValue(['label1', 'label2']),
|
||||
retrieveLabelKeys: jest.fn().mockReturnValue(['label1', 'label2']),
|
||||
queryLabelValues: jest.fn().mockResolvedValue(['value1', 'value2']),
|
||||
queryMetricsMetadata: jest.fn().mockResolvedValue({ metric1: { type: 'counter', help: 'help text' } }),
|
||||
retrieveMetricsMetadata: jest.fn().mockResolvedValue({ metric1: { type: 'counter', help: 'help text' } }),
|
||||
},
|
||||
};
|
||||
return datasource as unknown as PrometheusDatasource;
|
||||
@@ -211,9 +206,9 @@ describe('MetricsLabelsSection', () => {
|
||||
await onGetLabelNamesCallback({});
|
||||
|
||||
// Check that fetchLabels was called
|
||||
expect(datasource.languageProvider.fetchLabels).toHaveBeenCalledWith(defaultTimeRange);
|
||||
expect(datasource.languageProvider.queryLabelKeys).toHaveBeenCalledWith(defaultTimeRange);
|
||||
// Check that getLabelKeys was called
|
||||
expect(datasource.languageProvider.getLabelKeys).toHaveBeenCalled();
|
||||
expect(datasource.languageProvider.retrieveLabelKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onGetLabelNames with metric correctly', async () => {
|
||||
@@ -237,7 +232,7 @@ describe('MetricsLabelsSection', () => {
|
||||
await onGetLabelNamesCallback({});
|
||||
|
||||
// Check that fetchLabelsWithMatch was called
|
||||
expect(datasource.languageProvider.fetchLabelsWithMatch).toHaveBeenCalled();
|
||||
expect(datasource.languageProvider.queryLabelKeys).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle getLabelValuesAutocompleteSuggestions correctly', async () => {
|
||||
@@ -261,7 +256,7 @@ describe('MetricsLabelsSection', () => {
|
||||
await getLabelValuesCallback('val', 'label1');
|
||||
|
||||
// Check that fetchSeriesValuesWithMatch was called (since hasLabelsMatchAPISupport is true)
|
||||
expect(datasource.languageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
|
||||
expect(datasource.languageProvider.queryLabelValues).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onGetLabelValues with no metric correctly', async () => {
|
||||
@@ -286,7 +281,7 @@ describe('MetricsLabelsSection', () => {
|
||||
await onGetLabelValuesCallback({ label: 'label1' });
|
||||
|
||||
// Check that getLabelValues was called
|
||||
expect(datasource.languageProvider.getLabelValues).toHaveBeenCalledWith(defaultTimeRange, 'label1');
|
||||
expect(datasource.languageProvider.queryLabelValues).toHaveBeenCalledWith(defaultTimeRange, 'label1');
|
||||
});
|
||||
|
||||
it('should handle onGetLabelValues with metric correctly', async () => {
|
||||
@@ -310,7 +305,7 @@ describe('MetricsLabelsSection', () => {
|
||||
await onGetLabelValuesCallback({ label: 'label1' });
|
||||
|
||||
// Check that fetchSeriesValuesWithMatch was called (since hasLabelsMatchAPISupport is true)
|
||||
expect(datasource.languageProvider.fetchSeriesValuesWithMatch).toHaveBeenCalled();
|
||||
expect(datasource.languageProvider.queryLabelValues).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle onGetLabelValues with no label correctly', async () => {
|
||||
@@ -368,7 +363,6 @@ describe('MetricsLabelsSection', () => {
|
||||
it('should load metrics metadata if not present', async () => {
|
||||
const onChange = jest.fn();
|
||||
const datasource = createMockDatasource();
|
||||
datasource.languageProvider.metricsMetadata = undefined;
|
||||
|
||||
render(
|
||||
<MetricsLabelsSection
|
||||
@@ -386,6 +380,6 @@ describe('MetricsLabelsSection', () => {
|
||||
await onGetMetricsCallback();
|
||||
|
||||
// loadMetricsMetadata should be called
|
||||
expect(datasource.languageProvider.loadMetricsMetadata).toHaveBeenCalled();
|
||||
expect(datasource.languageProvider.queryMetricsMetadata).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -64,8 +64,8 @@ export function MetricsLabelsSection({
|
||||
const onGetLabelNames = async (forLabel: Partial<QueryBuilderLabelFilter>): Promise<SelectableValue[]> => {
|
||||
// If no metric we need to use a different method
|
||||
if (!query.metric) {
|
||||
await datasource.languageProvider.fetchLabels(timeRange);
|
||||
return datasource.languageProvider.getLabelKeys().map((k) => ({ value: k }));
|
||||
await datasource.languageProvider.queryLabelKeys(timeRange);
|
||||
return datasource.languageProvider.retrieveLabelKeys().map((k) => ({ value: k }));
|
||||
}
|
||||
|
||||
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
||||
@@ -73,15 +73,15 @@ export function MetricsLabelsSection({
|
||||
labelsToConsider.push({ label: '__name__', op: '=', value: query.metric });
|
||||
const expr = promQueryModeller.renderLabels(labelsToConsider);
|
||||
|
||||
let labelsIndex: Record<string, string[]> = await datasource.languageProvider.fetchLabelsWithMatch(timeRange, expr);
|
||||
let labelsIndex: string[] = await datasource.languageProvider.queryLabelKeys(timeRange, expr);
|
||||
|
||||
// filter out already used labels
|
||||
return Object.keys(labelsIndex)
|
||||
return labelsIndex
|
||||
.filter((labelName) => !labelsToConsider.find((filter) => filter.label === labelName))
|
||||
.map((k) => ({ value: k }));
|
||||
};
|
||||
|
||||
const getLabelValuesAutocompleteSuggestions = (
|
||||
const getLabelValuesAutocompleteSuggestions = async (
|
||||
queryString?: string,
|
||||
labelName?: string
|
||||
): Promise<SelectableValue[]> => {
|
||||
@@ -102,63 +102,8 @@ export function MetricsLabelsSection({
|
||||
value: datasource.interpolateString(labelObject.value),
|
||||
}));
|
||||
const expr = promQueryModeller.renderLabels(interpolatedLabelsToConsider);
|
||||
let response: Promise<SelectableValue[]>;
|
||||
if (datasource.hasLabelsMatchAPISupport()) {
|
||||
response = getLabelValuesFromLabelValuesAPI(forLabel, expr);
|
||||
} else {
|
||||
response = getLabelValuesFromSeriesAPI(forLabel, expr);
|
||||
}
|
||||
|
||||
return response.then((response: SelectableValue[]) => {
|
||||
truncateResult(response);
|
||||
return response;
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to fetch and format label value results from legacy API
|
||||
* @param forLabel
|
||||
* @param promQLExpression
|
||||
*/
|
||||
const getLabelValuesFromSeriesAPI = (
|
||||
forLabel: Partial<QueryBuilderLabelFilter>,
|
||||
promQLExpression: string
|
||||
): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
const result = datasource.languageProvider.fetchSeries(timeRange, promQLExpression);
|
||||
const forLabelInterpolated = datasource.interpolateString(forLabel.label);
|
||||
return result.then((result) => {
|
||||
// This query returns duplicate values, scrub them out
|
||||
const set = new Set<string>();
|
||||
result.forEach((labelValue) => {
|
||||
const labelNameString = labelValue[forLabelInterpolated];
|
||||
set.add(labelNameString);
|
||||
});
|
||||
|
||||
return Array.from(set).map((labelValues: string) => ({ label: labelValues, value: labelValues }));
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Helper function to fetch label values from a promql string expression and a label
|
||||
* @param forLabel
|
||||
* @param promQLExpression
|
||||
*/
|
||||
const getLabelValuesFromLabelValuesAPI = (
|
||||
forLabel: Partial<QueryBuilderLabelFilter>,
|
||||
promQLExpression: string
|
||||
): Promise<SelectableValue[]> => {
|
||||
if (!forLabel.label) {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
|
||||
const requestId = `[${datasource.uid}][${query.metric}][${forLabel.label}][${forLabel.op}]`;
|
||||
|
||||
return datasource.languageProvider
|
||||
.fetchSeriesValuesWithMatch(timeRange, forLabel.label, promQLExpression, requestId)
|
||||
.then((response) => response.map((v) => ({ value: v, label: v })));
|
||||
const values = await datasource.languageProvider.queryLabelValues(timeRange, forLabel.label, expr);
|
||||
return truncateResult(values).map(toSelectableValue);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -172,7 +117,7 @@ export function MetricsLabelsSection({
|
||||
}
|
||||
// If no metric is selected, we can get the raw list of labels
|
||||
if (!query.metric) {
|
||||
return (await datasource.languageProvider.getLabelValues(timeRange, forLabel.label)).map((v) => ({ value: v }));
|
||||
return (await datasource.languageProvider.queryLabelValues(timeRange, forLabel.label)).map((v) => ({ value: v }));
|
||||
}
|
||||
|
||||
const labelsToConsider = query.labels.filter((x) => x !== forLabel);
|
||||
@@ -186,12 +131,7 @@ export function MetricsLabelsSection({
|
||||
}));
|
||||
|
||||
const expr = promQueryModeller.renderLabels(interpolatedLabelsToConsider);
|
||||
|
||||
if (datasource.hasLabelsMatchAPISupport()) {
|
||||
return getLabelValuesFromLabelValuesAPI(forLabel, expr);
|
||||
} else {
|
||||
return getLabelValuesFromSeriesAPI(forLabel, expr);
|
||||
}
|
||||
return (await datasource.languageProvider.queryLabelValues(timeRange, forLabel.label, expr)).map(toSelectableValue);
|
||||
};
|
||||
|
||||
const onGetMetrics = useCallback(() => {
|
||||
@@ -209,6 +149,7 @@ export function MetricsLabelsSection({
|
||||
metricLookupDisabled={datasource.lookupsDisabled}
|
||||
onBlur={onBlur ? onBlur : () => {}}
|
||||
variableEditor={variableEditor}
|
||||
timeRange={timeRange}
|
||||
/>
|
||||
<LabelFilters
|
||||
debounceDuration={getDebounceTimeInMilliseconds(datasource.cacheLevel)}
|
||||
@@ -228,6 +169,7 @@ export function MetricsLabelsSection({
|
||||
* exists.
|
||||
* @param datasource
|
||||
* @param query
|
||||
* @param timeRange
|
||||
*/
|
||||
async function getMetrics(
|
||||
datasource: PrometheusDatasource,
|
||||
@@ -236,26 +178,19 @@ async function getMetrics(
|
||||
): Promise<Array<{ value: string; description?: string }>> {
|
||||
// Makes sure we loaded the metadata for metrics. Usually this is done in the start() method of the provider but we
|
||||
// don't use it with the visual builder and there is no need to run all the start() setup anyway.
|
||||
if (!datasource.languageProvider.metricsMetadata) {
|
||||
await datasource.languageProvider.loadMetricsMetadata();
|
||||
}
|
||||
|
||||
// Error handling for when metrics metadata returns as undefined
|
||||
if (!datasource.languageProvider.metricsMetadata) {
|
||||
datasource.languageProvider.metricsMetadata = {};
|
||||
const metadata = datasource.languageProvider.retrieveMetricsMetadata();
|
||||
if (Object.keys(metadata).length === 0) {
|
||||
await datasource.languageProvider.queryMetricsMetadata();
|
||||
}
|
||||
|
||||
let metrics: string[];
|
||||
if (query.labels.length > 0) {
|
||||
const expr = promQueryModeller.renderLabels(query.labels);
|
||||
metrics = (await datasource.languageProvider.getSeriesValues(timeRange, '__name__', expr)) ?? [];
|
||||
} else {
|
||||
metrics = (await datasource.languageProvider.getLabelValues(timeRange, '__name__')) ?? [];
|
||||
}
|
||||
const expr = promQueryModeller.renderLabels(query.labels);
|
||||
metrics =
|
||||
(await datasource.languageProvider.queryLabelValues(timeRange, '__name__', expr === '' ? undefined : expr)) ?? [];
|
||||
|
||||
return metrics.map((m) => ({
|
||||
value: m,
|
||||
description: getMetadataString(m, datasource.languageProvider.metricsMetadata!),
|
||||
description: getMetadataString(m, datasource.languageProvider.retrieveMetricsMetadata()),
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -266,3 +201,10 @@ export function getMetadataString(metric: string, metadata: PromMetricsMetadata)
|
||||
const { type, help } = metadata[metric];
|
||||
return `${type.toUpperCase()}: ${help}`;
|
||||
}
|
||||
|
||||
function toSelectableValue(lv: string) {
|
||||
return {
|
||||
label: lv,
|
||||
value: lv,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -89,7 +89,9 @@ describe('PromQueryBuilder', () => {
|
||||
it('tries to load metrics without labels', async () => {
|
||||
const { languageProvider, container } = setup();
|
||||
await openMetricSelect(container);
|
||||
await waitFor(() => expect(languageProvider.getLabelValues).toHaveBeenCalledWith(expect.anything(), '__name__'));
|
||||
await waitFor(() =>
|
||||
expect(languageProvider.queryLabelValues).toHaveBeenCalledWith(expect.anything(), '__name__', undefined)
|
||||
);
|
||||
});
|
||||
|
||||
it('tries to load metrics with labels', async () => {
|
||||
@@ -99,7 +101,7 @@ describe('PromQueryBuilder', () => {
|
||||
});
|
||||
await openMetricSelect(container);
|
||||
await waitFor(() =>
|
||||
expect(languageProvider.getSeriesValues).toHaveBeenCalledWith(
|
||||
expect(languageProvider.queryLabelValues).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
'{label_name="label_value"}'
|
||||
@@ -119,10 +121,7 @@ describe('PromQueryBuilder', () => {
|
||||
const { languageProvider } = setup();
|
||||
await openLabelNameSelect();
|
||||
await waitFor(() =>
|
||||
expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'{__name__="random_metric"}'
|
||||
)
|
||||
expect(languageProvider.queryLabelKeys).toHaveBeenCalledWith(expect.anything(), '{__name__="random_metric"}')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -143,7 +142,7 @@ describe('PromQueryBuilder', () => {
|
||||
});
|
||||
await openLabelNameSelect(1);
|
||||
await waitFor(() =>
|
||||
expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith(
|
||||
expect(languageProvider.queryLabelKeys).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'{label_name="label_value", __name__="random_metric"}'
|
||||
)
|
||||
@@ -157,7 +156,7 @@ describe('PromQueryBuilder', () => {
|
||||
metric: '',
|
||||
});
|
||||
await openLabelNameSelect();
|
||||
await waitFor(() => expect(languageProvider.fetchLabels).toBeCalled());
|
||||
await waitFor(() => expect(languageProvider.queryLabelKeys).toBeCalled());
|
||||
});
|
||||
|
||||
it('shows hints for histogram metrics', async () => {
|
||||
@@ -298,10 +297,7 @@ describe('PromQueryBuilder', () => {
|
||||
});
|
||||
await openLabelNameSelect();
|
||||
await waitFor(() =>
|
||||
expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'{__name__="random_metric"}'
|
||||
)
|
||||
expect(languageProvider.queryLabelKeys).toHaveBeenCalledWith(expect.anything(), '{__name__="random_metric"}')
|
||||
);
|
||||
});
|
||||
|
||||
@@ -328,7 +324,7 @@ describe('PromQueryBuilder', () => {
|
||||
);
|
||||
await openLabelNameSelect(1);
|
||||
await waitFor(() =>
|
||||
expect(languageProvider.fetchLabelsWithMatch).toHaveBeenCalledWith(
|
||||
expect(languageProvider.queryLabelKeys).toHaveBeenCalledWith(
|
||||
expect.anything(),
|
||||
'{label_name="label_value", __name__="random_metric"}'
|
||||
)
|
||||
|
||||
+8
-6
@@ -7,6 +7,7 @@ import { DataSourceInstanceSettings, DataSourcePluginMeta } from '@grafana/data'
|
||||
import { PrometheusDatasource } from '../../../datasource';
|
||||
import { PrometheusLanguageProviderInterface } from '../../../language_provider';
|
||||
import { EmptyLanguageProviderMock } from '../../../language_provider.mock';
|
||||
import { getMockTimeRange } from '../../../test/__mocks__/datasource';
|
||||
import { PromOptions } from '../../../types';
|
||||
import { PromVisualQuery } from '../../types';
|
||||
|
||||
@@ -245,17 +246,17 @@ const listOfMetrics: string[] = [
|
||||
function createDatasource(withLabels?: boolean) {
|
||||
const languageProvider = new EmptyLanguageProviderMock() as unknown as PrometheusLanguageProviderInterface;
|
||||
|
||||
// display different results if their are labels selected in the PromVisualQuery
|
||||
// display different results if their labels are selected in the PromVisualQuery
|
||||
if (withLabels) {
|
||||
languageProvider.metricsMetadata = {
|
||||
languageProvider.retrieveMetricsMetadata = jest.fn().mockReturnValue({
|
||||
'with-labels': {
|
||||
type: 'with-labels-type',
|
||||
help: 'with-labels-help',
|
||||
},
|
||||
};
|
||||
});
|
||||
} else {
|
||||
// all metrics
|
||||
languageProvider.metricsMetadata = {
|
||||
languageProvider.retrieveMetricsMetadata = jest.fn().mockReturnValue({
|
||||
'all-metrics': {
|
||||
type: 'all-metrics-type',
|
||||
help: 'all-metrics-help',
|
||||
@@ -273,7 +274,7 @@ function createDatasource(withLabels?: boolean) {
|
||||
help: 'a native histogram',
|
||||
},
|
||||
// missing metadata for other metrics is tested for, see below
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
const datasource = new PrometheusDatasource(
|
||||
@@ -296,6 +297,7 @@ function createProps(query: PromVisualQuery, datasource: PrometheusDatasource, m
|
||||
onClose: jest.fn(),
|
||||
query: query,
|
||||
initialMetrics: metrics,
|
||||
timeRange: getMockTimeRange(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -307,5 +309,5 @@ function setup(query: PromVisualQuery, metrics: string[], withlabels?: boolean)
|
||||
// render the modal only
|
||||
const { container } = render(<MetricsModal {...props} />);
|
||||
|
||||
return container;
|
||||
return { container, datasource };
|
||||
}
|
||||
|
||||
+16
-6
@@ -20,6 +20,9 @@ import {
|
||||
} from '@grafana/ui';
|
||||
|
||||
import { getDebounceTimeInMilliseconds } from '../../../caching';
|
||||
import { METRIC_LABEL } from '../../../components/metrics-browser/types';
|
||||
import { regexifyLabelValuesQueryString } from '../../parsingUtils';
|
||||
import { formatPrometheusLabelFilters } from '../MetricCombobox';
|
||||
|
||||
import { AdditionalSettings } from './AdditionalSettings';
|
||||
import { FeedbackLink } from './FeedbackLink';
|
||||
@@ -29,7 +32,6 @@ import {
|
||||
calculatePageList,
|
||||
calculateResultsPerPage,
|
||||
displayedMetrics,
|
||||
getBackendSearchMetrics,
|
||||
placeholders,
|
||||
promTypes,
|
||||
setMetrics,
|
||||
@@ -47,7 +49,7 @@ import { PromFilterOption } from './types';
|
||||
import { debouncedFuzzySearch } from './uFuzzy';
|
||||
|
||||
export const MetricsModal = (props: MetricsModalProps) => {
|
||||
const { datasource, isOpen, onClose, onChange, query, initialMetrics } = props;
|
||||
const { datasource, isOpen, onClose, onChange, query, initialMetrics, timeRange } = props;
|
||||
|
||||
const [state, dispatch] = useReducer(stateSlice.reducer, initialState(query));
|
||||
|
||||
@@ -99,17 +101,25 @@ export const MetricsModal = (props: MetricsModalProps) => {
|
||||
debounce(async (metricText: string) => {
|
||||
dispatch(setIsLoading(true));
|
||||
|
||||
const metrics = await getBackendSearchMetrics(metricText, query.labels, datasource);
|
||||
const queryString = regexifyLabelValuesQueryString(metricText);
|
||||
const filterArray = query.labels ? formatPrometheusLabelFilters(query.labels) : [];
|
||||
const match = `{__name__=~".*${queryString}"${filterArray ? filterArray.join('') : ''}}`;
|
||||
|
||||
const results = await datasource.languageProvider.queryLabelValues(timeRange, METRIC_LABEL, match);
|
||||
|
||||
const resultsOptions = results.map((result) => ({
|
||||
value: result,
|
||||
}));
|
||||
|
||||
dispatch(
|
||||
filterMetricsBackend({
|
||||
metrics: metrics,
|
||||
filteredMetricCount: metrics.length,
|
||||
metrics: resultsOptions,
|
||||
filteredMetricCount: resultsOptions.length,
|
||||
isLoading: false,
|
||||
})
|
||||
);
|
||||
}, getDebounceTimeInMilliseconds(datasource.cacheLevel)),
|
||||
[datasource, query]
|
||||
[datasource.cacheLevel, datasource.languageProvider, query.labels, timeRange]
|
||||
);
|
||||
|
||||
function fuzzyNameDispatch(haystackData: string[][]) {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
import { TimeRange } from '@grafana/data';
|
||||
|
||||
import { PrometheusDatasource } from '../../../../datasource';
|
||||
import { PromVisualQuery } from '../../../types';
|
||||
|
||||
@@ -16,6 +18,7 @@ export interface MetricsModalProps {
|
||||
onClose: () => void;
|
||||
onChange: (query: PromVisualQuery) => void;
|
||||
initialMetrics: string[] | (() => Promise<string[]>);
|
||||
timeRange: TimeRange;
|
||||
}
|
||||
|
||||
export interface AdditionalSettingsProps {
|
||||
|
||||
+3
-4
@@ -11,7 +11,6 @@ import { PromVisualQuery } from '../../../types';
|
||||
import { HaystackDictionary, MetricData, MetricsData, PromFilterOption } from '../types';
|
||||
|
||||
import { MetricsModalMetadata, MetricsModalState, setFilteredMetricCount } from './state';
|
||||
|
||||
export async function setMetrics(
|
||||
datasource: PrometheusDatasource,
|
||||
query: PromVisualQuery,
|
||||
@@ -20,7 +19,7 @@ export async function setMetrics(
|
||||
// metadata is set in the metric select now
|
||||
// use this to disable metadata search and display
|
||||
let hasMetadata = true;
|
||||
const metadata = datasource.languageProvider.metricsMetadata;
|
||||
const metadata = datasource.languageProvider.retrieveMetricsMetadata();
|
||||
if (metadata && Object.keys(metadata).length === 0) {
|
||||
hasMetadata = false;
|
||||
}
|
||||
@@ -61,9 +60,9 @@ export async function setMetrics(
|
||||
* @returns A MetricData object.
|
||||
*/
|
||||
function buildMetricData(metric: string, datasource: PrometheusDatasource): MetricData {
|
||||
let type = getMetadataType(metric, datasource.languageProvider.metricsMetadata!);
|
||||
let type = getMetadataType(metric, datasource.languageProvider.retrieveMetricsMetadata());
|
||||
|
||||
const description = getMetadataHelp(metric, datasource.languageProvider.metricsMetadata!);
|
||||
const description = getMetadataHelp(metric, datasource.languageProvider.retrieveMetricsMetadata());
|
||||
|
||||
['histogram', 'summary'].forEach((t) => {
|
||||
if (description?.toLowerCase().includes(t) && type !== t) {
|
||||
|
||||
@@ -132,6 +132,157 @@ describe('LabelsApiClient', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('LabelsCache', () => {
|
||||
let cache: any; // Using any to access private members for testing
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
cache = (client as any)._cache;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe('cache key generation', () => {
|
||||
it('should generate different cache keys for keys and values', () => {
|
||||
const keyKey = cache.getCacheKey(mockTimeRange, '{job="test"}', '1000', 'key');
|
||||
const valueKey = cache.getCacheKey(mockTimeRange, '{job="test"}', '1000', 'value');
|
||||
expect(keyKey).not.toEqual(valueKey);
|
||||
});
|
||||
|
||||
it('should use cache level from constructor for time range snapping', () => {
|
||||
const highLevelCache = new LabelsApiClient(mockRequest, {
|
||||
cacheLevel: PrometheusCacheLevel.High,
|
||||
getAdjustedInterval: mockGetAdjustedInterval,
|
||||
getTimeRangeParams: mockGetTimeRangeParams,
|
||||
interpolateString: mockInterpolateString,
|
||||
} as unknown as PrometheusDatasource);
|
||||
|
||||
const lowLevelCache = new LabelsApiClient(mockRequest, {
|
||||
cacheLevel: PrometheusCacheLevel.Low,
|
||||
getAdjustedInterval: mockGetAdjustedInterval,
|
||||
getTimeRangeParams: mockGetTimeRangeParams,
|
||||
interpolateString: mockInterpolateString,
|
||||
} as unknown as PrometheusDatasource);
|
||||
|
||||
const highKey = (highLevelCache as any)._cache.getCacheKey(mockTimeRange, '{job="test"}', '1000', 'key');
|
||||
const lowKey = (lowLevelCache as any)._cache.getCacheKey(mockTimeRange, '{job="test"}', '1000', 'key');
|
||||
|
||||
expect(highKey).not.toEqual(lowKey);
|
||||
});
|
||||
});
|
||||
|
||||
describe('cache size management', () => {
|
||||
beforeEach(() => {
|
||||
// Start with a clean cache for each test
|
||||
cache._cache = {};
|
||||
cache._accessTimestamps = {};
|
||||
});
|
||||
|
||||
it('should remove oldest entries when max entries limit is reached', () => {
|
||||
// Override MAX_CACHE_ENTRIES for testing
|
||||
Object.defineProperty(cache, 'MAX_CACHE_ENTRIES', { value: 5 });
|
||||
|
||||
// Add entries up to the limit
|
||||
cache.setLabelKeys(mockTimeRange, 'match1', '1000', ['key1']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
cache.setLabelKeys(mockTimeRange, 'match2', '1000', ['key2']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
cache.setLabelKeys(mockTimeRange, 'match3', '1000', ['key3']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
cache.setLabelKeys(mockTimeRange, 'match4', '1000', ['key4']);
|
||||
jest.advanceTimersByTime(1000);
|
||||
cache.setLabelKeys(mockTimeRange, 'match5', '1000', ['key5']);
|
||||
|
||||
// Access first entry to make it more recently used
|
||||
cache.getLabelKeys(mockTimeRange, 'match1', '1000');
|
||||
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
// Add sixth entry - this should trigger cache cleaning
|
||||
cache.setLabelKeys(mockTimeRange, 'match6', '1000', ['key6']);
|
||||
|
||||
// Verify cache state - should have removed one entry (match2)
|
||||
expect(Object.keys(cache._cache).length).toBe(5);
|
||||
|
||||
// Second entry should be removed (was least recently used)
|
||||
expect(cache.getLabelKeys(mockTimeRange, 'match2', '1000')).toBeUndefined();
|
||||
// First entry should exist (was accessed recently)
|
||||
expect(cache.getLabelKeys(mockTimeRange, 'match1', '1000')).toEqual(['key1']);
|
||||
// Third entry should exist
|
||||
expect(cache.getLabelKeys(mockTimeRange, 'match3', '1000')).toEqual(['key3']);
|
||||
// Fourth entry should exist
|
||||
expect(cache.getLabelKeys(mockTimeRange, 'match4', '1000')).toEqual(['key4']);
|
||||
// Fifth entry should exist
|
||||
expect(cache.getLabelKeys(mockTimeRange, 'match5', '1000')).toEqual(['key5']);
|
||||
// Sixth entry should exist (newest)
|
||||
expect(cache.getLabelKeys(mockTimeRange, 'match6', '1000')).toEqual(['key6']);
|
||||
});
|
||||
|
||||
it('should remove oldest entries when max size limit is reached', () => {
|
||||
// Override MAX_CACHE_SIZE_BYTES for testing - set to small value to trigger cleanup
|
||||
Object.defineProperty(cache, 'MAX_CACHE_SIZE_BYTES', { value: 10 }); // Very small size to force cleanup
|
||||
|
||||
// Create entries that will exceed the size limit
|
||||
const largeArray = Array(5).fill('large_value');
|
||||
|
||||
// Add first large entry
|
||||
cache.setLabelKeys(mockTimeRange, 'match1', '1000', largeArray);
|
||||
|
||||
// Verify initial size
|
||||
expect(Object.keys(cache._cache).length).toBe(1);
|
||||
expect(cache.getCacheSizeInBytes()).toBeGreaterThan(10);
|
||||
|
||||
// Add second large entry - should trigger size-based cleanup
|
||||
cache.setLabelKeys(mockTimeRange, 'match2', '1000', largeArray);
|
||||
|
||||
// Verify cache state - should only have the newest entry
|
||||
expect(Object.keys(cache._cache).length).toBe(1);
|
||||
expect(cache.getLabelKeys(mockTimeRange, 'match1', '1000')).toBeUndefined();
|
||||
expect(cache.getLabelKeys(mockTimeRange, 'match2', '1000')).toEqual(largeArray);
|
||||
|
||||
// Add third entry to verify the cleanup continues to work
|
||||
cache.setLabelKeys(mockTimeRange, 'match3', '1000', largeArray);
|
||||
expect(Object.keys(cache._cache).length).toBe(1);
|
||||
expect(cache.getLabelKeys(mockTimeRange, 'match2', '1000')).toBeUndefined();
|
||||
expect(cache.getLabelKeys(mockTimeRange, 'match3', '1000')).toEqual(largeArray);
|
||||
});
|
||||
|
||||
it('should update access time when getting cached values', () => {
|
||||
// Add an entry
|
||||
cache.setLabelKeys(mockTimeRange, 'match1', '1000', ['key1']);
|
||||
const cacheKey = cache.getCacheKey(mockTimeRange, 'match1', '1000', 'key');
|
||||
const initialTimestamp = cache._accessTimestamps[cacheKey];
|
||||
|
||||
// Advance time
|
||||
jest.advanceTimersByTime(1000);
|
||||
|
||||
// Access the entry
|
||||
cache.getLabelKeys(mockTimeRange, 'match1', '1000');
|
||||
const updatedTimestamp = cache._accessTimestamps[cacheKey];
|
||||
|
||||
// Verify timestamp was updated
|
||||
expect(updatedTimestamp).toBeGreaterThan(initialTimestamp);
|
||||
});
|
||||
});
|
||||
|
||||
describe('label values caching', () => {
|
||||
it('should cache and retrieve label values', () => {
|
||||
const values = ['value1', 'value2'];
|
||||
cache.setLabelValues(mockTimeRange, '{job="test"}', '1000', values);
|
||||
|
||||
const cachedValues = cache.getLabelValues(mockTimeRange, '{job="test"}', '1000');
|
||||
expect(cachedValues).toEqual(values);
|
||||
});
|
||||
|
||||
it('should return undefined for non-existent label values', () => {
|
||||
const result = cache.getLabelValues(mockTimeRange, '{job="nonexistent"}', '1000');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('SeriesApiClient', () => {
|
||||
@@ -293,7 +444,7 @@ describe('SeriesApiClient', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
jest.useFakeTimers();
|
||||
cache = (client as any)._seriesCache;
|
||||
cache = (client as any)._cache;
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
@@ -320,8 +471,8 @@ describe('SeriesApiClient', () => {
|
||||
getTimeRangeParams: mockGetTimeRangeParams,
|
||||
} as unknown as PrometheusDatasource);
|
||||
|
||||
const highKey = (highLevelCache as any)._seriesCache.getCacheKey(mockTimeRange, '{job="test"}', '1000', 'key');
|
||||
const lowKey = (lowLevelCache as any)._seriesCache.getCacheKey(mockTimeRange, '{job="test"}', '1000', 'key');
|
||||
const highKey = (highLevelCache as any)._cache.getCacheKey(mockTimeRange, '{job="test"}', '1000', 'key');
|
||||
const lowKey = (lowLevelCache as any)._cache.getCacheKey(mockTimeRange, '{job="test"}', '1000', 'key');
|
||||
|
||||
expect(highKey).not.toEqual(lowKey);
|
||||
});
|
||||
|
||||
@@ -77,6 +77,8 @@ export abstract class BaseResourceClient {
|
||||
}
|
||||
|
||||
export class LabelsApiClient extends BaseResourceClient implements ResourceApiClient {
|
||||
private _cache: ResourceClientsCache = new ResourceClientsCache(this.datasource.cacheLevel);
|
||||
|
||||
public histogramMetrics: string[] = [];
|
||||
public metrics: string[] = [];
|
||||
public labelKeys: string[] = [];
|
||||
@@ -90,6 +92,7 @@ export class LabelsApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
public queryMetrics = async (timeRange: TimeRange): Promise<{ metrics: string[]; histogramMetrics: string[] }> => {
|
||||
this.metrics = await this.queryLabelValues(timeRange, METRIC_LABEL);
|
||||
this.histogramMetrics = processHistogramMetrics(this.metrics);
|
||||
this._cache.setLabelValues(timeRange, MATCH_ALL_LABELS, DEFAULT_SERIES_LIMIT, this.metrics);
|
||||
return { metrics: this.metrics, histogramMetrics: this.histogramMetrics };
|
||||
};
|
||||
|
||||
@@ -110,10 +113,16 @@ export class LabelsApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
let url = '/api/v1/labels';
|
||||
const timeParams = getRangeSnapInterval(this.datasource.cacheLevel, timeRange);
|
||||
const searchParams = { limit, ...timeParams, ...(match ? { 'match[]': match } : {}) };
|
||||
const effectiveMatch = match ?? '';
|
||||
const maybeCachedKeys = this._cache.getLabelKeys(timeRange, effectiveMatch, limit);
|
||||
if (maybeCachedKeys) {
|
||||
return maybeCachedKeys;
|
||||
}
|
||||
|
||||
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);
|
||||
return this.labelKeys.slice();
|
||||
}
|
||||
|
||||
@@ -139,14 +148,21 @@ export class LabelsApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
const searchParams = { limit, ...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);
|
||||
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 ?? []);
|
||||
return value ?? [];
|
||||
};
|
||||
}
|
||||
|
||||
export class SeriesApiClient extends BaseResourceClient implements ResourceApiClient {
|
||||
private _seriesCache: SeriesCache = new SeriesCache(this.datasource.cacheLevel);
|
||||
private _cache: ResourceClientsCache = new ResourceClientsCache(this.datasource.cacheLevel);
|
||||
|
||||
public histogramMetrics: string[] = [];
|
||||
public metrics: string[] = [];
|
||||
@@ -163,8 +179,8 @@ export class SeriesApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
this.metrics = metrics;
|
||||
this.histogramMetrics = processHistogramMetrics(this.metrics);
|
||||
this.labelKeys = labelKeys;
|
||||
this._seriesCache.setLabelValues(timeRange, MATCH_ALL_LABELS, DEFAULT_SERIES_LIMIT, metrics);
|
||||
this._seriesCache.setLabelKeys(timeRange, MATCH_ALL_LABELS, DEFAULT_SERIES_LIMIT, labelKeys);
|
||||
this._cache.setLabelValues(timeRange, MATCH_ALL_LABELS, DEFAULT_SERIES_LIMIT, metrics);
|
||||
this._cache.setLabelKeys(timeRange, MATCH_ALL_LABELS, DEFAULT_SERIES_LIMIT, labelKeys);
|
||||
return { metrics: this.metrics, histogramMetrics: this.histogramMetrics };
|
||||
};
|
||||
|
||||
@@ -174,14 +190,14 @@ export class SeriesApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
limit: string = DEFAULT_SERIES_LIMIT
|
||||
): Promise<string[]> => {
|
||||
const effectiveMatch = !match || match === EMPTY_MATCHER ? MATCH_ALL_LABELS : match;
|
||||
const maybeCachedKeys = this._seriesCache.getLabelKeys(timeRange, effectiveMatch, limit);
|
||||
const maybeCachedKeys = this._cache.getLabelKeys(timeRange, effectiveMatch, limit);
|
||||
if (maybeCachedKeys) {
|
||||
return maybeCachedKeys;
|
||||
}
|
||||
|
||||
const series = await this.querySeries(timeRange, effectiveMatch, limit);
|
||||
const { labelKeys } = processSeries(series);
|
||||
this._seriesCache.setLabelKeys(timeRange, effectiveMatch, limit, labelKeys);
|
||||
this._cache.setLabelKeys(timeRange, effectiveMatch, limit, labelKeys);
|
||||
return labelKeys;
|
||||
};
|
||||
|
||||
@@ -196,19 +212,19 @@ export class SeriesApiClient extends BaseResourceClient implements ResourceApiCl
|
||||
!match || match === EMPTY_MATCHER
|
||||
? `{${utf8SafeLabelKey}!=""}`
|
||||
: match.slice(0, match.length - 1).concat(`,${utf8SafeLabelKey}!=""}`);
|
||||
const maybeCachedValues = this._seriesCache.getLabelValues(timeRange, effectiveMatch, limit);
|
||||
const maybeCachedValues = this._cache.getLabelValues(timeRange, effectiveMatch, limit);
|
||||
if (maybeCachedValues) {
|
||||
return maybeCachedValues;
|
||||
}
|
||||
|
||||
const series = await this.querySeries(timeRange, effectiveMatch, limit);
|
||||
const { labelValues } = processSeries(series, labelKey);
|
||||
this._seriesCache.setLabelValues(timeRange, effectiveMatch, limit, labelValues);
|
||||
this._cache.setLabelValues(timeRange, effectiveMatch, limit, labelValues);
|
||||
return labelValues;
|
||||
};
|
||||
}
|
||||
|
||||
class SeriesCache {
|
||||
class ResourceClientsCache {
|
||||
private readonly MAX_CACHE_ENTRIES = 1000; // Maximum number of cache entries
|
||||
private readonly MAX_CACHE_SIZE_BYTES = 50 * 1024 * 1024; // 50MB max cache size
|
||||
|
||||
|
||||
Reference in New Issue
Block a user