diff --git a/public/app/features/trails/DataTrail.tsx b/public/app/features/trails/DataTrail.tsx index dfc4a52a88e..0a7c491888d 100644 --- a/public/app/features/trails/DataTrail.tsx +++ b/public/app/features/trails/DataTrail.tsx @@ -58,7 +58,7 @@ import { VAR_OTEL_JOIN_QUERY, VAR_OTEL_RESOURCES, } from './shared'; -import { getTrailFor } from './utils'; +import { getTrailFor, limitAdhocProviders } from './utils'; export interface DataTrailState extends SceneObjectState { topScene?: SceneObject; @@ -574,15 +574,15 @@ export class DataTrail extends SceneObjectBase { const otelResourcesVariable = sceneGraph.lookupVariable(VAR_OTEL_RESOURCES, model); const otelDepEnvVariable = sceneGraph.lookupVariable(VAR_OTEL_DEPLOYMENT_ENV, model); const otelJoinQueryVariable = sceneGraph.lookupVariable(VAR_OTEL_JOIN_QUERY, model); - const filtersvariable = sceneGraph.lookupVariable(VAR_FILTERS, model); + const filtersVariable = sceneGraph.lookupVariable(VAR_FILTERS, model); if ( otelResourcesVariable instanceof AdHocFiltersVariable && otelDepEnvVariable instanceof CustomVariable && otelJoinQueryVariable instanceof ConstantVariable && - filtersvariable instanceof AdHocFiltersVariable + filtersVariable instanceof AdHocFiltersVariable ) { - model.resetOtelExperience(otelResourcesVariable, otelDepEnvVariable, otelJoinQueryVariable, filtersvariable); + model.resetOtelExperience(otelResourcesVariable, otelDepEnvVariable, otelJoinQueryVariable, filtersVariable); } } else { // if experience is enabled, check standardization and update the otel variables @@ -590,6 +590,12 @@ export class DataTrail extends SceneObjectBase { } }, [model, hasOtelResources, useOtelExperience]); + useEffect(() => { + const filtersVariable = sceneGraph.lookupVariable(VAR_FILTERS, model); + const datasourceHelper = model.datasourceHelper; + limitAdhocProviders(filtersVariable, datasourceHelper); + }, [model]); + return (
{showHeaderForFirstTimeUsers && } diff --git a/public/app/features/trails/utils.test.ts b/public/app/features/trails/utils.test.ts new file mode 100644 index 00000000000..832b0173faa --- /dev/null +++ b/public/app/features/trails/utils.test.ts @@ -0,0 +1,56 @@ +import { AdHocFiltersVariable } from '@grafana/scenes'; + +import { MetricDatasourceHelper } from './helpers/MetricDatasourceHelper'; +import { limitAdhocProviders } from './utils'; + +describe('limitAdhocProviders', () => { + let filtersVariable: AdHocFiltersVariable; + let datasourceHelper: MetricDatasourceHelper; + + beforeEach(() => { + // disable console.log called in Scenes for this test + // called in scenes/packages/scenes/src/variables/adhoc/patchGetAdhocFilters.ts + jest.spyOn(console, 'log').mockImplementation(jest.fn()); + + filtersVariable = new AdHocFiltersVariable({ + name: 'testVariable', + label: 'Test Variable', + type: 'adhoc', + }); + + datasourceHelper = { + getTagKeys: jest.fn().mockResolvedValue(Array(20000).fill({ text: 'key' })), + getTagValues: jest.fn().mockResolvedValue(Array(20000).fill({ text: 'value' })), + } as unknown as MetricDatasourceHelper; + }); + + afterAll(() => { + jest.restoreAllMocks(); + }); + + it('should limit the number of tag keys returned in the variable to 10000', async () => { + limitAdhocProviders(filtersVariable, datasourceHelper); + + if (filtersVariable instanceof AdHocFiltersVariable && filtersVariable.state.getTagKeysProvider) { + console.log = jest.fn(); + + const result = await filtersVariable.state.getTagKeysProvider(filtersVariable, null); + expect(result.values).toHaveLength(10000); + expect(result.replace).toBe(true); + } + }); + + it('should limit the number of tag values returned in the variable to 10000', async () => { + limitAdhocProviders(filtersVariable, datasourceHelper); + + if (filtersVariable instanceof AdHocFiltersVariable && filtersVariable.state.getTagValuesProvider) { + const result = await filtersVariable.state.getTagValuesProvider(filtersVariable, { + key: 'testKey', + operator: '=', + value: 'testValue', + }); + expect(result.values).toHaveLength(10000); + expect(result.replace).toBe(true); + } + }); +}); diff --git a/public/app/features/trails/utils.ts b/public/app/features/trails/utils.ts index c4207e82084..66bd90c30cc 100644 --- a/public/app/features/trails/utils.ts +++ b/public/app/features/trails/utils.ts @@ -1,4 +1,4 @@ -import { urlUtil } from '@grafana/data'; +import { AdHocVariableFilter, GetTagResponse, MetricFindValue, urlUtil } from '@grafana/data'; import { config, getDataSourceSrv } from '@grafana/runtime'; import { AdHocFiltersVariable, @@ -8,6 +8,8 @@ import { SceneObjectUrlValues, SceneTimeRange, sceneUtils, + SceneVariable, + SceneVariableState, } from '@grafana/scenes'; import { getDatasourceSrv } from '../plugins/datasource_srv'; @@ -16,6 +18,7 @@ import { DataTrail } from './DataTrail'; import { DataTrailSettings } from './DataTrailSettings'; import { MetricScene } from './MetricScene'; import { getTrailStore } from './TrailStore/TrailStore'; +import { MetricDatasourceHelper } from './helpers/MetricDatasourceHelper'; import { LOGS_METRIC, TRAILS_ROUTE, VAR_DATASOURCE_EXPR } from './shared'; export function getTrailFor(model: SceneObject): DataTrail { @@ -115,3 +118,69 @@ export function getFilters(scene: SceneObject) { } return null; } + +// frontend hardening limit +const MAX_ADHOC_VARIABLE_OPTIONS = 10000; + +/** + * Add custom providers for the adhoc filters variable that limit the responses for labels keys and label values. + * Currently hard coded to 10000. + * + * The current provider functions for adhoc filter variables are the functions getTagKeys and getTagValues in the data source. + * This function still uses these functions from inside the data source helper. + * + * @param filtersVariable + * @param datasourceHelper + */ +export function limitAdhocProviders( + filtersVariable: SceneVariable | null, + datasourceHelper: MetricDatasourceHelper +) { + if (!(filtersVariable instanceof AdHocFiltersVariable)) { + return; + } + + filtersVariable.setState({ + getTagKeysProvider: async ( + variable: AdHocFiltersVariable, + currentKey: string | null + ): Promise<{ + replace?: boolean; + values: GetTagResponse | MetricFindValue[]; + }> => { + // For the Prometheus label names endpoint, '/api/v1/labels' + // get the previously selected filters from the variable + // to use in the query to filter the response + // using filters, e.g. {previously_selected_label:"value"}, + // as the series match[] parameter in Prometheus labels endpoint + const filters = filtersVariable.state.filters; + // call getTagKeys and truncate the response + const values = (await datasourceHelper.getTagKeys({ filters })).slice(0, MAX_ADHOC_VARIABLE_OPTIONS); + // use replace: true to override the default lookup in adhoc filter variable + return { replace: true, values }; + }, + getTagValuesProvider: async ( + variable: AdHocFiltersVariable, + filter: AdHocVariableFilter + ): Promise<{ + replace?: boolean; + values: GetTagResponse | MetricFindValue[]; + }> => { + // For the Prometheus label values endpoint, /api/v1/label/${interpolatedName}/values + // get the previously selected filters from the variable + // to use in the query to filter the response + // using filters, e.g. {previously_selected_label:"value"}, + // as the series match[] parameter in Prometheus label values endpoint + const filtersValues = filtersVariable.state.filters; + // remove current selected filter if updating a chosen filter + const filters = filtersValues.filter((f) => f.key !== filter.key); + // call getTagValues and truncate the response + const values = (await datasourceHelper.getTagValues({ key: filter.key, filters })).slice( + 0, + MAX_ADHOC_VARIABLE_OPTIONS + ); + // use replace: true to override the default lookup in adhoc filter variable + return { replace: true, values }; + }, + }); +}